diff --git a/README.md b/README.md index 8e8c574a..a98e913a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ With your help, we can improve Open Source web development for everyone! > Donations from **China** are now accepted!
- + diff --git a/_examples/README.md b/_examples/README.md index dcdad0f7..ac3b655e 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -82,6 +82,7 @@ * [Listen and render Logs to a Client](logging/request-logger/accesslog-broker/main.go) * [The CSV Formatter](logging/request-logger/accesslog-csv/main.go) * [Create your own Formatter](logging/request-logger/accesslog-formatter/main.go) + * [Root and Proxy AccessLog instances](logging/request-logger/accesslog-proxy/main.go) * API Documentation * [Yaag](apidoc/yaag/main.go) diff --git a/_examples/logging/request-logger/accesslog-proxy/main.go b/_examples/logging/request-logger/accesslog-proxy/main.go new file mode 100644 index 00000000..84be8a9a --- /dev/null +++ b/_examples/logging/request-logger/accesslog-proxy/main.go @@ -0,0 +1,86 @@ +/*Package main is a proxy + accesslog example. +In this example we will make a small proxy which listens requests on "/proxy/+path". +With two accesslog instances, one for the main application and one for the /proxy/ requests. +Of cource, you could a single accesslog for the whole application, but for the sake of the example +let's log them separately. + +We will make use of iris.StripPrefix and host.ProxyHandler.*/ +package main + +import ( + "net/url" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/core/host" + "github.com/kataras/iris/v12/middleware/accesslog" + "github.com/kataras/iris/v12/middleware/recover" +) + +func main() { + app := iris.New() + app.Get("/", index) + + ac := accesslog.File("access.log") + defer ac.Close() + ac.Async = true + ac.RequestBody = true + ac.ResponseBody = true + ac.BytesReceived = false + ac.BytesSent = false + + app.UseRouter(ac.Handler) + app.UseRouter(recover.New()) + + proxy := app.Party("/proxy") + { + acProxy := accesslog.File("proxy_access.log") + defer acProxy.Close() + acProxy.Async = true + acProxy.RequestBody = true + acProxy.ResponseBody = true + acProxy.BytesReceived = false + acProxy.BytesSent = false + + // Unlike Use, the UseRouter method replaces any duplications automatically. + // (see UseOnce for the same behavior on Use). + // Therefore, this statement removes the parent's accesslog and registers this new one. + proxy.UseRouter(acProxy.Handler) + proxy.UseRouter(recover.New()) + proxy.Use(func(ctx iris.Context) { + ctx.CompressReader(true) + ctx.Next() + }) + + /* Listen for specific proxy paths: + // Listen on "/proxy" for "http://localhost:9090/read-write" + proxy.Any("/", iris.StripPrefix("/proxy", + newProxyHandler("http://localhost:9090/read-write"))) + */ + + // You can register an access log only for proxied requests, e.g. proxy_access.log: + // proxy.UseRouter(ac2.Handler) + + // Listen for any proxy path. + // Proxies the "/proxy/+$path" to "http://localhost:9090/$path". + proxy.Any("/{p:path}", iris.StripPrefix("/proxy", + newProxyHandler("http://localhost:9090"))) + } + + // $ go run target/main.go + // open new terminal + // $ go run main.go + app.Listen(":8080") +} + +func index(ctx iris.Context) { + ctx.WriteString("OK") +} + +func newProxyHandler(proxyURL string) iris.Handler { + target, err := url.Parse(proxyURL) + if err != nil { + panic(err) + } + reverseProxy := host.ProxyHandler(target) + return iris.FromStd(reverseProxy) +} diff --git a/_examples/logging/request-logger/accesslog-proxy/target/main.go b/_examples/logging/request-logger/accesslog-proxy/target/main.go new file mode 100644 index 00000000..b34007f2 --- /dev/null +++ b/_examples/logging/request-logger/accesslog-proxy/target/main.go @@ -0,0 +1,32 @@ +package main + +import "github.com/kataras/iris/v12" + +// The target server, can be written using any programming language and any web framework, of course. +func main() { + app := iris.New() + app.Logger().SetLevel("debug") + + // Just a test route which reads some data and responds back with json. + app.Post("/read-write", readWriteHandler) + + app.Get("/get", getHandler) + + // The target ip:port. + app.Listen(":9090") +} + +func readWriteHandler(ctx iris.Context) { + var req interface{} + ctx.ReadBody(&req) + + ctx.JSON(iris.Map{ + "message": "OK", + "request": req, + }) +} + +func getHandler(ctx iris.Context) { + // ctx.CompressWriter(true) + ctx.WriteString("Compressed data") +} diff --git a/_examples/request-body/read-headers/main_test.go b/_examples/request-body/read-headers/main_test.go index 68d6be1e..38371e85 100644 --- a/_examples/request-body/read-headers/main_test.go +++ b/_examples/request-body/read-headers/main_test.go @@ -17,29 +17,35 @@ func TestReadHeaders(t *testing.T) { headers map[string]string code int body string + regex bool }{ {headers: map[string]string{ "X-Request-Id": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", "Authentication": "Bearer my-token", - }, code: 200, body: expectedOKBody}, + }, code: 200, body: expectedOKBody, regex: false}, {headers: map[string]string{ "x-request-id": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", "authentication": "Bearer my-token", - }, code: 200, body: expectedOKBody}, + }, code: 200, body: expectedOKBody, regex: false}, {headers: map[string]string{ - "X-Request-ID": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", + "X-Request-Id": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", "Authentication": "Bearer my-token", - }, code: 200, body: expectedOKBody}, + }, code: 200, body: expectedOKBody, regex: false}, {headers: map[string]string{ "Authentication": "Bearer my-token", - }, code: 500, body: "X-Request-Id is empty"}, + }, code: 500, body: "X-Request-Id is empty", regex: false}, {headers: map[string]string{ - "X-Request-ID": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", - }, code: 500, body: "Authentication is empty"}, - {headers: map[string]string{}, code: 500, body: "X-Request-Id is empty (and 1 other error)"}, + "X-Request-Id": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", + }, code: 500, body: "Authentication is empty", regex: false}, + {headers: map[string]string{}, code: 500, body: ".*\\(and 1 other error\\)$", regex: true}, } for _, tt := range tests { - e.GET("/").WithHeaders(tt.headers).Expect().Status(tt.code).Body().Equal(tt.body) + te := e.GET("/").WithHeaders(tt.headers).Expect().Status(tt.code).Body() + if tt.regex { + te.Match(tt.body) + } else { + te.Equal(tt.body) + } } } diff --git a/context/counter.go b/context/counter.go new file mode 100644 index 00000000..66cbd611 --- /dev/null +++ b/context/counter.go @@ -0,0 +1,49 @@ +package context + +import ( + "math" + "sync/atomic" +) + +// Counter is the shared counter instances between Iris applications of the same process. +var Counter = NewGlobalCounter() // it's not used anywhere, currently but it's here. + +// NewGlobalCounter returns a fresh instance of a global counter. +// End developers can use it as a helper for their applications. +func NewGlobalCounter() *GlobalCounter { + return &GlobalCounter{Max: math.MaxUint64} +} + +// GlobalCounter is a counter which +// atomically increments until Max. +type GlobalCounter struct { + value uint64 + Max uint64 +} + +// Increment increments the Value. +// The value cannot exceed the Max one. +// It uses Compare and Swap with the atomic package. +// +// Returns the new number value. +func (c *GlobalCounter) Increment() (newValue uint64) { + for { + prev := atomic.LoadUint64(&c.value) + newValue = prev + 1 + + if newValue >= c.Max { + newValue = 0 + } + + if atomic.CompareAndSwapUint64(&c.value, prev, newValue) { + break + } + } + + return +} + +// Get returns the current counter without incrementing. +func (c *GlobalCounter) Get() uint64 { + return atomic.LoadUint64(&c.value) +} diff --git a/core/host/proxy.go b/core/host/proxy.go index 2f9ecee1..1bb115a4 100644 --- a/core/host/proxy.go +++ b/core/host/proxy.go @@ -5,24 +5,12 @@ import ( "net/http" "net/http/httputil" "net/url" - "strings" + "path" "time" "github.com/kataras/iris/v12/core/netutil" ) -func singleJoiningSlash(a, b string) string { - aslash := strings.HasSuffix(a, "/") - bslash := strings.HasPrefix(b, "/") - switch { - case aslash && bslash: - return a + b[1:] - case !aslash && !bslash: - return a + "/" + b - } - return a + b -} - // ProxyHandler returns a new ReverseProxy that rewrites // URLs to the scheme, host, and base path provided in target. If the // target's path is "/base" and the incoming request was for "/dir", @@ -35,7 +23,9 @@ func ProxyHandler(target *url.URL) *httputil.ReverseProxy { req.URL.Scheme = target.Scheme req.URL.Host = target.Host req.Host = target.Host - req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) + + req.URL.Path = path.Join(target.Path, req.URL.Path) + if targetQuery == "" || req.URL.RawQuery == "" { req.URL.RawQuery = targetQuery + req.URL.RawQuery } else { @@ -110,7 +100,7 @@ func RedirectHandler(target *url.URL, redirectStatus int) http.Handler { } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - redirectTo := singleJoiningSlash(targetURI, r.URL.Path) + redirectTo := path.Join(targetURI, r.URL.Path) if len(r.URL.RawQuery) > 0 { redirectTo += "?" + r.URL.RawQuery } diff --git a/core/router/fs.go b/core/router/fs.go index f336eb87..8a3b9f72 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -397,8 +397,12 @@ func StripPrefix(prefix string, h context.Handler) context.Handler { canonicalPrefix = toWebPath(canonicalPrefix) return func(ctx *context.Context) { - if p := strings.TrimPrefix(ctx.Request().URL.Path, canonicalPrefix); len(p) < len(ctx.Request().URL.Path) { - ctx.Request().URL.Path = p + u := ctx.Request().URL + if p := strings.TrimPrefix(u.Path, canonicalPrefix); len(p) < len(u.Path) { + if p == "" { + p = "/" + } + u.Path = p h(ctx) } else { ctx.NotFound() diff --git a/middleware/accesslog/accesslog.go b/middleware/accesslog/accesslog.go index 4bd5f15e..090cad81 100644 --- a/middleware/accesslog/accesslog.go +++ b/middleware/accesslog/accesslog.go @@ -6,6 +6,7 @@ import ( stdContext "context" "fmt" "io" + "net/http" "net/http/httputil" "os" "strconv" @@ -788,7 +789,9 @@ func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path bytesReceived = requestBodyLength // store it, if the total is enabled then this will be overridden. } if err != nil && ac.RequestBody { - requestBody = ac.getErrorText(err) + if err != http.ErrBodyReadAfterClose { // if body was already closed, don't send it as error. + requestBody = ac.getErrorText(err) + } } else if requestBodyLength > 0 { if ac.RequestBody { if ac.BodyMinify {