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 {