From 51986b2e7c183e4b9fe11318be0b232fcbc79a68 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Thu, 18 Feb 2021 17:06:40 +0800 Subject: [PATCH 01/44] =?UTF-8?q?=20=F0=9F=94=A5=20Fiber=20Client=20poc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 569 +++++++++++++++++++++++++++++++++++++++++++ client_test.go | 636 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1205 insertions(+) create mode 100644 client.go create mode 100644 client_test.go diff --git a/client.go b/client.go new file mode 100644 index 00000000..7bacade9 --- /dev/null +++ b/client.go @@ -0,0 +1,569 @@ +package fiber + +import ( + "bytes" + "crypto/tls" + "fmt" + "io" + "net" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2/internal/encoding/json" + "github.com/valyala/fasthttp" +) + +// Request represents HTTP request. +// +// It is forbidden copying Request instances. Create new instances +// and use CopyTo instead. +// +// Request instance MUST NOT be used from concurrently running goroutines. +type Request = fasthttp.Request + +// Response represents HTTP response. +// +// It is forbidden copying Response instances. Create new instances +// and use CopyTo instead. +// +// Response instance MUST NOT be used from concurrently running goroutines. +type Response = fasthttp.Response + +// Args represents query arguments. +// +// It is forbidden copying Args instances. Create new instances instead +// and use CopyTo(). +// +// Args instance MUST NOT be used from concurrently running goroutines. +type Args = fasthttp.Args + +var defaultClient Client + +// Client implements http client. +// +// It is safe calling Client methods from concurrently running goroutines. +type Client struct { + UserAgent string + NoDefaultUserAgentHeader bool +} + +// Get returns a agent with http method GET. +func Get(url string) *Agent { return defaultClient.Get(url) } + +// Get returns a agent with http method GET. +func (c *Client) Get(url string) *Agent { + return c.createAgent(MethodGet, url) +} + +// Post sends POST request to the given url. +func Post(url string) *Agent { return defaultClient.Post(url) } + +// Post sends POST request to the given url. +func (c *Client) Post(url string) *Agent { + return c.createAgent(MethodPost, url) +} + +func (c *Client) createAgent(method, url string) *Agent { + a := AcquireAgent() + a.req.Header.SetMethod(method) + a.req.SetRequestURI(url) + + a.Name = c.UserAgent + a.NoDefaultUserAgentHeader = c.NoDefaultUserAgentHeader + + if err := a.Parse(); err != nil { + a.errs = append(a.errs, err) + } + + return a +} + +// Agent is an object storing all request data for client. +type Agent struct { + *fasthttp.HostClient + req *Request + customReq *Request + args *Args + timeout time.Duration + errs []error + debugWriter io.Writer + maxRedirectsCount int + Name string + NoDefaultUserAgentHeader bool + reuse bool + parsed bool +} + +var ErrorInvalidURI = fasthttp.ErrorInvalidURI + +// Parse initializes URI and HostClient. +func (a *Agent) Parse() error { + if a.parsed { + return nil + } + a.parsed = true + + req := a.req + if a.customReq != nil { + req = a.customReq + } + + uri := req.URI() + if uri == nil { + return ErrorInvalidURI + } + + isTLS := false + scheme := uri.Scheme() + if bytes.Equal(scheme, strHTTPS) { + isTLS = true + } else if !bytes.Equal(scheme, strHTTP) { + return fmt.Errorf("unsupported protocol %q. http and https are supported", scheme) + } + + name := a.Name + if name == "" && !a.NoDefaultUserAgentHeader { + name = defaultUserAgent + } + + a.HostClient = &fasthttp.HostClient{ + Addr: addMissingPort(string(uri.Host()), isTLS), + Name: name, + NoDefaultUserAgentHeader: a.NoDefaultUserAgentHeader, + IsTLS: isTLS, + } + + return nil +} + +func addMissingPort(addr string, isTLS bool) string { + n := strings.Index(addr, ":") + if n >= 0 { + return addr + } + port := 80 + if isTLS { + port = 443 + } + return net.JoinHostPort(addr, strconv.Itoa(port)) +} + +// Set sets the given 'key: value' header. +// +// Use Add for setting multiple header values under the same key. +func (a *Agent) Set(k, v string) *Agent { + a.req.Header.Set(k, v) + + return a +} + +// Add adds the given 'key: value' header. +// +// Multiple headers with the same key may be added with this function. +// Use Set for setting a single header for the given key. +func (a *Agent) Add(k, v string) *Agent { + a.req.Header.Add(k, v) + + return a +} + +// Host sets host for the uri. +func (a *Agent) Host(host string) *Agent { + a.req.URI().SetHost(host) + + return a +} + +// ConnectionClose sets 'Connection: close' header. +func (a *Agent) ConnectionClose() *Agent { + a.req.Header.SetConnectionClose() + + return a +} + +// UserAgent sets User-Agent header value. +func (a *Agent) UserAgent(userAgent string) *Agent { + a.req.Header.SetUserAgent(userAgent) + + return a +} + +// Debug mode enables logging request and response detail +func (a *Agent) Debug(w ...io.Writer) *Agent { + a.debugWriter = os.Stdout + if len(w) > 0 { + a.debugWriter = w[0] + } + + return a +} + +// Cookie sets one 'key: value' cookie. +func (a *Agent) Cookie(key, value string) *Agent { + a.req.Header.SetCookie(key, value) + + return a +} + +// Cookies sets multiple 'key: value' cookies. +func (a *Agent) Cookies(kv ...string) *Agent { + for i := 1; i < len(kv); i += 2 { + a.req.Header.SetCookie(kv[i-1], kv[i]) + } + + return a +} + +// Timeout sets request timeout duration. +func (a *Agent) Timeout(timeout time.Duration) *Agent { + a.timeout = timeout + + return a +} + +// Json sends a json request. +func (a *Agent) Json(v interface{}) *Agent { + a.req.Header.SetContentType(MIMEApplicationJSON) + + if body, err := json.Marshal(v); err != nil { + a.errs = append(a.errs, err) + } else { + a.req.SetBody(body) + } + + return a +} + +// Form sends request with body if args is non-nil. +// +// Note that this will force http method to post. +func (a *Agent) Form(args *Args) *Agent { + a.req.Header.SetContentType(MIMEApplicationForm) + + if args != nil { + if _, err := args.WriteTo(a.req.BodyWriter()); err != nil { + a.errs = append(a.errs, err) + } + } + + return a +} + +// QueryString sets URI query string. +func (a *Agent) QueryString(queryString string) *Agent { + a.req.URI().SetQueryString(queryString) + + return a +} + +// BodyStream sets request body stream and, optionally body size. +// +// If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes +// before returning io.EOF. +// +// If bodySize < 0, then bodyStream is read until io.EOF. +// +// bodyStream.Close() is called after finishing reading all body data +// if it implements io.Closer. +// +// Note that GET and HEAD requests cannot have body. +func (a *Agent) BodyStream(bodyStream io.Reader, bodySize int) *Agent { + a.req.SetBodyStream(bodyStream, bodySize) + + return a +} + +// Reuse indicates the createAgent can be used again after one request. +func (a *Agent) Reuse() *Agent { + a.reuse = true + + return a +} + +// InsecureSkipVerify controls whether the createAgent verifies the server's +// certificate chain and host name. +func (a *Agent) InsecureSkipVerify() *Agent { + if a.HostClient.TLSConfig == nil { + a.HostClient.TLSConfig = &tls.Config{InsecureSkipVerify: true} + } else { + a.HostClient.TLSConfig.InsecureSkipVerify = true + } + + return a +} + +// TLSConfig sets tls config. +func (a *Agent) TLSConfig(config *tls.Config) *Agent { + a.HostClient.TLSConfig = config + + return a +} + +// Request sets custom request for createAgent. +func (a *Agent) Request(req *Request) *Agent { + a.customReq = req + + return a +} + +// Referer sets Referer header value. +func (a *Agent) Referer(referer string) *Agent { + a.req.Header.SetReferer(referer) + + return a +} + +// ContentType sets Content-Type header value. +func (a *Agent) ContentType(contentType string) *Agent { + a.req.Header.SetContentType(contentType) + + return a +} + +// MaxRedirectsCount sets max redirect count for GET and HEAD. +func (a *Agent) MaxRedirectsCount(count int) *Agent { + a.maxRedirectsCount = count + + return a +} + +// Bytes returns the status code, bytes body and errors of url. +func (a *Agent) Bytes(customResp ...*Response) (code int, body []byte, errs []error) { + defer a.release() + + if errs = append(errs, a.errs...); len(errs) > 0 { + return + } + + req := a.req + if a.customReq != nil { + req = a.customReq + } + + var ( + resp *Response + releaseResp bool + ) + if len(customResp) > 0 { + resp = customResp[0] + } else { + resp = AcquireResponse() + releaseResp = true + } + defer func() { + if a.debugWriter != nil { + printDebugInfo(req, resp, a.debugWriter) + } + + if len(errs) == 0 { + code = resp.StatusCode() + } + + if releaseResp { + body = append(body, resp.Body()...) + ReleaseResponse(resp) + } else { + body = resp.Body() + } + }() + + if a.timeout > 0 { + if err := a.HostClient.DoTimeout(req, resp, a.timeout); err != nil { + errs = append(errs, err) + return + } + } + + if a.maxRedirectsCount > 0 && (string(req.Header.Method()) == MethodGet || string(req.Header.Method()) == MethodHead) { + if err := a.HostClient.DoRedirects(req, resp, a.maxRedirectsCount); err != nil { + errs = append(errs, err) + return + } + } + + if err := a.HostClient.Do(req, resp); err != nil { + errs = append(errs, err) + } + + return +} + +func printDebugInfo(req *Request, resp *Response, w io.Writer) { + msg := fmt.Sprintf("Connected to %s(%s)\r\n\r\n", req.URI().Host(), resp.RemoteAddr()) + _, _ = w.Write(getBytes(msg)) + _, _ = req.WriteTo(w) + _, _ = resp.WriteTo(w) +} + +// String returns the status code, string body and errors of url. +func (a *Agent) String(resp ...*Response) (int, string, []error) { + code, body, errs := a.Bytes(resp...) + + return code, getString(body), errs +} + +// Struct returns the status code, bytes body and errors of url. +// And bytes body will be unmarshalled to given v. +func (a *Agent) Struct(v interface{}, resp ...*Response) (code int, body []byte, errs []error) { + code, body, errs = a.Bytes(resp...) + + if err := json.Unmarshal(body, v); err != nil { + errs = append(errs, err) + } + + return +} + +func (a *Agent) release() { + if !a.reuse { + ReleaseAgent(a) + } else { + a.errs = a.errs[:0] + } +} + +func (a *Agent) reset() { + a.HostClient = nil + a.req.Reset() + a.customReq = nil + a.timeout = 0 + a.args = nil + a.errs = a.errs[:0] + a.debugWriter = nil + a.reuse = false + a.parsed = false + a.maxRedirectsCount = 0 + a.Name = "" + a.NoDefaultUserAgentHeader = false +} + +var ( + clientPool sync.Pool + agentPool sync.Pool + requestPool sync.Pool + responsePool sync.Pool + argsPool sync.Pool +) + +// AcquireAgent returns an empty Agent instance from createAgent pool. +// +// The returned Agent instance may be passed to ReleaseAgent when it is +// no longer needed. This allows Agent recycling, reduces GC pressure +// and usually improves performance. +func AcquireAgent() *Agent { + v := agentPool.Get() + if v == nil { + return &Agent{req: fasthttp.AcquireRequest()} + } + return v.(*Agent) +} + +// ReleaseAgent returns a acquired via AcquireAgent to createAgent pool. +// +// It is forbidden accessing req and/or its' members after returning +// it to createAgent pool. +func ReleaseAgent(a *Agent) { + a.reset() + agentPool.Put(a) +} + +// AcquireClient returns an empty Client instance from client pool. +// +// The returned Client instance may be passed to ReleaseClient when it is +// no longer needed. This allows Client recycling, reduces GC pressure +// and usually improves performance. +func AcquireClient() *Client { + v := clientPool.Get() + if v == nil { + return &Client{} + } + return v.(*Client) +} + +// ReleaseClient returns c acquired via AcquireClient to client pool. +// +// It is forbidden accessing req and/or its' members after returning +// it to client pool. +func ReleaseClient(c *Client) { + c.UserAgent = "" + c.NoDefaultUserAgentHeader = false + + clientPool.Put(c) +} + +// AcquireRequest returns an empty Request instance from request pool. +// +// The returned Request instance may be passed to ReleaseRequest when it is +// no longer needed. This allows Request recycling, reduces GC pressure +// and usually improves performance. +func AcquireRequest() *Request { + v := requestPool.Get() + if v == nil { + return &Request{} + } + return v.(*Request) +} + +// ReleaseRequest returns req acquired via AcquireRequest to request pool. +// +// It is forbidden accessing req and/or its' members after returning +// it to request pool. +func ReleaseRequest(req *Request) { + req.Reset() + requestPool.Put(req) +} + +// AcquireResponse returns an empty Response instance from response pool. +// +// The returned Response instance may be passed to ReleaseResponse when it is +// no longer needed. This allows Response recycling, reduces GC pressure +// and usually improves performance. +// Copy from fasthttp +func AcquireResponse() *Response { + v := responsePool.Get() + if v == nil { + return &Response{} + } + return v.(*Response) +} + +// ReleaseResponse return resp acquired via AcquireResponse to response pool. +// +// It is forbidden accessing resp and/or its' members after returning +// it to response pool. +// Copy from fasthttp +func ReleaseResponse(resp *Response) { + resp.Reset() + responsePool.Put(resp) +} + +// AcquireArgs returns an empty Args object from the pool. +// +// The returned Args may be returned to the pool with ReleaseArgs +// when no longer needed. This allows reducing GC load. +// Copy from fasthttp +func AcquireArgs() *Args { + v := argsPool.Get() + if v == nil { + return &Args{} + } + return v.(*Args) +} + +// ReleaseArgs returns the object acquired via AcquireArgs to the pool. +// +// String not access the released Args object, otherwise data races may occur. +// Copy from fasthttp +func ReleaseArgs(a *Args) { + a.Reset() + argsPool.Put(a) +} + +var ( + strHTTP = []byte("http") + strHTTPS = []byte("https") + defaultUserAgent = "fiber" +) diff --git a/client_test.go b/client_test.go new file mode 100644 index 00000000..2ff99d39 --- /dev/null +++ b/client_test.go @@ -0,0 +1,636 @@ +package fiber + +import ( + "bytes" + "crypto/tls" + "net" + "strings" + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp/fasthttputil" +) + +func Test_Client_Invalid_URL(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString(c.Hostname()) + }) + + go app.Listener(ln) //nolint:errcheck + + a := Get("http://example.com\r\n\r\nGET /\r\n\r\n") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "missing required Host header in request", errs[0].Error()) +} + +func Test_Client_Unsupported_Protocol(t *testing.T) { + t.Parallel() + + a := Get("ftp://example.com") + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, `unsupported protocol "ftp". http and https are supported`, + errs[0].Error()) +} + +func Test_Client_Get(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString(c.Hostname()) + }) + + go app.Listener(ln) //nolint:errcheck + + for i := 0; i < 5; i++ { + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "example.com", body) + utils.AssertEqual(t, 0, len(errs)) + } +} + +func Test_Client_Post(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Post("/", func(c *Ctx) error { + return c.SendString(c.Hostname()) + }) + + go app.Listener(ln) //nolint:errcheck + + for i := 0; i < 5; i++ { + args := AcquireArgs() + + args.Set("foo", "bar") + + a := Post("http://example.com"). + Form(args) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "example.com", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseArgs(args) + } +} + +func Test_Client_UserAgent(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.Send(c.Request().Header.UserAgent()) + }) + + go app.Listener(ln) //nolint:errcheck + + t.Run("default", func(t *testing.T) { + for i := 0; i < 5; i++ { + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, defaultUserAgent, body) + utils.AssertEqual(t, 0, len(errs)) + } + }) + + t.Run("custom", func(t *testing.T) { + for i := 0; i < 5; i++ { + c := AcquireClient() + c.UserAgent = "ua" + + a := c.Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "ua", body) + utils.AssertEqual(t, 0, len(errs)) + ReleaseClient(c) + } + }) +} + +func Test_Client_Agent_Specific_Host(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString(c.Hostname()) + }) + + go app.Listener(ln) //nolint:errcheck + + a := Get("http://1.1.1.1:8080"). + Host("example.com") + + utils.AssertEqual(t, "1.1.1.1:8080", a.HostClient.Addr) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "example.com", body) + utils.AssertEqual(t, 0, len(errs)) +} + +func Test_Client_Agent_Headers(t *testing.T) { + handler := func(c *Ctx) error { + c.Request().Header.VisitAll(func(key, value []byte) { + if k := string(key); k == "K1" || k == "K2" { + _, _ = c.Write(key) + _, _ = c.Write(value) + } + }) + return nil + } + + wrapAgent := func(a *Agent) { + a.Set("k1", "v1"). + Add("k1", "v11"). + Set("k2", "v2") + } + + testAgent(t, handler, wrapAgent, "K1v1K1v11K2v2") +} + +func Test_Client_Agent_UserAgent(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().Header.UserAgent()) + } + + wrapAgent := func(a *Agent) { + a.UserAgent("ua") + } + + testAgent(t, handler, wrapAgent, "ua") +} + +func Test_Client_Agent_Connection_Close(t *testing.T) { + handler := func(c *Ctx) error { + if c.Request().Header.ConnectionClose() { + return c.SendString("close") + } + return c.SendString("not close") + } + + wrapAgent := func(a *Agent) { + a.ConnectionClose() + } + + testAgent(t, handler, wrapAgent, "close") +} + +func Test_Client_Agent_Referer(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().Header.Referer()) + } + + wrapAgent := func(a *Agent) { + a.Referer("http://referer.com") + } + + testAgent(t, handler, wrapAgent, "http://referer.com") +} + +func Test_Client_Agent_QueryString(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().URI().QueryString()) + } + + wrapAgent := func(a *Agent) { + a.QueryString("foo=bar&bar=baz") + } + + testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") +} + +func Test_Client_Agent_Cookie(t *testing.T) { + handler := func(c *Ctx) error { + return c.SendString( + c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3") + c.Cookies("k4")) + } + + wrapAgent := func(a *Agent) { + a.Cookie("k1", "v1"). + Cookie("k2", "v2"). + Cookies("k3", "v3", "k4", "v4") + } + + testAgent(t, handler, wrapAgent, "v1v2v3v4") +} + +func Test_Client_Agent_ContentType(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().Header.ContentType()) + } + + wrapAgent := func(a *Agent) { + a.ContentType("custom-type") + } + + testAgent(t, handler, wrapAgent, "custom-type") +} + +func Test_Client_Agent_BodyStream(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.BodyStream(strings.NewReader("body stream"), -1) + } + + testAgent(t, handler, wrapAgent, "body stream") +} + +func Test_Client_Agent_Form(t *testing.T) { + handler := func(c *Ctx) error { + utils.AssertEqual(t, MIMEApplicationForm, string(c.Request().Header.ContentType())) + + return c.Send(c.Request().Body()) + } + + args := AcquireArgs() + + args.Set("a", "b") + + wrapAgent := func(a *Agent) { + a.Form(args) + } + + testAgent(t, handler, wrapAgent, "a=b") + + ReleaseArgs(args) +} + +type jsonData struct { + F string `json:"f"` +} + +func Test_Client_Agent_Json(t *testing.T) { + handler := func(c *Ctx) error { + utils.AssertEqual(t, MIMEApplicationJSON, string(c.Request().Header.ContentType())) + + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.Json(jsonData{F: "f"}) + } + + testAgent(t, handler, wrapAgent, `{"f":"f"}`) +} + +func Test_Client_Agent_Json_Error(t *testing.T) { + a := Get("http://example.com"). + Json(complex(1, 1)) + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "json: unsupported type: complex128", errs[0].Error()) +} + +func Test_Client_Debug(t *testing.T) { + handler := func(c *Ctx) error { + return c.SendString("debug") + } + + var output bytes.Buffer + + wrapAgent := func(a *Agent) { + a.Debug(&output) + } + + testAgent(t, handler, wrapAgent, "debug", 1) + + str := output.String() + + utils.AssertEqual(t, true, strings.Contains(str, "Connected to example.com(pipe)")) + utils.AssertEqual(t, true, strings.Contains(str, "GET / HTTP/1.1")) + utils.AssertEqual(t, true, strings.Contains(str, "User-Agent: fiber")) + utils.AssertEqual(t, true, strings.Contains(str, "Host: example.com\r\n\r\n")) + utils.AssertEqual(t, true, strings.Contains(str, "HTTP/1.1 200 OK")) + utils.AssertEqual(t, true, strings.Contains(str, "Content-Type: text/plain; charset=utf-8\r\nContent-Length: 5\r\n\r\ndebug")) +} + +func testAgent(t *testing.T, handler Handler, wrapAgent func(agent *Agent), excepted string, count ...int) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", handler) + + go app.Listener(ln) //nolint:errcheck + + c := 1 + if len(count) > 0 { + c = count[0] + } + + for i := 0; i < c; i++ { + a := Get("http://example.com") + + wrapAgent(a) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, excepted, body) + utils.AssertEqual(t, 0, len(errs)) + } +} + +func Test_Client_Agent_Timeout(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + time.Sleep(time.Millisecond * 200) + return c.SendString("timeout") + }) + + go app.Listener(ln) //nolint:errcheck + + a := Get("http://example.com"). + Timeout(time.Millisecond * 100) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "timeout", errs[0].Error()) +} + +func Test_Client_Agent_MaxRedirectsCount(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + if c.Request().URI().QueryArgs().Has("foo") { + return c.Redirect("/foo") + } + return c.Redirect("/") + }) + app.Get("/foo", func(c *Ctx) error { + return c.SendString("redirect") + }) + + go app.Listener(ln) //nolint:errcheck + + t.Run("success", func(t *testing.T) { + a := Get("http://example.com?foo"). + MaxRedirectsCount(1) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, 200, code) + utils.AssertEqual(t, "redirect", body) + utils.AssertEqual(t, 0, len(errs)) + }) + + t.Run("error", func(t *testing.T) { + a := Get("http://example.com"). + MaxRedirectsCount(1) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "too many redirects detected when doing the request", errs[0].Error()) + }) +} + +func Test_Client_Agent_Custom(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("custom") + }) + + go app.Listener(ln) //nolint:errcheck + + for i := 0; i < 5; i++ { + a := AcquireAgent() + req := AcquireRequest() + resp := AcquireResponse() + + req.Header.SetMethod(MethodGet) + req.SetRequestURI("http://example.com") + a.Request(req) + + utils.AssertEqual(t, nil, a.Parse()) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String(resp) + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "custom", body) + utils.AssertEqual(t, "custom", string(resp.Body())) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseRequest(req) + ReleaseResponse(resp) + } +} + +func Test_Client_Agent_Reuse(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("reuse") + }) + + go app.Listener(ln) //nolint:errcheck + + a := Get("http://example.com"). + Reuse() + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "reuse", body) + utils.AssertEqual(t, 0, len(errs)) + + code, body, errs = a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "reuse", body) + utils.AssertEqual(t, 0, len(errs)) +} + +func Test_Client_Agent_Parse(t *testing.T) { + t.Parallel() + + a := Get("https://example.com:10443") + + utils.AssertEqual(t, nil, a.Parse()) +} + +func Test_Client_Agent_TLS(t *testing.T) { + t.Parallel() + + // Create tls certificate + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + utils.AssertEqual(t, nil, err) + + config := &tls.Config{ + Certificates: []tls.Certificate{cer}, + } + + ln, err := net.Listen(NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + + ln = tls.NewListener(ln, config) + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("tls") + }) + + go app.Listener(ln) //nolint:errcheck + + code, body, errs := Get("https://" + ln.Addr().String()). + InsecureSkipVerify(). + TLSConfig(config). + InsecureSkipVerify(). + String() + + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "tls", body) +} + +type data struct { + Success bool `json:"success"` +} + +func Test_Client_Agent_Struct(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.JSON(data{true}) + }) + + app.Get("/error", func(c *Ctx) error { + return c.SendString(`{"success"`) + }) + + go app.Listener(ln) //nolint:errcheck + + t.Run("success", func(t *testing.T) { + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + var d data + + code, body, errs := a.Struct(&d) + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, `{"success":true}`, string(body)) + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, true, d.Success) + }) + + t.Run("error", func(t *testing.T) { + a := Get("http://example.com/error") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + var d data + + code, body, errs := a.Struct(&d) + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, `{"success"`, string(body)) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "json: unexpected end of JSON input after object field key: ", errs[0].Error()) + }) +} + +func Test_AddMissingPort_TLS(t *testing.T) { + addr := addMissingPort("example.com", true) + utils.AssertEqual(t, "example.com:443", addr) +} From e93306ca61226f1df7ac5f364627cf12268b2eb0 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Thu, 18 Feb 2021 17:26:13 +0800 Subject: [PATCH 02/44] =?UTF-8?q?=20=F0=9F=91=B7=20Add=20BodyString?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 7 +++++++ client_test.go | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/client.go b/client.go index 7bacade9..069c220a 100644 --- a/client.go +++ b/client.go @@ -259,6 +259,13 @@ func (a *Agent) QueryString(queryString string) *Agent { return a } +// BodyString sets request body. +func (a *Agent) BodyString(bodyString string) *Agent { + a.req.SetBodyString(bodyString) + + return a +} + // BodyStream sets request body stream and, optionally body size. // // If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes diff --git a/client_test.go b/client_test.go index 2ff99d39..16c0c850 100644 --- a/client_test.go +++ b/client_test.go @@ -252,6 +252,18 @@ func Test_Client_Agent_QueryString(t *testing.T) { testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") } +func Test_Client_Agent_BodyString(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.BodyString("foo=bar&bar=baz") + } + + testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") +} + func Test_Client_Agent_Cookie(t *testing.T) { handler := func(c *Ctx) error { return c.SendString( From 6cc4a5ec047bff647e6560ace04feacb1e0a1dba Mon Sep 17 00:00:00 2001 From: Kiyon Date: Fri, 19 Feb 2021 09:01:54 +0800 Subject: [PATCH 03/44] =?UTF-8?q?=F0=9F=8D=80=20Cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 144 +++++++------- client_test.go | 506 ++++++++++++++++++++++++------------------------- 2 files changed, 333 insertions(+), 317 deletions(-) diff --git a/client.go b/client.go index 069c220a..c8e78d17 100644 --- a/client.go +++ b/client.go @@ -151,6 +151,8 @@ func addMissingPort(addr string, isTLS bool) string { return net.JoinHostPort(addr, strconv.Itoa(port)) } +/************************** Header Setting **************************/ + // Set sets the given 'key: value' header. // // Use Add for setting multiple header values under the same key. @@ -170,13 +172,6 @@ func (a *Agent) Add(k, v string) *Agent { return a } -// Host sets host for the uri. -func (a *Agent) Host(host string) *Agent { - a.req.URI().SetHost(host) - - return a -} - // ConnectionClose sets 'Connection: close' header. func (a *Agent) ConnectionClose() *Agent { a.req.Header.SetConnectionClose() @@ -191,16 +186,6 @@ func (a *Agent) UserAgent(userAgent string) *Agent { return a } -// Debug mode enables logging request and response detail -func (a *Agent) Debug(w ...io.Writer) *Agent { - a.debugWriter = os.Stdout - if len(w) > 0 { - a.debugWriter = w[0] - } - - return a -} - // Cookie sets one 'key: value' cookie. func (a *Agent) Cookie(key, value string) *Agent { a.req.Header.SetCookie(key, value) @@ -217,9 +202,69 @@ func (a *Agent) Cookies(kv ...string) *Agent { return a } -// Timeout sets request timeout duration. -func (a *Agent) Timeout(timeout time.Duration) *Agent { - a.timeout = timeout +// Referer sets Referer header value. +func (a *Agent) Referer(referer string) *Agent { + a.req.Header.SetReferer(referer) + + return a +} + +// ContentType sets Content-Type header value. +func (a *Agent) ContentType(contentType string) *Agent { + a.req.Header.SetContentType(contentType) + + return a +} + +/************************** End Header Setting **************************/ + +/************************** URI Setting **************************/ + +// Host sets host for the uri. +func (a *Agent) Host(host string) *Agent { + a.req.URI().SetHost(host) + + return a +} + +// QueryString sets URI query string. +func (a *Agent) QueryString(queryString string) *Agent { + a.req.URI().SetQueryString(queryString) + + return a +} + +/************************** End URI Setting **************************/ + +/************************** Request Setting **************************/ + +// BodyString sets request body. +func (a *Agent) BodyString(bodyString string) *Agent { + a.req.SetBodyString(bodyString) + + return a +} + +// BodyStream sets request body stream and, optionally body size. +// +// If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes +// before returning io.EOF. +// +// If bodySize < 0, then bodyStream is read until io.EOF. +// +// bodyStream.Close() is called after finishing reading all body data +// if it implements io.Closer. +// +// Note that GET and HEAD requests cannot have body. +func (a *Agent) BodyStream(bodyStream io.Reader, bodySize int) *Agent { + a.req.SetBodyStream(bodyStream, bodySize) + + return a +} + +// Request sets custom request for createAgent. +func (a *Agent) Request(req *Request) *Agent { + a.customReq = req return a } @@ -252,33 +297,23 @@ func (a *Agent) Form(args *Args) *Agent { return a } -// QueryString sets URI query string. -func (a *Agent) QueryString(queryString string) *Agent { - a.req.URI().SetQueryString(queryString) +/************************** End Request Setting **************************/ + +/************************** Agent Setting **************************/ + +// Debug mode enables logging request and response detail +func (a *Agent) Debug(w ...io.Writer) *Agent { + a.debugWriter = os.Stdout + if len(w) > 0 { + a.debugWriter = w[0] + } return a } -// BodyString sets request body. -func (a *Agent) BodyString(bodyString string) *Agent { - a.req.SetBodyString(bodyString) - - return a -} - -// BodyStream sets request body stream and, optionally body size. -// -// If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes -// before returning io.EOF. -// -// If bodySize < 0, then bodyStream is read until io.EOF. -// -// bodyStream.Close() is called after finishing reading all body data -// if it implements io.Closer. -// -// Note that GET and HEAD requests cannot have body. -func (a *Agent) BodyStream(bodyStream io.Reader, bodySize int) *Agent { - a.req.SetBodyStream(bodyStream, bodySize) +// Timeout sets request timeout duration. +func (a *Agent) Timeout(timeout time.Duration) *Agent { + a.timeout = timeout return a } @@ -309,27 +344,6 @@ func (a *Agent) TLSConfig(config *tls.Config) *Agent { return a } -// Request sets custom request for createAgent. -func (a *Agent) Request(req *Request) *Agent { - a.customReq = req - - return a -} - -// Referer sets Referer header value. -func (a *Agent) Referer(referer string) *Agent { - a.req.Header.SetReferer(referer) - - return a -} - -// ContentType sets Content-Type header value. -func (a *Agent) ContentType(contentType string) *Agent { - a.req.Header.SetContentType(contentType) - - return a -} - // MaxRedirectsCount sets max redirect count for GET and HEAD. func (a *Agent) MaxRedirectsCount(count int) *Agent { a.maxRedirectsCount = count @@ -337,6 +351,8 @@ func (a *Agent) MaxRedirectsCount(count int) *Agent { return a } +/************************** End Agent Setting **************************/ + // Bytes returns the status code, bytes body and errors of url. func (a *Agent) Bytes(customResp ...*Response) (code int, body []byte, errs []error) { defer a.release() diff --git a/client_test.go b/client_test.go index 16c0c850..1e952bcf 100644 --- a/client_test.go +++ b/client_test.go @@ -154,6 +154,92 @@ func Test_Client_UserAgent(t *testing.T) { }) } +func Test_Client_Agent_Headers(t *testing.T) { + handler := func(c *Ctx) error { + c.Request().Header.VisitAll(func(key, value []byte) { + if k := string(key); k == "K1" || k == "K2" { + _, _ = c.Write(key) + _, _ = c.Write(value) + } + }) + return nil + } + + wrapAgent := func(a *Agent) { + a.Set("k1", "v1"). + Add("k1", "v11"). + Set("k2", "v2") + } + + testAgent(t, handler, wrapAgent, "K1v1K1v11K2v2") +} + +func Test_Client_Agent_Connection_Close(t *testing.T) { + handler := func(c *Ctx) error { + if c.Request().Header.ConnectionClose() { + return c.SendString("close") + } + return c.SendString("not close") + } + + wrapAgent := func(a *Agent) { + a.ConnectionClose() + } + + testAgent(t, handler, wrapAgent, "close") +} + +func Test_Client_Agent_UserAgent(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().Header.UserAgent()) + } + + wrapAgent := func(a *Agent) { + a.UserAgent("ua") + } + + testAgent(t, handler, wrapAgent, "ua") +} + +func Test_Client_Agent_Cookie(t *testing.T) { + handler := func(c *Ctx) error { + return c.SendString( + c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3") + c.Cookies("k4")) + } + + wrapAgent := func(a *Agent) { + a.Cookie("k1", "v1"). + Cookie("k2", "v2"). + Cookies("k3", "v3", "k4", "v4") + } + + testAgent(t, handler, wrapAgent, "v1v2v3v4") +} + +func Test_Client_Agent_Referer(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().Header.Referer()) + } + + wrapAgent := func(a *Agent) { + a.Referer("http://referer.com") + } + + testAgent(t, handler, wrapAgent, "http://referer.com") +} + +func Test_Client_Agent_ContentType(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().Header.ContentType()) + } + + wrapAgent := func(a *Agent) { + a.ContentType("custom-type") + } + + testAgent(t, handler, wrapAgent, "custom-type") +} + func Test_Client_Agent_Specific_Host(t *testing.T) { t.Parallel() @@ -181,65 +267,6 @@ func Test_Client_Agent_Specific_Host(t *testing.T) { utils.AssertEqual(t, 0, len(errs)) } -func Test_Client_Agent_Headers(t *testing.T) { - handler := func(c *Ctx) error { - c.Request().Header.VisitAll(func(key, value []byte) { - if k := string(key); k == "K1" || k == "K2" { - _, _ = c.Write(key) - _, _ = c.Write(value) - } - }) - return nil - } - - wrapAgent := func(a *Agent) { - a.Set("k1", "v1"). - Add("k1", "v11"). - Set("k2", "v2") - } - - testAgent(t, handler, wrapAgent, "K1v1K1v11K2v2") -} - -func Test_Client_Agent_UserAgent(t *testing.T) { - handler := func(c *Ctx) error { - return c.Send(c.Request().Header.UserAgent()) - } - - wrapAgent := func(a *Agent) { - a.UserAgent("ua") - } - - testAgent(t, handler, wrapAgent, "ua") -} - -func Test_Client_Agent_Connection_Close(t *testing.T) { - handler := func(c *Ctx) error { - if c.Request().Header.ConnectionClose() { - return c.SendString("close") - } - return c.SendString("not close") - } - - wrapAgent := func(a *Agent) { - a.ConnectionClose() - } - - testAgent(t, handler, wrapAgent, "close") -} - -func Test_Client_Agent_Referer(t *testing.T) { - handler := func(c *Ctx) error { - return c.Send(c.Request().Header.Referer()) - } - - wrapAgent := func(a *Agent) { - a.Referer("http://referer.com") - } - - testAgent(t, handler, wrapAgent, "http://referer.com") -} - func Test_Client_Agent_QueryString(t *testing.T) { handler := func(c *Ctx) error { return c.Send(c.Request().URI().QueryString()) @@ -264,33 +291,6 @@ func Test_Client_Agent_BodyString(t *testing.T) { testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") } -func Test_Client_Agent_Cookie(t *testing.T) { - handler := func(c *Ctx) error { - return c.SendString( - c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3") + c.Cookies("k4")) - } - - wrapAgent := func(a *Agent) { - a.Cookie("k1", "v1"). - Cookie("k2", "v2"). - Cookies("k3", "v3", "k4", "v4") - } - - testAgent(t, handler, wrapAgent, "v1v2v3v4") -} - -func Test_Client_Agent_ContentType(t *testing.T) { - handler := func(c *Ctx) error { - return c.Send(c.Request().Header.ContentType()) - } - - wrapAgent := func(a *Agent) { - a.ContentType("custom-type") - } - - testAgent(t, handler, wrapAgent, "custom-type") -} - func Test_Client_Agent_BodyStream(t *testing.T) { handler := func(c *Ctx) error { return c.Send(c.Request().Body()) @@ -303,28 +303,42 @@ func Test_Client_Agent_BodyStream(t *testing.T) { testAgent(t, handler, wrapAgent, "body stream") } -func Test_Client_Agent_Form(t *testing.T) { - handler := func(c *Ctx) error { - utils.AssertEqual(t, MIMEApplicationForm, string(c.Request().Header.ContentType())) +func Test_Client_Agent_Custom_Request_And_Response(t *testing.T) { + t.Parallel() - return c.Send(c.Request().Body()) + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("custom") + }) + + go app.Listener(ln) //nolint:errcheck + + for i := 0; i < 5; i++ { + a := AcquireAgent() + req := AcquireRequest() + resp := AcquireResponse() + + req.Header.SetMethod(MethodGet) + req.SetRequestURI("http://example.com") + a.Request(req) + + utils.AssertEqual(t, nil, a.Parse()) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String(resp) + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "custom", body) + utils.AssertEqual(t, "custom", string(resp.Body())) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseRequest(req) + ReleaseResponse(resp) } - - args := AcquireArgs() - - args.Set("a", "b") - - wrapAgent := func(a *Agent) { - a.Form(args) - } - - testAgent(t, handler, wrapAgent, "a=b") - - ReleaseArgs(args) -} - -type jsonData struct { - F string `json:"f"` } func Test_Client_Agent_Json(t *testing.T) { @@ -352,6 +366,30 @@ func Test_Client_Agent_Json_Error(t *testing.T) { utils.AssertEqual(t, "json: unsupported type: complex128", errs[0].Error()) } +func Test_Client_Agent_Form(t *testing.T) { + handler := func(c *Ctx) error { + utils.AssertEqual(t, MIMEApplicationForm, string(c.Request().Header.ContentType())) + + return c.Send(c.Request().Body()) + } + + args := AcquireArgs() + + args.Set("a", "b") + + wrapAgent := func(a *Agent) { + a.Form(args) + } + + testAgent(t, handler, wrapAgent, "a=b") + + ReleaseArgs(args) +} + +type jsonData struct { + F string `json:"f"` +} + func Test_Client_Debug(t *testing.T) { handler := func(c *Ctx) error { return c.SendString("debug") @@ -375,37 +413,6 @@ func Test_Client_Debug(t *testing.T) { utils.AssertEqual(t, true, strings.Contains(str, "Content-Type: text/plain; charset=utf-8\r\nContent-Length: 5\r\n\r\ndebug")) } -func testAgent(t *testing.T, handler Handler, wrapAgent func(agent *Agent), excepted string, count ...int) { - t.Parallel() - - ln := fasthttputil.NewInmemoryListener() - - app := New(Config{DisableStartupMessage: true}) - - app.Get("/", handler) - - go app.Listener(ln) //nolint:errcheck - - c := 1 - if len(count) > 0 { - c = count[0] - } - - for i := 0; i < c; i++ { - a := Get("http://example.com") - - wrapAgent(a) - - a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } - - code, body, errs := a.String() - - utils.AssertEqual(t, StatusOK, code) - utils.AssertEqual(t, excepted, body) - utils.AssertEqual(t, 0, len(errs)) - } -} - func Test_Client_Agent_Timeout(t *testing.T) { t.Parallel() @@ -432,6 +439,76 @@ func Test_Client_Agent_Timeout(t *testing.T) { utils.AssertEqual(t, "timeout", errs[0].Error()) } +func Test_Client_Agent_Reuse(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("reuse") + }) + + go app.Listener(ln) //nolint:errcheck + + a := Get("http://example.com"). + Reuse() + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "reuse", body) + utils.AssertEqual(t, 0, len(errs)) + + code, body, errs = a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "reuse", body) + utils.AssertEqual(t, 0, len(errs)) +} + +func Test_Client_Agent_TLS(t *testing.T) { + t.Parallel() + + // Create tls certificate + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + utils.AssertEqual(t, nil, err) + + config := &tls.Config{ + Certificates: []tls.Certificate{cer}, + } + + ln, err := net.Listen(NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + + ln = tls.NewListener(ln, config) + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("tls") + }) + + go app.Listener(ln) //nolint:errcheck + + code, body, errs := Get("https://" + ln.Addr().String()). + InsecureSkipVerify(). + TLSConfig(config). + InsecureSkipVerify(). + String() + + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "tls", body) +} + +type data struct { + Success bool `json:"success"` +} + func Test_Client_Agent_MaxRedirectsCount(t *testing.T) { t.Parallel() @@ -478,122 +555,6 @@ func Test_Client_Agent_MaxRedirectsCount(t *testing.T) { }) } -func Test_Client_Agent_Custom(t *testing.T) { - t.Parallel() - - ln := fasthttputil.NewInmemoryListener() - - app := New(Config{DisableStartupMessage: true}) - - app.Get("/", func(c *Ctx) error { - return c.SendString("custom") - }) - - go app.Listener(ln) //nolint:errcheck - - for i := 0; i < 5; i++ { - a := AcquireAgent() - req := AcquireRequest() - resp := AcquireResponse() - - req.Header.SetMethod(MethodGet) - req.SetRequestURI("http://example.com") - a.Request(req) - - utils.AssertEqual(t, nil, a.Parse()) - - a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } - - code, body, errs := a.String(resp) - - utils.AssertEqual(t, StatusOK, code) - utils.AssertEqual(t, "custom", body) - utils.AssertEqual(t, "custom", string(resp.Body())) - utils.AssertEqual(t, 0, len(errs)) - - ReleaseRequest(req) - ReleaseResponse(resp) - } -} - -func Test_Client_Agent_Reuse(t *testing.T) { - t.Parallel() - - ln := fasthttputil.NewInmemoryListener() - - app := New(Config{DisableStartupMessage: true}) - - app.Get("/", func(c *Ctx) error { - return c.SendString("reuse") - }) - - go app.Listener(ln) //nolint:errcheck - - a := Get("http://example.com"). - Reuse() - - a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } - - code, body, errs := a.String() - - utils.AssertEqual(t, StatusOK, code) - utils.AssertEqual(t, "reuse", body) - utils.AssertEqual(t, 0, len(errs)) - - code, body, errs = a.String() - - utils.AssertEqual(t, StatusOK, code) - utils.AssertEqual(t, "reuse", body) - utils.AssertEqual(t, 0, len(errs)) -} - -func Test_Client_Agent_Parse(t *testing.T) { - t.Parallel() - - a := Get("https://example.com:10443") - - utils.AssertEqual(t, nil, a.Parse()) -} - -func Test_Client_Agent_TLS(t *testing.T) { - t.Parallel() - - // Create tls certificate - cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") - utils.AssertEqual(t, nil, err) - - config := &tls.Config{ - Certificates: []tls.Certificate{cer}, - } - - ln, err := net.Listen(NetworkTCP4, "127.0.0.1:0") - utils.AssertEqual(t, nil, err) - - ln = tls.NewListener(ln, config) - - app := New(Config{DisableStartupMessage: true}) - - app.Get("/", func(c *Ctx) error { - return c.SendString("tls") - }) - - go app.Listener(ln) //nolint:errcheck - - code, body, errs := Get("https://" + ln.Addr().String()). - InsecureSkipVerify(). - TLSConfig(config). - InsecureSkipVerify(). - String() - - utils.AssertEqual(t, 0, len(errs)) - utils.AssertEqual(t, StatusOK, code) - utils.AssertEqual(t, "tls", body) -} - -type data struct { - Success bool `json:"success"` -} - func Test_Client_Agent_Struct(t *testing.T) { t.Parallel() @@ -642,7 +603,46 @@ func Test_Client_Agent_Struct(t *testing.T) { }) } +func Test_Client_Agent_Parse(t *testing.T) { + t.Parallel() + + a := Get("https://example.com:10443") + + utils.AssertEqual(t, nil, a.Parse()) +} + func Test_AddMissingPort_TLS(t *testing.T) { addr := addMissingPort("example.com", true) utils.AssertEqual(t, "example.com:443", addr) } + +func testAgent(t *testing.T, handler Handler, wrapAgent func(agent *Agent), excepted string, count ...int) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", handler) + + go app.Listener(ln) //nolint:errcheck + + c := 1 + if len(count) > 0 { + c = count[0] + } + + for i := 0; i < c; i++ { + a := Get("http://example.com") + + wrapAgent(a) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, excepted, body) + utils.AssertEqual(t, 0, len(errs)) + } +} From 20104ba10f65fbb9e4bc62bad617264c0cce1d3c Mon Sep 17 00:00:00 2001 From: Kiyon Date: Fri, 19 Feb 2021 09:53:51 +0800 Subject: [PATCH 04/44] =?UTF-8?q?=F0=9F=91=B7=20Add=20BasicAuth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 12 +++++++++--- client_test.go | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index c8e78d17..d6b7668b 100644 --- a/client.go +++ b/client.go @@ -234,6 +234,14 @@ func (a *Agent) QueryString(queryString string) *Agent { return a } +// BasicAuth sets URI username and password. +func (a *Agent) BasicAuth(username, password string) *Agent { + a.req.URI().SetUsername(username) + a.req.URI().SetPassword(password) + + return a +} + /************************** End URI Setting **************************/ /************************** Request Setting **************************/ @@ -289,9 +297,7 @@ func (a *Agent) Form(args *Args) *Agent { a.req.Header.SetContentType(MIMEApplicationForm) if args != nil { - if _, err := args.WriteTo(a.req.BodyWriter()); err != nil { - a.errs = append(a.errs, err) - } + a.req.SetBody(args.QueryString()) } return a diff --git a/client_test.go b/client_test.go index 1e952bcf..0f3e2a48 100644 --- a/client_test.go +++ b/client_test.go @@ -3,6 +3,7 @@ package fiber import ( "bytes" "crypto/tls" + "encoding/base64" "net" "strings" "testing" @@ -279,6 +280,24 @@ func Test_Client_Agent_QueryString(t *testing.T) { testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") } +func Test_Client_Agent_BasicAuth(t *testing.T) { + handler := func(c *Ctx) error { + // Get authorization header + auth := c.Get(HeaderAuthorization) + // Decode the header contents + raw, err := base64.StdEncoding.DecodeString(auth[6:]) + utils.AssertEqual(t, nil, err) + + return c.Send(raw) + } + + wrapAgent := func(a *Agent) { + a.BasicAuth("foo", "bar") + } + + testAgent(t, handler, wrapAgent, "foo:bar") +} + func Test_Client_Agent_BodyString(t *testing.T) { handler := func(c *Ctx) error { return c.Send(c.Request().Body()) From 5af0d42e7bfad73019cbf2a327166c28645ad34b Mon Sep 17 00:00:00 2001 From: Kiyon Date: Fri, 19 Feb 2021 10:03:51 +0800 Subject: [PATCH 05/44] =?UTF-8?q?=F0=9F=91=B7=20Add=20XML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 18 ++++++++++++++++-- client_test.go | 43 ++++++++++++++++++++++++++++++++----------- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/client.go b/client.go index d6b7668b..07b139bf 100644 --- a/client.go +++ b/client.go @@ -3,6 +3,7 @@ package fiber import ( "bytes" "crypto/tls" + "encoding/xml" "fmt" "io" "net" @@ -277,8 +278,8 @@ func (a *Agent) Request(req *Request) *Agent { return a } -// Json sends a json request. -func (a *Agent) Json(v interface{}) *Agent { +// JSON sends a JSON request. +func (a *Agent) JSON(v interface{}) *Agent { a.req.Header.SetContentType(MIMEApplicationJSON) if body, err := json.Marshal(v); err != nil { @@ -290,6 +291,19 @@ func (a *Agent) Json(v interface{}) *Agent { return a } +// XML sends a XML request. +func (a *Agent) XML(v interface{}) *Agent { + a.req.Header.SetContentType(MIMEApplicationXML) + + if body, err := xml.Marshal(v); err != nil { + a.errs = append(a.errs, err) + } else { + a.req.SetBody(body) + } + + return a +} + // Form sends request with body if args is non-nil. // // Note that this will force http method to post. diff --git a/client_test.go b/client_test.go index 0f3e2a48..55496b0d 100644 --- a/client_test.go +++ b/client_test.go @@ -368,15 +368,15 @@ func Test_Client_Agent_Json(t *testing.T) { } wrapAgent := func(a *Agent) { - a.Json(jsonData{F: "f"}) + a.JSON(data{Success: true}) } - testAgent(t, handler, wrapAgent, `{"f":"f"}`) + testAgent(t, handler, wrapAgent, `{"success":true}`) } func Test_Client_Agent_Json_Error(t *testing.T) { a := Get("http://example.com"). - Json(complex(1, 1)) + JSON(complex(1, 1)) _, body, errs := a.String() @@ -385,6 +385,31 @@ func Test_Client_Agent_Json_Error(t *testing.T) { utils.AssertEqual(t, "json: unsupported type: complex128", errs[0].Error()) } +func Test_Client_Agent_XML(t *testing.T) { + handler := func(c *Ctx) error { + utils.AssertEqual(t, MIMEApplicationXML, string(c.Request().Header.ContentType())) + + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.XML(data{Success: true}) + } + + testAgent(t, handler, wrapAgent, "true") +} + +func Test_Client_Agent_XML_Error(t *testing.T) { + a := Get("http://example.com"). + XML(complex(1, 1)) + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "xml: unsupported type: complex128", errs[0].Error()) +} + func Test_Client_Agent_Form(t *testing.T) { handler := func(c *Ctx) error { utils.AssertEqual(t, MIMEApplicationForm, string(c.Request().Header.ContentType())) @@ -405,10 +430,6 @@ func Test_Client_Agent_Form(t *testing.T) { ReleaseArgs(args) } -type jsonData struct { - F string `json:"f"` -} - func Test_Client_Debug(t *testing.T) { handler := func(c *Ctx) error { return c.SendString("debug") @@ -524,10 +545,6 @@ func Test_Client_Agent_TLS(t *testing.T) { utils.AssertEqual(t, "tls", body) } -type data struct { - Success bool `json:"success"` -} - func Test_Client_Agent_MaxRedirectsCount(t *testing.T) { t.Parallel() @@ -665,3 +682,7 @@ func testAgent(t *testing.T, handler Handler, wrapAgent func(agent *Agent), exce utils.AssertEqual(t, 0, len(errs)) } } + +type data struct { + Success bool `json:"success" xml:"success"` +} From f4307905a4276a913ffe49d187ba88f171d7ad00 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Fri, 19 Feb 2021 17:21:59 +0800 Subject: [PATCH 06/44] =?UTF-8?q?=F0=9F=91=B7=20Add=20MultipartForm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 153 ++++++++++++++++++++++++++++++++++++++++++++++++- client_test.go | 148 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 298 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 07b139bf..40493bc7 100644 --- a/client.go +++ b/client.go @@ -6,8 +6,11 @@ import ( "encoding/xml" "fmt" "io" + "io/ioutil" + "mime/multipart" "net" "os" + "path/filepath" "strconv" "strings" "sync" @@ -90,8 +93,10 @@ type Agent struct { args *Args timeout time.Duration errs []error + formFiles []*FormFile debugWriter io.Writer maxRedirectsCount int + boundary string Name string NoDefaultUserAgentHeader bool reuse bool @@ -306,7 +311,8 @@ func (a *Agent) XML(v interface{}) *Agent { // Form sends request with body if args is non-nil. // -// Note that this will force http method to post. +// It is recommended obtaining args via AcquireArgs +// in performance-critical code. func (a *Agent) Form(args *Args) *Agent { a.req.Header.SetContentType(MIMEApplicationForm) @@ -317,6 +323,118 @@ func (a *Agent) Form(args *Args) *Agent { return a } +// FormFile represents multipart form file +type FormFile struct { + // Fieldname is form file's field name + Fieldname string + // Name is form file's name + Name string + // Content is form file's content + Content []byte + // autoRelease indicates if returns the object + // acquired via AcquireFormFile to the pool. + autoRelease bool +} + +// FileData appends files for multipart form request. +// +// It is recommended obtaining formFile via AcquireFormFile +// in performance-critical code. +func (a *Agent) FileData(formFiles ...*FormFile) *Agent { + a.formFiles = append(a.formFiles, formFiles...) + + return a +} + +// SendFile reads file and appends it to multipart form request. +func (a *Agent) SendFile(filename string, fieldname ...string) *Agent { + content, err := ioutil.ReadFile(filename) + if err != nil { + a.errs = append(a.errs, err) + return a + } + + ff := AcquireFormFile() + if len(fieldname) > 0 && fieldname[0] != "" { + ff.Fieldname = fieldname[0] + } else { + ff.Fieldname = "file" + strconv.Itoa(len(a.formFiles)+1) + } + ff.Name = filepath.Base(filename) + ff.Content = append(ff.Content, content...) + ff.autoRelease = true + + a.formFiles = append(a.formFiles, ff) + + return a +} + +// SendFiles reads files and appends them to multipart form request. +// +// Examples: +// SendFile("/path/to/file1", "fieldname1", "/path/to/file2") +func (a *Agent) SendFiles(filenamesAndFieldnames ...string) *Agent { + pairs := len(filenamesAndFieldnames) + if pairs&1 == 1 { + filenamesAndFieldnames = append(filenamesAndFieldnames, "") + } + + for i := 0; i < pairs; i += 2 { + a.SendFile(filenamesAndFieldnames[i], filenamesAndFieldnames[i+1]) + } + + return a +} + +// Boundary sets boundary for multipart form request. +func (a *Agent) Boundary(boundary string) *Agent { + a.boundary = boundary + + return a +} + +// MultipartForm sends multipart form request with k-v and files. +// +// It is recommended obtaining args via AcquireArgs +// in performance-critical code. +func (a *Agent) MultipartForm(args *Args) *Agent { + mw := multipart.NewWriter(a.req.BodyWriter()) + + if a.boundary != "" { + if err := mw.SetBoundary(a.boundary); err != nil { + a.errs = append(a.errs, err) + return a + } + } + + a.req.Header.SetMultipartFormBoundary(mw.Boundary()) + + if args != nil { + args.VisitAll(func(key, value []byte) { + if err := mw.WriteField(getString(key), getString(value)); err != nil { + a.errs = append(a.errs, err) + } + }) + } + + for _, ff := range a.formFiles { + w, err := mw.CreateFormFile(ff.Fieldname, ff.Name) + if err != nil { + a.errs = append(a.errs, err) + continue + } + if _, err = w.Write(ff.Content); err != nil { + a.errs = append(a.errs, err) + } + } + + if err := mw.Close(); err != nil { + a.errs = append(a.errs, err) + } + + return a +} + /************************** End Request Setting **************************/ /************************** Agent Setting **************************/ @@ -479,8 +597,16 @@ func (a *Agent) reset() { a.reuse = false a.parsed = false a.maxRedirectsCount = 0 + a.boundary = "" a.Name = "" a.NoDefaultUserAgentHeader = false + for i, ff := range a.formFiles { + if ff.autoRelease { + ReleaseFormFile(ff) + } + a.formFiles[i] = nil + } + a.formFiles = a.formFiles[:0] } var ( @@ -489,6 +615,7 @@ var ( requestPool sync.Pool responsePool sync.Pool argsPool sync.Pool + formFilePool sync.Pool ) // AcquireAgent returns an empty Agent instance from createAgent pool. @@ -605,6 +732,30 @@ func ReleaseArgs(a *Args) { argsPool.Put(a) } +// AcquireFormFile returns an empty FormFile object from the pool. +// +// The returned FormFile may be returned to the pool with ReleaseFormFile +// when no longer needed. This allows reducing GC load. +func AcquireFormFile() *FormFile { + v := formFilePool.Get() + if v == nil { + return &FormFile{} + } + return v.(*FormFile) +} + +// ReleaseFormFile returns the object acquired via AcquireFormFile to the pool. +// +// String not access the released FormFile object, otherwise data races may occur. +func ReleaseFormFile(ff *FormFile) { + ff.Fieldname = "" + ff.Name = "" + ff.Content = ff.Content[:0] + ff.autoRelease = false + + formFilePool.Put(ff) +} + var ( strHTTP = []byte("http") strHTTPS = []byte("https") diff --git a/client_test.go b/client_test.go index 55496b0d..64bb0a3f 100644 --- a/client_test.go +++ b/client_test.go @@ -4,7 +4,11 @@ import ( "bytes" "crypto/tls" "encoding/base64" + "io/ioutil" + "mime/multipart" "net" + "path/filepath" + "regexp" "strings" "testing" "time" @@ -419,17 +423,157 @@ func Test_Client_Agent_Form(t *testing.T) { args := AcquireArgs() - args.Set("a", "b") + args.Set("foo", "bar") wrapAgent := func(a *Agent) { a.Form(args) } - testAgent(t, handler, wrapAgent, "a=b") + testAgent(t, handler, wrapAgent, "foo=bar") ReleaseArgs(args) } +func Test_Client_Agent_Multipart(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Post("/", func(c *Ctx) error { + utils.AssertEqual(t, "multipart/form-data; boundary=myBoundary", c.Get(HeaderContentType)) + + mf, err := c.MultipartForm() + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "bar", mf.Value["foo"][0]) + + return c.Send(c.Request().Body()) + }) + + go app.Listener(ln) //nolint:errcheck + + args := AcquireArgs() + + args.Set("foo", "bar") + + a := Post("http://example.com"). + Boundary("myBoundary"). + MultipartForm(args) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "--myBoundary\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n--myBoundary--\r\n", body) + utils.AssertEqual(t, 0, len(errs)) + ReleaseArgs(args) +} + +func Test_Client_Agent_Multipart_SendFiles(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Post("/", func(c *Ctx) error { + utils.AssertEqual(t, "multipart/form-data; boundary=myBoundary", c.Get(HeaderContentType)) + + mf, err := c.MultipartForm() + utils.AssertEqual(t, nil, err) + + fh1 := mf.File["field1"][0] + utils.AssertEqual(t, fh1.Filename, "name") + buf := make([]byte, fh1.Size, fh1.Size) + f, err := fh1.Open() + utils.AssertEqual(t, nil, err) + defer func() { _ = f.Close() }() + _, err = f.Read(buf) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "form file", string(buf)) + + checkFormFile(t, mf.File["index"][0], ".github/testdata/index.html") + checkFormFile(t, mf.File["file3"][0], ".github/testdata/index.tmpl") + + return c.SendString("multipart form files") + }) + + go app.Listener(ln) //nolint:errcheck + + for i := 0; i < 5; i++ { + ff := AcquireFormFile() + ff.Fieldname = "field1" + ff.Name = "name" + ff.Content = []byte("form file") + + a := Post("http://example.com"). + Boundary("myBoundary"). + FileData(ff). + SendFiles(".github/testdata/index.html", "index", ".github/testdata/index.tmpl"). + MultipartForm(nil) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "multipart form files", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseFormFile(ff) + } +} + +func checkFormFile(t *testing.T, fh *multipart.FileHeader, filename string) { + basename := filepath.Base(filename) + utils.AssertEqual(t, fh.Filename, basename) + + b1, err := ioutil.ReadFile(filename) + utils.AssertEqual(t, nil, err) + + b2 := make([]byte, fh.Size, fh.Size) + f, err := fh.Open() + utils.AssertEqual(t, nil, err) + defer func() { _ = f.Close() }() + _, err = f.Read(b2) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, b1, b2) +} + +func Test_Client_Agent_Multipart_Random_Boundary(t *testing.T) { + t.Parallel() + + a := Post("http://example.com"). + MultipartForm(nil) + + reg := regexp.MustCompile(`multipart/form-data; boundary=\w{30}`) + + utils.AssertEqual(t, true, reg.Match(a.req.Header.Peek(HeaderContentType))) +} + +func Test_Client_Agent_Multipart_Invalid_Boundary(t *testing.T) { + t.Parallel() + + a := Post("http://example.com"). + Boundary("*"). + MultipartForm(nil) + + utils.AssertEqual(t, 1, len(a.errs)) + utils.AssertEqual(t, "mime: invalid boundary character", a.errs[0].Error()) +} + +func Test_Client_Agent_SendFile_Error(t *testing.T) { + t.Parallel() + + a := Post("http://example.com"). + SendFile("", "") + + utils.AssertEqual(t, 1, len(a.errs)) + utils.AssertEqual(t, "open : no such file or directory", a.errs[0].Error()) +} + func Test_Client_Debug(t *testing.T) { handler := func(c *Ctx) error { return c.SendString("debug") From 5093eb8e276fe11187ae7b1a2d7cded402406e3c Mon Sep 17 00:00:00 2001 From: Kiyon Date: Sat, 20 Feb 2021 14:31:17 +0800 Subject: [PATCH 07/44] =?UTF-8?q?=F0=9F=91=B7=20Fix=20G304=20and=20allow?= =?UTF-8?q?=20G402?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index 40493bc7..70556b9c 100644 --- a/client.go +++ b/client.go @@ -348,7 +348,7 @@ func (a *Agent) FileData(formFiles ...*FormFile) *Agent { // SendFile reads file and appends it to multipart form request. func (a *Agent) SendFile(filename string, fieldname ...string) *Agent { - content, err := ioutil.ReadFile(filename) + content, err := ioutil.ReadFile(filepath.Clean(filename)) if err != nil { a.errs = append(a.errs, err) return a @@ -467,8 +467,10 @@ func (a *Agent) Reuse() *Agent { // certificate chain and host name. func (a *Agent) InsecureSkipVerify() *Agent { if a.HostClient.TLSConfig == nil { + /* #nosec G402 */ a.HostClient.TLSConfig = &tls.Config{InsecureSkipVerify: true} } else { + /* #nosec G402 */ a.HostClient.TLSConfig.InsecureSkipVerify = true } From 7c5fac67cf796dfb6fef25ea13f50d4a6e92ab8c Mon Sep 17 00:00:00 2001 From: Kiyon Date: Sat, 20 Feb 2021 14:31:48 +0800 Subject: [PATCH 08/44] =?UTF-8?q?=F0=9F=91=B7=20Improve=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 55 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/client.go b/client.go index 70556b9c..c92660c1 100644 --- a/client.go +++ b/client.go @@ -26,6 +26,7 @@ import ( // and use CopyTo instead. // // Request instance MUST NOT be used from concurrently running goroutines. +// Copy from fasthttp type Request = fasthttp.Request // Response represents HTTP response. @@ -34,6 +35,7 @@ type Request = fasthttp.Request // and use CopyTo instead. // // Response instance MUST NOT be used from concurrently running goroutines. +// Copy from fasthttp type Response = fasthttp.Response // Args represents query arguments. @@ -42,6 +44,7 @@ type Response = fasthttp.Response // and use CopyTo(). // // Args instance MUST NOT be used from concurrently running goroutines. +// Copy from fasthttp type Args = fasthttp.Args var defaultClient Client @@ -50,7 +53,11 @@ var defaultClient Client // // It is safe calling Client methods from concurrently running goroutines. type Client struct { - UserAgent string + // UserAgent is used in User-Agent request header. + UserAgent string + + // NoDefaultUserAgentHeader when set to true, causes the default + // User-Agent header to be excluded from the Request. NoDefaultUserAgentHeader bool } @@ -620,28 +627,6 @@ var ( formFilePool sync.Pool ) -// AcquireAgent returns an empty Agent instance from createAgent pool. -// -// The returned Agent instance may be passed to ReleaseAgent when it is -// no longer needed. This allows Agent recycling, reduces GC pressure -// and usually improves performance. -func AcquireAgent() *Agent { - v := agentPool.Get() - if v == nil { - return &Agent{req: fasthttp.AcquireRequest()} - } - return v.(*Agent) -} - -// ReleaseAgent returns a acquired via AcquireAgent to createAgent pool. -// -// It is forbidden accessing req and/or its' members after returning -// it to createAgent pool. -func ReleaseAgent(a *Agent) { - a.reset() - agentPool.Put(a) -} - // AcquireClient returns an empty Client instance from client pool. // // The returned Client instance may be passed to ReleaseClient when it is @@ -666,11 +651,34 @@ func ReleaseClient(c *Client) { clientPool.Put(c) } +// AcquireAgent returns an empty Agent instance from createAgent pool. +// +// The returned Agent instance may be passed to ReleaseAgent when it is +// no longer needed. This allows Agent recycling, reduces GC pressure +// and usually improves performance. +func AcquireAgent() *Agent { + v := agentPool.Get() + if v == nil { + return &Agent{req: fasthttp.AcquireRequest()} + } + return v.(*Agent) +} + +// ReleaseAgent returns a acquired via AcquireAgent to createAgent pool. +// +// It is forbidden accessing req and/or its' members after returning +// it to createAgent pool. +func ReleaseAgent(a *Agent) { + a.reset() + agentPool.Put(a) +} + // AcquireRequest returns an empty Request instance from request pool. // // The returned Request instance may be passed to ReleaseRequest when it is // no longer needed. This allows Request recycling, reduces GC pressure // and usually improves performance. +// Copy from fasthttp func AcquireRequest() *Request { v := requestPool.Get() if v == nil { @@ -683,6 +691,7 @@ func AcquireRequest() *Request { // // It is forbidden accessing req and/or its' members after returning // it to request pool. +// Copy from fasthttp func ReleaseRequest(req *Request) { req.Reset() requestPool.Put(req) From 645e01181394dea5afb1ded476497d9443bf1175 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Sat, 20 Feb 2021 14:50:20 +0800 Subject: [PATCH 09/44] =?UTF-8?q?=F0=9F=91=B7=20Add=20HEAD=20PUT=20PATCH?= =?UTF-8?q?=20DELETE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 32 +++++++++++++ client_test.go | 123 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/client.go b/client.go index c92660c1..227f43a4 100644 --- a/client.go +++ b/client.go @@ -69,6 +69,14 @@ func (c *Client) Get(url string) *Agent { return c.createAgent(MethodGet, url) } +// Head returns a agent with http method HEAD. +func Head(url string) *Agent { return defaultClient.Head(url) } + +// Head returns a agent with http method GET. +func (c *Client) Head(url string) *Agent { + return c.createAgent(MethodHead, url) +} + // Post sends POST request to the given url. func Post(url string) *Agent { return defaultClient.Post(url) } @@ -77,6 +85,30 @@ func (c *Client) Post(url string) *Agent { return c.createAgent(MethodPost, url) } +// Put sends PUT request to the given url. +func Put(url string) *Agent { return defaultClient.Put(url) } + +// Put sends PUT request to the given url. +func (c *Client) Put(url string) *Agent { + return c.createAgent(MethodPut, url) +} + +// Patch sends PATCH request to the given url. +func Patch(url string) *Agent { return defaultClient.Patch(url) } + +// Patch sends PATCH request to the given url. +func (c *Client) Patch(url string) *Agent { + return c.createAgent(MethodPatch, url) +} + +// Delete sends DELETE request to the given url. +func Delete(url string) *Agent { return defaultClient.Delete(url) } + +// Delete sends DELETE request to the given url. +func (c *Client) Delete(url string) *Agent { + return c.createAgent(MethodDelete, url) +} + func (c *Client) createAgent(method, url string) *Agent { a := AcquireAgent() a.req.Header.SetMethod(method) diff --git a/client_test.go b/client_test.go index 64bb0a3f..1781762c 100644 --- a/client_test.go +++ b/client_test.go @@ -80,6 +80,32 @@ func Test_Client_Get(t *testing.T) { } } +func Test_Client_Head(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString(c.Hostname()) + }) + + go app.Listener(ln) //nolint:errcheck + + for i := 0; i < 5; i++ { + a := Head("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 0, len(errs)) + } +} + func Test_Client_Post(t *testing.T) { t.Parallel() @@ -113,6 +139,103 @@ func Test_Client_Post(t *testing.T) { } } +func Test_Client_Put(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Put("/", func(c *Ctx) error { + return c.SendString(c.FormValue("foo")) + }) + + go app.Listener(ln) //nolint:errcheck + + for i := 0; i < 5; i++ { + args := AcquireArgs() + + args.Set("foo", "bar") + + a := Put("http://example.com"). + Form(args) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "bar", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseArgs(args) + } +} + +func Test_Client_Patch(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Patch("/", func(c *Ctx) error { + return c.SendString(c.FormValue("foo")) + }) + + go app.Listener(ln) //nolint:errcheck + + for i := 0; i < 5; i++ { + args := AcquireArgs() + + args.Set("foo", "bar") + + a := Patch("http://example.com"). + Form(args) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "bar", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseArgs(args) + } +} + +func Test_Client_Delete(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Delete("/", func(c *Ctx) error { + return c.Status(StatusNoContent). + SendString("deleted") + }) + + go app.Listener(ln) //nolint:errcheck + + for i := 0; i < 5; i++ { + args := AcquireArgs() + + a := Delete("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusNoContent, code) + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseArgs(args) + } +} + func Test_Client_UserAgent(t *testing.T) { t.Parallel() From 040ab0e101555dcc5f7d8f90dd26428965b89031 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Sat, 20 Feb 2021 14:50:37 +0800 Subject: [PATCH 10/44] =?UTF-8?q?=F0=9F=91=B7=20Improve=20test=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client_test.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/client_test.go b/client_test.go index 1781762c..1288d92c 100644 --- a/client_test.go +++ b/client_test.go @@ -114,7 +114,8 @@ func Test_Client_Post(t *testing.T) { app := New(Config{DisableStartupMessage: true}) app.Post("/", func(c *Ctx) error { - return c.SendString(c.Hostname()) + return c.Status(StatusCreated). + SendString(c.FormValue("foo")) }) go app.Listener(ln) //nolint:errcheck @@ -131,8 +132,8 @@ func Test_Client_Post(t *testing.T) { code, body, errs := a.String() - utils.AssertEqual(t, StatusOK, code) - utils.AssertEqual(t, "example.com", body) + utils.AssertEqual(t, StatusCreated, code) + utils.AssertEqual(t, "bar", body) utils.AssertEqual(t, 0, len(errs)) ReleaseArgs(args) @@ -604,10 +605,8 @@ func Test_Client_Agent_Multipart_SendFiles(t *testing.T) { app.Post("/", func(c *Ctx) error { utils.AssertEqual(t, "multipart/form-data; boundary=myBoundary", c.Get(HeaderContentType)) - mf, err := c.MultipartForm() + fh1, err := c.FormFile("field1") utils.AssertEqual(t, nil, err) - - fh1 := mf.File["field1"][0] utils.AssertEqual(t, fh1.Filename, "name") buf := make([]byte, fh1.Size, fh1.Size) f, err := fh1.Open() @@ -617,8 +616,13 @@ func Test_Client_Agent_Multipart_SendFiles(t *testing.T) { utils.AssertEqual(t, nil, err) utils.AssertEqual(t, "form file", string(buf)) - checkFormFile(t, mf.File["index"][0], ".github/testdata/index.html") - checkFormFile(t, mf.File["file3"][0], ".github/testdata/index.tmpl") + fh2, err := c.FormFile("index") + utils.AssertEqual(t, nil, err) + checkFormFile(t, fh2, ".github/testdata/index.html") + + fh3, err := c.FormFile("file3") + utils.AssertEqual(t, nil, err) + checkFormFile(t, fh3, ".github/testdata/index.tmpl") return c.SendString("multipart form files") }) @@ -691,10 +695,10 @@ func Test_Client_Agent_SendFile_Error(t *testing.T) { t.Parallel() a := Post("http://example.com"). - SendFile("", "") + SendFile("non-exist-file!", "") utils.AssertEqual(t, 1, len(a.errs)) - utils.AssertEqual(t, "open : no such file or directory", a.errs[0].Error()) + utils.AssertEqual(t, "open non-exist-file!: no such file or directory", a.errs[0].Error()) } func Test_Client_Debug(t *testing.T) { From a60d23343c5143749a4a62bca5c287da01fa024d Mon Sep 17 00:00:00 2001 From: Kiyon Date: Sat, 20 Feb 2021 14:56:47 +0800 Subject: [PATCH 11/44] =?UTF-8?q?=F0=9F=91=B7=20Improve=20test=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index 1288d92c..6108a123 100644 --- a/client_test.go +++ b/client_test.go @@ -698,7 +698,7 @@ func Test_Client_Agent_SendFile_Error(t *testing.T) { SendFile("non-exist-file!", "") utils.AssertEqual(t, 1, len(a.errs)) - utils.AssertEqual(t, "open non-exist-file!: no such file or directory", a.errs[0].Error()) + utils.AssertEqual(t, true, strings.Contains(a.errs[0].Error(), "open non-exist-file!")) } func Test_Client_Debug(t *testing.T) { From 62d311133bb983861d6d7d23dbd327e7b2bbacf0 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Sat, 20 Feb 2021 15:46:41 +0800 Subject: [PATCH 12/44] =?UTF-8?q?=F0=9F=91=B7=20Add=20Bytes=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 130 +++++++++++++++++++++++++++++++++++++++++++++++++ client_test.go | 52 +++++++++++++++----- 2 files changed, 169 insertions(+), 13 deletions(-) diff --git a/client.go b/client.go index 227f43a4..07bb46b3 100644 --- a/client.go +++ b/client.go @@ -207,6 +207,33 @@ func (a *Agent) Set(k, v string) *Agent { return a } +// SetBytesK sets the given 'key: value' header. +// +// Use AddBytesK for setting multiple header values under the same key. +func (a *Agent) SetBytesK(k []byte, v string) *Agent { + a.req.Header.SetBytesK(k, v) + + return a +} + +// SetBytesV sets the given 'key: value' header. +// +// Use AddBytesV for setting multiple header values under the same key. +func (a *Agent) SetBytesV(k string, v []byte) *Agent { + a.req.Header.SetBytesV(k, v) + + return a +} + +// SetBytesKV sets the given 'key: value' header. +// +// Use AddBytesKV for setting multiple header values under the same key. +func (a *Agent) SetBytesKV(k []byte, v []byte) *Agent { + a.req.Header.SetBytesKV(k, v) + + return a +} + // Add adds the given 'key: value' header. // // Multiple headers with the same key may be added with this function. @@ -217,6 +244,36 @@ func (a *Agent) Add(k, v string) *Agent { return a } +// AddBytesK adds the given 'key: value' header. +// +// Multiple headers with the same key may be added with this function. +// Use SetBytesK for setting a single header for the given key. +func (a *Agent) AddBytesK(k []byte, v string) *Agent { + a.req.Header.AddBytesK(k, v) + + return a +} + +// AddBytesV adds the given 'key: value' header. +// +// Multiple headers with the same key may be added with this function. +// Use SetBytesV for setting a single header for the given key. +func (a *Agent) AddBytesV(k string, v []byte) *Agent { + a.req.Header.AddBytesV(k, v) + + return a +} + +// AddBytesKV adds the given 'key: value' header. +// +// Multiple headers with the same key may be added with this function. +// Use SetBytesKV for setting a single header for the given key. +func (a *Agent) AddBytesKV(k []byte, v []byte) *Agent { + a.req.Header.AddBytesKV(k, v) + + return a +} + // ConnectionClose sets 'Connection: close' header. func (a *Agent) ConnectionClose() *Agent { a.req.Header.SetConnectionClose() @@ -231,6 +288,13 @@ func (a *Agent) UserAgent(userAgent string) *Agent { return a } +// UserAgentBytes sets User-Agent header value. +func (a *Agent) UserAgentBytes(userAgent []byte) *Agent { + a.req.Header.SetUserAgentBytes(userAgent) + + return a +} + // Cookie sets one 'key: value' cookie. func (a *Agent) Cookie(key, value string) *Agent { a.req.Header.SetCookie(key, value) @@ -238,6 +302,20 @@ func (a *Agent) Cookie(key, value string) *Agent { return a } +// CookieBytesK sets one 'key: value' cookie. +func (a *Agent) CookieBytesK(key []byte, value string) *Agent { + a.req.Header.SetCookieBytesK(key, value) + + return a +} + +// CookieBytesKV sets one 'key: value' cookie. +func (a *Agent) CookieBytesKV(key, value []byte) *Agent { + a.req.Header.SetCookieBytesKV(key, value) + + return a +} + // Cookies sets multiple 'key: value' cookies. func (a *Agent) Cookies(kv ...string) *Agent { for i := 1; i < len(kv); i += 2 { @@ -247,6 +325,15 @@ func (a *Agent) Cookies(kv ...string) *Agent { return a } +// CookiesBytesKV sets multiple 'key: value' cookies. +func (a *Agent) CookiesBytesKV(kv ...[]byte) *Agent { + for i := 1; i < len(kv); i += 2 { + a.req.Header.SetCookieBytesKV(kv[i-1], kv[i]) + } + + return a +} + // Referer sets Referer header value. func (a *Agent) Referer(referer string) *Agent { a.req.Header.SetReferer(referer) @@ -254,6 +341,13 @@ func (a *Agent) Referer(referer string) *Agent { return a } +// RefererBytes sets Referer header value. +func (a *Agent) RefererBytes(referer []byte) *Agent { + a.req.Header.SetRefererBytes(referer) + + return a +} + // ContentType sets Content-Type header value. func (a *Agent) ContentType(contentType string) *Agent { a.req.Header.SetContentType(contentType) @@ -261,6 +355,13 @@ func (a *Agent) ContentType(contentType string) *Agent { return a } +// ContentTypeBytes sets Content-Type header value. +func (a *Agent) ContentTypeBytes(contentType []byte) *Agent { + a.req.Header.SetContentTypeBytes(contentType) + + return a +} + /************************** End Header Setting **************************/ /************************** URI Setting **************************/ @@ -272,6 +373,13 @@ func (a *Agent) Host(host string) *Agent { return a } +// HostBytes sets host for the uri. +func (a *Agent) HostBytes(host []byte) *Agent { + a.req.URI().SetHostBytes(host) + + return a +} + // QueryString sets URI query string. func (a *Agent) QueryString(queryString string) *Agent { a.req.URI().SetQueryString(queryString) @@ -279,6 +387,13 @@ func (a *Agent) QueryString(queryString string) *Agent { return a } +// QueryStringBytes sets URI query string. +func (a *Agent) QueryStringBytes(queryString []byte) *Agent { + a.req.URI().SetQueryStringBytes(queryString) + + return a +} + // BasicAuth sets URI username and password. func (a *Agent) BasicAuth(username, password string) *Agent { a.req.URI().SetUsername(username) @@ -287,6 +402,14 @@ func (a *Agent) BasicAuth(username, password string) *Agent { return a } +// BasicAuthBytes sets URI username and password. +func (a *Agent) BasicAuthBytes(username, password []byte) *Agent { + a.req.URI().SetUsernameBytes(username) + a.req.URI().SetPasswordBytes(password) + + return a +} + /************************** End URI Setting **************************/ /************************** Request Setting **************************/ @@ -298,6 +421,13 @@ func (a *Agent) BodyString(bodyString string) *Agent { return a } +// Body sets request body. +func (a *Agent) Body(body []byte) *Agent { + a.req.SetBody(body) + + return a +} + // BodyStream sets request body stream and, optionally body size. // // If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes diff --git a/client_test.go b/client_test.go index 6108a123..c28d2954 100644 --- a/client_test.go +++ b/client_test.go @@ -283,7 +283,7 @@ func Test_Client_UserAgent(t *testing.T) { }) } -func Test_Client_Agent_Headers(t *testing.T) { +func Test_Client_Agent_Set_Or_Add_Headers(t *testing.T) { handler := func(c *Ctx) error { c.Request().Header.VisitAll(func(key, value []byte) { if k := string(key); k == "K1" || k == "K2" { @@ -296,11 +296,17 @@ func Test_Client_Agent_Headers(t *testing.T) { wrapAgent := func(a *Agent) { a.Set("k1", "v1"). - Add("k1", "v11"). - Set("k2", "v2") + SetBytesK([]byte("k1"), "v1"). + SetBytesV("k1", []byte("v1")). + AddBytesK([]byte("k1"), "v11"). + AddBytesV("k1", []byte("v22")). + AddBytesKV([]byte("k1"), []byte("v33")). + SetBytesKV([]byte("k2"), []byte("v2")). + Add("k2", "v22") + } - testAgent(t, handler, wrapAgent, "K1v1K1v11K2v2") + testAgent(t, handler, wrapAgent, "K1v1K1v11K1v22K1v33K2v2K2v22") } func Test_Client_Agent_Connection_Close(t *testing.T) { @@ -324,7 +330,8 @@ func Test_Client_Agent_UserAgent(t *testing.T) { } wrapAgent := func(a *Agent) { - a.UserAgent("ua") + a.UserAgent("ua"). + UserAgentBytes([]byte("ua")) } testAgent(t, handler, wrapAgent, "ua") @@ -338,8 +345,10 @@ func Test_Client_Agent_Cookie(t *testing.T) { wrapAgent := func(a *Agent) { a.Cookie("k1", "v1"). - Cookie("k2", "v2"). - Cookies("k3", "v3", "k4", "v4") + CookieBytesK([]byte("k2"), "v2"). + CookieBytesKV([]byte("k2"), []byte("v2")). + Cookies("k3", "v3", "k4", "v4"). + CookiesBytesKV([]byte("k3"), []byte("v3"), []byte("k4"), []byte("v4")) } testAgent(t, handler, wrapAgent, "v1v2v3v4") @@ -351,7 +360,8 @@ func Test_Client_Agent_Referer(t *testing.T) { } wrapAgent := func(a *Agent) { - a.Referer("http://referer.com") + a.Referer("http://referer.com"). + RefererBytes([]byte("http://referer.com")) } testAgent(t, handler, wrapAgent, "http://referer.com") @@ -363,13 +373,14 @@ func Test_Client_Agent_ContentType(t *testing.T) { } wrapAgent := func(a *Agent) { - a.ContentType("custom-type") + a.ContentType("custom-type"). + ContentTypeBytes([]byte("custom-type")) } testAgent(t, handler, wrapAgent, "custom-type") } -func Test_Client_Agent_Specific_Host(t *testing.T) { +func Test_Client_Agent_Host(t *testing.T) { t.Parallel() ln := fasthttputil.NewInmemoryListener() @@ -383,7 +394,8 @@ func Test_Client_Agent_Specific_Host(t *testing.T) { go app.Listener(ln) //nolint:errcheck a := Get("http://1.1.1.1:8080"). - Host("example.com") + Host("example.com"). + HostBytes([]byte("example.com")) utils.AssertEqual(t, "1.1.1.1:8080", a.HostClient.Addr) @@ -402,7 +414,8 @@ func Test_Client_Agent_QueryString(t *testing.T) { } wrapAgent := func(a *Agent) { - a.QueryString("foo=bar&bar=baz") + a.QueryString("foo=bar&bar=baz"). + QueryStringBytes([]byte("foo=bar&bar=baz")) } testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") @@ -420,7 +433,8 @@ func Test_Client_Agent_BasicAuth(t *testing.T) { } wrapAgent := func(a *Agent) { - a.BasicAuth("foo", "bar") + a.BasicAuth("foo", "bar"). + BasicAuthBytes([]byte("foo"), []byte("bar")) } testAgent(t, handler, wrapAgent, "foo:bar") @@ -438,6 +452,18 @@ func Test_Client_Agent_BodyString(t *testing.T) { testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") } +func Test_Client_Agent_Body(t *testing.T) { + handler := func(c *Ctx) error { + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.Body([]byte("foo=bar&bar=baz")) + } + + testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") +} + func Test_Client_Agent_BodyStream(t *testing.T) { handler := func(c *Ctx) error { return c.Send(c.Request().Body()) From c477128e5b82f8ba6af8a00b0b2dd1aa00fcad35 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Sat, 20 Feb 2021 16:12:06 +0800 Subject: [PATCH 13/44] =?UTF-8?q?=F0=9F=91=B7=20Improve=20test=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 29 ++++++++++++++++++----------- client_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/client.go b/client.go index 07bb46b3..f8155576 100644 --- a/client.go +++ b/client.go @@ -134,6 +134,7 @@ type Agent struct { errs []error formFiles []*FormFile debugWriter io.Writer + mw multipartWriter maxRedirectsCount int boundary string Name string @@ -142,8 +143,6 @@ type Agent struct { parsed bool } -var ErrorInvalidURI = fasthttp.ErrorInvalidURI - // Parse initializes URI and HostClient. func (a *Agent) Parse() error { if a.parsed { @@ -157,9 +156,6 @@ func (a *Agent) Parse() error { } uri := req.URI() - if uri == nil { - return ErrorInvalidURI - } isTLS := false scheme := uri.Scheme() @@ -567,27 +563,29 @@ func (a *Agent) Boundary(boundary string) *Agent { // It is recommended obtaining args via AcquireArgs // in performance-critical code. func (a *Agent) MultipartForm(args *Args) *Agent { - mw := multipart.NewWriter(a.req.BodyWriter()) + if a.mw == nil { + a.mw = multipart.NewWriter(a.req.BodyWriter()) + } if a.boundary != "" { - if err := mw.SetBoundary(a.boundary); err != nil { + if err := a.mw.SetBoundary(a.boundary); err != nil { a.errs = append(a.errs, err) return a } } - a.req.Header.SetMultipartFormBoundary(mw.Boundary()) + a.req.Header.SetMultipartFormBoundary(a.mw.Boundary()) if args != nil { args.VisitAll(func(key, value []byte) { - if err := mw.WriteField(getString(key), getString(value)); err != nil { + if err := a.mw.WriteField(getString(key), getString(value)); err != nil { a.errs = append(a.errs, err) } }) } for _, ff := range a.formFiles { - w, err := mw.CreateFormFile(ff.Fieldname, ff.Name) + w, err := a.mw.CreateFormFile(ff.Fieldname, ff.Name) if err != nil { a.errs = append(a.errs, err) continue @@ -597,7 +595,7 @@ func (a *Agent) MultipartForm(args *Args) *Agent { } } - if err := mw.Close(); err != nil { + if err := a.mw.Close(); err != nil { a.errs = append(a.errs, err) } @@ -765,6 +763,7 @@ func (a *Agent) reset() { a.args = nil a.errs = a.errs[:0] a.debugWriter = nil + a.mw = nil a.reuse = false a.parsed = false a.maxRedirectsCount = 0 @@ -934,3 +933,11 @@ var ( strHTTPS = []byte("https") defaultUserAgent = "fiber" ) + +type multipartWriter interface { + Boundary() string + SetBoundary(boundary string) error + CreateFormFile(fieldname, filename string) (io.Writer, error) + WriteField(fieldname, value string) error + Close() error +} diff --git a/client_test.go b/client_test.go index c28d2954..424d69bc 100644 --- a/client_test.go +++ b/client_test.go @@ -4,6 +4,8 @@ import ( "bytes" "crypto/tls" "encoding/base64" + "errors" + "io" "io/ioutil" "mime/multipart" "net" @@ -584,7 +586,7 @@ func Test_Client_Agent_Form(t *testing.T) { ReleaseArgs(args) } -func Test_Client_Agent_Multipart(t *testing.T) { +func Test_Client_Agent_MultipartForm(t *testing.T) { t.Parallel() ln := fasthttputil.NewInmemoryListener() @@ -621,7 +623,25 @@ func Test_Client_Agent_Multipart(t *testing.T) { ReleaseArgs(args) } -func Test_Client_Agent_Multipart_SendFiles(t *testing.T) { +func Test_Client_Agent_MultipartForm_Errors(t *testing.T) { + t.Parallel() + + a := AcquireAgent() + a.mw = &errorMultipartWriter{} + + args := AcquireArgs() + args.Set("foo", "bar") + + ff1 := &FormFile{"", "name1", []byte("content"), false} + ff2 := &FormFile{"", "name2", []byte("content"), false} + a.FileData(ff1, ff2). + MultipartForm(args) + + utils.AssertEqual(t, 4, len(a.errs)) + ReleaseArgs(args) +} + +func Test_Client_Agent_MultipartForm_SendFiles(t *testing.T) { t.Parallel() ln := fasthttputil.NewInmemoryListener() @@ -983,3 +1003,23 @@ func testAgent(t *testing.T, handler Handler, wrapAgent func(agent *Agent), exce type data struct { Success bool `json:"success" xml:"success"` } + +type errorMultipartWriter struct { + count int +} + +func (e *errorMultipartWriter) Boundary() string { return "myBoundary" } +func (e *errorMultipartWriter) SetBoundary(_ string) error { return nil } +func (e *errorMultipartWriter) CreateFormFile(_, _ string) (io.Writer, error) { + if e.count == 0 { + e.count++ + return nil, errors.New("CreateFormFile error") + } + return errorWriter{}, nil +} +func (e *errorMultipartWriter) WriteField(_, _ string) error { return errors.New("WriteField error") } +func (e *errorMultipartWriter) Close() error { return errors.New("Close error") } + +type errorWriter struct{} + +func (errorWriter) Write(_ []byte) (int, error) { return 0, errors.New("Write error") } From c34ca83c064dac867d109cad5372d0ef381aff32 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Sat, 20 Feb 2021 16:45:13 +0800 Subject: [PATCH 14/44] =?UTF-8?q?=F0=9F=91=B7=20Add=20JSONEncoder=20and=20?= =?UTF-8?q?JSONDecoder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 44 ++++++++++++++++++++++++++++++++++++-- client_test.go | 6 +++++- utils/json.go | 9 ++++++++ utils/json_marshal.go | 5 ----- utils/json_marshal_test.go | 26 ---------------------- utils/json_test.go | 41 +++++++++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 34 deletions(-) create mode 100644 utils/json.go delete mode 100644 utils/json_marshal.go delete mode 100644 utils/json_marshal_test.go create mode 100644 utils/json_test.go diff --git a/client.go b/client.go index f8155576..3db4cb6f 100644 --- a/client.go +++ b/client.go @@ -16,6 +16,8 @@ import ( "sync" "time" + "github.com/gofiber/fiber/v2/utils" + "github.com/gofiber/fiber/v2/internal/encoding/json" "github.com/valyala/fasthttp" ) @@ -59,6 +61,18 @@ type Client struct { // NoDefaultUserAgentHeader when set to true, causes the default // User-Agent header to be excluded from the Request. NoDefaultUserAgentHeader bool + + // When set by an external client of Fiber it will use the provided implementation of a + // JSONMarshal + // + // Allowing for flexibility in using another json library for encoding + JSONEncoder utils.JSONMarshal + + // When set by an external client of Fiber it will use the provided implementation of a + // JSONUnmarshal + // + // Allowing for flexibility in using another json library for decoding + JSONDecoder utils.JSONUnmarshal } // Get returns a agent with http method GET. @@ -116,6 +130,8 @@ func (c *Client) createAgent(method, url string) *Agent { a.Name = c.UserAgent a.NoDefaultUserAgentHeader = c.NoDefaultUserAgentHeader + a.jsonDecoder = c.JSONDecoder + a.jsonEncoder = c.JSONEncoder if err := a.Parse(); err != nil { a.errs = append(a.errs, err) @@ -135,6 +151,8 @@ type Agent struct { formFiles []*FormFile debugWriter io.Writer mw multipartWriter + jsonEncoder utils.JSONMarshal + jsonDecoder utils.JSONUnmarshal maxRedirectsCount int boundary string Name string @@ -450,9 +468,13 @@ func (a *Agent) Request(req *Request) *Agent { // JSON sends a JSON request. func (a *Agent) JSON(v interface{}) *Agent { + if a.jsonEncoder == nil { + a.jsonEncoder = json.Marshal + } + a.req.Header.SetContentType(MIMEApplicationJSON) - if body, err := json.Marshal(v); err != nil { + if body, err := a.jsonEncoder(v); err != nil { a.errs = append(a.errs, err) } else { a.req.SetBody(body) @@ -658,6 +680,20 @@ func (a *Agent) MaxRedirectsCount(count int) *Agent { return a } +// JSONEncoder sets custom json encoder. +func (a *Agent) JSONEncoder(jsonEncoder utils.JSONMarshal) *Agent { + a.jsonEncoder = jsonEncoder + + return a +} + +// JSONDecoder sets custom json decoder. +func (a *Agent) JSONDecoder(jsonDecoder utils.JSONUnmarshal) *Agent { + a.jsonDecoder = jsonDecoder + + return a +} + /************************** End Agent Setting **************************/ // Bytes returns the status code, bytes body and errors of url. @@ -738,9 +774,13 @@ func (a *Agent) String(resp ...*Response) (int, string, []error) { // Struct returns the status code, bytes body and errors of url. // And bytes body will be unmarshalled to given v. func (a *Agent) Struct(v interface{}, resp ...*Response) (code int, body []byte, errs []error) { + if a.jsonDecoder == nil { + a.jsonDecoder = json.Unmarshal + } + code, body, errs = a.Bytes(resp...) - if err := json.Unmarshal(body, v); err != nil { + if err := a.jsonDecoder(body, v); err != nil { errs = append(errs, err) } diff --git a/client_test.go b/client_test.go index 424d69bc..3dd7b6f6 100644 --- a/client_test.go +++ b/client_test.go @@ -15,6 +15,8 @@ import ( "testing" "time" + "github.com/gofiber/fiber/v2/internal/encoding/json" + "github.com/gofiber/fiber/v2/utils" "github.com/valyala/fasthttp/fasthttputil" ) @@ -532,6 +534,7 @@ func Test_Client_Agent_Json(t *testing.T) { func Test_Client_Agent_Json_Error(t *testing.T) { a := Get("http://example.com"). + JSONEncoder(json.Marshal). JSON(complex(1, 1)) _, body, errs := a.String() @@ -947,7 +950,8 @@ func Test_Client_Agent_Struct(t *testing.T) { var d data - code, body, errs := a.Struct(&d) + code, body, errs := a.JSONDecoder(json.Unmarshal). + Struct(&d) utils.AssertEqual(t, StatusOK, code) utils.AssertEqual(t, `{"success"`, string(body)) diff --git a/utils/json.go b/utils/json.go new file mode 100644 index 00000000..477c8c33 --- /dev/null +++ b/utils/json.go @@ -0,0 +1,9 @@ +package utils + +// JSONMarshal returns the JSON encoding of v. +type JSONMarshal func(v interface{}) ([]byte, error) + +// JSONUnmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. If v is nil or not a pointer, +// Unmarshal returns an InvalidUnmarshalError. +type JSONUnmarshal func(data []byte, v interface{}) error diff --git a/utils/json_marshal.go b/utils/json_marshal.go deleted file mode 100644 index 692b49a4..00000000 --- a/utils/json_marshal.go +++ /dev/null @@ -1,5 +0,0 @@ -package utils - -// JSONMarshal is the standard definition of representing a Go structure in -// json format -type JSONMarshal func(interface{}) ([]byte, error) diff --git a/utils/json_marshal_test.go b/utils/json_marshal_test.go deleted file mode 100644 index 08501d96..00000000 --- a/utils/json_marshal_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package utils - -import ( - "encoding/json" - "testing" -) - -func TestDefaultJSONEncoder(t *testing.T) { - type SampleStructure struct { - ImportantString string `json:"important_string"` - } - - var ( - sampleStructure = &SampleStructure{ - ImportantString: "Hello World", - } - importantString = `{"important_string":"Hello World"}` - - jsonEncoder JSONMarshal = json.Marshal - ) - - raw, err := jsonEncoder(sampleStructure) - AssertEqual(t, err, nil) - - AssertEqual(t, string(raw), importantString) -} diff --git a/utils/json_test.go b/utils/json_test.go new file mode 100644 index 00000000..966faa83 --- /dev/null +++ b/utils/json_test.go @@ -0,0 +1,41 @@ +package utils + +import ( + "encoding/json" + "testing" +) + +type sampleStructure struct { + ImportantString string `json:"important_string"` +} + +func Test_DefaultJSONEncoder(t *testing.T) { + t.Parallel() + + var ( + ss = &sampleStructure{ + ImportantString: "Hello World", + } + importantString = `{"important_string":"Hello World"}` + jsonEncoder JSONMarshal = json.Marshal + ) + + raw, err := jsonEncoder(ss) + AssertEqual(t, err, nil) + + AssertEqual(t, string(raw), importantString) +} + +func Test_DefaultJSONDecoder(t *testing.T) { + t.Parallel() + + var ( + ss sampleStructure + importantString = []byte(`{"important_string":"Hello World"}`) + jsonDecoder JSONUnmarshal = json.Unmarshal + ) + + err := jsonDecoder(importantString, &ss) + AssertEqual(t, err, nil) + AssertEqual(t, "Hello World", ss.ImportantString) +} From a2eab0d754e0563827fc55a92b89138cac0fd11b Mon Sep 17 00:00:00 2001 From: Kiyon Date: Mon, 22 Feb 2021 08:19:44 +0800 Subject: [PATCH 15/44] =?UTF-8?q?=F0=9F=91=B7=20Improve=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 62 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/client.go b/client.go index 3db4cb6f..82631f25 100644 --- a/client.go +++ b/client.go @@ -142,23 +142,29 @@ func (c *Client) createAgent(method, url string) *Agent { // Agent is an object storing all request data for client. type Agent struct { - *fasthttp.HostClient - req *Request - customReq *Request - args *Args - timeout time.Duration - errs []error - formFiles []*FormFile - debugWriter io.Writer - mw multipartWriter - jsonEncoder utils.JSONMarshal - jsonDecoder utils.JSONUnmarshal - maxRedirectsCount int - boundary string - Name string + // Name is used in User-Agent request header. + Name string + // NoDefaultUserAgentHeader when set to true, causes the default + // User-Agent header to be excluded from the Request. NoDefaultUserAgentHeader bool - reuse bool - parsed bool + + // HostClient is an embedded fasthttp HostClient + *fasthttp.HostClient + + req *Request + customReq *Request + args *Args + timeout time.Duration + errs []error + formFiles []*FormFile + debugWriter io.Writer + mw multipartWriter + jsonEncoder utils.JSONMarshal + jsonDecoder utils.JSONUnmarshal + maxRedirectsCount int + boundary string + reuse bool + parsed bool } // Parse initializes URI and HostClient. @@ -498,8 +504,8 @@ func (a *Agent) XML(v interface{}) *Agent { // Form sends request with body if args is non-nil. // -// It is recommended obtaining args via AcquireArgs -// in performance-critical code. +// It is recommended obtaining args via AcquireArgs and release it +// manually in performance-critical code. func (a *Agent) Form(args *Args) *Agent { a.req.Header.SetContentType(MIMEApplicationForm) @@ -525,8 +531,8 @@ type FormFile struct { // FileData appends files for multipart form request. // -// It is recommended obtaining formFile via AcquireFormFile -// in performance-critical code. +// It is recommended obtaining formFile via AcquireFormFile and release it +// manually in performance-critical code. func (a *Agent) FileData(formFiles ...*FormFile) *Agent { a.formFiles = append(a.formFiles, formFiles...) @@ -582,8 +588,8 @@ func (a *Agent) Boundary(boundary string) *Agent { // MultipartForm sends multipart form request with k-v and files. // -// It is recommended obtaining args via AcquireArgs -// in performance-critical code. +// It is recommended obtaining args via AcquireArgs and release it +// manually in performance-critical code. func (a *Agent) MultipartForm(args *Args) *Agent { if a.mw == nil { a.mw = multipart.NewWriter(a.req.BodyWriter()) @@ -646,6 +652,9 @@ func (a *Agent) Timeout(timeout time.Duration) *Agent { } // Reuse indicates the createAgent can be used again after one request. +// +// If agent is reusable, then it should be released manually when it is no +// longer used. func (a *Agent) Reuse() *Agent { a.reuse = true @@ -697,6 +706,9 @@ func (a *Agent) JSONDecoder(jsonDecoder utils.JSONUnmarshal) *Agent { /************************** End Agent Setting **************************/ // Bytes returns the status code, bytes body and errors of url. +// +// It is recommended obtaining custom response via AcquireResponse and release it +// manually in performance-critical code. func (a *Agent) Bytes(customResp ...*Response) (code int, body []byte, errs []error) { defer a.release() @@ -765,6 +777,9 @@ func printDebugInfo(req *Request, resp *Response, w io.Writer) { } // String returns the status code, string body and errors of url. +// +// It is recommended obtaining custom response via AcquireResponse and release it +// manually in performance-critical code. func (a *Agent) String(resp ...*Response) (int, string, []error) { code, body, errs := a.Bytes(resp...) @@ -773,6 +788,9 @@ func (a *Agent) String(resp ...*Response) (int, string, []error) { // Struct returns the status code, bytes body and errors of url. // And bytes body will be unmarshalled to given v. +// +// It is recommended obtaining custom response via AcquireResponse and release it +// manually in performance-critical code. func (a *Agent) Struct(v interface{}, resp ...*Response) (code int, body []byte, errs []error) { if a.jsonDecoder == nil { a.jsonDecoder = json.Unmarshal From bc9651d58bafdabb1a4e49c1b2259325b2541c39 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Mon, 22 Feb 2021 08:38:54 +0800 Subject: [PATCH 16/44] =?UTF-8?q?=F0=9F=91=B7=20Remove=20custom=20request?= =?UTF-8?q?=20and=20export=20agent=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 51 ++++++++------------------------------------------ client_test.go | 6 ++---- 2 files changed, 10 insertions(+), 47 deletions(-) diff --git a/client.go b/client.go index 82631f25..5e0c7fec 100644 --- a/client.go +++ b/client.go @@ -141,6 +141,7 @@ func (c *Client) createAgent(method, url string) *Agent { } // Agent is an object storing all request data for client. +// Agent instance MUST NOT be used from concurrently running goroutines. type Agent struct { // Name is used in User-Agent request header. Name string @@ -152,7 +153,6 @@ type Agent struct { *fasthttp.HostClient req *Request - customReq *Request args *Args timeout time.Duration errs []error @@ -174,12 +174,7 @@ func (a *Agent) Parse() error { } a.parsed = true - req := a.req - if a.customReq != nil { - req = a.customReq - } - - uri := req.URI() + uri := a.req.URI() isTLS := false scheme := uri.Scheme() @@ -465,13 +460,6 @@ func (a *Agent) BodyStream(bodyStream io.Reader, bodySize int) *Agent { return a } -// Request sets custom request for createAgent. -func (a *Agent) Request(req *Request) *Agent { - a.customReq = req - - return a -} - // JSON sends a JSON request. func (a *Agent) JSON(v interface{}) *Agent { if a.jsonEncoder == nil { @@ -703,6 +691,11 @@ func (a *Agent) JSONDecoder(jsonDecoder utils.JSONUnmarshal) *Agent { return a } +// Request returns Agent request instance. +func (a *Agent) Request() *Request { + return a.req +} + /************************** End Agent Setting **************************/ // Bytes returns the status code, bytes body and errors of url. @@ -717,9 +710,6 @@ func (a *Agent) Bytes(customResp ...*Response) (code int, body []byte, errs []er } req := a.req - if a.customReq != nil { - req = a.customReq - } var ( resp *Response @@ -816,7 +806,6 @@ func (a *Agent) release() { func (a *Agent) reset() { a.HostClient = nil a.req.Reset() - a.customReq = nil a.timeout = 0 a.args = nil a.errs = a.errs[:0] @@ -878,7 +867,7 @@ func ReleaseClient(c *Client) { func AcquireAgent() *Agent { v := agentPool.Get() if v == nil { - return &Agent{req: fasthttp.AcquireRequest()} + return &Agent{req: &Request{}} } return v.(*Agent) } @@ -892,30 +881,6 @@ func ReleaseAgent(a *Agent) { agentPool.Put(a) } -// AcquireRequest returns an empty Request instance from request pool. -// -// The returned Request instance may be passed to ReleaseRequest when it is -// no longer needed. This allows Request recycling, reduces GC pressure -// and usually improves performance. -// Copy from fasthttp -func AcquireRequest() *Request { - v := requestPool.Get() - if v == nil { - return &Request{} - } - return v.(*Request) -} - -// ReleaseRequest returns req acquired via AcquireRequest to request pool. -// -// It is forbidden accessing req and/or its' members after returning -// it to request pool. -// Copy from fasthttp -func ReleaseRequest(req *Request) { - req.Reset() - requestPool.Put(req) -} - // AcquireResponse returns an empty Response instance from response pool. // // The returned Response instance may be passed to ReleaseResponse when it is diff --git a/client_test.go b/client_test.go index 3dd7b6f6..566f53e9 100644 --- a/client_test.go +++ b/client_test.go @@ -480,7 +480,7 @@ func Test_Client_Agent_BodyStream(t *testing.T) { testAgent(t, handler, wrapAgent, "body stream") } -func Test_Client_Agent_Custom_Request_And_Response(t *testing.T) { +func Test_Client_Agent_Custom_Response(t *testing.T) { t.Parallel() ln := fasthttputil.NewInmemoryListener() @@ -495,12 +495,11 @@ func Test_Client_Agent_Custom_Request_And_Response(t *testing.T) { for i := 0; i < 5; i++ { a := AcquireAgent() - req := AcquireRequest() resp := AcquireResponse() + req := a.Request() req.Header.SetMethod(MethodGet) req.SetRequestURI("http://example.com") - a.Request(req) utils.AssertEqual(t, nil, a.Parse()) @@ -513,7 +512,6 @@ func Test_Client_Agent_Custom_Request_And_Response(t *testing.T) { utils.AssertEqual(t, "custom", string(resp.Body())) utils.AssertEqual(t, 0, len(errs)) - ReleaseRequest(req) ReleaseResponse(resp) } } From 169001c2e1befeee8bf157d5edb3e33bd3b12768 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Mon, 22 Feb 2021 08:47:19 +0800 Subject: [PATCH 17/44] =?UTF-8?q?=F0=9F=91=B7=20fix=20golint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client_test.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/client_test.go b/client_test.go index 566f53e9..ff525227 100644 --- a/client_test.go +++ b/client_test.go @@ -32,7 +32,7 @@ func Test_Client_Invalid_URL(t *testing.T) { return c.SendString(c.Hostname()) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() a := Get("http://example.com\r\n\r\nGET /\r\n\r\n") @@ -69,7 +69,7 @@ func Test_Client_Get(t *testing.T) { return c.SendString(c.Hostname()) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() for i := 0; i < 5; i++ { a := Get("http://example.com") @@ -95,7 +95,7 @@ func Test_Client_Head(t *testing.T) { return c.SendString(c.Hostname()) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() for i := 0; i < 5; i++ { a := Head("http://example.com") @@ -122,7 +122,7 @@ func Test_Client_Post(t *testing.T) { SendString(c.FormValue("foo")) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() for i := 0; i < 5; i++ { args := AcquireArgs() @@ -155,7 +155,7 @@ func Test_Client_Put(t *testing.T) { return c.SendString(c.FormValue("foo")) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() for i := 0; i < 5; i++ { args := AcquireArgs() @@ -188,7 +188,7 @@ func Test_Client_Patch(t *testing.T) { return c.SendString(c.FormValue("foo")) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() for i := 0; i < 5; i++ { args := AcquireArgs() @@ -222,7 +222,7 @@ func Test_Client_Delete(t *testing.T) { SendString("deleted") }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() for i := 0; i < 5; i++ { args := AcquireArgs() @@ -252,7 +252,7 @@ func Test_Client_UserAgent(t *testing.T) { return c.Send(c.Request().Header.UserAgent()) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() t.Run("default", func(t *testing.T) { for i := 0; i < 5; i++ { @@ -395,7 +395,7 @@ func Test_Client_Agent_Host(t *testing.T) { return c.SendString(c.Hostname()) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() a := Get("http://1.1.1.1:8080"). Host("example.com"). @@ -491,7 +491,7 @@ func Test_Client_Agent_Custom_Response(t *testing.T) { return c.SendString("custom") }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() for i := 0; i < 5; i++ { a := AcquireAgent() @@ -604,7 +604,7 @@ func Test_Client_Agent_MultipartForm(t *testing.T) { return c.Send(c.Request().Body()) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() args := AcquireArgs() @@ -674,7 +674,7 @@ func Test_Client_Agent_MultipartForm_SendFiles(t *testing.T) { return c.SendString("multipart form files") }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() for i := 0; i < 5; i++ { ff := AcquireFormFile() @@ -783,7 +783,7 @@ func Test_Client_Agent_Timeout(t *testing.T) { return c.SendString("timeout") }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() a := Get("http://example.com"). Timeout(time.Millisecond * 100) @@ -808,7 +808,7 @@ func Test_Client_Agent_Reuse(t *testing.T) { return c.SendString("reuse") }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() a := Get("http://example.com"). Reuse() @@ -850,7 +850,7 @@ func Test_Client_Agent_TLS(t *testing.T) { return c.SendString("tls") }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() code, body, errs := Get("https://" + ln.Addr().String()). InsecureSkipVerify(). @@ -880,7 +880,7 @@ func Test_Client_Agent_MaxRedirectsCount(t *testing.T) { return c.SendString("redirect") }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() t.Run("success", func(t *testing.T) { a := Get("http://example.com?foo"). @@ -924,7 +924,7 @@ func Test_Client_Agent_Struct(t *testing.T) { return c.SendString(`{"success"`) }) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() t.Run("success", func(t *testing.T) { a := Get("http://example.com") @@ -980,7 +980,7 @@ func testAgent(t *testing.T, handler Handler, wrapAgent func(agent *Agent), exce app.Get("/", handler) - go app.Listener(ln) //nolint:errcheck + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() c := 1 if len(count) > 0 { From b5402e0f3830a6eaf7e71fbeb30c4e3929939ac9 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Mon, 22 Feb 2021 09:13:24 +0800 Subject: [PATCH 18/44] =?UTF-8?q?=F0=9F=91=B7=20Add=20SetResponse=20to=20s?= =?UTF-8?q?et=20custom=20response=20for=20full=20control=20of=20response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 46 ++++++++++++++++++++++++---------------------- client_test.go | 3 ++- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/client.go b/client.go index 5e0c7fec..02de7b5d 100644 --- a/client.go +++ b/client.go @@ -153,6 +153,7 @@ type Agent struct { *fasthttp.HostClient req *Request + resp *Response args *Args timeout time.Duration errs []error @@ -696,13 +697,20 @@ func (a *Agent) Request() *Request { return a.req } -/************************** End Agent Setting **************************/ - -// Bytes returns the status code, bytes body and errors of url. +// SetResponse sets custom response for the Agent instance. // // It is recommended obtaining custom response via AcquireResponse and release it // manually in performance-critical code. -func (a *Agent) Bytes(customResp ...*Response) (code int, body []byte, errs []error) { +func (a *Agent) SetResponse(customResp *Response) *Agent { + a.resp = customResp + + return a +} + +/************************** End Agent Setting **************************/ + +// Bytes returns the status code, bytes body and errors of url. +func (a *Agent) Bytes() (code int, body []byte, errs []error) { defer a.release() if errs = append(errs, a.errs...); len(errs) > 0 { @@ -712,14 +720,14 @@ func (a *Agent) Bytes(customResp ...*Response) (code int, body []byte, errs []er req := a.req var ( - resp *Response - releaseResp bool + resp *Response + nilResp bool ) - if len(customResp) > 0 { - resp = customResp[0] - } else { + if a.resp == nil { resp = AcquireResponse() - releaseResp = true + nilResp = true + } else { + resp = a.resp } defer func() { if a.debugWriter != nil { @@ -730,7 +738,7 @@ func (a *Agent) Bytes(customResp ...*Response) (code int, body []byte, errs []er code = resp.StatusCode() } - if releaseResp { + if nilResp { body = append(body, resp.Body()...) ReleaseResponse(resp) } else { @@ -767,26 +775,20 @@ func printDebugInfo(req *Request, resp *Response, w io.Writer) { } // String returns the status code, string body and errors of url. -// -// It is recommended obtaining custom response via AcquireResponse and release it -// manually in performance-critical code. -func (a *Agent) String(resp ...*Response) (int, string, []error) { - code, body, errs := a.Bytes(resp...) +func (a *Agent) String() (int, string, []error) { + code, body, errs := a.Bytes() return code, getString(body), errs } // Struct returns the status code, bytes body and errors of url. // And bytes body will be unmarshalled to given v. -// -// It is recommended obtaining custom response via AcquireResponse and release it -// manually in performance-critical code. -func (a *Agent) Struct(v interface{}, resp ...*Response) (code int, body []byte, errs []error) { +func (a *Agent) Struct(v interface{}) (code int, body []byte, errs []error) { if a.jsonDecoder == nil { a.jsonDecoder = json.Unmarshal } - code, body, errs = a.Bytes(resp...) + code, body, errs = a.Bytes() if err := a.jsonDecoder(body, v); err != nil { errs = append(errs, err) @@ -806,6 +808,7 @@ func (a *Agent) release() { func (a *Agent) reset() { a.HostClient = nil a.req.Reset() + a.resp = nil a.timeout = 0 a.args = nil a.errs = a.errs[:0] @@ -829,7 +832,6 @@ func (a *Agent) reset() { var ( clientPool sync.Pool agentPool sync.Pool - requestPool sync.Pool responsePool sync.Pool argsPool sync.Pool formFilePool sync.Pool diff --git a/client_test.go b/client_test.go index ff525227..2f1fa306 100644 --- a/client_test.go +++ b/client_test.go @@ -505,7 +505,8 @@ func Test_Client_Agent_Custom_Response(t *testing.T) { a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } - code, body, errs := a.String(resp) + code, body, errs := a.SetResponse(resp). + String() utils.AssertEqual(t, StatusOK, code) utils.AssertEqual(t, "custom", body) From 1fddaed0725cc3b5e14d44cd4d955ab2c9847994 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Mon, 22 Feb 2021 09:47:47 +0800 Subject: [PATCH 19/44] =?UTF-8?q?=F0=9F=91=B7=20Add=20Dest=20for=20reusing?= =?UTF-8?q?=20returned=20body=20bytes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 17 ++++++++++++++--- client_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 02de7b5d..b8e546ca 100644 --- a/client.go +++ b/client.go @@ -154,6 +154,7 @@ type Agent struct { req *Request resp *Response + dest []byte args *Args timeout time.Duration errs []error @@ -707,6 +708,16 @@ func (a *Agent) SetResponse(customResp *Response) *Agent { return a } +// Dest sets custom dest. +// +// The contents of dest will be replaced by the response body, if the dest +// is too small a new slice will be allocated. +func (a *Agent) Dest(dest []byte) *Agent { + a.dest = dest + + return a +} + /************************** End Agent Setting **************************/ // Bytes returns the status code, bytes body and errors of url. @@ -738,11 +749,10 @@ func (a *Agent) Bytes() (code int, body []byte, errs []error) { code = resp.StatusCode() } + body = append(a.dest[:0], resp.Body()...) + if nilResp { - body = append(body, resp.Body()...) ReleaseResponse(resp) - } else { - body = resp.Body() } }() @@ -809,6 +819,7 @@ func (a *Agent) reset() { a.HostClient = nil a.req.Reset() a.resp = nil + a.dest = nil a.timeout = 0 a.args = nil a.errs = a.errs[:0] diff --git a/client_test.go b/client_test.go index 2f1fa306..539b7377 100644 --- a/client_test.go +++ b/client_test.go @@ -517,6 +517,50 @@ func Test_Client_Agent_Custom_Response(t *testing.T) { } } +func Test_Client_Agent_Dest(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("dest") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + t.Run("small dest", func(t *testing.T) { + dest := []byte("de") + + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.Dest(dest).String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "dest", body) + utils.AssertEqual(t, "de", string(dest)) + utils.AssertEqual(t, 0, len(errs)) + }) + + t.Run("enough dest", func(t *testing.T) { + dest := []byte("foobar") + + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.Dest(dest).String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "dest", body) + utils.AssertEqual(t, "destar", string(dest)) + utils.AssertEqual(t, 0, len(errs)) + }) +} + func Test_Client_Agent_Json(t *testing.T) { handler := func(c *Ctx) error { utils.AssertEqual(t, MIMEApplicationJSON, string(c.Request().Header.ContentType())) From 2772af030e63aade0a1eb04e4b33beacd07d5804 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Mon, 22 Feb 2021 10:06:38 +0800 Subject: [PATCH 20/44] =?UTF-8?q?=F0=9F=91=B7=20handle=20pre=20errors=20fo?= =?UTF-8?q?r=20Struct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 9 ++++++--- client_test.go | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/client.go b/client.go index b8e546ca..0d4669c7 100644 --- a/client.go +++ b/client.go @@ -728,18 +728,19 @@ func (a *Agent) Bytes() (code int, body []byte, errs []error) { return } - req := a.req - var ( + req = a.req resp *Response nilResp bool ) + if a.resp == nil { resp = AcquireResponse() nilResp = true } else { resp = a.resp } + defer func() { if a.debugWriter != nil { printDebugInfo(req, resp, a.debugWriter) @@ -798,7 +799,9 @@ func (a *Agent) Struct(v interface{}) (code int, body []byte, errs []error) { a.jsonDecoder = json.Unmarshal } - code, body, errs = a.Bytes() + if code, body, errs = a.Bytes(); len(errs) > 0 { + return + } if err := a.jsonDecoder(body, v); err != nil { errs = append(errs, err) diff --git a/client_test.go b/client_test.go index 539b7377..ee1830fc 100644 --- a/client_test.go +++ b/client_test.go @@ -986,6 +986,21 @@ func Test_Client_Agent_Struct(t *testing.T) { utils.AssertEqual(t, true, d.Success) }) + t.Run("pre error", func(t *testing.T) { + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + var d data + + _, body, errs := a.Timeout(time.Nanosecond).Struct(&d) + + utils.AssertEqual(t, "", string(body)) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "timeout", errs[0].Error()) + utils.AssertEqual(t, false, d.Success) + }) + t.Run("error", func(t *testing.T) { a := Get("http://example.com/error") @@ -993,8 +1008,7 @@ func Test_Client_Agent_Struct(t *testing.T) { var d data - code, body, errs := a.JSONDecoder(json.Unmarshal). - Struct(&d) + code, body, errs := a.JSONDecoder(json.Unmarshal).Struct(&d) utils.AssertEqual(t, StatusOK, code) utils.AssertEqual(t, `{"success"`, string(body)) From f9d1074635e84902e4a96ca8e8016bd8c49f329d Mon Sep 17 00:00:00 2001 From: Kiyon Date: Mon, 22 Feb 2021 14:37:31 +0800 Subject: [PATCH 21/44] =?UTF-8?q?=F0=9F=91=B7=20Should=20not=20force=20Age?= =?UTF-8?q?nt.dest=20to=20Agent.dest[:0]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 2 +- client_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 0d4669c7..b76a77ab 100644 --- a/client.go +++ b/client.go @@ -750,7 +750,7 @@ func (a *Agent) Bytes() (code int, body []byte, errs []error) { code = resp.StatusCode() } - body = append(a.dest[:0], resp.Body()...) + body = append(a.dest, resp.Body()...) if nilResp { ReleaseResponse(resp) diff --git a/client_test.go b/client_test.go index ee1830fc..73fc94e6 100644 --- a/client_test.go +++ b/client_test.go @@ -537,7 +537,7 @@ func Test_Client_Agent_Dest(t *testing.T) { a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } - code, body, errs := a.Dest(dest).String() + code, body, errs := a.Dest(dest[:0]).String() utils.AssertEqual(t, StatusOK, code) utils.AssertEqual(t, "dest", body) @@ -552,7 +552,7 @@ func Test_Client_Agent_Dest(t *testing.T) { a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } - code, body, errs := a.Dest(dest).String() + code, body, errs := a.Dest(dest[:0]).String() utils.AssertEqual(t, StatusOK, code) utils.AssertEqual(t, "dest", body) From 9bcfb5109d3f1afe9a9679c033f36588af4afb99 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Mon, 22 Feb 2021 14:48:00 +0800 Subject: [PATCH 22/44] =?UTF-8?q?=F0=9F=91=B7=20Improve=20test=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client_test.go b/client_test.go index 73fc94e6..10aa97e3 100644 --- a/client_test.go +++ b/client_test.go @@ -990,14 +990,14 @@ func Test_Client_Agent_Struct(t *testing.T) { a := Get("http://example.com") a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + a.errs = append(a.errs, errors.New("pre errors")) var d data - - _, body, errs := a.Timeout(time.Nanosecond).Struct(&d) + _, body, errs := a.Struct(&d) utils.AssertEqual(t, "", string(body)) utils.AssertEqual(t, 1, len(errs)) - utils.AssertEqual(t, "timeout", errs[0].Error()) + utils.AssertEqual(t, "pre errors", errs[0].Error()) utils.AssertEqual(t, false, d.Success) }) From b46185e1b028f77edbf88876fd9c4e6e5f4e2833 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Tue, 23 Feb 2021 16:40:17 +0800 Subject: [PATCH 23/44] =?UTF-8?q?=F0=9F=91=B7=20Fix=20hardcode=20tls=20to?= =?UTF-8?q?=20startupMessage=20in=20app.Listener?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 2 +- app_test.go | 44 +++++++++++++++++++++----------------------- helpers.go | 13 +++++++++++-- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/app.go b/app.go index c256b465..46fc3e2c 100644 --- a/app.go +++ b/app.go @@ -568,7 +568,7 @@ func (app *App) Listener(ln net.Listener) error { app.startupProcess() // Print startup message if !app.config.DisableStartupMessage { - app.startupMessage(ln.Addr().String(), false, "") + app.startupMessage(ln.Addr().String(), getTlsConfig(ln) != nil, "") } // Start listening return app.server.Serve(ln) diff --git a/app_test.go b/app_test.go index c6e29f1b..c1b07355 100644 --- a/app_test.go +++ b/app_test.go @@ -382,29 +382,6 @@ func Test_App_Add_Method_Test(t *testing.T) { app.Add("JOHN", "/doe", testEmptyHandler) } -func Test_App_Listener_TLS(t *testing.T) { - app := New() - - // Create tls certificate - cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") - if err != nil { - utils.AssertEqual(t, nil, err) - } - config := &tls.Config{Certificates: []tls.Certificate{cer}} - - ln, err := net.Listen(NetworkTCP4, ":3078") - utils.AssertEqual(t, nil, err) - - ln = tls.NewListener(ln, config) - - go func() { - time.Sleep(1000 * time.Millisecond) - utils.AssertEqual(t, nil, app.Shutdown()) - }() - - utils.AssertEqual(t, nil, app.Listener(ln)) -} - // go test -run Test_App_GETOnly func Test_App_GETOnly(t *testing.T) { app := New(Config{ @@ -1011,6 +988,27 @@ func Test_App_Listener_Prefork(t *testing.T) { utils.AssertEqual(t, nil, app.Listener(ln)) } +func Test_App_Listener_TLS(t *testing.T) { + // Create tls certificate + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + if err != nil { + utils.AssertEqual(t, nil, err) + } + config := &tls.Config{Certificates: []tls.Certificate{cer}} + + ln, err := tls.Listen(NetworkTCP4, ":0", config) + utils.AssertEqual(t, nil, err) + + app := New() + + go func() { + time.Sleep(time.Millisecond * 500) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.Listener(ln)) +} + // go test -v -run=^$ -bench=Benchmark_AcquireCtx -benchmem -count=4 func Benchmark_AcquireCtx(b *testing.B) { app := New() diff --git a/helpers.go b/helpers.go index 4439a5c2..a0f8d161 100644 --- a/helpers.go +++ b/helpers.go @@ -49,6 +49,14 @@ func lnMetadata(network string, ln net.Listener) (addr string, cfg *tls.Config) panic("listener: " + addr + ": Only one usage of each socket address (protocol/network address/port) is normally permitted.") } + cfg = getTlsConfig(ln) + + return +} + +/* #nosec */ +// getTlsConfig returns a net listener's tls config +func getTlsConfig(ln net.Listener) *tls.Config { // Get listener type pointer := reflect.ValueOf(ln) @@ -63,13 +71,14 @@ func lnMetadata(network string, ln net.Listener) (addr string, cfg *tls.Config) // Get element from pointer if elem := newval.Elem(); elem.Type() != nil { // Cast value to *tls.Config - cfg = elem.Interface().(*tls.Config) + return elem.Interface().(*tls.Config) } } } } } - return + + return nil } // readContent opens a named file and read content from it From 6a5698277c4b58952c8c7e7ec6937caf39673dfd Mon Sep 17 00:00:00 2001 From: Andrew DeChristopher Date: Wed, 24 Feb 2021 11:42:54 -0500 Subject: [PATCH 24/44] =?UTF-8?q?=F0=9F=94=A5=20Add=20optional=20FileSyste?= =?UTF-8?q?m=20config=20to=20favicon=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change exists to support go 1.16 embedded filesystems and other network filesystems that can't simply be read as a file from a system path. --- middleware/favicon/favicon.go | 27 +++++++++++++++++++++++---- middleware/favicon/favicon_test.go | 19 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/middleware/favicon/favicon.go b/middleware/favicon/favicon.go index 6584fa25..924f7e61 100644 --- a/middleware/favicon/favicon.go +++ b/middleware/favicon/favicon.go @@ -2,6 +2,7 @@ package favicon import ( "io/ioutil" + "net/http" "strconv" "github.com/gofiber/fiber/v2" @@ -17,12 +18,18 @@ type Config struct { // File holds the path to an actual favicon that will be cached // // Optional. Default: "" - File string + File string `json:"file"` + + // FileSystem is an optional alternate filesystem to search for the favicon in. + // An example of this could be an embedded or network filesystem + // + // Optional. Default: nil + FileSystem http.FileSystem `json:"-"` // CacheControl defines how the Cache-Control header in the response should be set // // Optional. Default: "public, max-age=31536000" - CacheControl string + CacheControl string `json:"cache_control"` } // ConfigDefault is the default config @@ -66,9 +73,21 @@ func New(config ...Config) fiber.Handler { iconLen string ) if cfg.File != "" { - if icon, err = ioutil.ReadFile(cfg.File); err != nil { - panic(err) + // read from configured filesystem if present + if cfg.FileSystem != nil { + f, err := cfg.FileSystem.Open(cfg.File) + if err != nil { + panic(err) + } + if icon, err = ioutil.ReadAll(f); err != nil { + panic(err) + } + } else { + if icon, err = ioutil.ReadFile(cfg.File); err != nil { + panic(err) + } } + iconLen = strconv.Itoa(len(icon)) } diff --git a/middleware/favicon/favicon_test.go b/middleware/favicon/favicon_test.go index 5f7465ca..9f181b65 100644 --- a/middleware/favicon/favicon_test.go +++ b/middleware/favicon/favicon_test.go @@ -1,7 +1,9 @@ package favicon import ( + "net/http" "net/http/httptest" + "os" "testing" "github.com/gofiber/fiber/v2" @@ -71,6 +73,22 @@ func Test_Middleware_Favicon_Found(t *testing.T) { utils.AssertEqual(t, "public, max-age=31536000", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") } +// go test -run Test_Middleware_Favicon_FileSystem +func Test_Middleware_Favicon_FileSystem(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + File: "favicon.ico", + FileSystem: http.FS(os.DirFS("../../.github/testdata/")), + })) + + resp, err := app.Test(httptest.NewRequest("GET", "/favicon.ico", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, "image/x-icon", resp.Header.Get(fiber.HeaderContentType)) + utils.AssertEqual(t, "public, max-age=31536000", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") +} + // go test -run Test_Middleware_Favicon_CacheControl func Test_Middleware_Favicon_CacheControl(t *testing.T) { app := fiber.New() @@ -79,6 +97,7 @@ func Test_Middleware_Favicon_CacheControl(t *testing.T) { CacheControl: "public, max-age=100", File: "../../.github/testdata/favicon.ico", })) + resp, err := app.Test(httptest.NewRequest("GET", "/favicon.ico", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") From 13c4245242626c75baf18a824a5886f1e07de0ab Mon Sep 17 00:00:00 2001 From: Andrew DeChristopher Date: Wed, 24 Feb 2021 12:27:32 -0500 Subject: [PATCH 25/44] =?UTF-8?q?=F0=9F=94=A5=20Fix=20favicon=20filesystem?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change exists to support go 1.16 embedded filesystems and other network filesystems that can't simply be read as a file from a system path. --- middleware/favicon/favicon_test.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/middleware/favicon/favicon_test.go b/middleware/favicon/favicon_test.go index 9f181b65..17f43b67 100644 --- a/middleware/favicon/favicon_test.go +++ b/middleware/favicon/favicon_test.go @@ -4,11 +4,13 @@ import ( "net/http" "net/http/httptest" "os" + "strings" "testing" + "github.com/valyala/fasthttp" + "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/utils" - "github.com/valyala/fasthttp" ) // go test -run Test_Middleware_Favicon @@ -73,13 +75,32 @@ func Test_Middleware_Favicon_Found(t *testing.T) { utils.AssertEqual(t, "public, max-age=31536000", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") } +// mockFS wraps local filesystem for the purposes of +// Test_Middleware_Favicon_FileSystem located below +// TODO use os.Dir if fiber upgrades to 1.16 +type mockFS struct { +} + +func (m mockFS) Open(name string) (http.File, error) { + if name == "/" { + name = "." + } else { + name = strings.TrimPrefix(name, "/") + } + file, err := os.Open(name) + if err != nil { + return nil, err + } + return file, nil +} + // go test -run Test_Middleware_Favicon_FileSystem func Test_Middleware_Favicon_FileSystem(t *testing.T) { app := fiber.New() app.Use(New(Config{ - File: "favicon.ico", - FileSystem: http.FS(os.DirFS("../../.github/testdata/")), + File: "../../.github/testdata/favicon.ico", + FileSystem: mockFS{}, })) resp, err := app.Test(httptest.NewRequest("GET", "/favicon.ico", nil)) From 09ff81716c010aa697dcce3a4990ba3371e715e9 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Thu, 25 Feb 2021 10:41:39 +0800 Subject: [PATCH 26/44] =?UTF-8?q?=F0=9F=91=B7=20Add=20beta=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client.go b/client.go index b76a77ab..ef7bec75 100644 --- a/client.go +++ b/client.go @@ -722,6 +722,8 @@ func (a *Agent) Dest(dest []byte) *Agent { // Bytes returns the status code, bytes body and errors of url. func (a *Agent) Bytes() (code int, body []byte, errs []error) { + fmt.Println("[Warning] client is still in beta, API might change in the future!") + defer a.release() if errs = append(errs, a.errs...); len(errs) > 0 { From 6ea71e9464b54e4514b07b43c8fa00d3e5615513 Mon Sep 17 00:00:00 2001 From: Kiyon Date: Fri, 26 Feb 2021 10:48:53 +0800 Subject: [PATCH 27/44] =?UTF-8?q?=20=F0=9F=8D=BA=20Cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index ef7bec75..b5c36943 100644 --- a/client.go +++ b/client.go @@ -145,6 +145,7 @@ func (c *Client) createAgent(method, url string) *Agent { type Agent struct { // Name is used in User-Agent request header. Name string + // NoDefaultUserAgentHeader when set to true, causes the default // User-Agent header to be excluded from the Request. NoDefaultUserAgentHeader bool @@ -390,7 +391,7 @@ func (a *Agent) Host(host string) *Agent { return a } -// HostBytes sets host for the uri. +// HostBytes sets host for the URI. func (a *Agent) HostBytes(host []byte) *Agent { a.req.URI().SetHostBytes(host) @@ -492,7 +493,7 @@ func (a *Agent) XML(v interface{}) *Agent { return a } -// Form sends request with body if args is non-nil. +// Form sends form request with body if args is non-nil. // // It is recommended obtaining args via AcquireArgs and release it // manually in performance-critical code. @@ -641,7 +642,7 @@ func (a *Agent) Timeout(timeout time.Duration) *Agent { return a } -// Reuse indicates the createAgent can be used again after one request. +// Reuse enables the Agent instance to be used again after one request. // // If agent is reusable, then it should be released manually when it is no // longer used. @@ -651,7 +652,7 @@ func (a *Agent) Reuse() *Agent { return a } -// InsecureSkipVerify controls whether the createAgent verifies the server's +// InsecureSkipVerify controls whether the Agent verifies the server // certificate chain and host name. func (a *Agent) InsecureSkipVerify() *Agent { if a.HostClient.TLSConfig == nil { @@ -877,7 +878,7 @@ func ReleaseClient(c *Client) { clientPool.Put(c) } -// AcquireAgent returns an empty Agent instance from createAgent pool. +// AcquireAgent returns an empty Agent instance from Agent pool. // // The returned Agent instance may be passed to ReleaseAgent when it is // no longer needed. This allows Agent recycling, reduces GC pressure @@ -890,10 +891,10 @@ func AcquireAgent() *Agent { return v.(*Agent) } -// ReleaseAgent returns a acquired via AcquireAgent to createAgent pool. +// ReleaseAgent returns a acquired via AcquireAgent to Agent pool. // // It is forbidden accessing req and/or its' members after returning -// it to createAgent pool. +// it to Agent pool. func ReleaseAgent(a *Agent) { a.reset() agentPool.Put(a) From bbd101d70dc2d1361ce8c17b55f4255c8e18c2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Demirba=C5=9F?= <77154643+barandemirbas@users.noreply.github.com> Date: Sun, 28 Feb 2021 04:15:15 +0300 Subject: [PATCH 28/44] :pencil2: Fix typos and translate some sentences English to Turkish (#1192) --- .github/README_tr.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/README_tr.md b/.github/README_tr.md index a8778a95..2ac59dff 100644 --- a/.github/README_tr.md +++ b/.github/README_tr.md @@ -73,7 +73,7 @@

- Fiber, Go için en hızlı HTTP motoru olan Fasthttp üzerine inşa edilmiş, Express den ilham alan bir web çatısıdır. Sıfır bellek ayırma ve performans göz önünde bulundurularak hızlı geliştirme için işleri kolaylaştırmak üzere tasarlandı. + Fiber, Go için en hızlı HTTP motoru olan Fasthttp üzerine inşa edilmiş, Expressden ilham alan bir web çatısıdır. Sıfır bellek ayırma ve performans göz önünde bulundurularak hızlı geliştirme için işleri kolaylaştırmak üzere tasarlandı.

## ⚡️ Hızlı Başlangıç @@ -96,7 +96,7 @@ func main() { ## 🤖 Performans Ölçümleri -Bu testler [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ve [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ile koşuldu. Bütün sonuçları görmek için lütfen [Wiki](https://docs.gofiber.io/benchmarks) sayfasını ziyaret ediniz. +Bu testler [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ve [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ile gerçekleştirildi. Bütün sonuçları görmek için lütfen [Wiki](https://docs.gofiber.io/benchmarks) sayfasını ziyaret ediniz.

@@ -105,9 +105,9 @@ Bu testler [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r1 ## ⚙️ Kurulum -Make sure you have Go installed ([download](https://golang.org/dl/)). Version `1.14` or higher is required. +Go'nun `1.14` sürümü ([indir](https://golang.org/dl/)) ya da daha yüksek bir sürüm gerekli. -Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://blog.golang.org/using-go-modules)) inside the folder. Then install Fiber with the [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: +Bir klasör oluşturup klasörün içinde `go mod init github.com/your/repo` yazarak projenize başlayın ([daha fazla öğren](https://blog.golang.org/using-go-modules)). Ardından Fiberi kurmak için [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) komutunu çalıştırın: ```bash go get -u github.com/gofiber/fiber/v2 @@ -125,12 +125,12 @@ go get -u github.com/gofiber/fiber/v2 - [Template engines](https://github.com/gofiber/template) - [WebSocket support](https://github.com/gofiber/websocket) - [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- Available in [15 languages](https://docs.gofiber.io/) -- Ve daha fazlası, [Fiber ı keşfet](https://docs.gofiber.io/) +- [15 dilde](https://docs.gofiber.io/) mevcut +- Ve daha fazlası, [Fiber'ı keşfet](https://docs.gofiber.io/) ## 💡 Felsefe -[Node.js](https://nodejs.org/en/about/) den [Go](https://golang.org/doc/) ya geçen yeni gopher lar kendi web uygulamalarını ve mikroservislerini yazmaya başlamadan önce dili öğrenmek ile uğraşıyorlar. Fiber, bir **web çatısı** olarak, **minimalizm** ve **UNIX yolu**nu izlemek fikri ile oluşturuldu. Böylece yeni gopher lar sıcak ve güvenilir bir hoşgeldin ile Go dünyasına giriş yapabilirler. +[Node.js](https://nodejs.org/en/about/) den [Go](https://golang.org/doc/) ya geçen yeni gopher lar kendi web uygulamalarını ve mikroservislerini yazmaya başlamadan önce dili öğrenmek ile uğraşıyorlar. Fiber, bir **web çatısı** olarak, **minimalizm** ve **UNIX yolu**nu izlemek fikri ile oluşturuldu. Böylece yeni gopherlar sıcak ve güvenilir bir hoşgeldin ile Go dünyasına giriş yapabilirler. Fiber internet üzerinde en popüler olan Express web çatısından **esinlenmiştir**. Biz Express in **kolaylığını** ve Go nun **ham performansını** birleştirdik. Daha önce Node.js üzerinde (Express veya benzerini kullanarak) bir web uygulaması geliştirdiyseniz, pek çok metod ve prensip size **çok tanıdık** gelecektir. @@ -478,9 +478,9 @@ func main() { ## 🧬 Internal Middleware -Here is a list of middleware that are included within the Fiber framework. +Fibera dahil edilen middlewareların bir listesi aşağıda verilmiştir. -| Middleware | Description | +| Middleware | Açıklama | | :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | | [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | @@ -499,9 +499,9 @@ Here is a list of middleware that are included within the Fiber framework. ## 🧬 External Middleware -List of externally hosted middleware modules and maintained by the [Fiber team](https://github.com/orgs/gofiber/people). +Harici olarak barındırılan middlewareların modüllerinin listesi [Fiber ekibi](https://github.com/orgs/gofiber/people) tarafından korunur. -| Middleware | Description | +| Middleware | Açıklama | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | | [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | @@ -514,7 +514,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( ## 🌱 Third Party Middlewares -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! +Bu, Fiber topluluğu tarafından oluşturulan middlewareların bir listesidir, sizinkini görmek istiyorsanız lütfen bir PR oluşturun! - [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) - [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) @@ -571,7 +571,7 @@ Fiber, alan adı, gitbook, netlify, serverless yer sağlayıcısı giderleri ve Code Contributors -## ⭐️ Stargazers +## ⭐️ Projeyi Yıldızlayanlar Stargazers over time From 59b12fbcb205c5bddeefef4dc44a13d2c23886a6 Mon Sep 17 00:00:00 2001 From: tianjipeng Date: Tue, 2 Mar 2021 00:25:36 +0800 Subject: [PATCH 29/44] fix: lookup cookie in response header (#1191) --- middleware/session/session_test.go | 24 ++++++++++++++++++++ middleware/session/store.go | 36 +++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/middleware/session/session_test.go b/middleware/session/session_test.go index 28961230..53448866 100644 --- a/middleware/session/session_test.go +++ b/middleware/session/session_test.go @@ -274,6 +274,30 @@ func Test_Session_Cookie(t *testing.T) { utils.AssertEqual(t, 84, len(ctx.Response().Header.PeekCookie(store.CookieName))) } +// go test -run Test_Session_Cookie_In_Response +func Test_Session_Cookie_In_Response(t *testing.T) { + t.Parallel() + store := New() + app := fiber.New() + + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // get session + sess, _ := store.Get(ctx) + sess.Set("id", "1") + utils.AssertEqual(t, true, sess.Fresh()) + sess.Save() + + sess, _ = store.Get(ctx) + sess.Set("name", "john") + utils.AssertEqual(t, true, sess.Fresh()) + + utils.AssertEqual(t, "1", sess.Get("id")) + utils.AssertEqual(t, "john", sess.Get("name")) +} + // go test -v -run=^$ -bench=Benchmark_Session -benchmem -count=4 func Benchmark_Session(b *testing.B) { app, store := fiber.New(), New() diff --git a/middleware/session/store.go b/middleware/session/store.go index d3260f51..89d6bd07 100644 --- a/middleware/session/store.go +++ b/middleware/session/store.go @@ -6,6 +6,8 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/internal/gotiny" "github.com/gofiber/fiber/v2/internal/storage/memory" + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp" ) type Store struct { @@ -36,14 +38,23 @@ func (s *Store) RegisterType(i interface{}) { // Get will get/create a session func (s *Store) Get(c *fiber.Ctx) (*Session, error) { var fresh bool + var loadDada = true // Get key from cookie id := c.Cookies(s.CookieName) + if len(id) == 0 { + fresh = true + var err error + if id, err = s.responseCookies(c); err != nil { + return nil, err + } + } + // If no key exist, create new one if len(id) == 0 { + loadDada = false id = s.KeyGenerator() - fresh = true } // Create session object @@ -54,14 +65,13 @@ func (s *Store) Get(c *fiber.Ctx) (*Session, error) { sess.fresh = fresh // Fetch existing data - if !fresh { + if loadDada { raw, err := s.Storage.Get(id) // Unmashal if we found data if raw != nil && err == nil { mux.Lock() gotiny.Unmarshal(raw, &sess.data) mux.Unlock() - sess.fresh = false } else if err != nil { return nil, err } else { @@ -72,6 +82,26 @@ func (s *Store) Get(c *fiber.Ctx) (*Session, error) { return sess, nil } +func (s *Store) responseCookies(c *fiber.Ctx) (string, error) { + // Get key from response cookie + cookieValue := c.Response().Header.PeekCookie(s.CookieName) + if len(cookieValue) == 0 { + return "", nil + } + + cookie := fasthttp.AcquireCookie() + err := cookie.ParseBytes(cookieValue) + if err != nil { + return "", err + } + + value := make([]byte, len(cookie.Value())) + copy(value, cookie.Value()) + id := utils.UnsafeString(value) + fasthttp.ReleaseCookie(cookie) + return id, nil +} + // Reset will delete all session from the storage func (s *Store) Reset() error { return s.Storage.Reset() From 53e5dc523ec5acbb55c039a900577a81c71aec10 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Mon, 1 Mar 2021 12:44:17 -0400 Subject: [PATCH 30/44] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20CSRF=20middleware?= =?UTF-8?q?=20cookie<>storage=20bug=20squashed=20and=20other=20improvement?= =?UTF-8?q?s=20(#1180)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * expire cookie on Post, Delete, Patch and Put Cookie should always expire on Post, Delete, Patch and Put as it is either valid and will be removed from storage, or is not in storage and invalid * token and cookie match * retrigger checks * csrf tests * csrf per session strategy --- middleware/csrf/csrf.go | 56 ++++++++++++++---------------- middleware/csrf/csrf_test.go | 66 +++++++++++++++++++----------------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index db7e0724..a712079f 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -51,34 +51,12 @@ func New(config ...Config) fiber.Handler { // Action depends on the HTTP method switch c.Method() { - case fiber.MethodGet: + case fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace: // Declare empty token and try to get existing CSRF from cookie token = c.Cookies(cfg.CookieName) + default: + // Assume that anything not defined as 'safe' by RFC7231 needs protection - // Generate CSRF token if not exist - if token == "" { - // Generate new CSRF token - token = cfg.KeyGenerator() - - // Add token to Storage - manager.setRaw(token, dummyValue, cfg.Expiration) - } - - // Create cookie to pass token to client - cookie := &fiber.Cookie{ - Name: cfg.CookieName, - Value: token, - Domain: cfg.CookieDomain, - Path: cfg.CookiePath, - Expires: time.Now().Add(cfg.Expiration), - Secure: cfg.CookieSecure, - HTTPOnly: cfg.CookieHTTPOnly, - SameSite: cfg.CookieSameSite, - } - // Set cookie to response - c.Cookie(cookie) - - case fiber.MethodPost, fiber.MethodDelete, fiber.MethodPatch, fiber.MethodPut: // Extract token from client request i.e. header, query, param, form or cookie token, err = extractor(c) if err != nil { @@ -87,7 +65,6 @@ func New(config ...Config) fiber.Handler { // if token does not exist in Storage if manager.getRaw(token) == nil { - // Expire cookie c.Cookie(&fiber.Cookie{ Name: cfg.CookieName, @@ -98,14 +75,33 @@ func New(config ...Config) fiber.Handler { HTTPOnly: cfg.CookieHTTPOnly, SameSite: cfg.CookieSameSite, }) - return cfg.ErrorHandler(c, err) } - - // The token is validated, time to delete it - manager.delete(token) } + // Generate CSRF token if not exist + if token == "" { + // And generate a new token + token = cfg.KeyGenerator() + } + + // Add/update token to Storage + manager.setRaw(token, dummyValue, cfg.Expiration) + + // Create cookie to pass token to client + cookie := &fiber.Cookie{ + Name: cfg.CookieName, + Value: token, + Domain: cfg.CookieDomain, + Path: cfg.CookiePath, + Expires: time.Now().Add(cfg.Expiration), + Secure: cfg.CookieSecure, + HTTPOnly: cfg.CookieHTTPOnly, + SameSite: cfg.CookieSameSite, + } + // Set cookie to response + c.Cookie(cookie) + // Protect clients from caching the response by telling the browser // a new header value is generated c.Vary(fiber.HeaderCookie) diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index 0dbf30cb..364a05b8 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -22,41 +22,45 @@ func Test_CSRF(t *testing.T) { h := app.Handler() ctx := &fasthttp.RequestCtx{} - // Generate CSRF token - ctx.Request.Header.SetMethod("GET") - h(ctx) - token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) - token = strings.Split(strings.Split(token, ";")[0], "=")[1] + methods := [4]string{"GET", "HEAD", "OPTIONS", "TRACE"} - // Without CSRF cookie - ctx.Request.Reset() - ctx.Response.Reset() - ctx.Request.Header.SetMethod("POST") - h(ctx) - utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + for _, method := range methods { + // Generate CSRF token + ctx.Request.Header.SetMethod(method) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] - // Empty/invalid CSRF token - ctx.Request.Reset() - ctx.Response.Reset() - ctx.Request.Header.SetMethod("POST") - ctx.Request.Header.Set("X-CSRF-Token", "johndoe") - h(ctx) - utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + // Without CSRF cookie + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod("POST") + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) - // Valid CSRF token - ctx.Request.Reset() - ctx.Response.Reset() - ctx.Request.Header.SetMethod("GET") - h(ctx) - token = string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) - token = strings.Split(strings.Split(token, ";")[0], "=")[1] + // Empty/invalid CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.Set("X-CSRF-Token", "johndoe") + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) - ctx.Request.Reset() - ctx.Response.Reset() - ctx.Request.Header.SetMethod("POST") - ctx.Request.Header.Set("X-CSRF-Token", token) - h(ctx) - utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + // Valid CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(method) + h(ctx) + token = string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.Set("X-CSRF-Token", token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + } } // go test -run Test_CSRF_Next From 86e43593cd296843f2a249c6cce4f8dd946b2d13 Mon Sep 17 00:00:00 2001 From: hi019 <65871571+hi019@users.noreply.github.com> Date: Mon, 1 Mar 2021 16:25:32 -0500 Subject: [PATCH 31/44] CSRF MW Restructuring --- middleware/csrf/config.go | 27 ++++++++++ middleware/csrf/csrf.go | 93 ++--------------------------------- middleware/csrf/extractors.go | 69 ++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 89 deletions(-) create mode 100644 middleware/csrf/extractors.go diff --git a/middleware/csrf/config.go b/middleware/csrf/config.go index 52846f54..a506b454 100644 --- a/middleware/csrf/config.go +++ b/middleware/csrf/config.go @@ -2,6 +2,8 @@ package csrf import ( "fmt" + "net/textproto" + "strings" "time" "github.com/gofiber/fiber/v2" @@ -85,6 +87,9 @@ type Config struct { // // Optional. Default: DefaultErrorHandler ErrorHandler fiber.ErrorHandler + + // extractor returns the csrf token from the request based on KeyLookup + extractor func(c *fiber.Ctx) (string, error) } // ConfigDefault is the default config @@ -95,6 +100,7 @@ var ConfigDefault = Config{ Expiration: 1 * time.Hour, KeyGenerator: utils.UUID, ErrorHandler: defaultErrorHandler, + extractor: csrfFromHeader("X-Csrf-Token"), } // default ErrorHandler that process return error from fiber.Handler @@ -157,5 +163,26 @@ func configDefault(config ...Config) Config { cfg.ErrorHandler = ConfigDefault.ErrorHandler } + // Generate the correct extractor to get the token from the correct location + selectors := strings.Split(cfg.KeyLookup, ":") + + if len(selectors) != 2 { + panic("[CSRF] KeyLookup must in the form of :") + } + + // By default we extract from a header + cfg.extractor = csrfFromHeader(textproto.CanonicalMIMEHeaderKey(selectors[1])) + + switch selectors[0] { + case "form": + cfg.extractor = csrfFromForm(selectors[1]) + case "query": + cfg.extractor = csrfFromQuery(selectors[1]) + case "param": + cfg.extractor = csrfFromParam(selectors[1]) + case "cookie": + cfg.extractor = csrfFromCookie(selectors[1]) + } + return cfg } diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index a712079f..c840fd7d 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -1,43 +1,21 @@ package csrf import ( - "errors" - "net/textproto" - "strings" "time" "github.com/gofiber/fiber/v2" ) +var cfg Config + // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config - cfg := configDefault(config...) + cfg = configDefault(config...) // Create manager to simplify storage operations ( see manager.go ) manager := newManager(cfg.Storage) - // Generate the correct extractor to get the token from the correct location - selectors := strings.Split(cfg.KeyLookup, ":") - - if len(selectors) != 2 { - panic("[CSRF] KeyLookup must in the form of :") - } - - // By default we extract from a header - extractor := csrfFromHeader(textproto.CanonicalMIMEHeaderKey(selectors[1])) - - switch selectors[0] { - case "form": - extractor = csrfFromForm(selectors[1]) - case "query": - extractor = csrfFromQuery(selectors[1]) - case "param": - extractor = csrfFromParam(selectors[1]) - case "cookie": - extractor = csrfFromCookie(selectors[1]) - } - dummyValue := []byte{'+'} // Return new handler @@ -58,7 +36,7 @@ func New(config ...Config) fiber.Handler { // Assume that anything not defined as 'safe' by RFC7231 needs protection // Extract token from client request i.e. header, query, param, form or cookie - token, err = extractor(c) + token, err = cfg.extractor(c) if err != nil { return cfg.ErrorHandler(c, err) } @@ -115,66 +93,3 @@ func New(config ...Config) fiber.Handler { return c.Next() } } - -var ( - errMissingHeader = errors.New("missing csrf token in header") - errMissingQuery = errors.New("missing csrf token in query") - errMissingParam = errors.New("missing csrf token in param") - errMissingForm = errors.New("missing csrf token in form") - errMissingCookie = errors.New("missing csrf token in cookie") -) - -// csrfFromHeader returns a function that extracts token from the request header. -func csrfFromHeader(param string) func(c *fiber.Ctx) (string, error) { - return func(c *fiber.Ctx) (string, error) { - token := c.Get(param) - if token == "" { - return "", errMissingHeader - } - return token, nil - } -} - -// csrfFromQuery returns a function that extracts token from the query string. -func csrfFromQuery(param string) func(c *fiber.Ctx) (string, error) { - return func(c *fiber.Ctx) (string, error) { - token := c.Query(param) - if token == "" { - return "", errMissingQuery - } - return token, nil - } -} - -// csrfFromParam returns a function that extracts token from the url param string. -func csrfFromParam(param string) func(c *fiber.Ctx) (string, error) { - return func(c *fiber.Ctx) (string, error) { - token := c.Params(param) - if token == "" { - return "", errMissingParam - } - return token, nil - } -} - -// csrfFromForm returns a function that extracts a token from a multipart-form. -func csrfFromForm(param string) func(c *fiber.Ctx) (string, error) { - return func(c *fiber.Ctx) (string, error) { - token := c.FormValue(param) - if token == "" { - return "", errMissingForm - } - return token, nil - } -} - -// csrfFromCookie returns a function that extracts token from the cookie header. -func csrfFromCookie(param string) func(c *fiber.Ctx) (string, error) { - return func(c *fiber.Ctx) (string, error) { - token := c.Cookies(param) - if token == "" { - return "", errMissingCookie - } - return token, nil - } -} diff --git a/middleware/csrf/extractors.go b/middleware/csrf/extractors.go new file mode 100644 index 00000000..7d32dd8d --- /dev/null +++ b/middleware/csrf/extractors.go @@ -0,0 +1,69 @@ +package csrf + +import ( + "errors" + "github.com/gofiber/fiber/v2" +) + +var ( + errMissingHeader = errors.New("missing csrf token in header") + errMissingQuery = errors.New("missing csrf token in query") + errMissingParam = errors.New("missing csrf token in param") + errMissingForm = errors.New("missing csrf token in form") + errMissingCookie = errors.New("missing csrf token in cookie") +) + +// csrfFromParam returns a function that extracts token from the url param string. +func csrfFromParam(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Params(param) + if token == "" { + return "", errMissingParam + } + return token, nil + } +} + +// csrfFromForm returns a function that extracts a token from a multipart-form. +func csrfFromForm(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.FormValue(param) + if token == "" { + return "", errMissingForm + } + return token, nil + } +} + +// csrfFromCookie returns a function that extracts token from the cookie header. +func csrfFromCookie(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Cookies(param) + if token == "" { + return "", errMissingCookie + } + return token, nil + } +} + +// csrfFromHeader returns a function that extracts token from the request header. +func csrfFromHeader(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Get(param) + if token == "" { + return "", errMissingHeader + } + return token, nil + } +} + +// csrfFromQuery returns a function that extracts token from the query string. +func csrfFromQuery(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Query(param) + if token == "" { + return "", errMissingQuery + } + return token, nil + } +} From 2d4d2f7c47df631fec981ef822a7728c8dd68535 Mon Sep 17 00:00:00 2001 From: hi019 <65871571+hi019@users.noreply.github.com> Date: Mon, 1 Mar 2021 16:30:04 -0500 Subject: [PATCH 32/44] Remove global variable --- middleware/csrf/csrf.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index c840fd7d..854a9cf9 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -6,12 +6,10 @@ import ( "github.com/gofiber/fiber/v2" ) -var cfg Config - // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config - cfg = configDefault(config...) + cfg := configDefault(config...) // Create manager to simplify storage operations ( see manager.go ) manager := newManager(cfg.Storage) From b31953ab8d9c0a71664b9da245fc430aa0dce8fa Mon Sep 17 00:00:00 2001 From: hi019 <65871571+hi019@users.noreply.github.com> Date: Mon, 1 Mar 2021 16:38:56 -0500 Subject: [PATCH 33/44] Revert "Remove global variable" This reverts commit 2d4d2f7c --- middleware/csrf/csrf.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index 854a9cf9..c840fd7d 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -6,10 +6,12 @@ import ( "github.com/gofiber/fiber/v2" ) +var cfg Config + // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config - cfg := configDefault(config...) + cfg = configDefault(config...) // Create manager to simplify storage operations ( see manager.go ) manager := newManager(cfg.Storage) From 983919fd1838b900b039b7c144707362ffa9169e Mon Sep 17 00:00:00 2001 From: hi019 <65871571+hi019@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:32:22 -0500 Subject: [PATCH 34/44] CSRF Docs - Add note about how to get token (#1196) --- middleware/csrf/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/middleware/csrf/README.md b/middleware/csrf/README.md index 94e79bf9..dc39b9bf 100644 --- a/middleware/csrf/README.md +++ b/middleware/csrf/README.md @@ -1,7 +1,7 @@ # CSRF Middleware -CSRF middleware for [Fiber](https://github.com/gofiber/fiber) that provides [Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection by passing a csrf token via cookies. This cookie value will be used to compare against the client csrf token in POST requests. When the csrf token is invalid, this middleware will delete the `_csrf` cookie and return the `fiber.ErrForbidden` error. -CSRF Tokens are generated on GET requests. +CSRF middleware for [Fiber](https://github.com/gofiber/fiber) that provides [Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection by passing a csrf token via cookies. This cookie value will be used to compare against the client csrf token in POST requests. When the csrf token is invalid, this middleware will delete the `csrf_` cookie and return the `fiber.ErrForbidden` error. +CSRF Tokens are generated on GET requests. You can retrieve the CSRF token with `c.Locals(contextKey)`, where `contextKey` is the string you set in the config (see Custom Config below). _NOTE: This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases._ From a179c6665cfcd5b676a2034c75d03ba5d7072833 Mon Sep 17 00:00:00 2001 From: iRedMail <2048991+iredmail@users.noreply.github.com> Date: Wed, 3 Mar 2021 01:12:40 +0800 Subject: [PATCH 35/44] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20FS=20MW:=20add=20exa?= =?UTF-8?q?mple=20of=20embedding=20directory=20(#1197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hi019 <65871571+hi019@users.noreply.github.com> --- middleware/filesystem/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/middleware/filesystem/README.md b/middleware/filesystem/README.md index 690b848b..6785506f 100644 --- a/middleware/filesystem/README.md +++ b/middleware/filesystem/README.md @@ -68,6 +68,7 @@ package main import ( "embed" + "io/fs" "log" "net/http" @@ -75,9 +76,14 @@ import ( "github.com/gofiber/fiber/v2/middleware/filesystem" ) +// Embed a single file //go:embed index.html var f embed.FS +// Embed a directory +//go:embed static/* +var embedDirStatic embed.FS + func main() { app := fiber.New() @@ -85,6 +91,15 @@ func main() { Root: http.FS(f), })) + // Access file "image.png" under `static/` directory via URL: `http:///static/image.png`. + // With `http.FS(embedDirStatic)`, you have to access it via URL: + // `http:///static/static/image.png`. + subFS, _ := fs.Sub(embedDirStatic, "static") + app.Use("/static", filesystem.New(filesystem.Config{ + Root: http.FS(subFS), + Browse: true, + })) + log.Fatal(app.Listen(":3000")) } ``` From 8e47ccd9ede251ad58666123f8216631b1c33af0 Mon Sep 17 00:00:00 2001 From: wowpoppy <66640708+wowpoppy@users.noreply.github.com> Date: Fri, 5 Mar 2021 16:18:21 +0600 Subject: [PATCH 36/44] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Remove=20unused=20pa?= =?UTF-8?q?th=20parameter=20in=20README=20example=20(#1201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✏️ Remove unused path parameter in example * Update translations --- .github/README.md | 2 +- .github/README_de.md | 2 +- .github/README_es.md | 2 +- .github/README_fr.md | 2 +- .github/README_he.md | 2 +- .github/README_id.md | 2 +- .github/README_ja.md | 2 +- .github/README_ko.md | 2 +- .github/README_nl.md | 2 +- .github/README_pt.md | 2 +- .github/README_ru.md | 2 +- .github/README_sa.md | 2 +- .github/README_tr.md | 2 +- .github/README_zh-CN.md | 2 +- .github/README_zh-TW.md | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/README.md b/.github/README.md index a9b1e366..316d833c 100644 --- a/.github/README.md +++ b/.github/README.md @@ -153,7 +153,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_de.md b/.github/README_de.md index fea1e3e5..b1fe8961 100644 --- a/.github/README_de.md +++ b/.github/README_de.md @@ -153,7 +153,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_es.md b/.github/README_es.md index 3f1c3dc7..0584d110 100644 --- a/.github/README_es.md +++ b/.github/README_es.md @@ -150,7 +150,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_fr.md b/.github/README_fr.md index c211ccbc..10f398fe 100644 --- a/.github/README_fr.md +++ b/.github/README_fr.md @@ -153,7 +153,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_he.md b/.github/README_he.md index d044e499..a9a00ee0 100644 --- a/.github/README_he.md +++ b/.github/README_he.md @@ -206,7 +206,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_id.md b/.github/README_id.md index ae3be2af..985c84fb 100644 --- a/.github/README_id.md +++ b/.github/README_id.md @@ -153,7 +153,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_ja.md b/.github/README_ja.md index 61e86dbc..f08c14eb 100644 --- a/.github/README_ja.md +++ b/.github/README_ja.md @@ -156,7 +156,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_ko.md b/.github/README_ko.md index bb5da37b..e7b8eb7c 100644 --- a/.github/README_ko.md +++ b/.github/README_ko.md @@ -157,7 +157,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_nl.md b/.github/README_nl.md index 92c4215b..bbdf91cc 100644 --- a/.github/README_nl.md +++ b/.github/README_nl.md @@ -157,7 +157,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_pt.md b/.github/README_pt.md index 74db273c..7209493a 100644 --- a/.github/README_pt.md +++ b/.github/README_pt.md @@ -153,7 +153,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_ru.md b/.github/README_ru.md index 104e928e..4a4c82a3 100644 --- a/.github/README_ru.md +++ b/.github/README_ru.md @@ -153,7 +153,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_sa.md b/.github/README_sa.md index ffb64548..00bc6ab5 100644 --- a/.github/README_sa.md +++ b/.github/README_sa.md @@ -172,7 +172,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_tr.md b/.github/README_tr.md index 2ac59dff..8627b3fd 100644 --- a/.github/README_tr.md +++ b/.github/README_tr.md @@ -151,7 +151,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_zh-CN.md b/.github/README_zh-CN.md index a02b581d..828b67a0 100644 --- a/.github/README_zh-CN.md +++ b/.github/README_zh-CN.md @@ -152,7 +152,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) diff --git a/.github/README_zh-TW.md b/.github/README_zh-TW.md index 63ed7fd8..461c5548 100644 --- a/.github/README_zh-TW.md +++ b/.github/README_zh-TW.md @@ -155,7 +155,7 @@ func main() { }) // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + app.Get("/:name/:age", func(c *fiber.Ctx) error { msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) return c.SendString(msg) // => 👴 john is 75 years old }) From aa044d090b7ce063ff75e3761390ef121f0350b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vic=20Sh=C3=B3stak?= Date: Sun, 7 Mar 2021 22:20:14 +0300 Subject: [PATCH 37/44] Update code snippets --- .github/README_ru.md | 49 ++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/README_ru.md b/.github/README_ru.md index 4a4c82a3..84e347bf 100644 --- a/.github/README_ru.md +++ b/.github/README_ru.md @@ -207,27 +207,27 @@ func main() { ```go func main() { - app := fiber.New() + app := fiber.New() - // Match any route - app.Use(func(c *fiber.Ctx) error { - fmt.Println("🥇 First handler") - return c.Next() - }) + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) error { - fmt.Println("🥈 Second handler") - return c.Next() - }) + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) error { - fmt.Println("🥉 Last handler") - return c.SendString("Hello, World 👋!") - }) + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) - log.Fatal(app.Listen(":3000")) + log.Fatal(app.Listen(":3000")) } ``` @@ -248,8 +248,6 @@ func main() { Ознакомьтесь с пакетом [Template](https://github.com/gofiber/template), который поддерживает множество движков для views. ```go -package main - import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/template/pug" @@ -271,6 +269,7 @@ func main() { log.Fatal(app.Listen(":3000")) } + ``` ### Группировка путей в цепочки @@ -313,8 +312,6 @@ func main() { 📖 [Logger](https://docs.gofiber.io/middleware/logger) ```go -package main - import ( "log" @@ -331,6 +328,7 @@ func main() { log.Fatal(app.Listen(":3000")) } + ``` ### Cross-Origin Resource Sharing (CORS) @@ -354,6 +352,7 @@ func main() { log.Fatal(app.Listen(":3000")) } + ``` Проверем CORS, присвоив домен в заголовок `Origin`, отличный от `localhost`: @@ -388,6 +387,7 @@ func main() { log.Fatal(app.Listen(":3000")) } + ``` ### JSON Response @@ -418,6 +418,7 @@ func main() { log.Fatal(app.Listen(":3000")) } + ``` ### WebSocket Upgrade @@ -440,7 +441,9 @@ func main() { log.Println("read:", err) break } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) if err != nil { log.Println("write:", err) @@ -450,8 +453,9 @@ func main() { })) log.Fatal(app.Listen(":3000")) - // ws://localhost:3000/ws + // => ws://localhost:3000/ws } + ``` ### Recover middleware @@ -475,6 +479,7 @@ func main() { log.Fatal(app.Listen(":3000")) } + ``` From cc262daae31f0449cae282750cb1c133ac1b6bbb Mon Sep 17 00:00:00 2001 From: gregchalmers Date: Tue, 9 Mar 2021 17:14:52 +1300 Subject: [PATCH 38/44] nice to have #1207 PR --- app.go | 66 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/app.go b/app.go index 46fc3e2c..2507b2d9 100644 --- a/app.go +++ b/app.go @@ -817,17 +817,6 @@ func (app *App) startupMessage(addr string, tls bool, pids string) { return } - var logo string - logo += "%s" - logo += " ┌───────────────────────────────────────────────────┐\n" - logo += " │ %s │\n" - logo += " │ %s │\n" - logo += " │ │\n" - logo += " │ Handlers %s Processes %s │\n" - logo += " │ Prefork .%s PID ....%s │\n" - logo += " └───────────────────────────────────────────────────┘" - logo += "%s" - const ( cBlack = "\u001b[90m" // cRed = "\u001b[91m" @@ -886,12 +875,13 @@ func (app *App) startupMessage(addr string, tls bool, pids string) { } host, port := parseAddr(addr) - if host == "" || host == "0.0.0.0" { + if host == "" { host = "127.0.0.1" } - addr = "http://" + host + ":" + port + + scheme := "http" if tls { - addr = "https://" + host + ":" + port + scheme = "https" } isPrefork := "Disabled" @@ -904,14 +894,46 @@ func (app *App) startupMessage(addr string, tls bool, pids string) { procs = "1" } - mainLogo := fmt.Sprintf(logo, - cBlack, - centerValue(" Fiber v"+Version, 49), - center(addr, 49), - value(strconv.Itoa(app.handlerCount), 14), value(procs, 12), - value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), - cReset, - ) + var mainLogo string + if host == "0.0.0.0" { + const logo string = "%s" + + " ┌───────────────────────────────────────────────────┐\n" + + " │ %s │\n" + + " │ %s │\n" + + " │ %s │\n" + + " │ │\n" + + " │ Handlers %s Processes %s │\n" + + " │ Prefork .%s PID ....%s │\n" + + " └───────────────────────────────────────────────────┘" + + "%s" + mainLogo = fmt.Sprintf(logo, + cBlack, + centerValue(" Fiber v"+Version, 49), + center(fmt.Sprintf("bound %s://%v:%v", scheme, host, port), 49), + center(fmt.Sprintf("visit %s://127.0.0.1:%v", scheme, port), 49), + value(strconv.Itoa(app.handlerCount), 14), value(procs, 12), + value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), + cReset, + ) + } else { + const logo string = "%s" + + " ┌───────────────────────────────────────────────────┐\n" + + " │ %s │\n" + + " │ %s │\n" + + " │ │\n" + + " │ Handlers %s Processes %s │\n" + + " │ Prefork .%s PID ....%s │\n" + + " └───────────────────────────────────────────────────┘" + + "%s" + mainLogo = fmt.Sprintf(logo, + cBlack, + centerValue(" Fiber v"+Version, 49), + center(fmt.Sprintf("%s://%v:%v", scheme, host, port), 49), + value(strconv.Itoa(app.handlerCount), 14), value(procs, 12), + value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), + cReset, + ) + } var childPidsLogo string if app.config.Prefork { From 25c5e4c26484633d1277d320836800c134c1b781 Mon Sep 17 00:00:00 2001 From: gregchalmers Date: Mon, 8 Mar 2021 22:51:19 -0800 Subject: [PATCH 39/44] fixed host=="" to "0.0.0.0" from "127.0.0.1" --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 2507b2d9..7412cf29 100644 --- a/app.go +++ b/app.go @@ -876,7 +876,7 @@ func (app *App) startupMessage(addr string, tls bool, pids string) { host, port := parseAddr(addr) if host == "" { - host = "127.0.0.1" + host = "0.0.0.0" } scheme := "http" From 3d5123671c2b592e4cfc749babd633ee79b18e65 Mon Sep 17 00:00:00 2001 From: gregchalmers <> Date: Mon, 8 Mar 2021 23:09:52 -0800 Subject: [PATCH 40/44] %v -> %s --- app.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app.go b/app.go index 7412cf29..3974cd80 100644 --- a/app.go +++ b/app.go @@ -909,8 +909,8 @@ func (app *App) startupMessage(addr string, tls bool, pids string) { mainLogo = fmt.Sprintf(logo, cBlack, centerValue(" Fiber v"+Version, 49), - center(fmt.Sprintf("bound %s://%v:%v", scheme, host, port), 49), - center(fmt.Sprintf("visit %s://127.0.0.1:%v", scheme, port), 49), + center(fmt.Sprintf("bound %s://%s:%s", scheme, host, port), 49), + center(fmt.Sprintf("visit %s://127.0.0.1:%s", scheme, port), 49), value(strconv.Itoa(app.handlerCount), 14), value(procs, 12), value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), cReset, @@ -928,7 +928,7 @@ func (app *App) startupMessage(addr string, tls bool, pids string) { mainLogo = fmt.Sprintf(logo, cBlack, centerValue(" Fiber v"+Version, 49), - center(fmt.Sprintf("%s://%v:%v", scheme, host, port), 49), + center(fmt.Sprintf("%s://%s:%s", scheme, host, port), 49), value(strconv.Itoa(app.handlerCount), 14), value(procs, 12), value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), cReset, From c09f9a675f744af14a16d1d509a1aa490d0baf0c Mon Sep 17 00:00:00 2001 From: gregchalmers <> Date: Mon, 8 Mar 2021 23:18:17 -0800 Subject: [PATCH 41/44] swapped bound & link --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 3974cd80..219275d1 100644 --- a/app.go +++ b/app.go @@ -909,8 +909,8 @@ func (app *App) startupMessage(addr string, tls bool, pids string) { mainLogo = fmt.Sprintf(logo, cBlack, centerValue(" Fiber v"+Version, 49), + center(fmt.Sprintf("%s://127.0.0.1:%s", scheme, port), 49), center(fmt.Sprintf("bound %s://%s:%s", scheme, host, port), 49), - center(fmt.Sprintf("visit %s://127.0.0.1:%s", scheme, port), 49), value(strconv.Itoa(app.handlerCount), 14), value(procs, 12), value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), cReset, From f25c667a8a0f2c6ea23c7a4e31c5c499b6e83c9b Mon Sep 17 00:00:00 2001 From: gregchalmers <> Date: Tue, 9 Mar 2021 00:35:51 -0800 Subject: [PATCH 42/44] refactor for repeated code --- app.go | 56 +++++++++++++++++++------------------------------------- 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/app.go b/app.go index 219275d1..137dd152 100644 --- a/app.go +++ b/app.go @@ -894,47 +894,29 @@ func (app *App) startupMessage(addr string, tls bool, pids string) { procs = "1" } - var mainLogo string + mainLogo := cBlack+ + " ┌───────────────────────────────────────────────────┐\n"+ + " │ "+centerValue(" Fiber v"+Version, 49)+" │\n" + if host == "0.0.0.0" { - const logo string = "%s" + - " ┌───────────────────────────────────────────────────┐\n" + - " │ %s │\n" + - " │ %s │\n" + - " │ %s │\n" + - " │ │\n" + - " │ Handlers %s Processes %s │\n" + - " │ Prefork .%s PID ....%s │\n" + - " └───────────────────────────────────────────────────┘" + - "%s" - mainLogo = fmt.Sprintf(logo, - cBlack, - centerValue(" Fiber v"+Version, 49), - center(fmt.Sprintf("%s://127.0.0.1:%s", scheme, port), 49), - center(fmt.Sprintf("bound %s://%s:%s", scheme, host, port), 49), - value(strconv.Itoa(app.handlerCount), 14), value(procs, 12), - value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), - cReset, - ) + mainLogo += + " │ "+center(fmt.Sprintf("%s://127.0.0.1:%s", scheme, port), 49)+ " │\n" + + " │ "+center(fmt.Sprintf("bound %s://0.0.0.0:%s", scheme, port), 49)+ " │\n" } else { - const logo string = "%s" + - " ┌───────────────────────────────────────────────────┐\n" + - " │ %s │\n" + - " │ %s │\n" + - " │ │\n" + - " │ Handlers %s Processes %s │\n" + - " │ Prefork .%s PID ....%s │\n" + - " └───────────────────────────────────────────────────┘" + - "%s" - mainLogo = fmt.Sprintf(logo, - cBlack, - centerValue(" Fiber v"+Version, 49), - center(fmt.Sprintf("%s://%s:%s", scheme, host, port), 49), - value(strconv.Itoa(app.handlerCount), 14), value(procs, 12), - value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), - cReset, - ) + mainLogo += + " │ "+center(fmt.Sprintf("%s://%s:%s", scheme, host, port), 49)+ " │\n" } + mainLogo += fmt.Sprintf( + " │ │\n"+ + " │ Handlers %s Processes %s │\n"+ + " │ Prefork .%s PID ....%s │\n"+ + " └───────────────────────────────────────────────────┘"+ + cReset, + value(strconv.Itoa(app.handlerCount), 14), value(procs, 12), + value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), + ) + var childPidsLogo string if app.config.Prefork { var childPidsTemplate string From 0da74f62f317ba7736a3ccc3f4bf05bddc491b1c Mon Sep 17 00:00:00 2001 From: gregchalmers <> Date: Tue, 9 Mar 2021 02:46:28 -0800 Subject: [PATCH 43/44] bound made msg clearer --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 137dd152..8bc83821 100644 --- a/app.go +++ b/app.go @@ -901,7 +901,7 @@ func (app *App) startupMessage(addr string, tls bool, pids string) { if host == "0.0.0.0" { mainLogo += " │ "+center(fmt.Sprintf("%s://127.0.0.1:%s", scheme, port), 49)+ " │\n" + - " │ "+center(fmt.Sprintf("bound %s://0.0.0.0:%s", scheme, port), 49)+ " │\n" + " │ "+center(fmt.Sprintf("(bound on host 0.0.0.0 and port %s)", port), 49)+ " │\n" } else { mainLogo += " │ "+center(fmt.Sprintf("%s://%s:%s", scheme, host, port), 49)+ " │\n" From 13f0d5bb61c217771624eb7aeb38868826a47969 Mon Sep 17 00:00:00 2001 From: hi019 <65871571+hi019@users.noreply.github.com> Date: Tue, 9 Mar 2021 09:29:47 -0500 Subject: [PATCH 44/44] Remove global variable --- middleware/csrf/csrf.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index c840fd7d..854a9cf9 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -6,12 +6,10 @@ import ( "github.com/gofiber/fiber/v2" ) -var cfg Config - // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config - cfg = configDefault(config...) + cfg := configDefault(config...) // Create manager to simplify storage operations ( see manager.go ) manager := newManager(cfg.Storage)