- added working upload formm which can also create an upload
    - cleaned up auth.go
    - enhanced server/SetupAuthStore() to also look up form ids
    - added form template (put into .go file by Makefile
This commit is contained in:
2023-03-27 13:26:31 +02:00
parent 07bb5569a7
commit 9c7db0e2a4
10 changed files with 190 additions and 27 deletions

View File

@@ -29,7 +29,7 @@ VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)
HAVE_POD := $(shell pod2text -h 2>/dev/null) HAVE_POD := $(shell pod2text -h 2>/dev/null)
DAEMON := cenod DAEMON := cenod
all: buildlocal buildlocalctl all: cmd/formtemplate.go buildlocal buildlocalctl
buildlocalctl: buildlocalctl:
make -C upctl make -C upctl
@@ -80,3 +80,10 @@ show-versions: buildlocal
goupdate: goupdate:
go get -t -u=patch ./... 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

View File

@@ -24,7 +24,7 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/keyauth/v2" "github.com/gofiber/keyauth/v2"
"github.com/tlinden/cenophane/cfg" "github.com/tlinden/cenophane/cfg"
"regexp" "github.com/tlinden/cenophane/common"
) )
// these vars can be savely global, since they don't change ever // these vars can be savely global, since they don't change ever
@@ -39,8 +39,7 @@ var (
Message: "Invalid API key", Message: "Invalid API key",
} }
Authurls []*regexp.Regexp Apikeys []cfg.Apicontext
Apikeys []cfg.Apicontext
) )
// fill from server: accepted keys // fill from server: accepted keys
@@ -48,13 +47,6 @@ func AuthSetApikeys(keys []cfg.Apicontext) {
Apikeys = keys 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 // make sure we always return JSON encoded errors
func AuthErrHandler(ctx *fiber.Ctx, err error) error { func AuthErrHandler(ctx *fiber.Ctx, err error) error {
ctx.Status(fiber.StatusForbidden) ctx.Status(fiber.StatusForbidden)
@@ -66,6 +58,33 @@ func AuthErrHandler(ctx *fiber.Ctx, err error) error {
return ctx.JSON(errInvalid) 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() // validator hook, called by fiber via server keyauth.New()
func AuthValidateAPIKey(c *fiber.Ctx, key string) (bool, error) { func AuthValidateAPIKey(c *fiber.Ctx, key string) (bool, error) {
// create a new session, it will be thrown away if something fails // create a new session, it will be thrown away if something fails

View File

@@ -83,7 +83,7 @@ func ProcessFormFiles(cfg *cfg.Config, members []string, id string) (string, str
return "", "", err 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 Filename = zipfile
// clean up after us // clean up after us

View File

@@ -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()}} entry := &common.Form{Id: id, Created: common.Timestamp{Time: time.Now()}}
// retrieve the API Context name from the session // retrieve the API Context name from the session
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError, return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error()) "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 // retrieve the API Context name from the session
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError, return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error()) "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 // retrieve the API Context name from the session
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError, return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error()) "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 // retrieve the API Context name from the session
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError, return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error()) "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!") return c.Status(fiber.StatusForbidden).SendString("Invalid id provided!")
} }
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Unable to initialize session store from context:" + err.Error()) 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()) 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 var out bytes.Buffer
if err := t.Execute(&out, response.Forms[0]); err != nil { if err := t.Execute(&out, response.Forms[0]); err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Unable to render form template: " + err.Error()) return c.Status(fiber.StatusInternalServerError).SendString("Unable to render form template: " + err.Error())

View File

@@ -47,7 +47,7 @@ func Runserver(conf *cfg.Config, args []string) error {
defer db.Close() defer db.Close()
// setup authenticated endpoints // setup authenticated endpoints
auth := SetupAuthStore(conf) auth := SetupAuthStore(conf, db)
// setup api server // setup api server
router := SetupServer(conf) router := SetupServer(conf)
@@ -135,12 +135,23 @@ func Runserver(conf *cfg.Config, args []string) error {
return router.Listen(conf.Listen) return router.Listen(conf.Listen)
} }
func SetupAuthStore(conf *cfg.Config) func(*fiber.Ctx) error { func SetupAuthStore(conf *cfg.Config, db *Db) func(*fiber.Ctx) error {
AuthSetEndpoints(conf.ApiPrefix, ApiVersion, []string{"/file"})
AuthSetApikeys(conf.Apicontexts) AuthSetApikeys(conf.Apicontexts)
return keyauth.New(keyauth.Config{ 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, ErrorHandler: AuthErrHandler,
}) })
} }

View File

@@ -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()}} entry := &common.Upload{Id: id, Created: common.Timestamp{Time: time.Now()}}
// retrieve the API Context name from the session // retrieve the API Context name from the session
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError, return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error()) "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 := &common.Response{Uploads: []*common.Upload{entry}}
res.Success = true res.Success = true
res.Code = fiber.StatusOK 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) 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 // retrieve the API Context name from the session
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError, return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error()) "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 // retrieve the API Context name from the session
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError, return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error()) "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 // retrieve the API Context name from the session
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError, return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error()) "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 // retrieve the API Context name from the session
apicontext, err := GetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError, return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error()) "Unable to initialize session store from context: "+err.Error())

View File

@@ -55,7 +55,7 @@ func Ts() string {
If there's no apicontext in the session, assume unauth user, return "" 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) sess, err := Sessionstore.Get(c)
if err != nil { if err != nil {
return "", fmt.Errorf("Unable to initialize session store from context: " + err.Error()) 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 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 Calculate if time is up based on start time.Time and
duration. Returns true if time is expired. Start time comes from duration. Returns true if time is expired. Start time comes from

77
cmd/formtemplate.go Normal file
View File

@@ -0,0 +1,77 @@
package cmd
const formtemplate = `
<!DOCTYPE html>
<!-- -*-web-*- -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="description" content="upload form" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>File upload form</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<h4>Upload form {{ .Id }}</h4>
<!-- Status message -->
<div class="statusMsg"></div>
<!-- File upload form -->
<div class="col-lg-12">
<form id="fupForm" enctype="multipart/form-data" action="/v1/uploads" method="POST">
<div class="form-group">
<label for="expire">Expire</label>
<input type="expire" class="form-control" id="expire" name="expire" placeholder="Enter expire"/>
</div>
<div class="form-group">
<label for="file">Files</label>
<input type="file" class="form-control" id="file" name="uploads[]" multiple />
</div>
<input type="submit" name="submit" class="btn btn-success submitBtn" value="Upload"/>
</form>
</div>
<script>
$(document).ready(function(){
// Submit form data via Ajax
$("#fupForm").on('submit', function(e){
e.preventDefault();
$.ajax({
type: 'POST',
url: '/v1/uploads',
data: new FormData(this),
dataType: 'json',
contentType: false,
cache: false,
processData:false,
beforeSend: function(xhr){
$('.submitBtn').attr("disabled","disabled");
$('#fupForm').css("opacity",".5");
xhr.setRequestHeader('Authorization', 'Bearer {{.Id}}');
},
success: function(response){
$('.statusMsg').html('');
if(response.success){
$('#fupForm')[0].reset();
$('.statusMsg').html('<p class="alert alert-success">Your upload is available at <a href="'
+response.uploads[0].url+'">here</a> for download</p>');
$('#fupForm').hide();
}else{
$('.statusMsg').html('<p class="alert alert-danger">'+response.message+'</p>');
}
$('#fupForm').css("opacity","");
$(".submitBtn").removeAttr("disabled");
}
});
});
});
</script>
</body>
</html>
`

View File

@@ -155,6 +155,9 @@ func Execute() error {
// replace the filename // replace the filename
conf.Formpage = string(content) conf.Formpage = string(content)
} }
} else {
// use builtin default
conf.Formpage = formtemplate
} }
switch { switch {

View File

@@ -55,6 +55,11 @@ type Response struct {
} }
type Form struct { type Form struct {
// Note the dual use of the Id: it will be used as onetime api key
// from generated upload forms and stored in the session store so
// that the upload handler is able to check if the form object has
// to be deleted immediately (if its expire field has been set to
// asap)
Id string `json:"id"` Id string `json:"id"`
Expire string `json:"expire"` Expire string `json:"expire"`
Description string `json:"description"` Description string `json:"description"`