diff --git a/app.go b/app.go index dcdb7a55..3a852477 100644 --- a/app.go +++ b/app.go @@ -54,23 +54,23 @@ type ( Compression bool `default:"false"` // CompressionLevel int `default:"1"` // fasthttp settings - GETOnly bool `default:"false"` - IdleTimeout time.Duration `default:"0"` - Concurrency int `default:"256 * 1024"` - ReadTimeout time.Duration `default:"0"` - WriteTimeout time.Duration `default:"0"` - TCPKeepalive bool `default:"false"` - MaxConnsPerIP int `default:"0"` - ReadBufferSize int `default:"4096"` - WriteBufferSize int `default:"4096"` - ConcurrencySleep time.Duration `default:"0"` - DisableKeepAlive bool `default:"false"` - ReduceMemoryUsage bool `default:"false"` - MaxRequestsPerConn int `default:"0"` - TCPKeepalivePeriod time.Duration `default:"0"` - MaxRequestBodySize int `default:"4 * 1024 * 1024"` - NoHeaderNormalizing bool `default:"false"` - NoDefaultContentType bool `default:"false"` + // GETOnly bool `default:"false"` + // IdleTimeout time.Duration `default:"0"` + // Concurrency int `default:"256 * 1024"` + // ReadTimeout time.Duration `default:"0"` + // WriteTimeout time.Duration `default:"0"` + // TCPKeepalive bool `default:"false"` + // MaxConnsPerIP int `default:"0"` + // ReadBufferSize int `default:"4096"` + // WriteBufferSize int `default:"4096"` + // ConcurrencySleep time.Duration `default:"0"` + // DisableKeepAlive bool `default:"false"` + // ReduceMemoryUsage bool `default:"false"` + // MaxRequestsPerConn int `default:"0"` + // TCPKeepalivePeriod time.Duration `default:"0"` + BodyLimit int `default:"4 * 1024 * 1024"` + // NoHeaderNormalizing bool `default:"false"` + // NoDefaultContentType bool `default:"false"` // template settings TemplateFolder string `default:""` TemplateEngine string `default:""` @@ -107,17 +107,17 @@ func New(settings ...*Settings) (app *App) { return string(b) } } - if opt.Concurrency == 0 { - opt.Concurrency = 256 * 1024 - } - if opt.ReadBufferSize == 0 { - opt.ReadBufferSize = 4096 - } - if opt.WriteBufferSize == 0 { - opt.WriteBufferSize = 4096 - } - if opt.MaxRequestBodySize == 0 { - opt.MaxRequestBodySize = 4 * 1024 * 1024 + // if opt.Concurrency == 0 { + // opt.Concurrency = 256 * 1024 + // } + // if opt.ReadBufferSize == 0 { + // opt.ReadBufferSize = 4096 + // } + // if opt.WriteBufferSize == 0 { + // opt.WriteBufferSize = 4096 + // } + if opt.BodyLimit == 0 { + opt.BodyLimit = 4 * 1024 * 1024 } // if opt.CompressionLevel == 0 { // opt.CompressionLevel = 1 @@ -126,11 +126,11 @@ func New(settings ...*Settings) (app *App) { return } app.Settings = &Settings{ - Prefork: prefork, - Concurrency: 256 * 1024, - ReadBufferSize: 4096, - WriteBufferSize: 4096, - MaxRequestBodySize: 4 * 1024 * 1024, + Prefork: prefork, + // Concurrency: 256 * 1024, + // ReadBufferSize: 4096, + // WriteBufferSize: 4096, + BodyLimit: 4 * 1024 * 1024, } return } @@ -379,31 +379,44 @@ func (app *App) prefork(address string) (ln net.Listener, err error) { return ln, err } +type disableLogger struct{} + +func (dl *disableLogger) Printf(format string, args ...interface{}) { + // fmt.Println(fmt.Sprintf(format, args...)) +} + func (app *App) newServer() *fasthttp.Server { return &fasthttp.Server{ Handler: app.handler, + Name: app.Settings.ServerHeader, + // Concurrency: app.Settings.Concurrency, + // SleepWhenConcurrencyLimitsExceeded: app.Settings.ConcurrencySleep, + // DisableKeepalive: app.Settings.DisableKeepAlive, + // ReadBufferSize: app.Settings.ReadBufferSize, + // WriteBufferSize: app.Settings.WriteBufferSize, + // ReadTimeout: app.Settings.ReadTimeout, + // WriteTimeout: app.Settings.WriteTimeout, + // IdleTimeout: app.Settings.IdleTimeout, + // MaxConnsPerIP: app.Settings.MaxConnsPerIP, + // MaxRequestsPerConn: app.Settings.MaxRequestsPerConn, + // TCPKeepalive: app.Settings.TCPKeepalive, + // TCPKeepalivePeriod: app.Settings.TCPKeepalivePeriod, + MaxRequestBodySize: app.Settings.BodyLimit, + // ReduceMemoryUsage: app.Settings.ReduceMemoryUsage, + // GetOnly: app.Settings.GETOnly, + // DisableHeaderNamesNormalizing: app.Settings.NoHeaderNormalizing, + NoDefaultServerHeader: app.Settings.ServerHeader == "", + // NoDefaultContentType: app.Settings.NoDefaultContentType, + Logger: &disableLogger{}, + LogAllErrors: false, ErrorHandler: func(ctx *fasthttp.RequestCtx, err error) { - ctx.Response.SetStatusCode(400) - ctx.Response.SetBodyString("Bad Request") + if err.Error() == "body size exceeds the given limit" { + ctx.Response.SetStatusCode(413) + ctx.Response.SetBodyString("Request Entity Too Large") + } else { + ctx.Response.SetStatusCode(400) + ctx.Response.SetBodyString("Bad Request") + } }, - Name: app.Settings.ServerHeader, - Concurrency: app.Settings.Concurrency, - SleepWhenConcurrencyLimitsExceeded: app.Settings.ConcurrencySleep, - DisableKeepalive: app.Settings.DisableKeepAlive, - ReadBufferSize: app.Settings.ReadBufferSize, - WriteBufferSize: app.Settings.WriteBufferSize, - ReadTimeout: app.Settings.ReadTimeout, - WriteTimeout: app.Settings.WriteTimeout, - IdleTimeout: app.Settings.IdleTimeout, - MaxConnsPerIP: app.Settings.MaxConnsPerIP, - MaxRequestsPerConn: app.Settings.MaxRequestsPerConn, - TCPKeepalive: app.Settings.TCPKeepalive, - TCPKeepalivePeriod: app.Settings.TCPKeepalivePeriod, - MaxRequestBodySize: app.Settings.MaxRequestBodySize, - ReduceMemoryUsage: app.Settings.ReduceMemoryUsage, - GetOnly: app.Settings.GETOnly, - DisableHeaderNamesNormalizing: app.Settings.NoHeaderNormalizing, - NoDefaultServerHeader: app.Settings.ServerHeader == "", - NoDefaultContentType: app.Settings.NoDefaultContentType, } } diff --git a/go.mod b/go.mod index 61666dda..7c2bf8f2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.11 require ( github.com/fasthttp/websocket v1.4.2 github.com/gofiber/template v1.0.0 + github.com/google/uuid v1.1.1 github.com/gorilla/schema v1.1.0 github.com/json-iterator/go v1.1.9 github.com/valyala/fasthttp v1.9.0 + github.com/valyala/fasttemplate v1.1.0 ) diff --git a/go.sum b/go.sum index ae4af774..dc7473ae 100644 --- a/go.sum +++ b/go.sum @@ -1,40 +1,30 @@ github.com/Joker/hpp v0.0.0-20180418125244-6893e659854a/go.mod h1:MzD2WMdSxvbHw5fM/OXOFily/lipJWRc9C1px0Mt0ZE= -github.com/Joker/jade v1.0.0 h1:lOCEPvTAtWfLpSZYMOv/g44MGQFAolbKh2khHHGu0Kc= github.com/Joker/jade v1.0.0/go.mod h1:efZIdO0py/LtcJRSa/j2WEklMSAw84WV0zZVMxNToB8= -github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= -github.com/cbroglie/mustache v1.0.1 h1:ivMg8MguXq/rrz2eu3tw6g3b16+PQhoTn6EZAhst2mw= github.com/cbroglie/mustache v1.0.1/go.mod h1:R/RUa+SobQ14qkP4jtx5Vke5sDytONDQXNLPY/PO69g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= -github.com/fasthttp/websocket v1.4.2 h1:AU/zSiIIAuJjBMf5o+vO0syGOnEfvZRu40xIhW/3RuM= github.com/fasthttp/websocket v1.4.2/go.mod h1:smsv/h4PBEBaU0XDTY5UwJTpZv69fQ0FfcLJr21mA6Y= -github.com/gofiber/template v1.0.0 h1:Vf4Fby9zUWVQyY2y69KKyRHsEYlIE+Pxb25M+jiaEL0= github.com/gofiber/template v1.0.0/go.mod h1:+bij+R0NI6urTg2jtQvPj5wb2uWMxW9eYGsAN3QhnP0= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f h1:PgA+Olipyj258EIEYnpFFONrrCcAIWNUNoFhUfMqAGY= github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.9.0 h1:hNpmUdy/+ZXYpGy0OBfm7K0UQTzb73W0T0U4iJIVrMw= github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= +github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/middleware/basic_auth.go b/middleware/basic_auth.go new file mode 100644 index 00000000..650ecc5b --- /dev/null +++ b/middleware/basic_auth.go @@ -0,0 +1,74 @@ +package middleware + +import ( + "encoding/base64" + "strings" + + ".." +) + +// BasicAuthConfig defines the config for BasicAuth middleware +type BasicAuthConfig struct { + // Skip defines a function to skip middleware. + // Optional. Default: nil + Skip func(*fiber.Ctx) bool + // Users + // Required. + Users map[string]string + // Realm is a string to define realm attribute of BasicAuth. + // Optional. Default: "Restricted". + Realm string +} + +// BasicAuthConfigDefault is the default BasicAuth middleware config. +var BasicAuthConfigDefault = BasicAuthConfig{ + Skip: nil, + Realm: "Restricted", +} + +// BasicAuth ... +func BasicAuth(config ...BasicAuthConfig) func(*fiber.Ctx) { + // Init config + var cfg BasicAuthConfig + if len(config) > 0 { + cfg = config[0] + } + // Set config default values + if cfg.Realm == "" { + cfg.Realm = BasicAuthConfigDefault.Realm + } + // Return middleware handler + return func(c *fiber.Ctx) { + // Skip middleware if Skip returns true + if cfg.Skip != nil && cfg.Skip(c) { + c.Next() + return + } + // Get authorization header + auth := c.Get(fiber.HeaderAuthorization) + // Check if characters are provided + if len(auth) > 6 && strings.ToLower(auth[:5]) == "basic" { + // Try to decode + if raw, err := base64.StdEncoding.DecodeString(auth[6:]); err == nil { + // Convert to string + cred := string(raw) + // Find semicolumn + for i := 0; i < len(cred); i++ { + if cred[i] == ':' { + // Split into user & pass + user := cred[:i] + pass := cred[i+1:] + // If exist & match in Users, we let him pass + if cfg.Users[user] == pass { + c.Next() + return + } + } + } + } + } + // Authentication required + c.Set(fiber.HeaderWWWAuthenticate, "basic realm="+cfg.Realm) + c.SendStatus(401) + } +} diff --git a/middleware/basicauth.go b/middleware/basicauth.go deleted file mode 100644 index 0beb8b44..00000000 --- a/middleware/basicauth.go +++ /dev/null @@ -1,23 +0,0 @@ -package middleware - -import "github.com/gofiber/fiber" - -// BasicAuthConfig ... -type BasicAuthConfig struct { -} - -// BasicAuth ... -func BasicAuth(config ...BasicAuthConfig) func(*fiber.Ctx) { - // Init config - var cfg BasicAuthConfig - // Set config if provided - if len(config) > 0 { - cfg = config[0] - } - // Don't forget to remove this - _ = cfg - // Set config default values - return func(c *fiber.Ctx) { - c.Next() - } -} diff --git a/middleware/cors.go b/middleware/cors.go index 32360c7e..c065ee17 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -5,11 +5,12 @@ import ( "strconv" "strings" - "github.com/gofiber/fiber" + ".." ) // CORSConfig ... type CORSConfig struct { + Skip func(*fiber.Ctx) bool // Optional. Default value []string{"*"}. AllowOrigins []string // Optional. Default value []string{"GET","POST","HEAD","PUT","DELETE","PATCH"} @@ -24,6 +25,20 @@ type CORSConfig struct { MaxAge int } +// CorsConfigDefault is the defaul Cors middleware config. +var CorsConfigDefault = CORSConfig{ + Skip: nil, + AllowOrigins: []string{"*"}, + AllowMethods: []string{ + http.MethodGet, + http.MethodPost, + http.MethodHead, + http.MethodPut, + http.MethodDelete, + http.MethodPatch, + }, +} + // Cors ... func Cors(config ...CORSConfig) func(*fiber.Ctx) { // Init config @@ -32,19 +47,12 @@ func Cors(config ...CORSConfig) func(*fiber.Ctx) { if len(config) > 0 { cfg = config[0] } - // Set config default vvalues + // Set config default values if len(cfg.AllowOrigins) == 0 { - cfg.AllowOrigins = []string{"*"} + cfg.AllowOrigins = CorsConfigDefault.AllowOrigins } if len(cfg.AllowMethods) == 0 { - cfg.AllowMethods = []string{ - http.MethodGet, - http.MethodPost, - http.MethodHead, - http.MethodPut, - http.MethodDelete, - http.MethodPatch, - } + cfg.AllowMethods = CorsConfigDefault.AllowMethods } // Middleware settings allowMethods := strings.Join(cfg.AllowMethods, ",") @@ -53,6 +61,11 @@ func Cors(config ...CORSConfig) func(*fiber.Ctx) { maxAge := strconv.Itoa(cfg.MaxAge) // Middleware function return func(c *fiber.Ctx) { + // Skip middleware if Skip returns true + if cfg.Skip != nil && cfg.Skip(c) { + c.Next() + return + } origin := c.Get(fiber.HeaderOrigin) allowOrigin := "" // Check allowed origins diff --git a/middleware/limiter.go b/middleware/limiter.go index ee55d359..17ab1e33 100644 --- a/middleware/limiter.go +++ b/middleware/limiter.go @@ -1,9 +1,49 @@ package middleware -import "github.com/gofiber/fiber" +import ( + "strconv" + "time" + + ".." +) // LimiterConfig ... type LimiterConfig struct { + Skip func(*fiber.Ctx) bool + // Timeout in seconds on how long to keep records of requests in memory + // Default: 60 + Timeout int + // Max number of recent connections during `Timeout` seconds before sending a 429 response + // Default: 10 + Max int + // Message + // default: "Too many requests, please try again later." + Message string + // StatusCode + // Default: 429 Too Many Requests + StatusCode int + // Key allows to use a custom handler to create custom keys + // Default: func(c *fiber.Ctx) string { + // return c.IP() + // } + Key func(*fiber.Ctx) string + // Handler is called when a request hits the limit + // Default: func(c *fiber.Ctx) { + // c.Status(cfg.StatusCode).SendString(cfg.Message) + // } + Handler func(*fiber.Ctx) +} + +// LimiterConfigDefault is the defaul Limiter middleware config. +var LimiterConfigDefault = LimiterConfig{ + Skip: nil, + Timeout: 60, + Max: 10, + Message: "Too many requests, please try again later.", + StatusCode: 429, + Key: func(c *fiber.Ctx) string { + return c.IP() + }, } // Limiter ... @@ -14,10 +54,88 @@ func Limiter(config ...LimiterConfig) func(*fiber.Ctx) { if len(config) > 0 { cfg = config[0] } - // Don't forget to remove this - _ = cfg // Set config default values + if cfg.Timeout == 0 { + cfg.Timeout = LimiterConfigDefault.Timeout + } + if cfg.Max == 0 { + cfg.Max = LimiterConfigDefault.Max + } + if cfg.Message == "" { + cfg.Message = LimiterConfigDefault.Message + } + if cfg.StatusCode == 0 { + cfg.StatusCode = LimiterConfigDefault.StatusCode + } + if cfg.Key == nil { + cfg.Key = LimiterConfigDefault.Key + } + if cfg.Handler == nil { + cfg.Handler = func(c *fiber.Ctx) { + c.Status(cfg.StatusCode).SendString(cfg.Message) + } + } + // Limiter settings + var hits = map[string]int{} + var reset = map[string]int{} + var timestamp = int(time.Now().Unix()) + // Update timestamp every second + go func() { + for { + timestamp = int(time.Now().Unix()) + time.Sleep(1 * time.Second) + } + }() + // Reset hits every cfg.Timeout + go func() { + for { + // For every key in reset + for key := range reset { + // If resetTime exist and current time is equal or bigger + if reset[key] != 0 && timestamp >= reset[key] { + // Reset hits and resetTime + hits[key] = 0 + reset[key] = 0 + } + } + // Wait cfg.Timeout + time.Sleep(time.Duration(cfg.Timeout) * time.Second) + } + }() return func(c *fiber.Ctx) { + // Skip middleware if Skip returns true + if cfg.Skip != nil && cfg.Skip(c) { + c.Next() + return + } + // Get key (default is the remote IP) + key := cfg.Key(c) + // Increment key hits + hits[key]++ + // Set unix timestamp if not exist + if reset[key] == 0 { + reset[key] = timestamp + cfg.Timeout + } + // Get current hits + hitCount := hits[key] + // Set how many hits we have left + remaining := cfg.Max - hitCount + // Calculate when it resets in seconds + resetTime := reset[key] - timestamp + // Check if hits exceed the cfg.Max + if remaining < 1 { + // Call Handler func + cfg.Handler(c) + // Return response with Retry-After header + // https://tools.ietf.org/html/rfc6584 + c.Set("Retry-After", strconv.Itoa(resetTime)) + return + } + // We can continue, update RateLimit headers + c.Set("X-RateLimit-Limit", strconv.Itoa(cfg.Max)) + c.Set("X-RateLimit-Remaining", strconv.Itoa(remaining)) + c.Set("X-RateLimit-Reset", strconv.Itoa(resetTime)) + // Bye! c.Next() } } diff --git a/middleware/logger.go b/middleware/logger.go index 0f1e03da..4afeaac6 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -1,9 +1,41 @@ package middleware -import "github.com/gofiber/fiber" +import ( + "bytes" + "io" + "os" + "strings" + "sync" + "time" + + ".." + "github.com/valyala/fasttemplate" +) // LoggerConfig ... type LoggerConfig struct { + // Skip defines a function to skip middleware. + // Optional. Default: nil + Skip func(*fiber.Ctx) bool + // Format defines the logging format with defined variables + // Optional. Default: "${time} - ${ip} - ${method} ${path}\t${ua}\n" + // Possible values: time, ip, url, host, method, path, protocol + // referer, ua, header:, query:, formform:, cookie: + Format string + // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html + // Optional. Default: 15:04:05 + TimeFormat string + // Output is a writter where logs are written + // Default: os.Stderr + Output io.Writer +} + +// LoggerConfigDefault is the defaul Logger middleware config. +var LoggerConfigDefault = LoggerConfig{ + Skip: nil, + Format: "${time} - ${ip} - ${method} ${path}\t${ua}\n", + TimeFormat: "15:04:05", + Output: os.Stderr, } // Logger ... @@ -14,10 +46,78 @@ func Logger(config ...LoggerConfig) func(*fiber.Ctx) { if len(config) > 0 { cfg = config[0] } - // Don't forget to remove this - _ = cfg // Set config default values + if cfg.Format == "" { + cfg.Format = LoggerConfigDefault.Format + } + if cfg.TimeFormat == "" { + cfg.TimeFormat = LoggerConfigDefault.TimeFormat + } + if cfg.Output == nil { + cfg.Output = LoggerConfigDefault.Output + } + // Middleware settings + tmpl := fasttemplate.New(cfg.Format, "${", "}") + pool := &sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 256)) + }, + } + timestamp := time.Now().Format(cfg.TimeFormat) + // Update date/time every second in a seperate go routine + if strings.Contains(cfg.Format, "${time}") { + go func() { + for { + timestamp = time.Now().Format(cfg.TimeFormat) + time.Sleep(1 * time.Second) + } + }() + } + // Middleware function return func(c *fiber.Ctx) { + // Skip middleware if Skip returns true + if cfg.Skip != nil && cfg.Skip(c) { + c.Next() + return + } + buf := pool.Get().(*bytes.Buffer) + buf.Reset() + defer pool.Put(buf) + tmpl.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) { + switch tag { + case "time": + return buf.WriteString(timestamp) + case "referer": + return buf.WriteString(c.Get(fiber.HeaderReferer)) + case "protocol": + return buf.WriteString(c.Protocol()) + case "ip": + return buf.WriteString(c.IP()) + case "host": + return buf.WriteString(c.Hostname()) + case "method": + return buf.WriteString(c.Method()) + case "path": + return buf.WriteString(c.Path()) + case "url": + return buf.WriteString(c.OriginalURL()) + case "ua": + return buf.WriteString(c.Get(fiber.HeaderUserAgent)) + default: + switch { + case strings.HasPrefix(tag, "header:"): + return buf.WriteString(c.Get(tag[7:])) + case strings.HasPrefix(tag, "query:"): + return buf.WriteString(c.Query(tag[6:])) + case strings.HasPrefix(tag, "form:"): + return buf.WriteString(c.FormValue(tag[5:])) + case strings.HasPrefix(tag, "cookie:"): + return buf.WriteString(c.Cookies(tag[7:])) + } + } + return 0, nil + }) + cfg.Output.Write(buf.Bytes()) c.Next() } } diff --git a/middleware/request_id.go b/middleware/request_id.go new file mode 100644 index 00000000..03d46e6f --- /dev/null +++ b/middleware/request_id.go @@ -0,0 +1,63 @@ +package middleware + +import ( + "fmt" + + ".." + "github.com/google/uuid" +) + +// RequestIDConfig defines the config for RequestID middleware +type RequestIDConfig struct { + // Skip defines a function to skip middleware. + // Optional. Default: nil + Skip func(*fiber.Ctx) bool + // Generator defines a function to generate an ID. + // Optional. Default: func() string { + // return uuid.New().String() + // } + Generator func() string +} + +// RequestIDConfigDefault is the default RequestID middleware config. +var RequestIDConfigDefault = RequestIDConfig{ + Skip: nil, + Generator: func() string { + return uuid.New().String() + }, +} + +// RequestID adds an indentifier to the request using the `X-Request-ID` header +func RequestID(config ...RequestIDConfig) func(*fiber.Ctx) { + // Init config + var cfg RequestIDConfig + if len(config) > 0 { + cfg = config[0] + } + // Set config default values + if cfg.Generator == nil { + cfg.Skip = RequestIDConfigDefault.Skip + } + if cfg.Generator == nil { + cfg.Generator = RequestIDConfigDefault.Generator + } + // Return middleware handler + return func(c *fiber.Ctx) { + // Skip middleware if Skip returns true + if cfg.Skip != nil && cfg.Skip(c) { + c.Next() + return + } + // Get value from RequestID + rid := c.Get(fiber.HeaderXRequestID) + fmt.Println(rid) + // Create new ID + if rid == "" { + rid = cfg.Generator() + } + // Set X-Request-ID + c.Set(fiber.HeaderXRequestID, rid) + // Bye! + c.Next() + } +} diff --git a/middleware/secure.go b/middleware/secure.go index c5abe6e3..5d2c2111 100644 --- a/middleware/secure.go +++ b/middleware/secure.go @@ -3,51 +3,76 @@ package middleware import ( "fmt" - "github.com/gofiber/fiber" + ".." ) // SecureConfig ... type SecureConfig struct { + // Skip defines a function to skip middleware. + // Optional. Default: nil + Skip func(*fiber.Ctx) bool + // XSSProtection // Optional. Default value "1; mode=block". XSSProtection string + // ContentTypeNosniff // Optional. Default value "nosniff". ContentTypeNosniff string - // Optional. Default value "SAMEORIGIN". Possible values: "SAMEORIGIN", "DENY", "ALLOW-FROM uri" + // XFrameOptions + // Optional. Default value "SAMEORIGIN". + // Possible values: "SAMEORIGIN", "DENY", "ALLOW-FROM uri" XFrameOptions string + // HSTSMaxAge // Optional. Default value 0. HSTSMaxAge int + // HSTSExcludeSubdomains // Optional. Default value false. HSTSExcludeSubdomains bool + // ContentSecurityPolicy // Optional. Default value "". ContentSecurityPolicy string + // CSPReportOnly // Optional. Default value false. CSPReportOnly bool + // HSTSPreloadEnabled // Optional. Default value false. HSTSPreloadEnabled bool + // ReferrerPolicy // Optional. Default value "". ReferrerPolicy string } +// SecureConfigDefault is the defaul Secure middleware config. +var SecureConfigDefault = SecureConfig{ + Skip: nil, + XSSProtection: "1; mode=block", + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", +} + // Secure ... func Secure(config ...SecureConfig) func(*fiber.Ctx) { // Init config var cfg SecureConfig - // Set config if provided if len(config) > 0 { cfg = config[0] } - // Set config default options + // Set config default values if cfg.XSSProtection == "" { - cfg.XSSProtection = "1; mode=block" + cfg.XSSProtection = SecureConfigDefault.XSSProtection } if cfg.ContentTypeNosniff == "" { - cfg.ContentTypeNosniff = "nosniff" + cfg.ContentTypeNosniff = SecureConfigDefault.ContentTypeNosniff } if cfg.XFrameOptions == "" { - cfg.XFrameOptions = "SAMEORIGIN" + cfg.XFrameOptions = SecureConfigDefault.XFrameOptions } // Return middleware handler return func(c *fiber.Ctx) { + // Skip middleware if Skip returns true + if cfg.Skip != nil && cfg.Skip(c) { + c.Next() + return + } if cfg.XSSProtection != "" { c.Set(fiber.HeaderXXSSProtection, cfg.XSSProtection) } diff --git a/router.go b/router.go index 5ed93f31..2cd26692 100644 --- a/router.go +++ b/router.go @@ -260,8 +260,8 @@ func (app *App) handler(fctx *fasthttp.RequestCtx) { ctx := acquireCtx() defer releaseCtx(ctx) ctx.app = app - ctx.Fasthttp = fctx ctx.compress = app.Settings.Compression + ctx.Fasthttp = fctx // get path and method path := ctx.Path() if !app.Settings.CaseSensitive { diff --git a/utils.go b/utils.go index fbf9f4f5..ba980e06 100644 --- a/utils.go +++ b/utils.go @@ -493,6 +493,7 @@ const ( HeaderUpgrade = "Upgrade" HeaderXDNSPrefetchControl = "X-DNS-Prefetch-Control" HeaderXPingback = "X-Pingback" + HeaderXRequestID = "X-Request-ID" HeaderXRequestedWith = "X-Requested-With" HeaderXRobotsTag = "X-Robots-Tag" HeaderXUACompatible = "X-UA-Compatible"