diff --git a/Makefile b/Makefile index cdb3b0a..456abed 100644 --- a/Makefile +++ b/Makefile @@ -26,9 +26,13 @@ BRANCH = $(shell git branch --show-current) COMMIT = $(shell git rev-parse --short=8 HEAD) BUILD = $(shell date +%Y.%m.%d.%H%M%S) VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version)) +ONMAIN := $(if $(filter $(BRANCH), main),"main","") HAVE_POD := $(shell pod2text -h 2>/dev/null) HAVE_LINT:= $(shell golangci-lint -h 2>/dev/null) DAEMON := ephemerupd +CLIENT := upctl +DATE = $(shell date +%Y-%m-%d) + all: cmd/formtemplate.go lint buildlocal buildlocalctl @@ -47,8 +51,14 @@ buildimage: clean docker-compose --verbose build release: - ./mkrel.sh $(DAEMON) $(version) +ifdef BR_MAIN + git tag -a $(version) -m "$(version) released on $(DATE)" + git push origin --tags + ./mkrel.sh $(DAEMON) $(CLIENT) $(version) gh release create $(version) --generate-notes releases/* +else + @echo "Cannot create release on branch $(BRANCH), checkout main and retry!" +endif install: buildlocal install -d -o $(UID) -g $(GID) $(PREFIX)/bin diff --git a/api/form_handlers.go b/api/form_handlers.go index 9711bfa..2c9536c 100644 --- a/api/form_handlers.go +++ b/api/form_handlers.go @@ -26,10 +26,27 @@ import ( "bytes" "html/template" + "regexp" "strings" "time" ) +/* + Validate a fied by untainting it, modifies field value inplace. +*/ +func untaintField(c *fiber.Ctx, orig *string, r *regexp.Regexp, caption string) error { + if len(*orig) != 0 { + nt, err := common.Untaint(*orig, r) + if err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "Invalid "+caption+": "+err.Error()) + } + *orig = nt + } + + return nil +} + func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { id := uuid.NewString() @@ -52,35 +69,25 @@ func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { "bodyparser error : "+err.Error()) } - // post process expire + // post process inputdata if len(formdata.Expire) == 0 { entry.Expire = "asap" } else { - ex, err := common.Untaint(formdata.Expire, cfg.RegDuration) // duration or asap allowed - if err != nil { - return JsonStatus(c, fiber.StatusForbidden, - "Invalid expire data: "+err.Error()) + if err := untaintField(c, &formdata.Expire, cfg.RegDuration, "expire data"); err != nil { + return err } - entry.Expire = ex + entry.Expire = formdata.Expire } - if len(formdata.Notify) != 0 { - nt, err := common.Untaint(formdata.Notify, cfg.RegEmail) - if err != nil { - return JsonStatus(c, fiber.StatusForbidden, - "Invalid email address: "+err.Error()) - } - entry.Notify = nt + if err := untaintField(c, &formdata.Notify, cfg.RegDuration, "email address"); err != nil { + return err } + entry.Notify = formdata.Notify - if len(formdata.Description) != 0 { - des, err := common.Untaint(formdata.Description, cfg.RegText) - if err != nil { - return JsonStatus(c, fiber.StatusForbidden, - "Invalid description: "+err.Error()) - } - entry.Description = des + if err := untaintField(c, &formdata.Description, cfg.RegDuration, "description"); err != nil { + return err } + entry.Description = formdata.Description // get url [and zip if there are multiple files] returnUrl := strings.Join([]string{cfg.Url, "form", id}, "/") @@ -192,7 +199,7 @@ func FormDescribe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } response, err := db.Get(apicontext, id, common.TypeForm) - if err != nil { + if err != nil || len(response.Forms) == 0 { return JsonStatus(c, fiber.StatusForbidden, "No form with that id could be found!") } @@ -222,17 +229,20 @@ func FormPage(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallexpire bool) error { apicontext, err := SessionGetApicontext(c) 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()) } response, err := db.Get(apicontext, id, common.TypeForm) if err != nil || len(response.Forms) == 0 { - return c.Status(fiber.StatusForbidden).SendString("No form with that id could be found!") + return c.Status(fiber.StatusForbidden). + SendString("No form with that id could be found!") } t := template.New("form") if t, err = t.Parse(cfg.Formpage); err != nil { - 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 @@ -241,9 +251,79 @@ func FormPage(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallexpire bool) error { 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()) + return c.Status(fiber.StatusInternalServerError). + SendString("Unable to render form template: " + err.Error()) } c.Set("Content-type", "text/html; charset=utf-8") return c.Status(fiber.StatusOK).SendString(out.String()) } + +func FormModify(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { + var formdata common.Form + + // retrieve the API Context name from the session + apicontext, err := SessionGetApicontext(c) + if err != nil { + return JsonStatus(c, fiber.StatusInternalServerError, + "Unable to initialize session store from context: "+err.Error()) + } + + id, err := common.Untaint(c.Params("id"), cfg.RegKey) + if err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "Invalid id provided!") + } + + // extract form data + if err := c.BodyParser(&formdata); err != nil { + return JsonStatus(c, fiber.StatusInternalServerError, + "bodyparser error : "+err.Error()) + } + + // post process input data + if err := untaintField(c, &formdata.Expire, cfg.RegDuration, "expire data"); err != nil { + return err + } + + if err := untaintField(c, &formdata.Notify, cfg.RegDuration, "email address"); err != nil { + return err + } + + if err := untaintField(c, &formdata.Description, cfg.RegDuration, "description"); err != nil { + return err + } + + // lookup orig entry + response, err := db.Get(apicontext, id, common.TypeForm) + if err != nil || len(response.Forms) == 0 { + return JsonStatus(c, fiber.StatusForbidden, + "No form with that id could be found!") + } + + form := response.Forms[0] + + // modify fields + if formdata.Expire != "" { + form.Expire = formdata.Expire + } + + if formdata.Notify != "" { + form.Notify = formdata.Notify + } + + if formdata.Description != "" { + form.Description = formdata.Description + } + + // run in foreground because we need the feedback here + if err := db.Insert(id, form); err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "Failed to insert: "+err.Error()) + } + + res := &common.Response{Forms: []*common.Form{form}} + res.Success = true + res.Code = fiber.StatusOK + return c.Status(fiber.StatusOK).JSON(res) +} diff --git a/api/server.go b/api/server.go index 4ec61d3..5e7fe99 100644 --- a/api/server.go +++ b/api/server.go @@ -76,6 +76,11 @@ func Runserver(conf *cfg.Config, args []string) error { return UploadDescribe(c, conf, db) }) + // modify + api.Put("/uploads/:id", auth, func(c *fiber.Ctx) error { + return UploadModify(c, conf, db) + }) + // download w/o expire api.Get("/uploads/:id/file", auth, func(c *fiber.Ctx) error { return UploadFetch(c, conf, db) @@ -101,6 +106,11 @@ func Runserver(conf *cfg.Config, args []string) error { api.Get("/forms/:id", auth, func(c *fiber.Ctx) error { return FormDescribe(c, conf, db) }) + + // modify + api.Put("/forms/:id", auth, func(c *fiber.Ctx) error { + return FormModify(c, conf, db) + }) } // public routes diff --git a/api/upload_handlers.go b/api/upload_handlers.go index e02dcd4..16bfbe4 100644 --- a/api/upload_handlers.go +++ b/api/upload_handlers.go @@ -328,3 +328,64 @@ func UploadDescribe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { return c.Status(fiber.StatusOK).JSON(response) } + +func UploadModify(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { + var formdata common.Upload + + // retrieve the API Context name from the session + apicontext, err := SessionGetApicontext(c) + if err != nil { + return JsonStatus(c, fiber.StatusInternalServerError, + "Unable to initialize session store from context: "+err.Error()) + } + + id, err := common.Untaint(c.Params("id"), cfg.RegKey) + if err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "Invalid id provided!") + } + + // extract form data + if err := c.BodyParser(&formdata); err != nil { + return JsonStatus(c, fiber.StatusInternalServerError, + "bodyparser error : "+err.Error()) + } + + // post process input data + if err := untaintField(c, &formdata.Expire, cfg.RegDuration, "expire data"); err != nil { + return err + } + + if err := untaintField(c, &formdata.Description, cfg.RegDuration, "description"); err != nil { + return err + } + + // lookup orig entry + response, err := db.Get(apicontext, id, common.TypeUpload) + if err != nil || len(response.Uploads) == 0 { + return JsonStatus(c, fiber.StatusForbidden, + "No upload with that id could be found!") + } + + upload := response.Uploads[0] + + // modify fields + if formdata.Expire != "" { + upload.Expire = formdata.Expire + } + + if formdata.Description != "" { + upload.Description = formdata.Description + } + + // run in foreground because we need the feedback here + if err := db.Insert(id, upload); err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "Failed to insert: "+err.Error()) + } + + res := &common.Response{Uploads: []*common.Upload{upload}} + res.Success = true + res.Code = fiber.StatusOK + return c.Status(fiber.StatusOK).JSON(res) +} diff --git a/cfg/config.go b/cfg/config.go index eb04af6..1935502 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -23,7 +23,7 @@ import ( "time" ) -const Version string = "v0.0.1" +const Version string = "v0.0.2" var VERSION string // maintained by -x diff --git a/mkrel.sh b/mkrel.sh new file mode 100755 index 0000000..158e2ba --- /dev/null +++ b/mkrel.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# 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 . + + +# get list with: go tool dist list +DIST="darwin/amd64 +freebsd/amd64 +linux/amd64 +windows/amd64" + +daemon="$1" +client="$2" +version="$3" + +if test -z "$version"; then + echo "Usage: $0 " + exit 1 +fi + +rm -rf releases +mkdir -p releases + + +for D in $DIST; do + os=${D/\/*/} + arch=${D/*\//} + binfile="releases/${daemon}-${os}-${arch}-${version}" + clientfile="releases/${client}-${os}-${arch}-${version}" + tardir="${daemon}-${os}-${arch}-${version}" + tarfile="releases/${daemon}-${os}-${arch}-${version}.tar.gz" + set -x + + GOOS=${os} GOARCH=${arch} go build -o ${binfile} -ldflags "-X 'github.com/tlinden/ephemerup/cfg.VERSION=${version}'" + cd $client + GOOS=${os} GOARCH=${arch} go build -o ../${clientfile} -ldflags "-X 'github.com/tlinden/ephemerup/upctl/cfg.VERSION=${version}'" + cd - + + mkdir -p ${tardir} + cp ${binfile} ${clientfile} README.md LICENSE ${tardir}/ + + echo 'daemon = ephemerupd +client = upctl +PREFIX = /usr/local +UID = root +GID = 0 + +install-client: + install -d -o $(UID) -g $(GID) $(PREFIX)/bin + install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1 + install -o $(UID) -g $(GID) -m 555 $(client) $(PREFIX)/bin/ + install -o $(UID) -g $(GID) -m 444 $(client).1 $(PREFIX)/man/man1/ + +install: install-client + install -d -o $(UID) -g $(GID) $(PREFIX)/sbin + install -o $(UID) -g $(GID) -m 555 $(daemon) $(PREFIX)/sbin/ + install -o $(UID) -g $(GID) -m 444 $(daemon).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile + + tar cpzf ${tarfile} ${tardir} + + for file in ${binfile} ${tarfile} ${clientfile}; do + sha256sum ${file} | cut -d' ' -f1 > ${file}.sha256 + done + + rm -rf ${tardir} + set +x +done + diff --git a/upctl/cmd/formcommands.go b/upctl/cmd/formcommands.go index 15929a9..e1973d8 100644 --- a/upctl/cmd/formcommands.go +++ b/upctl/cmd/formcommands.go @@ -48,6 +48,7 @@ func FormCommand(conf *cfg.Config) *cobra.Command { formCmd.AddCommand(FormListCommand(conf)) formCmd.AddCommand(FormDeleteCommand(conf)) formCmd.AddCommand(FormDescribeCommand(conf)) + formCmd.AddCommand(FormModifyCommand(conf)) return formCmd } @@ -66,9 +67,12 @@ func FormCreateCommand(conf *cfg.Config) *cobra.Command { } // options - formCreateCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)") - formCreateCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "", "Description of the form") - formCreateCmd.PersistentFlags().StringVarP(&conf.Notify, "notify", "n", "", "Email address to get notified when consumer has uploaded files") + formCreateCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", + "Expire setting: asap or duration (accepted shortcuts: dmh)") + formCreateCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "", + "Description of the form") + formCreateCmd.PersistentFlags().StringVarP(&conf.Notify, "notify", "n", "", + "Email address to get notified when consumer has uploaded files") formCreateCmd.Aliases = append(formCreateCmd.Aliases, "add") formCreateCmd.Aliases = append(formCreateCmd.Aliases, "+") @@ -76,6 +80,37 @@ func FormCreateCommand(conf *cfg.Config) *cobra.Command { return formCreateCmd } +func FormModifyCommand(conf *cfg.Config) *cobra.Command { + var formModifyCmd = &cobra.Command{ + Use: "modify [options] ", + Short: "Modify a form", + Long: `Modify an existing form.`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Help() + } + + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + return lib.Modify(os.Stdout, conf, args, common.TypeForm) + }, + } + + // options + formModifyCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", + "Expire setting: asap or duration (accepted shortcuts: dmh)") + formModifyCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "", + "Description of the form") + formModifyCmd.PersistentFlags().StringVarP(&conf.Notify, "notify", "n", "", + "Email address to get notified when consumer has uploaded files") + + formModifyCmd.Aliases = append(formModifyCmd.Aliases, "mod") + formModifyCmd.Aliases = append(formModifyCmd.Aliases, "change") + + return formModifyCmd +} + func FormListCommand(conf *cfg.Config) *cobra.Command { var listCmd = &cobra.Command{ Use: "list [options]", diff --git a/upctl/cmd/maincommands.go b/upctl/cmd/maincommands.go index fc20d62..a6254d9 100644 --- a/upctl/cmd/maincommands.go +++ b/upctl/cmd/maincommands.go @@ -146,3 +146,32 @@ func DownloadCommand(conf *cfg.Config) *cobra.Command { return listCmd } + +func ModifyCommand(conf *cfg.Config) *cobra.Command { + var uploadModifyCmd = &cobra.Command{ + Use: "modify [options] ", + Short: "Modify an upload", + Long: `Modify an existing upload.`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Help() + } + + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + return lib.Modify(os.Stdout, conf, args, common.TypeUpload) + }, + } + + // options + uploadModifyCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", + "Expire setting: asap or duration (accepted shortcuts: dmh)") + uploadModifyCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "", + "Description of the upload") + + uploadModifyCmd.Aliases = append(uploadModifyCmd.Aliases, "mod") + uploadModifyCmd.Aliases = append(uploadModifyCmd.Aliases, "change") + + return uploadModifyCmd +} diff --git a/upctl/cmd/root.go b/upctl/cmd/root.go index 7e52e52..0aeebda 100644 --- a/upctl/cmd/root.go +++ b/upctl/cmd/root.go @@ -92,6 +92,9 @@ func Execute() { rootCmd.AddCommand(DeleteCommand(&conf)) rootCmd.AddCommand(DescribeCommand(&conf)) rootCmd.AddCommand(DownloadCommand(&conf)) + rootCmd.AddCommand(ModifyCommand(&conf)) + + // forms are being handled with its own subcommand rootCmd.AddCommand(FormCommand(&conf)) err := rootCmd.Execute() diff --git a/upctl/lib/client.go b/upctl/lib/client.go index 4f7c560..cc90f8d 100644 --- a/upctl/lib/client.go +++ b/upctl/lib/client.go @@ -357,6 +357,43 @@ func Download(w io.Writer, c *cfg.Config, args []string) error { return nil } +func Modify(w io.Writer, c *cfg.Config, args []string, typ int) error { + id := args[0] + var rq *Request + + // setup url, req.Request, timeout handling etc + switch typ { + case common.TypeUpload: + rq = Setup(c, "/uploads/"+id) + rq.R. + SetBody(&common.Upload{ + Expire: c.Expire, + Description: c.Description, + }) + case common.TypeForm: + rq = Setup(c, "/forms/"+id) + rq.R. + SetBody(&common.Form{ + Expire: c.Expire, + Description: c.Description, + Notify: c.Notify, + }) + } + + // actual put w/ settings + resp, err := rq.R.Put(rq.Url) + + if err != nil { + return err + } + + if err := HandleResponse(c, resp); err != nil { + return err + } + + return RespondExtended(w, resp) +} + /**** Forms stuff ****/ func CreateForm(w io.Writer, c *cfg.Config) error { // setup url, req.Request, timeout handling etc diff --git a/upctl/lib/output.go b/upctl/lib/output.go index ef250c0..cdc26ad 100644 --- a/upctl/lib/output.go +++ b/upctl/lib/output.go @@ -80,7 +80,7 @@ func WriteExtended(w io.Writer, response *common.Response) { for _, entry := range response.Uploads { expire := prepareExpire(entry.Expire, entry.Created) fmt.Fprintf(w, format, "Upload-Id", entry.Id) - fmt.Fprintf(w, format, "Description", entry.Id) + fmt.Fprintf(w, format, "Description", entry.Description) fmt.Fprintf(w, format, "Expire", expire) fmt.Fprintf(w, format, "Context", entry.Context) fmt.Fprintf(w, format, "Created", entry.Created)