diff --git a/upctl/cfg/config.go b/upctl/cfg/config.go index 6b5a0df..d2ddd4a 100644 --- a/upctl/cfg/config.go +++ b/upctl/cfg/config.go @@ -23,7 +23,6 @@ import ( const Version string = "v0.0.1" -var ApiVersion string = "/v1" var VERSION string // maintained by -x type Config struct { @@ -31,6 +30,7 @@ type Config struct { Debug bool Retries int Expire string + Apikey string } func Getversion() string { diff --git a/upctl/cmd/root.go b/upctl/cmd/root.go index 5baa20d..93da3f5 100644 --- a/upctl/cmd/root.go +++ b/upctl/cmd/root.go @@ -83,7 +83,8 @@ func Execute() { rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging") rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "custom config file") rootCmd.PersistentFlags().IntVarP(&conf.Retries, "retries", "r", 3, "How often shall we retry to access our endpoint") - rootCmd.PersistentFlags().StringVarP(&conf.Endpoint, "endpoint", "p", "http://localhost:8080/api", "upload api endpoint url") + rootCmd.PersistentFlags().StringVarP(&conf.Endpoint, "endpoint", "p", "http://localhost:8080/api/v1", "upload api endpoint url") + rootCmd.PersistentFlags().StringVarP(&conf.Apikey, "apikey", "a", "", "Api key to use") rootCmd.AddCommand(UploadCommand(&conf)) diff --git a/upctl/lib/client.go b/upctl/lib/client.go index ea33561..696804e 100644 --- a/upctl/lib/client.go +++ b/upctl/lib/client.go @@ -64,7 +64,11 @@ func Setup(c *cfg.Config, path string) *Request { }) } - return &Request{Url: c.Endpoint + cfg.ApiVersion + "/file/", R: R} + if len(c.Apikey) > 0 { + client.SetCommonBearerAuthToken(c.Apikey) + } + + return &Request{Url: c.Endpoint + "/file/", R: R} } diff --git a/upctl/upctl.hcl b/upctl/upctl.hcl index 7d177f8..5b521d5 100644 --- a/upctl/upctl.hcl +++ b/upctl/upctl.hcl @@ -1 +1,2 @@ -endpoint = "http://localhost:8080/api" +endpoint = "http://localhost:8080/api/v1" +apikey = "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a" diff --git a/upd/api/auth.go b/upd/api/auth.go new file mode 100644 index 0000000..ca5fad2 --- /dev/null +++ b/upd/api/auth.go @@ -0,0 +1,64 @@ +/* +Copyright © 2023 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package api + +import ( + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v2" + //"github.com/gofiber/keyauth/v2" + "regexp" + "strings" +) + +var Authurls []*regexp.Regexp +var Apikeys []string + +func AuthSetApikeys(keys []string) { + Apikeys = keys +} + +func AuthSetEndpoints(prefix string, version string, endpoints []string) { + for _, endpoint := range endpoints { + Authurls = append(Authurls, regexp.MustCompile("^"+prefix+version+endpoint)) + } +} + +func validateAPIKey(c *fiber.Ctx, key string) (bool, error) { + for _, apiKey := range Apikeys { + hashedAPIKey := sha256.Sum256([]byte(apiKey)) + hashedKey := sha256.Sum256([]byte(key)) + + if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { + return true, nil + } + } + return true, nil + //return false, keyauth.ErrMissingOrMalformedAPIKey +} + +func authFilter(c *fiber.Ctx) bool { + originalURL := strings.ToLower(c.OriginalURL()) + + for _, pattern := range Authurls { + if pattern.MatchString(originalURL) { + return false + } + } + return true +} diff --git a/upd/api/fileio.go b/upd/api/fileio.go index b4e8d82..35fc801 100644 --- a/upd/api/fileio.go +++ b/upd/api/fileio.go @@ -63,7 +63,7 @@ func ProcessFormFiles(cfg *cfg.Config, members []string, id string) (string, str Filename := "" if len(members) == 1 { - returnUrl = strings.Join([]string{cfg.Url + cfg.ApiPrefix + ApiVersion, "file", id, members[0]}, "/") + returnUrl = strings.Join([]string{cfg.Url, "download", id, members[0]}, "/") Filename = members[0] } else { zipfile := Ts() + "data.zip" diff --git a/upd/api/handlers.go b/upd/api/handlers.go index e8a8ff5..fc70f5f 100644 --- a/upd/api/handlers.go +++ b/upd/api/handlers.go @@ -39,8 +39,7 @@ func FilePut(c *fiber.Ctx, cfg *cfg.Config, db *Db) (string, error) { // the file is being stored as is. // // Returns the name of the uploaded file. - // - // FIXME: normalize or rename filename of single file to avoid dumb file names + id := uuid.NewString() var returnUrl string @@ -99,7 +98,7 @@ func FilePut(c *fiber.Ctx, cfg *cfg.Config, db *Db) (string, error) { return returnUrl, nil } -func FileGet(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { +func FileGet(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) error { // deliver a file and delete it if expire is set to asap // we ignore c.Params("file"), cause it may be malign. Also we've @@ -127,13 +126,17 @@ func FileGet(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { // finally put the file to the client err = c.Download(filename, file) - go func() { - // check if we need to delete the file now and do it in the background - if upload.Expire == "asap" { - cleanup(filepath.Join(cfg.StorageDir, id)) - db.Delete(id) + if len(shallExpire) > 0 { + if shallExpire[0] == true { + go func() { + // check if we need to delete the file now and do it in the background + if upload.Expire == "asap" { + cleanup(filepath.Join(cfg.StorageDir, id)) + db.Delete(id) + } + }() } - }() + } return err } diff --git a/upd/api/server.go b/upd/api/server.go index 7f5bdf2..297233b 100644 --- a/upd/api/server.go +++ b/upd/api/server.go @@ -21,6 +21,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/requestid" + "github.com/gofiber/keyauth/v2" "github.com/tlinden/up/upd/cfg" ) @@ -47,30 +48,49 @@ func Runserver(cfg *cfg.Config, args []string) error { } defer db.Close() + AuthSetEndpoints(cfg.ApiPrefix, ApiVersion, []string{"/file"}) + AuthSetApikeys(cfg.Apikeys) + + auth := keyauth.New(keyauth.Config{ + Validator: validateAPIKey, + }) + + shallExpire := true + api := router.Group(cfg.ApiPrefix + ApiVersion) { - api.Post("/file/", func(c *fiber.Ctx) error { + // authenticated routes + api.Post("/file/", auth, func(c *fiber.Ctx) error { msg, err := FilePut(c, cfg, db) return SendResponse(c, msg, err) }) - api.Get("/file/:id/:file", func(c *fiber.Ctx) error { + api.Get("/file/:id/:file", auth, func(c *fiber.Ctx) error { return FileGet(c, cfg, db) }) - api.Get("/file/:id/", func(c *fiber.Ctx) error { + api.Get("/file/:id/", auth, func(c *fiber.Ctx) error { return FileGet(c, cfg, db) }) - api.Delete("/file/:id/", func(c *fiber.Ctx) error { + api.Delete("/file/:id/", auth, func(c *fiber.Ctx) error { return FileDelete(c, cfg, db) }) } + // public routes router.Get("/", func(c *fiber.Ctx) error { return c.Send([]byte("welcome to upload api, use /api enpoint!")) }) + router.Get("/download/:id/:file", func(c *fiber.Ctx) error { + return FileGet(c, cfg, db, shallExpire) + }) + + router.Get("/download/:id/", func(c *fiber.Ctx) error { + return FileGet(c, cfg, db, shallExpire) + }) + return router.Listen(cfg.Listen) } diff --git a/upd/cfg/config.go b/upd/cfg/config.go index 41c9b9f..86fb389 100644 --- a/upd/cfg/config.go +++ b/upd/cfg/config.go @@ -41,6 +41,9 @@ type Config struct { V4only bool V6only bool Network string + + // only settable via config + Apikeys []string } func Getversion() string { diff --git a/upd/cmd/root.go b/upd/cmd/root.go index 6a05b2c..bb9fd3a 100644 --- a/upd/cmd/root.go +++ b/upd/cmd/root.go @@ -96,7 +96,7 @@ func Execute() { rootCmd.PersistentFlags().BoolVarP(&conf.Prefork, "prefork", "p", false, "Prefork server threads") rootCmd.PersistentFlags().StringVarP(&conf.AppName, "appname", "n", "upd "+conf.GetVersion(), "App name to say hi as") rootCmd.PersistentFlags().IntVarP(&conf.BodyLimit, "bodylimit", "b", 10250000000, "Max allowed upload size in bytes") - + rootCmd.PersistentFlags().StringSliceVarP(&conf.Apikeys, "apikey", "", []string{}, "Api key[s] to allow access") err := rootCmd.Execute() if err != nil { os.Exit(1) diff --git a/upd/go.mod b/upd/go.mod index 69170e0..ea68896 100644 --- a/upd/go.mod +++ b/upd/go.mod @@ -19,6 +19,7 @@ require ( github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/goccy/go-json v0.9.11 // indirect github.com/gofiber/fiber/v2 v2.42.0 // indirect + github.com/gofiber/keyauth/v2 v2.1.32 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/upd/go.sum b/upd/go.sum index 9252124..4070075 100644 --- a/upd/go.sum +++ b/upd/go.sum @@ -81,6 +81,8 @@ github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofiber/fiber/v2 v2.42.0 h1:Fnp7ybWvS+sjNQsFvkhf4G8OhXswvB6Vee8hM/LyS+8= github.com/gofiber/fiber/v2 v2.42.0/go.mod h1:3+SGNjqMh5VQH5Vz2Wdi43zTIV16ktlFd3x3R6O1Zlc= +github.com/gofiber/keyauth/v2 v2.1.32 h1:ExnCEUlgF4pQn8BLPa4VMVR12R78KtrJe0h4SqQAK5Q= +github.com/gofiber/keyauth/v2 v2.1.32/go.mod h1:zeJzlvfvjMH31A1b0NaK3FkO7mCoOiK02Xl748XHHNI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/upd/upd.hcl b/upd/upd.hcl index 1fa902a..a07c2b3 100644 --- a/upd/upd.hcl +++ b/upd/upd.hcl @@ -1 +1,6 @@ -listen = ":9191" +# -*-ruby-*- +listen = ":8080" +apikeys = [ + "0fddbff5d8010f81cd28a7d77f3e38981b13d6164c2fd6e1c3f60a4287630c37", + "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a" +]