diff --git a/Dockerfile b/Dockerfile index 8a90096..9b9c615 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG TZ=America/Chicago -FROM umputun/baseimage:buildgo-latest as build-backend +FROM umputun/baseimage:buildgo-latest AS build-backend ARG CI ARG GIT_BRANCH diff --git a/README.md b/README.md index 13515f1..9d13a79 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ _Feel free to suggest any other ways to make the process safer._ - MAX_EXPIRE - maximum lifetime period, default 24h - PIN_SIZE - size (in characters) of the pin, default 5 - PIN_ATTEMPTS - maximum number of failed attempts to enter pin, default 3 + - PROTOCOL - http or https 1. Setup SSL: - The system can make valid certificates for you automatically with integrated [nginx-le](https://github.com/umputun/nginx-le). Just set: - LETSENCRYPT=true diff --git a/app/main.go b/app/main.go index ebef965..b41d824 100644 --- a/app/main.go +++ b/app/main.go @@ -24,6 +24,7 @@ var opts struct { WebRoot string `long:"web" env:"WEB" default:"./ui/static/" description:"web ui location"` Dbg bool `long:"dbg" description:"debug mode"` Domain string `short:"d" long:"domain" env:"DOMAIN" description:"site domain" required:"true"` + Protocol string `short:"p" long:"protocol" env:"PROTOCOL" description:"site protocol" choice:"http" choice:"https" default:"https" required:"true"` // nolint } var revision string @@ -36,26 +37,24 @@ func main() { setupLog(opts.Dbg) - templateCache, err := server.NewTemplateCache() - if err != nil { - log.Printf("[ERROR] can't create template cache, %+v", err) - os.Exit(1) - } - dataStore := getEngine(opts.Engine, opts.BoltDB) crypter := messager.Crypt{Key: messager.MakeSignKey(opts.SignKey, opts.PinSize)} params := messager.Params{MaxDuration: opts.MaxExpire, MaxPinAttempts: opts.MaxPinAttempts} - srv := server.Server{ - Messager: messager.New(dataStore, crypter, params), + + srv, err := server.New(messager.New(dataStore, crypter, params), revision, server.Config{ + Domain: opts.Domain, + Protocol: opts.Protocol, PinSize: opts.PinSize, - MaxExpire: opts.MaxExpire, MaxPinAttempts: opts.MaxPinAttempts, + MaxExpire: opts.MaxExpire, WebRoot: opts.WebRoot, - Version: revision, - Domain: opts.Domain, - TemplateCache: templateCache, + }) + + if err != nil { + log.Fatalf("[ERROR] can't create server, %v", err) } - if err := srv.Run(context.Background()); err != nil { + + if err = srv.Run(context.Background()); err != nil { log.Printf("[ERROR] failed, %+v", err) } } diff --git a/app/server/server.go b/app/server/server.go index bd77e0b..5c00844 100644 --- a/app/server/server.go +++ b/app/server/server.go @@ -22,16 +22,36 @@ import ( "github.com/umputun/secrets/app/store" ) -// Server is a rest with store -type Server struct { - Messager Messager +// Config is a configuration for the server +type Config struct { + Domain string + Protocol string PinSize int MaxPinAttempts int MaxExpire time.Duration WebRoot string - Version string - Domain string - TemplateCache map[string]*template.Template +} + +// Server is a rest with store +type Server struct { + messager Messager + cfg Config + version string + templateCache map[string]*template.Template +} + +// New creates a new server +func New(m Messager, version string, cfg Config) (Server, error) { + cache, err := newTemplateCache() + if err != nil { + return Server{}, errors.Wrap(err, "can't create template cache") + } + return Server{ + messager: m, + cfg: cfg, + version: version, + templateCache: cache, + }, nil } // Messager interface making and loading messages @@ -75,7 +95,7 @@ func (s Server) routes() chi.Router { router.Use(middleware.RequestID, middleware.RealIP, um.Recoverer(log.Default())) router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second)) - router.Use(um.AppInfo("secrets", "Umputun", s.Version), um.Ping, um.SizeLimit(64*1024)) + router.Use(um.AppInfo("secrets", "Umputun", s.version), um.Ping, um.SizeLimit(64*1024)) router.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil))) router.Route("/api/v1", func(r chi.Router) { @@ -109,7 +129,7 @@ func (s Server) routes() chi.Router { r.Get("/", s.indexCtrl) }) - fs, err := um.NewFileServer("/static", s.WebRoot) + fs, err := um.NewFileServer("/static", s.cfg.WebRoot) if err != nil { log.Fatalf("[ERROR] can't create file server %v", err) } @@ -133,14 +153,14 @@ func (s Server) saveMessageCtrl(w http.ResponseWriter, r *http.Request) { return } - if len(request.Pin) != s.PinSize { + if len(request.Pin) != s.cfg.PinSize { log.Printf("[WARN] incorrect pin size %d", len(request.Pin)) render.Status(r, http.StatusBadRequest) render.JSON(w, r, JSON{"error": "Incorrect pin size"}) return } - msg, err := s.Messager.MakeMessage(time.Second*time.Duration(request.Exp), request.Message, request.Pin) + msg, err := s.messager.MakeMessage(time.Second*time.Duration(request.Exp), request.Message, request.Pin) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, JSON{"error": err.Error()}) @@ -154,7 +174,7 @@ func (s Server) saveMessageCtrl(w http.ResponseWriter, r *http.Request) { func (s Server) getMessageCtrl(w http.ResponseWriter, r *http.Request) { key, pin := chi.URLParam(r, "key"), chi.URLParam(r, "pin") - if key == "" || pin == "" || len(pin) != s.PinSize { + if key == "" || pin == "" || len(pin) != s.cfg.PinSize { log.Print("[WARN] no valid key or pin in get request") render.Status(r, http.StatusBadRequest) render.JSON(w, r, JSON{"error": "no key or pin passed"}) @@ -162,7 +182,7 @@ func (s Server) getMessageCtrl(w http.ResponseWriter, r *http.Request) { } serveRequest := func() (status int, res JSON) { - msg, err := s.Messager.LoadMessage(key, pin) + msg, err := s.messager.LoadMessage(key, pin) if err != nil { log.Printf("[WARN] failed to load key %v", key) if err == messager.ErrBadPinAttempt { @@ -188,9 +208,9 @@ func (s Server) getParamsCtrl(w http.ResponseWriter, r *http.Request) { MaxPinAttempts int `json:"max_pin_attempts"` MaxExpSecs int `json:"max_exp_sec"` }{ - PinSize: s.PinSize, - MaxPinAttempts: s.MaxPinAttempts, - MaxExpSecs: int(s.MaxExpire.Seconds()), + PinSize: s.cfg.PinSize, + MaxPinAttempts: s.cfg.MaxPinAttempts, + MaxExpSecs: int(s.cfg.MaxExpire.Seconds()), } render.JSON(w, r, params) } diff --git a/app/server/server_test.go b/app/server/server_test.go index 420765d..e0f7e2b 100644 --- a/app/server/server_test.go +++ b/app/server/server_test.go @@ -74,16 +74,20 @@ func TestServer_saveAndLoadBolt(t *testing.T) { require.NoError(t, os.Remove("/tmp/secrets-test.bdb")) }() signKey := messager.MakeSignKey("stew-pub-barcan-scatty-daimio-wicker-yakona", 5) - srv := Server{ - Messager: messager.New(eng, messager.Crypt{Key: signKey}, messager.Params{ + srv, err := New( + messager.New(eng, messager.Crypt{Key: signKey}, messager.Params{ MaxDuration: 10 * time.Hour, MaxPinAttempts: 3, }), - PinSize: 5, - MaxPinAttempts: 3, - MaxExpire: 10 * time.Hour, - Version: "1", - } + "1", + Config{ + PinSize: 5, + MaxPinAttempts: 3, + MaxExpire: 10 * time.Hour, + }) + + assert.NoError(t, err) + ts := httptest.NewServer(srv.routes()) defer ts.Close() @@ -246,16 +250,19 @@ func TestServer_getParams(t *testing.T) { func prepTestServer() (ts *httptest.Server, teardown func()) { eng := store.NewInMemory(time.Second) - srv := Server{ - Messager: messager.New(eng, messager.Crypt{Key: "123456789012345678901234567"}, messager.Params{ + + srv, _ := New( + messager.New(eng, messager.Crypt{Key: "123456789012345678901234567"}, messager.Params{ MaxDuration: 10 * time.Hour, MaxPinAttempts: 3, }), - PinSize: 5, - MaxPinAttempts: 3, - MaxExpire: 10 * time.Hour, - Version: "1", - } + "1", + Config{ + PinSize: 5, + MaxPinAttempts: 3, + MaxExpire: 10 * time.Hour, + }) + ts = httptest.NewServer(srv.routes()) return ts, ts.Close } diff --git a/app/server/web.go b/app/server/web.go index cd446ca..c7b561f 100644 --- a/app/server/web.go +++ b/app/server/web.go @@ -54,7 +54,7 @@ type templateData struct { // render renders a template func (s Server) render(w http.ResponseWriter, status int, page, tmplName string, data any) { - ts, ok := s.TemplateCache[page] + ts, ok := s.templateCache[page] if !ok { err := fmt.Errorf("the template %s does not exist", page) log.Printf("[ERROR] %v", err) @@ -89,9 +89,9 @@ func (s Server) indexCtrl(w http.ResponseWriter, r *http.Request) { // nolint data := templateData{ Form: createMsgForm{ Exp: 15, - MaxExp: humanDuration(s.MaxExpire), + MaxExp: humanDuration(s.cfg.MaxExpire), }, - PinSize: s.PinSize, + PinSize: s.cfg.PinSize, } s.render(w, http.StatusOK, "home.tmpl.html", baseTmpl, data) @@ -113,13 +113,13 @@ func (s Server) generateLinkCtrl(w http.ResponseWriter, r *http.Request) { form := createMsgForm{ Message: r.PostForm.Get(msgKey), ExpUnit: r.PostForm.Get(expUnitKey), - MaxExp: humanDuration(s.MaxExpire), + MaxExp: humanDuration(s.cfg.MaxExpire), } pinValues := r.Form["pin"] for _, p := range pinValues { if validator.Blank(p) || !validator.IsNumber(p) { - form.AddFieldError(pinKey, fmt.Sprintf("Pin must be %d digits long without empty values", s.PinSize)) + form.AddFieldError(pinKey, fmt.Sprintf("Pin must be %d digits long without empty values", s.cfg.PinSize)) break } } @@ -138,12 +138,12 @@ func (s Server) generateLinkCtrl(w http.ResponseWriter, r *http.Request) { form.Exp = expInt expDuration := duration(expInt, r.PostFormValue(expUnitKey)) - form.CheckField(validator.MaxDuration(expDuration, s.MaxExpire), expKey, fmt.Sprintf("Expire must be less than %s", humanDuration(s.MaxExpire))) + form.CheckField(validator.MaxDuration(expDuration, s.cfg.MaxExpire), expKey, fmt.Sprintf("Expire must be less than %s", humanDuration(s.cfg.MaxExpire))) if !form.Valid() { data := templateData{ Form: form, - PinSize: s.PinSize, + PinSize: s.cfg.PinSize, } // attach event listeners to pin inputs @@ -153,13 +153,13 @@ func (s Server) generateLinkCtrl(w http.ResponseWriter, r *http.Request) { return } - msg, err := s.Messager.MakeMessage(expDuration, form.Message, strings.Join(pinValues, "")) + msg, err := s.messager.MakeMessage(expDuration, form.Message, strings.Join(pinValues, "")) if err != nil { s.render(w, http.StatusOK, "secure-link.tmpl.html", errorTmpl, err.Error()) return } - msgURL := fmt.Sprintf("http://%s/message/%s", s.Domain, msg.Key) + msgURL := fmt.Sprintf("%s://%s/message/%s", s.cfg.Protocol, s.cfg.Domain, msg.Key) s.render(w, http.StatusOK, "secure-link.tmpl.html", "secure-link", msgURL) } @@ -175,7 +175,7 @@ func (s Server) showMessageViewCtrl(w http.ResponseWriter, r *http.Request) { Form: showMsgForm{ Key: key, }, - PinSize: s.PinSize, + PinSize: s.cfg.PinSize, } w.Header().Add("HX-Trigger-After-Swap", "setUpPinInputListeners") @@ -208,7 +208,7 @@ func (s Server) loadMessageCtrl(w http.ResponseWriter, r *http.Request) { pinValues := r.Form["pin"] for _, p := range pinValues { if validator.Blank(p) || !validator.IsNumber(p) { - form.AddFieldError(pinKey, fmt.Sprintf("Pin must be %d digits long without empty values", s.PinSize)) + form.AddFieldError(pinKey, fmt.Sprintf("Pin must be %d digits long without empty values", s.cfg.PinSize)) break } } @@ -216,7 +216,7 @@ func (s Server) loadMessageCtrl(w http.ResponseWriter, r *http.Request) { if !form.Valid() { data := templateData{ Form: form, - PinSize: s.PinSize, + PinSize: s.cfg.PinSize, } // attach event listeners to pin inputs @@ -226,7 +226,7 @@ func (s Server) loadMessageCtrl(w http.ResponseWriter, r *http.Request) { return } - msg, err := s.Messager.LoadMessage(form.Key, strings.Join(pinValues, "")) + msg, err := s.messager.LoadMessage(form.Key, strings.Join(pinValues, "")) if err != nil { if errors.Is(err, messager.ErrExpired) || errors.Is(err, store.ErrLoadRejected) { s.render(w, http.StatusOK, "error.tmpl.html", errorTmpl, err.Error()) @@ -237,7 +237,7 @@ func (s Server) loadMessageCtrl(w http.ResponseWriter, r *http.Request) { data := templateData{ Form: form, - PinSize: s.PinSize, + PinSize: s.cfg.PinSize, } // attach event listeners to pin inputs w.Header().Add("HX-Trigger-After-Swap", "setUpPinInputListeners") @@ -278,8 +278,8 @@ func humanDuration(d time.Duration) string { } } -// NewTemplateCache creates a template cache as a map -func NewTemplateCache() (map[string]*template.Template, error) { +// newTemplateCache creates a template cache as a map +func newTemplateCache() (map[string]*template.Template, error) { cache := map[string]*template.Template{} pages, err := fs.Glob(ui.Files, "html/*/*.tmpl.html") diff --git a/app/server/web_test.go b/app/server/web_test.go index bb630c3..4ae2e4b 100644 --- a/app/server/web_test.go +++ b/app/server/web_test.go @@ -87,7 +87,7 @@ func TestTemplates_HumanDuration(t *testing.T) { } func TestTemplates_NewTemplateCache(t *testing.T) { - cache, err := NewTemplateCache() + cache, err := newTemplateCache() assert.NoError(t, err) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 4e58bc8..bcac925 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -1,4 +1,3 @@ -version: '2' services: secrets: @@ -22,5 +21,6 @@ services: - PIN_ATTEMPTS=3 - MAX_EXPIRE=24h - DOMAIN=localhost:8080 + - PROTOCOL=http ports: - "8080:8080"