diff --git a/HISTORY.md b/HISTORY.md index d3afa52c43..237cfa66f2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,7 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- Add `iris.AllowQuerySemicolons` and `iris.WithoutServerError(iris.ErrURLQuerySemicolon)` to handle golang.org/issue/25192 as reported at: https://github.com/kataras/iris/issues/1875. - Add new `Application.SetContextErrorHandler` to globally customize the default behavior (status code 500 without body) on `JSON`, `JSONP`, `Protobuf`, `MsgPack`, `XML`, `YAML` and `Markdown` method call write errors instead of catching the error on each handler. - Add new [x/pagination](x/pagination/pagination.go) sub-package which supports generics code (go 1.18+). - Add new [middleware/modrevision](middleware/modrevision) middleware (example at [_examples/project/api/router.go]_examples/project/api/router.go). diff --git a/_examples/file-server/webdav/main.go b/_examples/file-server/webdav/main.go index 0db0779234..be981a8e5e 100644 --- a/_examples/file-server/webdav/main.go +++ b/_examples/file-server/webdav/main.go @@ -32,7 +32,7 @@ func main() { app.HandleMany(strings.Join(iris.WebDAVMethods, " "), "/{p:path}", iris.FromStd(webdavHandler)) app.Listen(":8080", - iris.WithoutServerError(iris.ErrServerClosed), + iris.WithoutServerError(iris.ErrServerClosed, iris.ErrURLQuerySemicolon), iris.WithoutPathCorrection, ) } diff --git a/_examples/request-body/read-query/main.go b/_examples/request-body/read-query/main.go index e8b763a5cf..e8c0de5450 100644 --- a/_examples/request-body/read-query/main.go +++ b/_examples/request-body/read-query/main.go @@ -13,6 +13,7 @@ type MyType struct { func main() { app := iris.New() + app.UseRouter(iris.AllowQuerySemicolons) // Optionally: to restore pre go1.17 behavior of url parsing. app.Get("/", func(ctx iris.Context) { var t MyType @@ -45,5 +46,6 @@ func main() { // http://localhost:8080/simple?name=john&name=doe&name=kataras // // Note: this `WithEmptyFormError` will give an error if the query was empty. - app.Listen(":8080", iris.WithEmptyFormError) + app.Listen(":8080", iris.WithEmptyFormError, + iris.WithoutServerError(iris.ErrServerClosed, iris.ErrURLQuerySemicolon)) } diff --git a/aliases.go b/aliases.go index 76968a66c5..a686410cae 100644 --- a/aliases.go +++ b/aliases.go @@ -2,8 +2,10 @@ package iris import ( "net/http" + "net/url" "path" "regexp" + "strings" "github.com/kataras/iris/v12/cache" "github.com/kataras/iris/v12/context" @@ -328,6 +330,35 @@ var ( ctx.Next() } + // AllowQuerySemicolons returns a middleware that serves requests by converting any + // unescaped semicolons(;) in the URL query to ampersands(&). + // + // This restores the pre-Go 1.17 behavior of splitting query parameters on both + // semicolons and ampersands. + // (See golang.org/issue/25192 and https://github.com/kataras/iris/issues/1875). + // Note that this behavior doesn't match that of many proxies, + // and the mismatch can lead to security issues. + // + // AllowQuerySemicolons should be invoked before any Context read query or + // form methods are called. + // + // To skip HTTP Server logging for this type of warning: + // app.Listen/Run(..., iris.WithoutServerError(iris.ErrURLQuerySemicolon)). + AllowQuerySemicolons = func(ctx Context) { + // clopy of net/http.AllowQuerySemicolons. + r := ctx.Request() + if s := r.URL.RawQuery; strings.Contains(s, ";") { + r2 := new(http.Request) + *r2 = *r + r2.URL = new(url.URL) + *r2.URL = *r.URL + r2.URL.RawQuery = strings.ReplaceAll(s, ";", "&") + ctx.ResetRequest(r2) + } + + ctx.Next() + } + // MatchImagesAssets is a simple regex expression // that can be passed to the DirOptions.Cache.CompressIgnore field // in order to skip compression on already-compressed file types diff --git a/iris.go b/iris.go index c28a7cdfe7..f5acfd84ac 100644 --- a/iris.go +++ b/iris.go @@ -1,6 +1,7 @@ package iris import ( + "bytes" stdContext "context" "errors" "fmt" @@ -475,6 +476,40 @@ func (app *Application) ConfigureHost(configurators ...host.Configurator) *Appli return app } +const serverLoggerPrefix = "[HTTP Server] " + +type customHostServerLogger struct { // see #1875 + parent io.Writer + ignoreLogs [][]byte +} + +var newLineBytes = []byte("\n") + +func newCustomHostServerLogger(w io.Writer, ignoreLogs []string) *customHostServerLogger { + prefixAsByteSlice := []byte(serverLoggerPrefix) + + // build the ignore lines. + ignoreLogsAsByteSlice := make([][]byte, 0, len(ignoreLogs)) + for _, s := range ignoreLogs { + ignoreLogsAsByteSlice = append(ignoreLogsAsByteSlice, append(prefixAsByteSlice, []byte(s)...)) // append([]byte(s), newLineBytes...) + } + + return &customHostServerLogger{ + parent: w, + ignoreLogs: ignoreLogsAsByteSlice, + } +} + +func (l *customHostServerLogger) Write(p []byte) (int, error) { + for _, ignoredLogBytes := range l.ignoreLogs { + if bytes.Equal(bytes.TrimSuffix(p, newLineBytes), ignoredLogBytes) { + return 0, nil + } + } + + return l.parent.Write(p) +} + // NewHost accepts a standard *http.Server object, // completes the necessary missing parts of that "srv" // and returns a new, ready-to-use, host (supervisor). @@ -487,9 +522,10 @@ func (app *Application) NewHost(srv *http.Server) *host.Supervisor { srv.Handler = app.Router } - // check if different ErrorLog provided, if not bind it with the framework's logger + // check if different ErrorLog provided, if not bind it with the framework's logger. if srv.ErrorLog == nil { - srv.ErrorLog = log.New(app.logger.Printer.Output, "[HTTP Server] ", 0) + serverLogger := newCustomHostServerLogger(app.logger.Printer.Output, app.config.IgnoreServerErrors) + srv.ErrorLog = log.New(serverLogger, serverLoggerPrefix, 0) } if addr := srv.Addr; addr == "" { @@ -913,11 +949,23 @@ func Raw(f func() error) Runner { } } -// ErrServerClosed is returned by the Server's Serve, ServeTLS, ListenAndServe, -// and ListenAndServeTLS methods after a call to Shutdown or Close. -// -// A shortcut for the `http#ErrServerClosed`. -var ErrServerClosed = http.ErrServerClosed +var ( + // ErrServerClosed is logged by the standard net/http server when the server is terminated. + // Ignore it by passing this error to the `iris.WithoutServerError` configurator + // on `Application.Run/Listen` method. + // + // An alias of the `http#ErrServerClosed`. + ErrServerClosed = http.ErrServerClosed + + // ErrURLQuerySemicolon is logged by the standard net/http server when + // the request contains a semicolon (;) wihch, after go1.17 it's not used as a key-value separator character. + // + // Ignore it by passing this error to the `iris.WithoutServerError` configurator + // on `Application.Run/Listen` method. + // + // An alias of the `http#ErrServerClosed`. + ErrURLQuerySemicolon = errors.New("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192") +) // Listen builds the application and starts the server // on the TCP network address "host:port" which