diff --git a/.woodpecker.yml b/.woodpecker.yml index ba44f82..75c82f3 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,4 +1,4 @@ -pipeline: +steps: # use vendor to cache dependencies vendor: image: golang:1.20 diff --git a/server/context/context.go b/server/context/context.go index 481fee2..6650164 100644 --- a/server/context/context.go +++ b/server/context/context.go @@ -48,11 +48,9 @@ func (c *Context) Redirect(uri string, statusCode int) { http.Redirect(c.RespWriter, c.Req, uri, statusCode) } -// Path returns requested path. -// -// The returned bytes are valid until your request handler returns. +// Path returns the cleaned requested path. func (c *Context) Path() string { - return c.Req.URL.Path + return utils.CleanPath(c.Req.URL.Path) } func (c *Context) Host() string { diff --git a/server/database/xorm.go b/server/database/xorm.go index a14f887..32d5bc2 100644 --- a/server/database/xorm.go +++ b/server/database/xorm.go @@ -64,7 +64,7 @@ func (x xDB) Put(domain string, cert *certificate.Resource) error { } defer sess.Close() - if exist, _ := sess.ID(c.Domain).Exist(); exist { + if exist, _ := sess.ID(c.Domain).Exist(new(Cert)); exist { if _, err := sess.ID(c.Domain).Update(c); err != nil { return err } diff --git a/server/database/xorm_test.go b/server/database/xorm_test.go index 9c032ee..50d8a7f 100644 --- a/server/database/xorm_test.go +++ b/server/database/xorm_test.go @@ -37,7 +37,7 @@ func TestSanitizeWildcardCerts(t *testing.T) { })) // update existing cert - assert.Error(t, certDB.Put(".wildcard.de", &certificate.Resource{ + assert.NoError(t, certDB.Put(".wildcard.de", &certificate.Resource{ Domain: "*.wildcard.de", Certificate: localhost_mock_directory_certificate, })) diff --git a/server/handler/handler_test.go b/server/handler/handler_test.go index 626564a..d716ef4 100644 --- a/server/handler/handler_test.go +++ b/server/handler/handler_test.go @@ -1,6 +1,7 @@ package handler import ( + "net/http" "net/http/httptest" "testing" "time" @@ -24,7 +25,7 @@ func TestHandlerPerformance(t *testing.T) { testCase := func(uri string, status int) { t.Run(uri, func(t *testing.T) { - req := httptest.NewRequest("GET", uri, nil) + req := httptest.NewRequest("GET", uri, http.NoBody) w := httptest.NewRecorder() log.Printf("Start: %v\n", time.Now()) diff --git a/server/utils/utils.go b/server/utils/utils.go index 30f948d..91ed359 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -1,6 +1,8 @@ package utils import ( + "net/url" + "path" "strings" ) @@ -11,3 +13,15 @@ func TrimHostPort(host string) string { } return host } + +func CleanPath(uriPath string) string { + unescapedPath, _ := url.PathUnescape(uriPath) + cleanedPath := path.Join("/", unescapedPath) + + // If the path refers to a directory, add a trailing slash. + if !strings.HasSuffix(cleanedPath, "/") && (strings.HasSuffix(unescapedPath, "/") || strings.HasSuffix(unescapedPath, "/.") || strings.HasSuffix(unescapedPath, "/..")) { + cleanedPath += "/" + } + + return cleanedPath +} diff --git a/server/utils/utils_test.go b/server/utils/utils_test.go index 2532392..b8fcea9 100644 --- a/server/utils/utils_test.go +++ b/server/utils/utils_test.go @@ -11,3 +11,59 @@ func TestTrimHostPort(t *testing.T) { assert.EqualValues(t, "", TrimHostPort(":")) assert.EqualValues(t, "example.com", TrimHostPort("example.com:80")) } + +// TestCleanPath is mostly copied from fasthttp, to keep the behaviour we had before migrating away from it. +// Source (MIT licensed): https://github.com/valyala/fasthttp/blob/v1.48.0/uri_test.go#L154 +// Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors +func TestCleanPath(t *testing.T) { + // double slash + testURIPathNormalize(t, "/aa//bb", "/aa/bb") + + // triple slash + testURIPathNormalize(t, "/x///y/", "/x/y/") + + // multi slashes + testURIPathNormalize(t, "/abc//de///fg////", "/abc/de/fg/") + + // encoded slashes + testURIPathNormalize(t, "/xxxx%2fyyy%2f%2F%2F", "/xxxx/yyy/") + + // dotdot + testURIPathNormalize(t, "/aaa/..", "/") + + // dotdot with trailing slash + testURIPathNormalize(t, "/xxx/yyy/../", "/xxx/") + + // multi dotdots + testURIPathNormalize(t, "/aaa/bbb/ccc/../../ddd", "/aaa/ddd") + + // dotdots separated by other data + testURIPathNormalize(t, "/a/b/../c/d/../e/..", "/a/c/") + + // too many dotdots + testURIPathNormalize(t, "/aaa/../../../../xxx", "/xxx") + testURIPathNormalize(t, "/../../../../../..", "/") + testURIPathNormalize(t, "/../../../../../../", "/") + + // encoded dotdots + testURIPathNormalize(t, "/aaa%2Fbbb%2F%2E.%2Fxxx", "/aaa/xxx") + + // double slash with dotdots + testURIPathNormalize(t, "/aaa////..//b", "/b") + + // fake dotdot + testURIPathNormalize(t, "/aaa/..bbb/ccc/..", "/aaa/..bbb/") + + // single dot + testURIPathNormalize(t, "/a/./b/././c/./d.html", "/a/b/c/d.html") + testURIPathNormalize(t, "./foo/", "/foo/") + testURIPathNormalize(t, "./../.././../../aaa/bbb/../../../././../", "/") + testURIPathNormalize(t, "./a/./.././../b/./foo.html", "/b/foo.html") +} + +func testURIPathNormalize(t *testing.T, requestURI, expectedPath string) { + cleanedPath := CleanPath(requestURI) + if cleanedPath != expectedPath { + t.Fatalf("Unexpected path %q. Expected %q. requestURI=%q", cleanedPath, expectedPath, requestURI) + } +}