diff --git a/cmd/main.go b/cmd/main.go index 2d65fa2b..726b5bba 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,7 +1,9 @@ package main import ( + "fmt" "io" + "log" "os" "github.com/OCAP2/web/server" @@ -9,31 +11,41 @@ import ( "github.com/labstack/echo/v4/middleware" ) -func check(err error) { - if err != nil { - panic(err) +func main() { + if err := app(); err != nil { + log.Panicln(err) } } -func main() { +func app() error { setting, err := server.NewSetting() - check(err) + if err != nil { + return fmt.Errorf("setting: %w", err) + } operation, err := server.NewRepoOperation(setting.DB) - check(err) + if err != nil { + return fmt.Errorf("operation: %w", err) + } marker, err := server.NewRepoMarker(setting.Markers) - check(err) + if err != nil { + return fmt.Errorf("marker: %w", err) + } ammo, err := server.NewRepoAmmo(setting.Ammo) - check(err) + if err != nil { + return fmt.Errorf("ammo: %w", err) + } e := echo.New() loggerConfig := middleware.DefaultLoggerConfig if setting.Logger { flog, err := os.OpenFile("ocap.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) - check(err) + if err != nil { + return fmt.Errorf("open logger file: %w", err) + } defer flog.Close() loggerConfig.Output = io.MultiWriter(os.Stdout, flog) @@ -45,5 +57,9 @@ func main() { server.NewHandler(e, operation, marker, ammo, setting) err = e.Start(setting.Listen) - check(err) + if err != nil { + return fmt.Errorf("start server: %w", err) + } + + return nil } diff --git a/server/error.go b/server/error.go index e9b40366..d85207f9 100644 --- a/server/error.go +++ b/server/error.go @@ -2,4 +2,7 @@ package server import "errors" -var ErrNotFound = errors.New("not found") +var ( + ErrNotFound = errors.New("not found") + ErrInvalidPath = errors.New("invalid path") +) diff --git a/server/handler.go b/server/handler.go index 21a42d41..33560ff6 100644 --- a/server/handler.go +++ b/server/handler.go @@ -13,6 +13,7 @@ import ( "time" "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" ) const CacheDuration = 7 * 24 * time.Hour @@ -45,47 +46,57 @@ func NewHandler( e.Use(hdlr.errorHandler) - e.GET( + prefixURL := strings.TrimRight(hdlr.setting.PrefixURL, "/") + g := e.Group(prefixURL) + + g.GET( "/api/v1/operations", hdlr.GetOperations, ) - e.POST( + g.POST( "/api/v1/operations/add", hdlr.StoreOperation, ) - e.GET( + g.GET( "/api/v1/customize", hdlr.GetCustomize, ) - e.GET( + g.GET( "/api/version", hdlr.GetVersion, ) - e.GET( + g.GET( "/data/:name", hdlr.GetCapture, hdlr.cacheControl(CacheDuration), ) - e.GET( + g.GET( "/images/markers/:name/:color", hdlr.GetMarker, hdlr.cacheControl(CacheDuration), ) - e.GET( + g.GET( "/images/markers/magicons/:name", hdlr.GetAmmo, hdlr.cacheControl(CacheDuration), ) - e.GET( + g.GET( "/images/maps/*", hdlr.GetMapTitle, hdlr.cacheControl(CacheDuration), ) - e.GET( + g.GET( "/*", hdlr.GetStatic, hdlr.cacheControl(0), ) + g.GET( + "", + hdlr.GetStatic, + middleware.AddTrailingSlashWithConfig(middleware.TrailingSlashConfig{ + RedirectCode: http.StatusMovedPermanently, + }), + ) } func (*Handler) cacheControl(duration time.Duration) echo.MiddlewareFunc { @@ -242,23 +253,25 @@ func (h *Handler) GetMarker(c echo.Context) error { } func (h *Handler) GetMapTitle(c echo.Context) error { - upath, err := url.PathUnescape(c.Param("*")) + relativePath, err := paramPath(c, "*") if err != nil { - return err + return fmt.Errorf("clean path: %s: %w", err.Error(), ErrNotFound) } - upath = filepath.Join(h.setting.Maps, filepath.Clean("/"+upath)) - return c.File(upath) + absolutePath := filepath.Join(h.setting.Maps, relativePath) + + return c.File(absolutePath) } func (h *Handler) GetStatic(c echo.Context) error { - upath, err := url.PathUnescape(c.Param("*")) + relativePath, err := paramPath(c, "*") if err != nil { - return err + return fmt.Errorf("clean path: %s: %w", err.Error(), ErrNotFound) } - upath = filepath.Join(h.setting.Static, filepath.Clean("/"+upath)) - return c.File(upath) + absolutePath := filepath.Join(h.setting.Static, relativePath) + + return c.File(absolutePath) } func (h *Handler) GetAmmo(c echo.Context) error { @@ -289,3 +302,17 @@ func (h *Handler) GetAmmo(c echo.Context) error { return c.File(upath) } + +func paramPath(c echo.Context, param string) (string, error) { + urlPath, err := url.PathUnescape(c.Param(param)) + if err != nil { + return "", fmt.Errorf("path unescape: %w", err) + } + + cleanPath := filepath.Clean("/" + urlPath) + if cleanPath != "/"+urlPath { + return "", ErrInvalidPath + } + + return cleanPath, nil +} diff --git a/server/handler_test.go b/server/handler_test.go new file mode 100644 index 00000000..e5658230 --- /dev/null +++ b/server/handler_test.go @@ -0,0 +1,50 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/labstack/echo/v4" +) + +type MockContext struct { + param string + echo.Context +} + +func (c *MockContext) Param(_ string) string { + return c.param +} + +func Test_cleanPath(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", nil) + rec := httptest.NewRecorder() + + tests := []struct { + path string + want string + wantErr bool + }{ + {"", "/", false}, + {"images/favicon.png", "/images/favicon.png", false}, + {"/images/favicon.png", "", true}, + {"//images/favicon.png", "", true}, + {"//../../images/favicon.png", "", true}, + } + for _, tt := range tests { + c := &MockContext{ + param: tt.path, + Context: e.NewContext(req, rec), + } + got, err := paramPath(c, tt.path) + if (err != nil) != tt.wantErr { + t.Errorf("cleanPath(%s) error = %v, wantErr %v", tt.path, err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("cleanPath(%s) = %v, want %v", tt.path, got, tt.want) + } + } +} diff --git a/server/setting.go b/server/setting.go index 187881c6..d4704d97 100644 --- a/server/setting.go +++ b/server/setting.go @@ -9,6 +9,7 @@ import ( type Setting struct { Listen string `json:"listen" yaml:"listen"` + PrefixURL string `json:"prefixURL" yaml:"prefixURL"` Secret string `json:"secret" yaml:"secret"` DB string `json:"db" yaml:"db"` Markers string `json:"markers" yaml:"markers"` @@ -42,6 +43,7 @@ func NewSetting() (setting Setting, err error) { viper.AddConfigPath(".") viper.SetDefault("listen", "127.0.0.1:5000") + viper.SetDefault("prefixURL", "") viper.SetDefault("db", "data.db") viper.SetDefault("markers", "markers") viper.SetDefault("ammo", "ammo") @@ -52,7 +54,7 @@ func NewSetting() (setting Setting, err error) { viper.SetDefault("customize.websiteLogoSize", "32px") // workaround for https://github.com/spf13/viper/issues/761 - envKeys := []string{"listen", "secret", "db", "markers", "ammo", "maps", "data", "static", "customize.websiteurl", "customize.websitelogo", "customize.websitelogosize", "customize.disableKillCount"} + envKeys := []string{"listen", "prefixURL", "secret", "db", "markers", "ammo", "maps", "data", "static", "customize.websiteurl", "customize.websitelogo", "customize.websitelogosize", "customize.disableKillCount"} for _, key := range envKeys { env := strings.ToUpper(strings.ReplaceAll(key, ".", "_")) if err = viper.BindEnv(key, env); err != nil { diff --git a/setting.json b/setting.json index cb7d64d9..b7088097 100644 --- a/setting.json +++ b/setting.json @@ -1,5 +1,6 @@ { "listen": "127.0.0.1:5000", + "prefixURL": "/aar/", "secret": "same-secret", "logger": true, "customize": { diff --git a/static/scripts/ocap.ui.js b/static/scripts/ocap.ui.js index a9df2907..2f063a86 100644 --- a/static/scripts/ocap.ui.js +++ b/static/scripts/ocap.ui.js @@ -496,7 +496,7 @@ class UI { var DateNewer = calendar1.value; var DateOlder = calendar2.value; - return fetch(`/api/v1/operations?tag=${tag}&name=${name}&newer=${DateNewer}&older=${DateOlder}`, { + return fetch(`api/v1/operations?tag=${tag}&name=${name}&newer=${DateNewer}&older=${DateOlder}`, { cache: "no-cache" }) .then((response) => response.json()) @@ -817,7 +817,7 @@ class UI { } updateCustomize() { - return fetch("/api/v1/customize") + return fetch("api/v1/customize") .then(response => response.json()) .then((data) => { const container = document.getElementById("container");