diff --git a/.vscode/launch.json b/.vscode/launch.json index 9d415a2..6e9374c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,13 +4,19 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Launch file", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${file}" + }, { "type": "chrome", "request": "launch", "name": "Launch Chrome against localhost", - "url": "http://localhost:1234", + "url": "http://localhost:5173", "webRoot": "${workspaceFolder}", - "breakOnLoad": true, "sourceMapPathOverrides": { "/__parcel_source_root/*": "${webRoot}/*" } diff --git a/go.mod b/go.mod index 9902091..eb02485 100644 --- a/go.mod +++ b/go.mod @@ -3,29 +3,12 @@ module github.com/marcopeocchi/yt-dlp-web-ui go 1.20 require ( + github.com/go-chi/chi/v5 v5.0.10 github.com/goccy/go-json v0.10.2 - github.com/gofiber/fiber/v2 v2.47.0 - github.com/gofiber/websocket/v2 v2.2.1 github.com/golang-jwt/jwt/v5 v5.0.0 github.com/google/uuid v1.3.0 + github.com/gorilla/websocket v1.5.0 github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa golang.org/x/sys v0.9.0 gopkg.in/yaml.v3 v3.0.1 ) - -require ( - github.com/andybalholm/brotli v1.0.5 // indirect - github.com/fasthttp/websocket v1.5.3 // indirect - github.com/klauspost/compress v1.16.6 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/philhofer/fwd v1.1.2 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect - github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect - github.com/tinylib/msgp v1.1.8 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.48.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect -) diff --git a/go.sum b/go.sum index eb9af97..7f15cb1 100644 --- a/go.sum +++ b/go.sum @@ -1,94 +1,17 @@ -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek= -github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gofiber/fiber/v2 v2.47.0 h1:EN5lHVCc+Pyqh5OEsk8fzRiifgwpbrP0rulQ4iNf3fs= -github.com/gofiber/fiber/v2 v2.47.0/go.mod h1:mbFMVN1lQuzziTkkakgtKKdjfsXSw9BKR5lmcNksUoU= -github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w= -github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk= -github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa h1:uaAQLGhN4SesB9inOQ1Q6EH+BwTWHQOvwhR0TIJvnYc= github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa/go.mod h1:RvfVo/6Sbnfra9kkvIxDW8NYOOaYsHjF0DdtMCs9cdo= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= -github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= -github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= -github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8= -github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= -github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= -github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= -github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= -github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= -github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc= -github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/server/internal/memory_db.go b/server/internal/memory_db.go index 1c2e8c1..a177b32 100644 --- a/server/internal/memory_db.go +++ b/server/internal/memory_db.go @@ -1,14 +1,13 @@ package internal import ( + "encoding/gob" "errors" "fmt" "log" "os" "sync" - "github.com/goccy/go-json" - "github.com/google/uuid" "github.com/marcopeocchi/yt-dlp-web-ui/server/cli" ) @@ -93,25 +92,36 @@ func (m *MemoryDB) All() *[]ProcessResponse { func (m *MemoryDB) Persist() { running := m.All() - session, err := json.Marshal(Session{ - Processes: *running, - }) + fd, err := os.Create("session.dat") if err != nil { - log.Println(cli.Red, "Failed to persist database", cli.Reset) - return + log.Println(cli.Red, "Failed to persist session", cli.Reset) } - err = os.WriteFile("session.dat", session, 0700) - if err != nil { - log.Println(cli.Red, "Failed to persist database", cli.Reset) + session := Session{ + Processes: *running, } + + err = gob.NewEncoder(fd).Encode(session) + if err != nil { + log.Println(cli.Red, "Failed to persist session", cli.Reset) + } + + log.Println(cli.BgBlue, "Successfully serialized session", cli.Reset) } // WIP: Restore a persisted state func (m *MemoryDB) Restore() { - feed, _ := os.ReadFile("session.dat") + fd, err := os.Open("session.dat") + if err != nil { + return + } + session := Session{} - json.Unmarshal(feed, &session) + + err = gob.NewDecoder(fd).Decode(&session) + if err != nil { + return + } for _, proc := range session.Processes { m.table.Store(proc.Id, &Process{ @@ -122,4 +132,6 @@ func (m *MemoryDB) Restore() { DB: m, }) } + + log.Println(cli.BgGreen, "Successfully restored session", cli.Reset) } diff --git a/server/middleware/cors.go b/server/middleware/cors.go new file mode 100644 index 0000000..8841fce --- /dev/null +++ b/server/middleware/cors.go @@ -0,0 +1,15 @@ +package middlewares + +import "net/http" + +// Middleware for applying CORS policy for ALL hosts and for +// allowing ALL request headers. +func CORS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Set("Access-Control-Allow-Credentials", "true") + next.ServeHTTP(w, r) + }) +} diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go index de85ca9..5295fb6 100644 --- a/server/middleware/jwt.go +++ b/server/middleware/jwt.go @@ -2,10 +2,10 @@ package middlewares import ( "fmt" + "net/http" "os" "time" - "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v5" "github.com/marcopeocchi/yt-dlp-web-ui/server/config" ) @@ -14,37 +14,49 @@ const ( TOKEN_COOKIE_NAME = "jwt" ) -var Authenticated = func(c *fiber.Ctx) error { - if !config.Instance().GetConfig().RequireAuth { - return c.Next() - } - - cookie := c.Cookies(TOKEN_COOKIE_NAME) - - if cookie == "" { - return c.Status(fiber.StatusUnauthorized).SendString("invalid token") - } - - token, _ := jwt.Parse(cookie, func(t *jwt.Token) (interface{}, error) { - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) +func Authenticated(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !config.Instance().GetConfig().RequireAuth { + next.ServeHTTP(w, r) + return } - return []byte(os.Getenv("JWT_SECRET")), nil - }) - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - expiresAt, err := time.Parse(time.RFC3339, claims["expiresAt"].(string)) + cookie, err := r.Cookie(TOKEN_COOKIE_NAME) if err != nil { - return c.SendStatus(fiber.StatusInternalServerError) + http.Error(w, "invalid token", http.StatusBadRequest) + return } - if time.Now().After(expiresAt) { - return c.Status(fiber.StatusBadRequest).SendString("expired token") + if cookie == nil { + http.Error(w, "invalid token", http.StatusBadRequest) + return } - } else { - return c.Status(fiber.StatusUnauthorized).SendString("invalid token") - } - return c.Next() + token, _ := jwt.Parse(cookie.Value, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) + } + return []byte(os.Getenv("JWTSECRET")), nil + }) + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + expiresAt, err := time.Parse(time.RFC3339, claims["expiresAt"].(string)) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if time.Now().After(expiresAt) { + http.Error(w, "token expired", http.StatusBadRequest) + return + } + } else { + http.Error(w, "invalid token", http.StatusBadRequest) + return + } + + next.ServeHTTP(w, r) + }) } diff --git a/server/middleware/spa_handler.go b/server/middleware/spa_handler.go new file mode 100644 index 0000000..f729754 --- /dev/null +++ b/server/middleware/spa_handler.go @@ -0,0 +1,93 @@ +package middlewares + +import ( + "fmt" + "io" + "io/fs" + "mime" + "net/http" + "os" + "path/filepath" + "strings" +) + +type SpaHandler struct { + Entrypoint string + Filesystem fs.FS + routes []string +} + +func NewSpaHandler(index string, fs fs.FS) *SpaHandler { + return &SpaHandler{ + Entrypoint: index, + Filesystem: fs, + } +} + +func (s *SpaHandler) AddClientRoute(route string) *SpaHandler { + s.routes = append(s.routes, route) + return s +} + +// Handler for serving a compiled react frontend +// each client-side routes must be provided +func (s *SpaHandler) Handler() http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error( + w, + http.StatusText(http.StatusMethodNotAllowed), + http.StatusMethodNotAllowed, + ) + return + } + + path := filepath.Clean(r.URL.Path) + + // basically all frontend routes are needed :/ + hasRoute := false + for _, route := range s.routes { + hasRoute = strings.HasPrefix(path, route) + if hasRoute { + break + } + } + + if path == "/" || hasRoute { + path = s.Entrypoint + } + + path = strings.TrimPrefix(path, "/") + + file, err := s.Filesystem.Open(path) + + if err != nil { + if os.IsNotExist(err) { + http.NotFound(w, r) + return + } + http.Error( + w, + http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError, + ) + return + } + + contentType := mime.TypeByExtension(filepath.Ext(path)) + w.Header().Set("Content-Type", contentType) + + if strings.HasPrefix(path, "assets/") { + w.Header().Set("Cache-Control", "public, max-age=2592000") + } + + stat, err := file.Stat() + if err == nil && stat.Size() > 0 { + w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size())) + } + + w.WriteHeader(http.StatusOK) + + io.Copy(w, file) + }) +} diff --git a/server/rest/handlers.go b/server/rest/handlers.go index e85ef29..20c260f 100644 --- a/server/rest/handlers.go +++ b/server/rest/handlers.go @@ -2,7 +2,7 @@ package rest import ( "encoding/hex" - "errors" + "encoding/json" "net/http" "os" "path/filepath" @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/gofiber/fiber/v2" + "github.com/go-chi/chi/v5" "github.com/golang-jwt/jwt/v5" "github.com/marcopeocchi/yt-dlp-web-ui/server/config" "github.com/marcopeocchi/yt-dlp-web-ui/server/utils" @@ -69,18 +69,20 @@ type ListRequest struct { OrderBy string `json:"orderBy"` } -func ListDownloaded(ctx *fiber.Ctx) error { +func ListDownloaded(w http.ResponseWriter, r *http.Request) { root := config.Instance().GetConfig().DownloadPath req := new(ListRequest) - err := ctx.BodyParser(req) + err := json.NewDecoder(r.Body).Decode(&req) if err != nil { - return err + http.Error(w, err.Error(), http.StatusBadRequest) + return } files, err := walkDir(filepath.Join(root, req.SubDir)) if err != nil { - return err + http.Error(w, err.Error(), http.StatusBadRequest) + return } if req.OrderBy == "modtime" { @@ -89,45 +91,55 @@ func ListDownloaded(ctx *fiber.Ctx) error { }) } - ctx.Status(http.StatusOK) - return ctx.JSON(files) + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(files) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } } type DeleteRequest = DirectoryEntry -func DeleteFile(ctx *fiber.Ctx) error { +func DeleteFile(w http.ResponseWriter, r *http.Request) { req := new(DeleteRequest) - err := ctx.BodyParser(req) + err := json.NewDecoder(r.Body).Decode(&req) if err != nil { - return err + http.Error(w, err.Error(), http.StatusBadRequest) + return } sum := utils.ShaSumString(req.Path) if sum != req.SHASum { - return errors.New("shasum mismatch") + http.Error(w, "shasum mismatch", http.StatusBadRequest) + return } err = os.Remove(req.Path) if err != nil { - return err + http.Error(w, "shasum mismatch", http.StatusBadRequest) + return } - ctx.Status(fiber.StatusOK) - return ctx.JSON("ok") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode("ok") } -func SendFile(ctx *fiber.Ctx) error { - path := ctx.Params("id") +func SendFile(w http.ResponseWriter, r *http.Request) { + path := chi.URLParam(r, "id") if path == "" { - return errors.New("inexistent path") + http.Error(w, "inexistent path", http.StatusBadRequest) + return } decoded, err := hex.DecodeString(path) if err != nil { - return err + http.Error(w, err.Error(), http.StatusBadRequest) + return } + decodedStr := string(decoded) root := config.Instance().GetConfig().DownloadPath @@ -138,26 +150,28 @@ func SendFile(ctx *fiber.Ctx) error { // "Content-Disposition", // "inline; filename="+filepath.Base(decodedStr), // ) - ctx.SendStatus(fiber.StatusOK) - return ctx.SendFile(decodedStr) + + http.ServeFile(w, r, decodedStr) } - return ctx.SendStatus(fiber.StatusUnauthorized) + w.WriteHeader(http.StatusUnauthorized) } type LoginRequest struct { Secret string `json:"secret"` } -func Login(ctx *fiber.Ctx) error { +func Login(w http.ResponseWriter, r *http.Request) { req := new(LoginRequest) - err := ctx.BodyParser(req) + err := json.NewDecoder(r.Body).Decode(&req) if err != nil { - return ctx.SendStatus(fiber.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusInternalServerError) + return } if config.Instance().GetConfig().RPCSecret != req.Secret { - return ctx.SendStatus(fiber.StatusBadRequest) + http.Error(w, err.Error(), http.StatusBadRequest) + return } expiresAt := time.Now().Add(time.Hour * 24 * 30) @@ -168,30 +182,31 @@ func Login(ctx *fiber.Ctx) error { tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) if err != nil { - return ctx.SendStatus(fiber.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - ctx.Cookie(&fiber.Cookie{ + cookie := &http.Cookie{ Name: TOKEN_COOKIE_NAME, - HTTPOnly: true, + HttpOnly: true, Secure: false, Expires: expiresAt, // 30 days Value: tokenString, Path: "/", - }) + } - return ctx.SendStatus(fiber.StatusOK) + http.SetCookie(w, cookie) } -func Logout(ctx *fiber.Ctx) error { - ctx.Cookie(&fiber.Cookie{ +func Logout(w http.ResponseWriter, r *http.Request) { + cookie := &http.Cookie{ Name: TOKEN_COOKIE_NAME, - HTTPOnly: true, + HttpOnly: true, Secure: false, Expires: time.Now(), Value: "", Path: "/", - }) + } - return ctx.SendStatus(fiber.StatusOK) + http.SetCookie(w, cookie) } diff --git a/server/rpc/handlers.go b/server/rpc/handlers.go new file mode 100644 index 0000000..4bb7b7f --- /dev/null +++ b/server/rpc/handlers.go @@ -0,0 +1,53 @@ +package rpc + +import ( + "io" + "net/http" + + "github.com/gorilla/websocket" +) + +var upgrader websocket.Upgrader + +func WebSocket(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + defer c.Close() + + // notify client that conn is open and ok + c.WriteJSON(struct{ Status string }{Status: "connected"}) + + for { + mtype, reader, err := c.NextReader() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + break + } + + res := newRequest(reader).Call() + + writer, err := c.NextWriter(mtype) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + break + } + + io.Copy(writer, res) + } +} + +func Post(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + res := newRequest(r.Body).Call() + _, err := io.Copy(w, res) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/server/rpc.go b/server/rpc/wrapper.go similarity index 90% rename from server/rpc.go rename to server/rpc/wrapper.go index ec48f1f..631bce2 100644 --- a/server/rpc.go +++ b/server/rpc/wrapper.go @@ -1,4 +1,4 @@ -package server +package rpc import ( "bytes" @@ -13,7 +13,7 @@ type rpcRequest struct { done chan bool } -func NewRPCRequest(r io.Reader) *rpcRequest { +func newRequest(r io.Reader) *rpcRequest { var buf bytes.Buffer done := make(chan bool) return &rpcRequest{r, &buf, done} diff --git a/server/server.go b/server/server.go index 04776bc..57ce638 100644 --- a/server/server.go +++ b/server/server.go @@ -3,7 +3,6 @@ package server import ( "context" "fmt" - "io" "io/fs" "log" "net/http" @@ -13,16 +12,21 @@ import ( "syscall" "time" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/filesystem" - "github.com/gofiber/websocket/v2" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/marcopeocchi/yt-dlp-web-ui/server/internal" middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware" "github.com/marcopeocchi/yt-dlp-web-ui/server/rest" ytdlpRPC "github.com/marcopeocchi/yt-dlp-web-ui/server/rpc" ) +type serverConfig struct { + frontend fs.FS + port int + db *internal.MemoryDB + mq *internal.MessageQueue +} + func RunBlocking(port int, frontend fs.FS) { var db internal.MemoryDB db.Restore() @@ -30,80 +34,64 @@ func RunBlocking(port int, frontend fs.FS) { mq := internal.NewMessageQueue() go mq.Subscriber() - service := ytdlpRPC.Container(&db, mq) - rpc.Register(service) - - app := fiber.New() - - app.Use(cors.New()) - app.Use("/", filesystem.New(filesystem.Config{ - Root: http.FS(frontend), - })) - - // Client side routes - app.Get("/settings", func(c *fiber.Ctx) error { - return c.Redirect("/") - }) - app.Get("/archive", func(c *fiber.Ctx) error { - return c.Redirect("/") - }) - app.Get("/login", func(c *fiber.Ctx) error { - return c.Redirect("/") + srv := newServer(serverConfig{ + frontend: frontend, + port: port, + db: &db, + mq: mq, }) - // Archive routes - archive := app.Group("archive", middlewares.Authenticated) - archive.Post("/downloaded", rest.ListDownloaded) - archive.Post("/delete", rest.DeleteFile) - archive.Get("/d/:id", rest.SendFile) - - // Authentication routes - app.Post("/auth/login", rest.Login) - app.Get("/auth/logout", rest.Logout) - - // RPC handlers - // websocket - rpc := app.Group("/rpc", middlewares.Authenticated) - - rpc.Get("/ws", websocket.New(func(c *websocket.Conn) { - c.WriteMessage(websocket.TextMessage, []byte(`{ - "status": "connected" - }`)) - - for { - mtype, reader, err := c.NextReader() - if err != nil { - break - } - res := NewRPCRequest(reader).Call() - - writer, err := c.NextWriter(mtype) - if err != nil { - break - } - io.Copy(writer, res) - } - })) // http-post - rpc.Post("/http", func(c *fiber.Ctx) error { - reader := c.Context().RequestBodyStream() - writer := c.Response().BodyWriter() - - res := NewRPCRequest(reader).Call() - io.Copy(writer, res) - - return nil - }) - - app.Server().StreamRequestBody = true - - go gracefulShutdown(app, &db) + go gracefulShutdown(srv, &db) go autoPersist(time.Minute*5, &db) - log.Fatal(app.Listen(fmt.Sprintf(":%d", port))) + log.Fatal(srv.ListenAndServe()) } -func gracefulShutdown(app *fiber.App, db *internal.MemoryDB) { +func newServer(c serverConfig) *http.Server { + service := ytdlpRPC.Container(c.db, c.mq) + rpc.Register(service) + + r := chi.NewRouter() + + r.Use(middlewares.CORS) + r.Use(middleware.Logger) + + sh := middlewares.NewSpaHandler("index.html", c.frontend) + sh.AddClientRoute("/settings") + sh.AddClientRoute("/archive") + sh.AddClientRoute("/login") + + r.Get("/*", sh.Handler()) + + // Archive routes + r.Route("/archive", func(r chi.Router) { + r.Use(middlewares.Authenticated) + r.Post("/downloaded", rest.ListDownloaded) + r.Post("/delete", rest.DeleteFile) + r.Get("/d/{id}", rest.SendFile) + }) + + // Authentication routes + r.Route("/auth", func(r chi.Router) { + r.Post("/login", rest.Login) + r.Get("/logout", rest.Logout) + }) + + // RPC handlers + r.Route("/rpc", func(r chi.Router) { + r.Use(middlewares.Authenticated) + r.Get("/ws", ytdlpRPC.WebSocket) + r.Post("/http", ytdlpRPC.Post) + }) + + return &http.Server{ + Addr: fmt.Sprintf(":%d", c.port), + Handler: r, + } +} + +func gracefulShutdown(srv *http.Server, db *internal.MemoryDB) { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, @@ -117,7 +105,7 @@ func gracefulShutdown(app *fiber.App, db *internal.MemoryDB) { defer func() { db.Persist() stop() - app.ShutdownWithTimeout(time.Second * 5) + srv.Shutdown(context.TODO()) }() }() }