diff --git a/Makefile b/Makefile index a05d2b9..671fefc 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT) HAVE_POD := $(shell pod2text -h 2>/dev/null) DAEMON := cenod -all: buildlocal buildlocalctl +all: cmd/formtemplate.go buildlocal buildlocalctl buildlocalctl: make -C upctl @@ -80,3 +80,10 @@ show-versions: buildlocal goupdate: go get -t -u=patch ./... + +cmd/%.go: templates/%.tpl + echo "package cmd" > cmd/$*.go + echo >> cmd/$*.go + echo "const formtemplate = \`" >> cmd/$*.go + cat templates/$*.tpl >> cmd/$*.go + echo "\`" >> cmd/$*.go diff --git a/api/auth.go b/api/auth.go index 0f57145..8b259d8 100644 --- a/api/auth.go +++ b/api/auth.go @@ -24,7 +24,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/keyauth/v2" "github.com/tlinden/cenophane/cfg" - "regexp" + "github.com/tlinden/cenophane/common" ) // these vars can be savely global, since they don't change ever @@ -39,8 +39,7 @@ var ( Message: "Invalid API key", } - Authurls []*regexp.Regexp - Apikeys []cfg.Apicontext + Apikeys []cfg.Apicontext ) // fill from server: accepted keys @@ -48,13 +47,6 @@ func AuthSetApikeys(keys []cfg.Apicontext) { Apikeys = keys } -// fill from server: endpoints we need to authenticate -func AuthSetEndpoints(prefix string, version string, endpoints []string) { - for _, endpoint := range endpoints { - Authurls = append(Authurls, regexp.MustCompile("^"+prefix+version+endpoint)) - } -} - // make sure we always return JSON encoded errors func AuthErrHandler(ctx *fiber.Ctx, err error) error { ctx.Status(fiber.StatusForbidden) @@ -66,6 +58,33 @@ func AuthErrHandler(ctx *fiber.Ctx, err error) error { return ctx.JSON(errInvalid) } +// validator hook, validates incoming api key against form id, which +// also acts as onetime api key +func AuthValidateOnetimeKey(c *fiber.Ctx, key string, db *Db) (bool, error) { + resp, err := db.Get("", key, common.TypeForm) + if err != nil { + return false, errors.New("Onetime key doesn't match any form id!") + } + + if len(resp.Forms) != 1 { + return false, errors.New("db.Get(form) returned no results and no errors!") + } + + sess, err := Sessionstore.Get(c) + + // store the result into the session, the 'formid' key tells the + // upload handler that the apicontext it sees is in fact a form id + // and has to be deleted if set to asap. + sess.Set("apicontext", resp.Forms[0].Context) + sess.Set("formid", key) + + if err := sess.Save(); err != nil { + return false, errors.New("Unable to save session store!") + } + + return true, nil +} + // validator hook, called by fiber via server keyauth.New() func AuthValidateAPIKey(c *fiber.Ctx, key string) (bool, error) { // create a new session, it will be thrown away if something fails diff --git a/api/fileio.go b/api/fileio.go index 9ad11aa..f0d446b 100644 --- a/api/fileio.go +++ b/api/fileio.go @@ -83,7 +83,7 @@ func ProcessFormFiles(cfg *cfg.Config, members []string, id string) (string, str return "", "", err } - returnUrl = strings.Join([]string{cfg.Url + cfg.ApiPrefix + ApiVersion, "file", id, zipfile}, "/") + returnUrl = strings.Join([]string{cfg.Url, "download", id, zipfile}, "/") Filename = zipfile // clean up after us diff --git a/api/form_handlers.go b/api/form_handlers.go index 9d9807f..134a9c5 100644 --- a/api/form_handlers.go +++ b/api/form_handlers.go @@ -39,7 +39,7 @@ func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { entry := &common.Form{Id: id, Created: common.Timestamp{Time: time.Now()}} // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return JsonStatus(c, fiber.StatusInternalServerError, "Unable to initialize session store from context: "+err.Error()) @@ -96,7 +96,7 @@ func FormDelete(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return JsonStatus(c, fiber.StatusInternalServerError, "Unable to initialize session store from context: "+err.Error()) @@ -128,7 +128,7 @@ func FormsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return JsonStatus(c, fiber.StatusInternalServerError, "Unable to initialize session store from context: "+err.Error()) @@ -157,7 +157,7 @@ func FormDescribe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return JsonStatus(c, fiber.StatusInternalServerError, "Unable to initialize session store from context: "+err.Error()) @@ -192,7 +192,7 @@ func FormPage(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallexpire bool) error { return c.Status(fiber.StatusForbidden).SendString("Invalid id provided!") } - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Unable to initialize session store from context:" + err.Error()) } @@ -207,6 +207,10 @@ func FormPage(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallexpire bool) error { return c.Status(fiber.StatusInternalServerError).SendString("Unable to load form template: " + err.Error()) } + // prepare upload url + uploadurl := strings.Join([]string{cfg.ApiPrefix + ApiVersion, "uploads"}, "/") + response.Forms[0].Url = uploadurl + var out bytes.Buffer if err := t.Execute(&out, response.Forms[0]); err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Unable to render form template: " + err.Error()) diff --git a/api/server.go b/api/server.go index 697dd24..31189ef 100644 --- a/api/server.go +++ b/api/server.go @@ -47,7 +47,7 @@ func Runserver(conf *cfg.Config, args []string) error { defer db.Close() // setup authenticated endpoints - auth := SetupAuthStore(conf) + auth := SetupAuthStore(conf, db) // setup api server router := SetupServer(conf) @@ -135,12 +135,23 @@ func Runserver(conf *cfg.Config, args []string) error { return router.Listen(conf.Listen) } -func SetupAuthStore(conf *cfg.Config) func(*fiber.Ctx) error { - AuthSetEndpoints(conf.ApiPrefix, ApiVersion, []string{"/file"}) +func SetupAuthStore(conf *cfg.Config, db *Db) func(*fiber.Ctx) error { AuthSetApikeys(conf.Apicontexts) return keyauth.New(keyauth.Config{ - Validator: AuthValidateAPIKey, + Validator: func(c *fiber.Ctx, key string) (bool, error) { + // we use a wrapper closure to be able to forward the db object + formuser, err := AuthValidateOnetimeKey(c, key, db) + + // incoming apicontext matches a form id, accept it + if err == nil { + Log("Incoming API Context equals formuser: %t, id: %s", formuser, key) + return formuser, err + } + + // nope, we need to check against regular configured apicontexts + return AuthValidateAPIKey(c, key) + }, ErrorHandler: AuthErrHandler, }) } diff --git a/api/upload_handlers.go b/api/upload_handlers.go index dcb06bc..b230264 100644 --- a/api/upload_handlers.go +++ b/api/upload_handlers.go @@ -65,7 +65,7 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { entry := &common.Upload{Id: id, Created: common.Timestamp{Time: time.Now()}} // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return JsonStatus(c, fiber.StatusInternalServerError, "Unable to initialize session store from context: "+err.Error()) @@ -119,6 +119,24 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { res := &common.Response{Uploads: []*common.Upload{entry}} res.Success = true res.Code = fiber.StatusOK + + // ok, check if we need to remove a form, if so we do it in the + // background. delete error doesn't lead to upload failure, we + // only log it. + formid, _ := SessionGetFormId(c) + if formid != "" { + go func() { + r, err := db.Get(apicontext, formid, common.TypeForm) + if err == nil { + if len(r.Forms) == 1 { + if r.Forms[0].Expire == "asap" { + db.Delete(apicontext, formid) + } + } + } + }() + } + return c.Status(fiber.StatusOK).JSON(res) } @@ -133,7 +151,7 @@ func UploadFetch(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) err } // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return JsonStatus(c, fiber.StatusInternalServerError, "Unable to initialize session store from context: "+err.Error()) @@ -192,7 +210,7 @@ func UploadDelete(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return JsonStatus(c, fiber.StatusInternalServerError, "Unable to initialize session store from context: "+err.Error()) @@ -226,7 +244,7 @@ func UploadsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return JsonStatus(c, fiber.StatusInternalServerError, "Unable to initialize session store from context: "+err.Error()) @@ -255,7 +273,7 @@ func UploadDescribe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) + apicontext, err := SessionGetApicontext(c) if err != nil { return JsonStatus(c, fiber.StatusInternalServerError, "Unable to initialize session store from context: "+err.Error()) diff --git a/api/utils.go b/api/utils.go index 57d962e..19e37bb 100644 --- a/api/utils.go +++ b/api/utils.go @@ -55,7 +55,7 @@ func Ts() string { If there's no apicontext in the session, assume unauth user, return "" */ -func GetApicontext(c *fiber.Ctx) (string, error) { +func SessionGetApicontext(c *fiber.Ctx) (string, error) { sess, err := Sessionstore.Get(c) if err != nil { return "", fmt.Errorf("Unable to initialize session store from context: " + err.Error()) @@ -69,6 +69,25 @@ func GetApicontext(c *fiber.Ctx) (string, error) { return "", nil } +/* + Retrieve the formid (aka onetime api key) from the session. It is + configured if an upload request has been successfully authenticated + using a onetime key. +*/ +func SessionGetFormId(c *fiber.Ctx) (string, error) { + sess, err := Sessionstore.Get(c) + if err != nil { + return "", fmt.Errorf("Unable to initialize session store from context: " + err.Error()) + } + + formid := sess.Get("formid") + if formid != nil { + return formid.(string), nil + } + + return "", nil +} + /* Calculate if time is up based on start time.Time and duration. Returns true if time is expired. Start time comes from diff --git a/cmd/formtemplate.go b/cmd/formtemplate.go new file mode 100644 index 0000000..5b45e7b --- /dev/null +++ b/cmd/formtemplate.go @@ -0,0 +1,77 @@ +package cmd + +const formtemplate = ` + + + +
+ + + + + +