diff --git a/README.md b/README.md index 413ab0b..3c3c739 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,7 @@ Available Commands: delete Delete an upload describe Describe an upload. download Download a file. + form Form commands help Help about any command list List uploads upload Upload files @@ -268,13 +269,9 @@ The `endpoint` is the **ephemerup** server running somewhere and the ## TODO -- also serve a html upload page - add metrics (as in https://github.com/ansrivas/fiberprometheus) - do not manually generate output urls, use fiber.GetRoute() - upd: https://docs.gofiber.io/guide/error-handling/ to always use json output -- upctl: get rid of HandleResponse(), used only once anyway -- add form so that public users can upload -- use Writer for output.go so we can unit test the stuff in there - add (default by time!) sorting to list outputs, and add sort flag diff --git a/api/db.go b/api/db.go index c3f43d7..ae68159 100644 --- a/api/db.go +++ b/api/db.go @@ -23,6 +23,7 @@ import ( "github.com/tlinden/ephemerup/common" //"github.com/alecthomas/repr" bolt "go.etcd.io/bbolt" + "regexp" ) const Bucket string = "data" @@ -102,8 +103,9 @@ func (db *Db) Delete(apicontext string, id string) error { return err } -func (db *Db) List(apicontext string, filter string, t int) (*common.Response, error) { +func (db *Db) List(apicontext string, filter string, query string, t int) (*common.Response, error) { response := &common.Response{} + qr := regexp.MustCompile(query) err := db.bolt.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(Bucket)) @@ -112,11 +114,17 @@ func (db *Db) List(apicontext string, filter string, t int) (*common.Response, e } err := bucket.ForEach(func(id, j []byte) error { + allowed := false entry, err := common.Unmarshal(j, t) + if err != nil { return fmt.Errorf("unable to unmarshal json: %s", err) } + if !entry.IsType(t) { + return nil + } + var entryContext string if t == common.TypeUpload { entryContext = entry.(*common.Upload).Context @@ -124,22 +132,42 @@ func (db *Db) List(apicontext string, filter string, t int) (*common.Response, e entryContext = entry.(*common.Form).Context } - //fmt.Printf("apicontext: %s, filter: %s\n", apicontext, filter) + // check if the user is allowed to list this entry if apicontext != "" && db.cfg.Super != apicontext { - // only return the uploads for this context + // authenticated user but not member of super + // only return the uploads matching her context if apicontext == entryContext { - // unless a filter needed OR no filter specified + // unless a filter OR no filter specified if (filter != "" && entryContext == filter) || filter == "" { - response.Append(entry) + allowed = true } } } else { // return all, because we operate a public service or current==super if (filter != "" && entryContext == filter) || filter == "" { - response.Append(entry) + allowed = true } } + if allowed { + // user is allowed to view this entry, check if she also wants to see it + if query != "" { + if entry.MatchDescription(qr) || + entry.MatchExpire(qr) || + entry.MatchCreated(qr) || + entry.MatchFile(qr) { + allowed = true + } else { + allowed = false + } + } + } + + if allowed { + // ok, legit and wanted + response.Append(entry) + } + return nil }) diff --git a/api/db_test.go b/api/db_test.go index c2dc66f..fff8efc 100644 --- a/api/db_test.go +++ b/api/db_test.go @@ -72,12 +72,13 @@ var dbtests = []struct { context string ts string filter string + query string upload common.Upload form common.Form }{ { "upload", "test.db", false, "1", "foo", - "2023-03-10T11:45:00.000Z", "", + "2023-03-10T11:45:00.000Z", "", "", common.Upload{ Id: "1", Expire: "asap", File: "none", Context: "foo", Created: common.Timestamp{}}, @@ -85,7 +86,7 @@ var dbtests = []struct { }, { "form", "test.db", false, "2", "foo", - "2023-03-10T11:45:00.000Z", "", + "2023-03-10T11:45:00.000Z", "", "", common.Upload{}, common.Form{ Id: "1", Expire: "asap", Description: "none", Context: "foo", @@ -149,7 +150,7 @@ func TestDboperation(t *testing.T) { td.Cmp(t, response.Uploads[0], &tt.upload, tt.name) // fetch list - response, err = db.List(tt.context, tt.filter, common.TypeUpload) + response, err = db.List(tt.context, tt.filter, tt.query, common.TypeUpload) if err != nil { t.Errorf("Could not fetch uploads list: " + err.Error()) } @@ -211,7 +212,7 @@ func TestDboperation(t *testing.T) { td.Cmp(t, response.Forms[0], &tt.form, tt.name) // fetch list - response, err = db.List(tt.context, tt.filter, common.TypeForm) + response, err = db.List(tt.context, tt.filter, tt.query, common.TypeForm) if err != nil { t.Errorf("Could not fetch forms list: " + err.Error()) } diff --git a/api/form_handlers.go b/api/form_handlers.go index b45f914..9711bfa 100644 --- a/api/form_handlers.go +++ b/api/form_handlers.go @@ -36,7 +36,7 @@ func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { var formdata common.Form // init form obj - entry := &common.Form{Id: id, Created: common.Timestamp{Time: time.Now()}} + entry := &common.Form{Id: id, Created: common.Timestamp{Time: time.Now()}, Type: common.TypeForm} // retrieve the API Context name from the session apicontext, err := SessionGetApicontext(c) @@ -149,6 +149,12 @@ func FormsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { "Invalid api context filter provided!") } + query, err := common.Untaint(setcontext.Query, cfg.RegQuery) + if err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "Invalid query provided!") + } + // retrieve the API Context name from the session apicontext, err := SessionGetApicontext(c) if err != nil { @@ -157,7 +163,7 @@ func FormsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // get list - response, err := db.List(apicontext, filter, common.TypeForm) + response, err := db.List(apicontext, filter, query, common.TypeForm) if err != nil { return JsonStatus(c, fiber.StatusForbidden, "Unable to list forms: "+err.Error()) diff --git a/api/upload_handlers.go b/api/upload_handlers.go index e6a5fef..e02dcd4 100644 --- a/api/upload_handlers.go +++ b/api/upload_handlers.go @@ -33,6 +33,7 @@ import ( type SetContext struct { Apicontext string `json:"apicontext" form:"apicontext"` + Query string `json:"query" form:"query"` } func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { @@ -66,7 +67,7 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // init upload obj - entry := &common.Upload{Id: id, Created: common.Timestamp{Time: time.Now()}} + entry := &common.Upload{Id: id, Created: common.Timestamp{Time: time.Now()}, Type: common.TypeUpload} // retrieve the API Context name from the session apicontext, err := SessionGetApicontext(c) @@ -256,17 +257,23 @@ func UploadDelete(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { // returns the whole list + error code, no post processing by server func UploadsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { - // fetch filter from body(json expected) + // fetch apifilter+query from body(json expected) setcontext := new(SetContext) if err := c.BodyParser(setcontext); err != nil { return JsonStatus(c, fiber.StatusForbidden, "Unable to parse body: "+err.Error()) } - filter, err := common.Untaint(setcontext.Apicontext, cfg.RegKey) + apifilter, err := common.Untaint(setcontext.Apicontext, cfg.RegKey) if err != nil { return JsonStatus(c, fiber.StatusForbidden, - "Invalid api context filter provided!") + "Invalid api context apifilter provided!") + } + + query, err := common.Untaint(setcontext.Query, cfg.RegQuery) + if err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "Invalid query provided!") } // retrieve the API Context name from the session @@ -277,7 +284,7 @@ func UploadsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // get list - uploads, err := db.List(apicontext, filter, common.TypeUpload) + uploads, err := db.List(apicontext, apifilter, query, common.TypeUpload) if err != nil { return JsonStatus(c, fiber.StatusForbidden, "Unable to list uploads: "+err.Error()) diff --git a/cfg/config.go b/cfg/config.go index af82a8b..eb04af6 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -73,8 +73,10 @@ type Config struct { RegKey *regexp.Regexp RegEmail *regexp.Regexp RegText *regexp.Regexp - CleanInterval time.Duration - DefaultExpire int + RegQuery *regexp.Regexp + + CleanInterval time.Duration + DefaultExpire int } func Getversion() string { @@ -120,7 +122,8 @@ func (c *Config) ApplyDefaults() { c.RegDuration = regexp.MustCompile(`[^dhms0-9]`) c.RegKey = regexp.MustCompile(`[^a-zA-Z0-9\-]`) c.RegEmail = regexp.MustCompile(`[^a-zA-Z0-9._%+\-@0-9]`) - c.RegText = regexp.MustCompile(`[^a-zA-Z0-9._%+\-@0-9 #/\.]`) + c.RegText = regexp.MustCompile(`[^a-zA-Z0-9_%+\-@0-9 #/\.]`) + c.RegQuery = regexp.MustCompile(`[^a-zA-Z0-9_%+\-@0-9 #/\.\*\[\]\(\)\\]`) c.CleanInterval = 10 * time.Second c.DefaultExpire = 30 * 86400 // 1 month diff --git a/cmd/formtemplate.go b/cmd/formtemplate.go index bc3b79b..40e583b 100644 --- a/cmd/formtemplate.go +++ b/cmd/formtemplate.go @@ -78,8 +78,8 @@ const formtemplate = ` $('.statusMsg').html(''); if(response.success){ $('#UploadForm')[0].reset(); - $('.statusMsg').html('
Your upload is available at '
- +response.uploads[0].url+' for download
Your upload is available for download.'); $('#UploadForm').hide(); }else{ $('.statusMsg').html('
'+response.message+'
'); diff --git a/common/types.go b/common/types.go index 8c343e9..a380042 100644 --- a/common/types.go +++ b/common/types.go @@ -20,6 +20,7 @@ package common import ( "encoding/json" "fmt" + "regexp" ) // used to return to the api client @@ -33,9 +34,15 @@ type Result struct { type Dbentry interface { Getcontext(j []byte) (string, error) Marshal() ([]byte, error) + MatchExpire(r *regexp.Regexp) bool + MatchDescription(r *regexp.Regexp) bool + MatchFile(r *regexp.Regexp) bool + MatchCreated(r *regexp.Regexp) bool + IsType(t int) bool } type Upload struct { + Type int `json:"type"` Id string `json:"id"` Expire string `json:"expire"` File string `json:"file"` // final filename (visible to the downloader) @@ -61,6 +68,7 @@ type Form struct { // 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) + Type int `json:"type"` Id string `json:"id"` Expire string `json:"expire"` Description string `json:"description"` @@ -112,6 +120,52 @@ func (form Form) Marshal() ([]byte, error) { return jsonentry, nil } +func (upload Upload) MatchExpire(r *regexp.Regexp) bool { + return r.MatchString(upload.Expire) +} + +func (upload Upload) MatchDescription(r *regexp.Regexp) bool { + return r.MatchString(upload.Description) +} + +func (upload Upload) MatchCreated(r *regexp.Regexp) bool { + return r.MatchString(upload.Created.Time.String()) +} + +func (upload Upload) MatchFile(r *regexp.Regexp) bool { + return r.MatchString(upload.File) +} + +func (form Form) MatchExpire(r *regexp.Regexp) bool { + return r.MatchString(form.Expire) +} + +func (form Form) MatchDescription(r *regexp.Regexp) bool { + return r.MatchString(form.Description) +} + +func (form Form) MatchCreated(r *regexp.Regexp) bool { + return r.MatchString(form.Created.Time.String()) +} + +func (form Form) MatchFile(r *regexp.Regexp) bool { + return false +} + +func (upload Upload) IsType(t int) bool { + if upload.Type == t { + return true + } + return false +} + +func (form Form) IsType(t int) bool { + if form.Type == t { + return true + } + return false +} + /* Response methods */ diff --git a/upctl/cfg/config.go b/upctl/cfg/config.go index c283ba4..f50c771 100644 --- a/upctl/cfg/config.go +++ b/upctl/cfg/config.go @@ -44,6 +44,9 @@ type Config struct { // required to intercept requests using httpmock in tests Mock bool + // used to filter lists + Query string + // required for forms Description string Notify string diff --git a/upctl/cmd/formcommands.go b/upctl/cmd/formcommands.go index 3c18847..15929a9 100644 --- a/upctl/cmd/formcommands.go +++ b/upctl/cmd/formcommands.go @@ -18,6 +18,7 @@ package cmd import ( //"errors" + "errors" "github.com/spf13/cobra" "github.com/tlinden/ephemerup/common" "github.com/tlinden/ephemerup/upctl/cfg" @@ -45,6 +46,8 @@ func FormCommand(conf *cfg.Config) *cobra.Command { formCmd.AddCommand(FormCreateCommand(conf)) formCmd.AddCommand(FormListCommand(conf)) + formCmd.AddCommand(FormDeleteCommand(conf)) + formCmd.AddCommand(FormDescribeCommand(conf)) return formCmd } @@ -88,9 +91,57 @@ func FormListCommand(conf *cfg.Config) *cobra.Command { // options listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context") + listCmd.PersistentFlags().StringVarP(&conf.Query, "query", "q", "", "Filter by given query regexp") listCmd.Aliases = append(listCmd.Aliases, "ls") listCmd.Aliases = append(listCmd.Aliases, "l") return listCmd } + +func FormDeleteCommand(conf *cfg.Config) *cobra.Command { + var deleteCmd = &cobra.Command{ + Use: "delete [options]