Skip to content

Commit

Permalink
Server supports relative paths (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
tekig authored Jun 3, 2023
1 parent 4d609e1 commit 9b0d865
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 31 deletions.
36 changes: 26 additions & 10 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,51 @@
package main

import (
"fmt"
"io"
"log"
"os"

"github.com/OCAP2/web/server"
"github.com/labstack/echo/v4"
"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)
Expand All @@ -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
}
5 changes: 4 additions & 1 deletion server/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
61 changes: 44 additions & 17 deletions server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

const CacheDuration = 7 * 24 * time.Hour
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
50 changes: 50 additions & 0 deletions server/handler_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
4 changes: 3 additions & 1 deletion server/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions setting.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"listen": "127.0.0.1:5000",
"prefixURL": "/aar/",
"secret": "same-secret",
"logger": true,
"customize": {
Expand Down
4 changes: 2 additions & 2 deletions static/scripts/ocap.ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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");
Expand Down

0 comments on commit 9b0d865

Please sign in to comment.