Skip to content

Commit 045bec5

Browse files
committed
Merge branch 'master' into routing_misses_performance_improvements
2 parents 71325a6 + 4422e3b commit 045bec5

File tree

5 files changed

+199
-50
lines changed

5 files changed

+199
-50
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,14 @@ For older versions, please use the latest v3 tag.
4242

4343
## Benchmarks
4444

45-
Date: 2018/03/15<br>
45+
Date: 2020/11/11<br>
4646
Source: https://github.com/vishr/web-framework-benchmark<br>
4747
Lower is better!
4848

49-
<img src="https://i.imgur.com/I32VdMJ.png">
49+
<img src="https://i.imgur.com/qwPNQbl.png">
50+
<img src="https://i.imgur.com/s8yKQjx.png">
51+
52+
The benchmarks above were run on an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz
5053

5154
## [Guide](https://echo.labstack.com/guide)
5255

echo.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ import (
4949
"net/http"
5050
"net/url"
5151
"os"
52-
"path"
5352
"path/filepath"
5453
"reflect"
5554
"runtime"
@@ -92,6 +91,7 @@ type (
9291
Renderer Renderer
9392
Logger Logger
9493
IPExtractor IPExtractor
94+
ListenerNetwork string
9595
}
9696

9797
// Route contains a handler and information for matching against requests.
@@ -281,6 +281,7 @@ var (
281281
ErrInvalidRedirectCode = errors.New("invalid redirect status code")
282282
ErrCookieNotFound = errors.New("cookie not found")
283283
ErrInvalidCertOrKeyType = errors.New("invalid cert or key type, must be string or []byte")
284+
ErrInvalidListenerNetwork = errors.New("invalid listener network")
284285
)
285286

286287
// Error handlers
@@ -302,9 +303,10 @@ func New() (e *Echo) {
302303
AutoTLSManager: autocert.Manager{
303304
Prompt: autocert.AcceptTOS,
304305
},
305-
Logger: log.New("echo"),
306-
colorer: color.New(),
307-
maxParam: new(int),
306+
Logger: log.New("echo"),
307+
colorer: color.New(),
308+
maxParam: new(int),
309+
ListenerNetwork: "tcp",
308310
}
309311
e.Server.Handler = e
310312
e.TLSServer.Handler = e
@@ -483,7 +485,7 @@ func (common) static(prefix, root string, get func(string, HandlerFunc, ...Middl
483485
return err
484486
}
485487

486-
name := filepath.Join(root, path.Clean("/"+p)) // "/"+ for security
488+
name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security
487489
fi, err := os.Stat(name)
488490
if err != nil {
489491
// The access path does not exist
@@ -714,7 +716,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
714716

715717
if s.TLSConfig == nil {
716718
if e.Listener == nil {
717-
e.Listener, err = newListener(s.Addr)
719+
e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
718720
if err != nil {
719721
return err
720722
}
@@ -725,7 +727,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
725727
return s.Serve(e.Listener)
726728
}
727729
if e.TLSListener == nil {
728-
l, err := newListener(s.Addr)
730+
l, err := newListener(s.Addr, e.ListenerNetwork)
729731
if err != nil {
730732
return err
731733
}
@@ -754,7 +756,7 @@ func (e *Echo) StartH2CServer(address string, h2s *http2.Server) (err error) {
754756
}
755757

756758
if e.Listener == nil {
757-
e.Listener, err = newListener(s.Addr)
759+
e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
758760
if err != nil {
759761
return err
760762
}
@@ -875,8 +877,11 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
875877
return
876878
}
877879

878-
func newListener(address string) (*tcpKeepAliveListener, error) {
879-
l, err := net.Listen("tcp", address)
880+
func newListener(address, network string) (*tcpKeepAliveListener, error) {
881+
if network != "tcp" && network != "tcp4" && network != "tcp6" {
882+
return nil, ErrInvalidListenerNetwork
883+
}
884+
l, err := net.Listen(network, address)
880885
if err != nil {
881886
return nil, err
882887
}

echo_test.go

Lines changed: 161 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
stdContext "context"
66
"errors"
7+
"fmt"
78
"io/ioutil"
89
"net/http"
910
"net/http/httptest"
@@ -59,45 +60,105 @@ func TestEcho(t *testing.T) {
5960
}
6061

6162
func TestEchoStatic(t *testing.T) {
62-
e := New()
63-
64-
assert := assert.New(t)
65-
66-
// OK
67-
e.Static("/images", "_fixture/images")
68-
c, b := request(http.MethodGet, "/images/walle.png", e)
69-
assert.Equal(http.StatusOK, c)
70-
assert.NotEmpty(b)
71-
72-
// No file
73-
e.Static("/images", "_fixture/scripts")
74-
c, _ = request(http.MethodGet, "/images/bolt.png", e)
75-
assert.Equal(http.StatusNotFound, c)
76-
77-
// Directory
78-
e.Static("/images", "_fixture/images")
79-
c, _ = request(http.MethodGet, "/images/", e)
80-
assert.Equal(http.StatusNotFound, c)
81-
82-
// Directory Redirect
83-
e.Static("/", "_fixture")
84-
req := httptest.NewRequest(http.MethodGet, "/folder", nil)
85-
rec := httptest.NewRecorder()
86-
e.ServeHTTP(rec, req)
87-
assert.Equal(http.StatusMovedPermanently, rec.Code)
88-
assert.Equal("/folder/", rec.HeaderMap["Location"][0])
89-
90-
// Directory with index.html
91-
e.Static("/", "_fixture")
92-
c, r := request(http.MethodGet, "/", e)
93-
assert.Equal(http.StatusOK, c)
94-
assert.Equal(true, strings.HasPrefix(r, "<!doctype html>"))
63+
var testCases = []struct {
64+
name string
65+
givenPrefix string
66+
givenRoot string
67+
whenURL string
68+
expectStatus int
69+
expectHeaderLocation string
70+
expectBodyStartsWith string
71+
}{
72+
{
73+
name: "ok",
74+
givenPrefix: "/images",
75+
givenRoot: "_fixture/images",
76+
whenURL: "/images/walle.png",
77+
expectStatus: http.StatusOK,
78+
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
79+
},
80+
{
81+
name: "No file",
82+
givenPrefix: "/images",
83+
givenRoot: "_fixture/scripts",
84+
whenURL: "/images/bolt.png",
85+
expectStatus: http.StatusNotFound,
86+
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
87+
},
88+
{
89+
name: "Directory",
90+
givenPrefix: "/images",
91+
givenRoot: "_fixture/images",
92+
whenURL: "/images/",
93+
expectStatus: http.StatusNotFound,
94+
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
95+
},
96+
{
97+
name: "Directory Redirect",
98+
givenPrefix: "/",
99+
givenRoot: "_fixture",
100+
whenURL: "/folder",
101+
expectStatus: http.StatusMovedPermanently,
102+
expectHeaderLocation: "/folder/",
103+
expectBodyStartsWith: "",
104+
},
105+
{
106+
name: "Directory with index.html",
107+
givenPrefix: "/",
108+
givenRoot: "_fixture",
109+
whenURL: "/",
110+
expectStatus: http.StatusOK,
111+
expectBodyStartsWith: "<!doctype html>",
112+
},
113+
{
114+
name: "Sub-directory with index.html",
115+
givenPrefix: "/",
116+
givenRoot: "_fixture",
117+
whenURL: "/folder/",
118+
expectStatus: http.StatusOK,
119+
expectBodyStartsWith: "<!doctype html>",
120+
},
121+
{
122+
name: "do not allow directory traversal (backslash - windows separator)",
123+
givenPrefix: "/",
124+
givenRoot: "_fixture/",
125+
whenURL: `/..\\middleware/basic_auth.go`,
126+
expectStatus: http.StatusNotFound,
127+
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
128+
},
129+
{
130+
name: "do not allow directory traversal (slash - unix separator)",
131+
givenPrefix: "/",
132+
givenRoot: "_fixture/",
133+
whenURL: `/../middleware/basic_auth.go`,
134+
expectStatus: http.StatusNotFound,
135+
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
136+
},
137+
}
95138

96-
// Sub-directory with index.html
97-
c, r = request(http.MethodGet, "/folder/", e)
98-
assert.Equal(http.StatusOK, c)
99-
assert.Equal(true, strings.HasPrefix(r, "<!doctype html>"))
139+
for _, tc := range testCases {
140+
t.Run(tc.name, func(t *testing.T) {
141+
e := New()
142+
e.Static(tc.givenPrefix, tc.givenRoot)
143+
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
144+
rec := httptest.NewRecorder()
145+
e.ServeHTTP(rec, req)
146+
assert.Equal(t, tc.expectStatus, rec.Code)
147+
body := rec.Body.String()
148+
if tc.expectBodyStartsWith != "" {
149+
assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith))
150+
} else {
151+
assert.Equal(t, "", body)
152+
}
100153

154+
if tc.expectHeaderLocation != "" {
155+
assert.Equal(t, tc.expectHeaderLocation, rec.Result().Header["Location"][0])
156+
} else {
157+
_, ok := rec.Result().Header["Location"]
158+
assert.False(t, ok)
159+
}
160+
})
161+
}
101162
}
102163

103164
func TestEchoFile(t *testing.T) {
@@ -658,6 +719,69 @@ func TestEchoShutdown(t *testing.T) {
658719
assert.Equal(t, err.Error(), "http: Server closed")
659720
}
660721

722+
var listenerNetworkTests = []struct {
723+
test string
724+
network string
725+
address string
726+
}{
727+
{"tcp ipv4 address", "tcp", "127.0.0.1:1323"},
728+
{"tcp ipv6 address", "tcp", "[::1]:1323"},
729+
{"tcp4 ipv4 address", "tcp4", "127.0.0.1:1323"},
730+
{"tcp6 ipv6 address", "tcp6", "[::1]:1323"},
731+
}
732+
733+
func TestEchoListenerNetwork(t *testing.T) {
734+
for _, tt := range listenerNetworkTests {
735+
t.Run(tt.test, func(t *testing.T) {
736+
e := New()
737+
e.ListenerNetwork = tt.network
738+
739+
// HandlerFunc
740+
e.GET("/ok", func(c Context) error {
741+
return c.String(http.StatusOK, "OK")
742+
})
743+
744+
errCh := make(chan error)
745+
746+
go func() {
747+
errCh <- e.Start(tt.address)
748+
}()
749+
750+
time.Sleep(200 * time.Millisecond)
751+
752+
if resp, err := http.Get(fmt.Sprintf("http://%s/ok", tt.address)); err == nil {
753+
defer resp.Body.Close()
754+
assert.Equal(t, http.StatusOK, resp.StatusCode)
755+
756+
if body, err := ioutil.ReadAll(resp.Body); err == nil {
757+
assert.Equal(t, "OK", string(body))
758+
} else {
759+
assert.Fail(t, err.Error())
760+
}
761+
762+
} else {
763+
assert.Fail(t, err.Error())
764+
}
765+
766+
if err := e.Close(); err != nil {
767+
t.Fatal(err)
768+
}
769+
})
770+
}
771+
}
772+
773+
func TestEchoListenerNetworkInvalid(t *testing.T) {
774+
e := New()
775+
e.ListenerNetwork = "unix"
776+
777+
// HandlerFunc
778+
e.GET("/ok", func(c Context) error {
779+
return c.String(http.StatusOK, "OK")
780+
})
781+
782+
assert.Equal(t, ErrInvalidListenerNetwork, e.Start(":1323"))
783+
}
784+
661785
func TestEchoReverse(t *testing.T) {
662786
assert := assert.New(t)
663787

middleware/request_id_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,20 @@ func TestRequestID(t *testing.T) {
3131
h(c)
3232
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "customGenerator")
3333
}
34+
35+
func TestRequestID_IDNotAltered(t *testing.T) {
36+
e := echo.New()
37+
req := httptest.NewRequest(http.MethodGet, "/", nil)
38+
req.Header.Add(echo.HeaderXRequestID, "<sample-request-id>")
39+
40+
rec := httptest.NewRecorder()
41+
c := e.NewContext(req, rec)
42+
handler := func(c echo.Context) error {
43+
return c.String(http.StatusOK, "test")
44+
}
45+
46+
rid := RequestIDWithConfig(RequestIDConfig{})
47+
h := rid(handler)
48+
_ = h(c)
49+
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "<sample-request-id>")
50+
}

middleware/static.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
167167
if err != nil {
168168
return
169169
}
170-
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
170+
name := filepath.Join(config.Root, filepath.Clean("/"+p)) // "/"+ for security
171171

172172
if config.IgnoreBase {
173173
routePath := path.Base(strings.TrimRight(c.Path(), "/*"))

0 commit comments

Comments
 (0)