1
0
mirror of https://github.com/labstack/echo.git synced 2024-12-24 20:14:31 +02:00

Merge branch 'master' into routing_misses_performance_improvements

This commit is contained in:
Pablo Andres Fuente 2020-12-16 01:59:13 +00:00
commit 045bec51d4
5 changed files with 199 additions and 50 deletions

View File

@ -42,11 +42,14 @@ For older versions, please use the latest v3 tag.
## Benchmarks ## Benchmarks
Date: 2018/03/15<br> Date: 2020/11/11<br>
Source: https://github.com/vishr/web-framework-benchmark<br> Source: https://github.com/vishr/web-framework-benchmark<br>
Lower is better! Lower is better!
<img src="https://i.imgur.com/I32VdMJ.png"> <img src="https://i.imgur.com/qwPNQbl.png">
<img src="https://i.imgur.com/s8yKQjx.png">
The benchmarks above were run on an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz
## [Guide](https://echo.labstack.com/guide) ## [Guide](https://echo.labstack.com/guide)

25
echo.go
View File

@ -49,7 +49,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime" "runtime"
@ -92,6 +91,7 @@ type (
Renderer Renderer Renderer Renderer
Logger Logger Logger Logger
IPExtractor IPExtractor IPExtractor IPExtractor
ListenerNetwork string
} }
// Route contains a handler and information for matching against requests. // Route contains a handler and information for matching against requests.
@ -281,6 +281,7 @@ var (
ErrInvalidRedirectCode = errors.New("invalid redirect status code") ErrInvalidRedirectCode = errors.New("invalid redirect status code")
ErrCookieNotFound = errors.New("cookie not found") ErrCookieNotFound = errors.New("cookie not found")
ErrInvalidCertOrKeyType = errors.New("invalid cert or key type, must be string or []byte") ErrInvalidCertOrKeyType = errors.New("invalid cert or key type, must be string or []byte")
ErrInvalidListenerNetwork = errors.New("invalid listener network")
) )
// Error handlers // Error handlers
@ -302,9 +303,10 @@ func New() (e *Echo) {
AutoTLSManager: autocert.Manager{ AutoTLSManager: autocert.Manager{
Prompt: autocert.AcceptTOS, Prompt: autocert.AcceptTOS,
}, },
Logger: log.New("echo"), Logger: log.New("echo"),
colorer: color.New(), colorer: color.New(),
maxParam: new(int), maxParam: new(int),
ListenerNetwork: "tcp",
} }
e.Server.Handler = e e.Server.Handler = e
e.TLSServer.Handler = e e.TLSServer.Handler = e
@ -483,7 +485,7 @@ func (common) static(prefix, root string, get func(string, HandlerFunc, ...Middl
return err return err
} }
name := filepath.Join(root, path.Clean("/"+p)) // "/"+ for security name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
// The access path does not exist // The access path does not exist
@ -714,7 +716,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
if s.TLSConfig == nil { if s.TLSConfig == nil {
if e.Listener == nil { if e.Listener == nil {
e.Listener, err = newListener(s.Addr) e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
if err != nil { if err != nil {
return err return err
} }
@ -725,7 +727,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
return s.Serve(e.Listener) return s.Serve(e.Listener)
} }
if e.TLSListener == nil { if e.TLSListener == nil {
l, err := newListener(s.Addr) l, err := newListener(s.Addr, e.ListenerNetwork)
if err != nil { if err != nil {
return err return err
} }
@ -754,7 +756,7 @@ func (e *Echo) StartH2CServer(address string, h2s *http2.Server) (err error) {
} }
if e.Listener == nil { if e.Listener == nil {
e.Listener, err = newListener(s.Addr) e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
if err != nil { if err != nil {
return err return err
} }
@ -875,8 +877,11 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
return return
} }
func newListener(address string) (*tcpKeepAliveListener, error) { func newListener(address, network string) (*tcpKeepAliveListener, error) {
l, err := net.Listen("tcp", address) if network != "tcp" && network != "tcp4" && network != "tcp6" {
return nil, ErrInvalidListenerNetwork
}
l, err := net.Listen(network, address)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
stdContext "context" stdContext "context"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -59,45 +60,105 @@ func TestEcho(t *testing.T) {
} }
func TestEchoStatic(t *testing.T) { func TestEchoStatic(t *testing.T) {
e := New() var testCases = []struct {
name string
givenPrefix string
givenRoot string
whenURL string
expectStatus int
expectHeaderLocation string
expectBodyStartsWith string
}{
{
name: "ok",
givenPrefix: "/images",
givenRoot: "_fixture/images",
whenURL: "/images/walle.png",
expectStatus: http.StatusOK,
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
},
{
name: "No file",
givenPrefix: "/images",
givenRoot: "_fixture/scripts",
whenURL: "/images/bolt.png",
expectStatus: http.StatusNotFound,
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
},
{
name: "Directory",
givenPrefix: "/images",
givenRoot: "_fixture/images",
whenURL: "/images/",
expectStatus: http.StatusNotFound,
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
},
{
name: "Directory Redirect",
givenPrefix: "/",
givenRoot: "_fixture",
whenURL: "/folder",
expectStatus: http.StatusMovedPermanently,
expectHeaderLocation: "/folder/",
expectBodyStartsWith: "",
},
{
name: "Directory with index.html",
givenPrefix: "/",
givenRoot: "_fixture",
whenURL: "/",
expectStatus: http.StatusOK,
expectBodyStartsWith: "<!doctype html>",
},
{
name: "Sub-directory with index.html",
givenPrefix: "/",
givenRoot: "_fixture",
whenURL: "/folder/",
expectStatus: http.StatusOK,
expectBodyStartsWith: "<!doctype html>",
},
{
name: "do not allow directory traversal (backslash - windows separator)",
givenPrefix: "/",
givenRoot: "_fixture/",
whenURL: `/..\\middleware/basic_auth.go`,
expectStatus: http.StatusNotFound,
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
},
{
name: "do not allow directory traversal (slash - unix separator)",
givenPrefix: "/",
givenRoot: "_fixture/",
whenURL: `/../middleware/basic_auth.go`,
expectStatus: http.StatusNotFound,
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
},
}
assert := assert.New(t) for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// OK e := New()
e.Static("/images", "_fixture/images") e.Static(tc.givenPrefix, tc.givenRoot)
c, b := request(http.MethodGet, "/images/walle.png", e) req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
assert.Equal(http.StatusOK, c) rec := httptest.NewRecorder()
assert.NotEmpty(b) e.ServeHTTP(rec, req)
assert.Equal(t, tc.expectStatus, rec.Code)
// No file body := rec.Body.String()
e.Static("/images", "_fixture/scripts") if tc.expectBodyStartsWith != "" {
c, _ = request(http.MethodGet, "/images/bolt.png", e) assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith))
assert.Equal(http.StatusNotFound, c) } else {
assert.Equal(t, "", body)
// Directory }
e.Static("/images", "_fixture/images")
c, _ = request(http.MethodGet, "/images/", e)
assert.Equal(http.StatusNotFound, c)
// Directory Redirect
e.Static("/", "_fixture")
req := httptest.NewRequest(http.MethodGet, "/folder", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(http.StatusMovedPermanently, rec.Code)
assert.Equal("/folder/", rec.HeaderMap["Location"][0])
// Directory with index.html
e.Static("/", "_fixture")
c, r := request(http.MethodGet, "/", e)
assert.Equal(http.StatusOK, c)
assert.Equal(true, strings.HasPrefix(r, "<!doctype html>"))
// Sub-directory with index.html
c, r = request(http.MethodGet, "/folder/", e)
assert.Equal(http.StatusOK, c)
assert.Equal(true, strings.HasPrefix(r, "<!doctype html>"))
if tc.expectHeaderLocation != "" {
assert.Equal(t, tc.expectHeaderLocation, rec.Result().Header["Location"][0])
} else {
_, ok := rec.Result().Header["Location"]
assert.False(t, ok)
}
})
}
} }
func TestEchoFile(t *testing.T) { func TestEchoFile(t *testing.T) {
@ -658,6 +719,69 @@ func TestEchoShutdown(t *testing.T) {
assert.Equal(t, err.Error(), "http: Server closed") assert.Equal(t, err.Error(), "http: Server closed")
} }
var listenerNetworkTests = []struct {
test string
network string
address string
}{
{"tcp ipv4 address", "tcp", "127.0.0.1:1323"},
{"tcp ipv6 address", "tcp", "[::1]:1323"},
{"tcp4 ipv4 address", "tcp4", "127.0.0.1:1323"},
{"tcp6 ipv6 address", "tcp6", "[::1]:1323"},
}
func TestEchoListenerNetwork(t *testing.T) {
for _, tt := range listenerNetworkTests {
t.Run(tt.test, func(t *testing.T) {
e := New()
e.ListenerNetwork = tt.network
// HandlerFunc
e.GET("/ok", func(c Context) error {
return c.String(http.StatusOK, "OK")
})
errCh := make(chan error)
go func() {
errCh <- e.Start(tt.address)
}()
time.Sleep(200 * time.Millisecond)
if resp, err := http.Get(fmt.Sprintf("http://%s/ok", tt.address)); err == nil {
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
if body, err := ioutil.ReadAll(resp.Body); err == nil {
assert.Equal(t, "OK", string(body))
} else {
assert.Fail(t, err.Error())
}
} else {
assert.Fail(t, err.Error())
}
if err := e.Close(); err != nil {
t.Fatal(err)
}
})
}
}
func TestEchoListenerNetworkInvalid(t *testing.T) {
e := New()
e.ListenerNetwork = "unix"
// HandlerFunc
e.GET("/ok", func(c Context) error {
return c.String(http.StatusOK, "OK")
})
assert.Equal(t, ErrInvalidListenerNetwork, e.Start(":1323"))
}
func TestEchoReverse(t *testing.T) { func TestEchoReverse(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)

View File

@ -31,3 +31,20 @@ func TestRequestID(t *testing.T) {
h(c) h(c)
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "customGenerator") assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "customGenerator")
} }
func TestRequestID_IDNotAltered(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Add(echo.HeaderXRequestID, "<sample-request-id>")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "test")
}
rid := RequestIDWithConfig(RequestIDConfig{})
h := rid(handler)
_ = h(c)
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "<sample-request-id>")
}

View File

@ -167,7 +167,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
if err != nil { if err != nil {
return return
} }
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security name := filepath.Join(config.Root, filepath.Clean("/"+p)) // "/"+ for security
if config.IgnoreBase { if config.IgnoreBase {
routePath := path.Base(strings.TrimRight(c.Path(), "/*")) routePath := path.Base(strings.TrimRight(c.Path(), "/*"))