From 8a791d8017ad2a013f930b2fc132d3cd3e5cd7d7 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Thu, 30 Mar 2023 10:22:57 +0200 Subject: [PATCH] Changes: - had to add a Type field to interface DbEntry so that db.List() is able to distinguish between Upload and Form properly. - added form describe and delete commands - added --query parameter to form+upload list for filtering --- README.md | 5 +--- api/db.go | 40 ++++++++++++++++++++++++----- api/db_test.go | 9 ++++--- api/form_handlers.go | 10 ++++++-- api/upload_handlers.go | 17 ++++++++---- cfg/config.go | 9 ++++--- cmd/formtemplate.go | 4 +-- common/types.go | 54 +++++++++++++++++++++++++++++++++++++++ upctl/cfg/config.go | 3 +++ upctl/cmd/formcommands.go | 51 ++++++++++++++++++++++++++++++++++++ upctl/cmd/maincommands.go | 11 ++++---- upctl/lib/client.go | 29 ++++++++++++++++----- upctl/lib/client_test.go | 6 ++--- 13 files changed, 208 insertions(+), 40 deletions(-) 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

'); + $('.statusMsg').html('

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] ", + Short: "Delete an form", + Long: `Delete an form identified by its id`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("No id specified to delete!") + } + + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + return lib.Delete(os.Stdout, conf, args, common.TypeForm) + }, + } + + deleteCmd.Aliases = append(deleteCmd.Aliases, "rm") + deleteCmd.Aliases = append(deleteCmd.Aliases, "d") + + return deleteCmd +} + +func FormDescribeCommand(conf *cfg.Config) *cobra.Command { + var listCmd = &cobra.Command{ + Use: "describe [options] form-id", + Long: "Show detailed informations about an form object.", + Short: `Describe an form.`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("No id specified to delete!") + } + + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + return lib.Describe(os.Stdout, conf, args, common.TypeForm) + }, + } + + listCmd.Aliases = append(listCmd.Aliases, "des") + listCmd.Aliases = append(listCmd.Aliases, "info") + listCmd.Aliases = append(listCmd.Aliases, "i") + + return listCmd +} diff --git a/upctl/cmd/maincommands.go b/upctl/cmd/maincommands.go index c0a2ed6..fc20d62 100644 --- a/upctl/cmd/maincommands.go +++ b/upctl/cmd/maincommands.go @@ -56,7 +56,7 @@ func ListCommand(conf *cfg.Config) *cobra.Command { var listCmd = &cobra.Command{ Use: "list [options] [file ..]", Short: "List uploads", - Long: `List uploads.`, + Long: `List uploads`, RunE: func(cmd *cobra.Command, args []string) error { // errors at this stage do not cause the usage to be shown cmd.SilenceUsage = true @@ -67,6 +67,7 @@ func ListCommand(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") @@ -87,7 +88,7 @@ func DeleteCommand(conf *cfg.Config) *cobra.Command { // errors at this stage do not cause the usage to be shown cmd.SilenceUsage = true - return lib.Delete(os.Stdout, conf, args) + return lib.Delete(os.Stdout, conf, args, common.TypeUpload) }, } @@ -101,7 +102,7 @@ func DescribeCommand(conf *cfg.Config) *cobra.Command { var listCmd = &cobra.Command{ Use: "describe [options] upload-id", Long: "Show detailed informations about an upload object.", - Short: `Describe an upload.`, + Short: `Describe an upload`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return errors.New("No id specified to delete!") @@ -110,7 +111,7 @@ func DescribeCommand(conf *cfg.Config) *cobra.Command { // errors at this stage do not cause the usage to be shown cmd.SilenceUsage = true - return lib.Describe(os.Stdout, conf, args) + return lib.Describe(os.Stdout, conf, args, common.TypeUpload) }, } @@ -125,7 +126,7 @@ func DownloadCommand(conf *cfg.Config) *cobra.Command { var listCmd = &cobra.Command{ Use: "download [options] upload-id", Long: "Download the file associated with an upload object.", - Short: `Download a file.`, + Short: `Download a file`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return errors.New("No id specified to delete!") diff --git a/upctl/lib/client.go b/upctl/lib/client.go index f9ab95e..4f7c560 100644 --- a/upctl/lib/client.go +++ b/upctl/lib/client.go @@ -48,6 +48,7 @@ type Request struct { type ListParams struct { Apicontext string `json:"apicontext"` + Query string `json:"query"` } const Maxwidth = 12 @@ -216,7 +217,7 @@ func List(w io.Writer, c *cfg.Config, args []string, typ int) error { rq = Setup(c, "/forms") } - params := &ListParams{Apicontext: c.Apicontext} + params := &ListParams{Apicontext: c.Apicontext, Query: c.Query} resp, err := rq.R. SetBodyJsonMarshal(params). Get(rq.Url) @@ -239,9 +240,18 @@ func List(w io.Writer, c *cfg.Config, args []string, typ int) error { return nil } -func Delete(w io.Writer, c *cfg.Config, args []string) error { +func Delete(w io.Writer, c *cfg.Config, args []string, typ int) error { for _, id := range args { - rq := Setup(c, "/uploads/"+id+"/") + var rq *Request + caption := "Upload" + + switch typ { + case common.TypeUpload: + rq = Setup(c, "/uploads/"+id) + case common.TypeForm: + rq = Setup(c, "/forms/"+id) + caption = "Form" + } resp, err := rq.R.Delete(rq.Url) @@ -253,20 +263,27 @@ func Delete(w io.Writer, c *cfg.Config, args []string) error { return err } - fmt.Fprintf(w, "Upload %s successfully deleted.\n", id) + fmt.Fprintf(w, "%s %s successfully deleted.\n", caption, id) } return nil } -func Describe(w io.Writer, c *cfg.Config, args []string) error { +func Describe(w io.Writer, c *cfg.Config, args []string, typ int) error { if len(args) == 0 { return errors.New("No id provided!") } + var rq *Request id := args[0] // we describe only 1 object - rq := Setup(c, "/uploads/"+id) + switch typ { + case common.TypeUpload: + rq = Setup(c, "/uploads/"+id) + case common.TypeForm: + rq = Setup(c, "/forms/"+id) + } + resp, err := rq.R.Get(rq.Url) if err != nil { diff --git a/upctl/lib/client_test.go b/upctl/lib/client_test.go index cd743c1..3b92a22 100644 --- a/upctl/lib/client_test.go +++ b/upctl/lib/client_test.go @@ -295,7 +295,7 @@ func TestDescribe(t *testing.T) { var w bytes.Buffer unit.route += unit.files[0] Intercept(unit) - Check(t, unit, &w, Describe(&w, conf, unit.files)) + Check(t, unit, &w, Describe(&w, conf, unit.files, common.TypeUpload)) } } @@ -345,9 +345,9 @@ func TestDelete(t *testing.T) { for _, unit := range tests { var w bytes.Buffer - unit.route += unit.files[0] + "/" + unit.route += unit.files[0] Intercept(unit) - Check(t, unit, &w, Delete(&w, conf, unit.files)) + Check(t, unit, &w, Delete(&w, conf, unit.files, common.TypeUpload)) } }