diff --git a/srv/api/api.go b/srv/api/api.go new file mode 100644 index 0000000..ae0970b --- /dev/null +++ b/srv/api/api.go @@ -0,0 +1,174 @@ +// Package api implements the HTTP-based api for the mediocre-blog. +package api + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + + "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" + "github.com/mediocregopher/blog.mediocregopher.com/srv/pow" + "github.com/mediocregopher/mediocre-go-lib/v2/mctx" + "github.com/mediocregopher/mediocre-go-lib/v2/mlog" +) + +// Params are used to instantiate a new API instance. All fields are required +// unless otherwise noted. +type Params struct { + Logger *mlog.Logger + PowManager pow.Manager + MailingList mailinglist.MailingList + + // ListenProto and ListenAddr are passed into net.Listen to create the + // API's listener. Both "tcp" and "unix" protocols are explicitly + // supported. + ListenProto, ListenAddr string + + // StaticDir and StaticProxy are mutually exclusive. + // + // If StaticDir is set then that directory on the filesystem will be used to + // serve the static site. + // + // Otherwise if StaticProxy is set all requests for the static site will be + // reverse-proxied there. + StaticDir string + StaticProxy *url.URL +} + +// SetupCfg implement the cfg.Cfger interface. +func (p *Params) SetupCfg(cfg *cfg.Cfg) { + + cfg.StringVar(&p.ListenProto, "listen-proto", "tcp", "Protocol to listen for HTTP requests with") + cfg.StringVar(&p.ListenAddr, "listen-addr", ":4000", "Address/path to listen for HTTP requests on") + + cfg.StringVar(&p.StaticDir, "static-dir", "", "Directory from which static files are served (mutually exclusive with -static-proxy-url)") + staticProxyURLStr := cfg.String("static-proxy-url", "", "HTTP address from which static files are served (mutually exclusive with -static-dir)") + + cfg.OnInit(func(ctx context.Context) error { + if *staticProxyURLStr != "" { + var err error + if p.StaticProxy, err = url.Parse(*staticProxyURLStr); err != nil { + return fmt.Errorf("parsing -static-proxy-url: %w", err) + } + + } else if p.StaticDir == "" { + return errors.New("-static-dir or -static-proxy-url is required") + } + + return nil + }) +} + +// Annotate implements mctx.Annotator interface. +func (p *Params) Annotate(a mctx.Annotations) { + a["listenProto"] = p.ListenProto + a["listenAddr"] = p.ListenAddr + + if p.StaticProxy != nil { + a["staticProxy"] = p.StaticProxy.String() + return + } + + a["staticDir"] = p.StaticDir +} + +// API will listen on the port configured for it, and serve HTTP requests for +// the mediocre-blog. +type API interface { + Shutdown(ctx context.Context) error +} + +type api struct { + params Params + srv *http.Server +} + +// New initializes and returns a new API instance, including setting up all +// listening ports. +func New(params Params) (API, error) { + + l, err := net.Listen(params.ListenProto, params.ListenAddr) + if err != nil { + return nil, fmt.Errorf("creating listen socket: %w", err) + } + + if params.ListenProto == "unix" { + if err := os.Chmod(params.ListenAddr, 0777); err != nil { + return nil, fmt.Errorf("chmod-ing unix socket: %w", err) + } + } + + a := &api{ + params: params, + } + + a.srv = &http.Server{Handler: a.handler()} + + go func() { + + err := a.srv.Serve(l) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + ctx := mctx.Annotate(context.Background(), a.params) + params.Logger.Fatal(ctx, fmt.Sprintf("%s: %v", "serving http server", err)) + } + }() + + return a, nil +} + +func (a *api) Shutdown(ctx context.Context) error { + if err := a.srv.Shutdown(ctx); err != nil { + return err + } + + return nil +} + +func (a *api) handler() http.Handler { + + var staticHandler http.Handler + if a.params.StaticDir != "" { + staticHandler = http.FileServer(http.Dir(a.params.StaticDir)) + } else { + staticHandler = httputil.NewSingleHostReverseProxy(a.params.StaticProxy) + } + + // sugar + requirePow := func(h http.Handler) http.Handler { + return a.requirePowMiddleware(h) + } + + mux := http.NewServeMux() + + mux.Handle("/", staticHandler) + + apiMux := http.NewServeMux() + apiMux.Handle("/pow/challenge", a.newPowChallengeHandler()) + apiMux.Handle("/pow/check", + requirePow( + http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}), + ), + ) + + apiMux.Handle("/mailinglist/subscribe", requirePow(a.mailingListSubscribeHandler())) + apiMux.Handle("/mailinglist/finalize", a.mailingListFinalizeHandler()) + apiMux.Handle("/mailinglist/unsubscribe", a.mailingListUnsubscribeHandler()) + + apiHandler := logMiddleware(a.params.Logger, apiMux) + apiHandler = annotateMiddleware(apiHandler) + apiHandler = addResponseHeaders(map[string]string{ + "Cache-Control": "no-store, max-age=0", + "Pragma": "no-cache", + "Expires": "0", + }, apiHandler) + + mux.Handle("/api/", http.StripPrefix("/api", apiHandler)) + + return mux +} diff --git a/srv/cmd/mediocre-blog/mailinglist.go b/srv/api/mailinglist.go similarity index 80% rename from srv/cmd/mediocre-blog/mailinglist.go rename to srv/api/mailinglist.go index 39ab0d4..2ddfbe6 100644 --- a/srv/cmd/mediocre-blog/mailinglist.go +++ b/srv/api/mailinglist.go @@ -1,4 +1,4 @@ -package main +package api import ( "errors" @@ -8,7 +8,7 @@ import ( "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" ) -func mailingListSubscribeHandler(ml mailinglist.MailingList) http.Handler { +func (a *api) mailingListSubscribeHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { email := r.PostFormValue("email") if parts := strings.Split(email, "@"); len(parts) != 2 || @@ -19,7 +19,9 @@ func mailingListSubscribeHandler(ml mailinglist.MailingList) http.Handler { return } - if err := ml.BeginSubscription(email); errors.Is(err, mailinglist.ErrAlreadyVerified) { + err := a.params.MailingList.BeginSubscription(email) + + if errors.Is(err, mailinglist.ErrAlreadyVerified) { // just eat the error, make it look to the user like the // verification email was sent. } else if err != nil { @@ -31,7 +33,7 @@ func mailingListSubscribeHandler(ml mailinglist.MailingList) http.Handler { }) } -func mailingListFinalizeHandler(ml mailinglist.MailingList) http.Handler { +func (a *api) mailingListFinalizeHandler() http.Handler { var errInvalidSubToken = errors.New("invalid subToken") return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -41,7 +43,7 @@ func mailingListFinalizeHandler(ml mailinglist.MailingList) http.Handler { return } - err := ml.FinalizeSubscription(subToken) + err := a.params.MailingList.FinalizeSubscription(subToken) if errors.Is(err, mailinglist.ErrNotFound) { badRequest(rw, r, errInvalidSubToken) @@ -59,7 +61,7 @@ func mailingListFinalizeHandler(ml mailinglist.MailingList) http.Handler { }) } -func mailingListUnsubscribeHandler(ml mailinglist.MailingList) http.Handler { +func (a *api) mailingListUnsubscribeHandler() http.Handler { var errInvalidUnsubToken = errors.New("invalid unsubToken") return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -69,7 +71,7 @@ func mailingListUnsubscribeHandler(ml mailinglist.MailingList) http.Handler { return } - err := ml.Unsubscribe(unsubToken) + err := a.params.MailingList.Unsubscribe(unsubToken) if errors.Is(err, mailinglist.ErrNotFound) { badRequest(rw, r, errInvalidUnsubToken) diff --git a/srv/cmd/mediocre-blog/middleware.go b/srv/api/middleware.go similarity index 99% rename from srv/cmd/mediocre-blog/middleware.go rename to srv/api/middleware.go index 165f82f..e3e85bb 100644 --- a/srv/cmd/mediocre-blog/middleware.go +++ b/srv/api/middleware.go @@ -1,4 +1,4 @@ -package main +package api import ( "net" diff --git a/srv/cmd/mediocre-blog/pow.go b/srv/api/pow.go similarity index 74% rename from srv/cmd/mediocre-blog/pow.go rename to srv/api/pow.go index a505a64..096e252 100644 --- a/srv/cmd/mediocre-blog/pow.go +++ b/srv/api/pow.go @@ -1,18 +1,16 @@ -package main +package api import ( "encoding/hex" "errors" "fmt" "net/http" - - "github.com/mediocregopher/blog.mediocregopher.com/srv/pow" ) -func newPowChallengeHandler(mgr pow.Manager) http.Handler { +func (a *api) newPowChallengeHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - challenge := mgr.NewChallenge() + challenge := a.params.PowManager.NewChallenge() jsonResult(rw, r, struct { Seed string `json:"seed"` @@ -24,7 +22,7 @@ func newPowChallengeHandler(mgr pow.Manager) http.Handler { }) } -func requirePowMiddleware(mgr pow.Manager, h http.Handler) http.Handler { +func (a *api) requirePowMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { seedHex := r.PostFormValue("powSeed") @@ -41,7 +39,9 @@ func requirePowMiddleware(mgr pow.Manager, h http.Handler) http.Handler { return } - if err := mgr.CheckSolution(seed, solution); err != nil { + err = a.params.PowManager.CheckSolution(seed, solution) + + if err != nil { badRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err)) return } diff --git a/srv/cmd/mediocre-blog/utils.go b/srv/api/utils.go similarity index 98% rename from srv/cmd/mediocre-blog/utils.go rename to srv/api/utils.go index 1c9408c..8e2a63b 100644 --- a/srv/cmd/mediocre-blog/utils.go +++ b/srv/api/utils.go @@ -1,4 +1,4 @@ -package main +package api import ( "context" diff --git a/srv/cfg/cfg.go b/srv/cfg/cfg.go new file mode 100644 index 0000000..08a9e53 --- /dev/null +++ b/srv/cfg/cfg.go @@ -0,0 +1,52 @@ +// Package cfg implements a simple wrapper around go's flag package, in order to +// implement initialization hooks. +package cfg + +import ( + "context" + "flag" + "os" +) + +// Cfger is a component which can be used with Cfg to setup its initialization. +type Cfger interface { + SetupCfg(*Cfg) +} + +// Cfg is a wrapper around the stdlib's FlagSet and a set of initialization +// hooks. +type Cfg struct { + *flag.FlagSet + + hooks []func(ctx context.Context) error +} + +// New initializes and returns a new instance of *Cfg. +func New() *Cfg { + return &Cfg{ + FlagSet: flag.NewFlagSet("", flag.ExitOnError), + } +} + +// OnInit appends the given callback to the sequence of hooks which will run on +// a call to Init. +func (c *Cfg) OnInit(cb func(context.Context) error) { + c.hooks = append(c.hooks, cb) +} + +// Init runs all hooks registered using OnInit, in the same order OnInit was +// called. If one returns an error that error is returned and no further hooks +// are run. +func (c *Cfg) Init(ctx context.Context) error { + if err := c.FlagSet.Parse(os.Args[1:]); err != nil { + return err + } + + for _, h := range c.hooks { + if err := h(ctx); err != nil { + return err + } + } + + return nil +} diff --git a/srv/cmd/mediocre-blog/main.go b/srv/cmd/mediocre-blog/main.go index 0a5f8b7..7d3f722 100644 --- a/srv/cmd/mediocre-blog/main.go +++ b/srv/cmd/mediocre-blog/main.go @@ -2,22 +2,15 @@ package main import ( "context" - "errors" - "flag" "fmt" - "net" - "net/http" - "net/http/httputil" - "net/url" "os" "os/signal" "path" - "strconv" - "strings" "syscall" "time" - "github.com/emersion/go-sasl" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api" + "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" "github.com/mediocregopher/blog.mediocregopher.com/srv/pow" "github.com/mediocregopher/mediocre-go-lib/v2/mctx" @@ -32,111 +25,59 @@ func loggerFatalErr(ctx context.Context, logger *mlog.Logger, descr string, err func main() { ctx := context.Background() + cfg := cfg.New() - logger := mlog.NewLogger(nil) - defer logger.Close() - - logger.Info(ctx, "process started") - defer logger.Info(ctx, "process exiting") - - publicURLStr := flag.String("public-url", "http://localhost:4000", "URL this service is accessible at") - listenProto := flag.String("listen-proto", "tcp", "Protocol to listen for HTTP requests with") - listenAddr := flag.String("listen-addr", ":4000", "Address/path to listen for HTTP requests on") - dataDir := flag.String("data-dir", ".", "Directory to use for long term storage") + dataDir := cfg.String("data-dir", ".", "Directory to use for long term storage") - staticDir := flag.String("static-dir", "", "Directory from which static files are served (mutually exclusive with -static-proxy-url)") - staticProxyURLStr := flag.String("static-proxy-url", "", "HTTP address from which static files are served (mutually exclusive with -static-dir)") + var powMgrParams pow.ManagerParams + powMgrParams.SetupCfg(cfg) + ctx = mctx.WithAnnotator(ctx, &powMgrParams) - powTargetStr := flag.String("pow-target", "0x0000FFFF", "Proof-of-work target, lower is more difficult") - powSecret := flag.String("pow-secret", "", "Secret used to sign proof-of-work challenge seeds") + var mailerParams mailinglist.MailerParams + mailerParams.SetupCfg(cfg) + ctx = mctx.WithAnnotator(ctx, &mailerParams) - smtpAddr := flag.String("ml-smtp-addr", "", "Address of SMTP server to use for sending emails for the mailing list") - smtpAuthStr := flag.String("ml-smtp-auth", "", "user:pass to use when authenticating with the mailing list SMTP server. The given user will also be used as the From address.") + var mlParams mailinglist.Params + mlParams.SetupCfg(cfg) + ctx = mctx.WithAnnotator(ctx, &mlParams) - // parse config + var apiParams api.Params + apiParams.SetupCfg(cfg) + ctx = mctx.WithAnnotator(ctx, &apiParams) - flag.Parse() - - switch { - case *staticDir == "" && *staticProxyURLStr == "": - logger.Fatal(ctx, "-static-dir or -static-proxy-url is required") - case *powSecret == "": - logger.Fatal(ctx, "-pow-secret is required") - } + // initialization + err := cfg.Init(ctx) - publicURL, err := url.Parse(*publicURLStr) - if err != nil { - loggerFatalErr(ctx, logger, "parsing -public-url", err) - } + logger := mlog.NewLogger(nil) + defer logger.Close() - var staticProxyURL *url.URL - if *staticProxyURLStr != "" { - var err error - if staticProxyURL, err = url.Parse(*staticProxyURLStr); err != nil { - loggerFatalErr(ctx, logger, "parsing -static-proxy-url", err) - } - } + logger.Info(ctx, "process started") + defer logger.Info(ctx, "process exiting") - powTargetUint, err := strconv.ParseUint(*powTargetStr, 0, 32) if err != nil { - loggerFatalErr(ctx, logger, "parsing -pow-target", err) - } - powTarget := uint32(powTargetUint) - - var mailerCfg mailinglist.MailerParams - - if *smtpAddr != "" { - mailerCfg.SMTPAddr = *smtpAddr - smtpAuthParts := strings.SplitN(*smtpAuthStr, ":", 2) - if len(smtpAuthParts) < 2 { - logger.Fatal(ctx, "invalid -ml-smtp-auth") - } - mailerCfg.SMTPAuth = sasl.NewPlainClient("", smtpAuthParts[0], smtpAuthParts[1]) - mailerCfg.SendAs = smtpAuthParts[0] - - ctx = mctx.Annotate(ctx, - "smtpAddr", mailerCfg.SMTPAddr, - "smtpSendAs", mailerCfg.SendAs, - ) + loggerFatalErr(ctx, logger, "initializing", err) } ctx = mctx.Annotate(ctx, - "publicURL", publicURL.String(), - "listenProto", *listenProto, - "listenAddr", *listenAddr, "dataDir", *dataDir, - "powTarget", fmt.Sprintf("%x", powTarget), ) - // initialization - - if *staticDir != "" { - ctx = mctx.Annotate(ctx, "staticDir", *staticDir) - } else { - ctx = mctx.Annotate(ctx, "staticProxyURL", *staticProxyURLStr) - } - clock := clock.Realtime() powStore := pow.NewMemoryStore(clock) defer powStore.Close() - powMgr := pow.NewManager(pow.ManagerParams{ - Clock: clock, - Store: powStore, - Secret: []byte(*powSecret), - Target: powTarget, - }) + powMgrParams.Store = powStore + powMgrParams.Clock = clock - // sugar - requirePow := func(h http.Handler) http.Handler { return requirePowMiddleware(powMgr, h) } + powMgr := pow.NewManager(powMgrParams) var mailer mailinglist.Mailer - if *smtpAddr == "" { + if mailerParams.SMTPAddr == "" { logger.Info(ctx, "-smtp-addr not given, using NullMailer") mailer = mailinglist.NullMailer } else { - mailer = mailinglist.NewMailer(mailerCfg) + mailer = mailinglist.NewMailer(mailerParams) } mlStore, err := mailinglist.NewStore(path.Join(*dataDir, "mailinglist.sqlite3")) @@ -145,80 +86,32 @@ func main() { } defer mlStore.Close() - ml := mailinglist.New(mailinglist.Params{ - Store: mlStore, - Mailer: mailer, - Clock: clock, - FinalizeSubURL: publicURL.String() + "/mailinglist/finalize.html", - UnsubURL: publicURL.String() + "/mailinglist/unsubscribe.html", - }) - - mux := http.NewServeMux() - - var staticHandler http.Handler - if *staticDir != "" { - staticHandler = http.FileServer(http.Dir(*staticDir)) - } else { - staticHandler = httputil.NewSingleHostReverseProxy(staticProxyURL) - } - - mux.Handle("/", staticHandler) - - apiMux := http.NewServeMux() - apiMux.Handle("/pow/challenge", newPowChallengeHandler(powMgr)) - apiMux.Handle("/pow/check", - requirePow( - http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}), - ), - ) - - apiMux.Handle("/mailinglist/subscribe", requirePow(mailingListSubscribeHandler(ml))) - apiMux.Handle("/mailinglist/finalize", mailingListFinalizeHandler(ml)) - apiMux.Handle("/mailinglist/unsubscribe", mailingListUnsubscribeHandler(ml)) + mlParams.Store = mlStore + mlParams.Mailer = mailer + mlParams.Clock = clock - apiHandler := logMiddleware(logger.WithNamespace("api"), apiMux) - apiHandler = annotateMiddleware(apiHandler) - apiHandler = addResponseHeaders(map[string]string{ - "Cache-Control": "no-store, max-age=0", - "Pragma": "no-cache", - "Expires": "0", - }, apiHandler) + ml := mailinglist.New(mlParams) - mux.Handle("/api/", http.StripPrefix("/api", apiHandler)) - - // run + apiParams.Logger = logger.WithNamespace("api") + apiParams.PowManager = powMgr + apiParams.MailingList = ml logger.Info(ctx, "listening") - - l, err := net.Listen(*listenProto, *listenAddr) + a, err := api.New(apiParams) if err != nil { - loggerFatalErr(ctx, logger, "creating listen socket", err) - } - - if *listenProto == "unix" { - if err := os.Chmod(*listenAddr, 0777); err != nil { - loggerFatalErr(ctx, logger, "chmod-ing unix socket", err) - } + loggerFatalErr(ctx, logger, "initializing api", err) } - - srv := &http.Server{Handler: mux} - go func() { - if err := srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) { - loggerFatalErr(ctx, logger, "serving http server", err) - } - }() - defer func() { - closeCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - logger.Info(ctx, "beginning graceful shutdown of http server") - - if err := srv.Shutdown(closeCtx); err != nil { - loggerFatalErr(ctx, logger, "gracefully shutting down http server", err) + if err := a.Shutdown(shutdownCtx); err != nil { + loggerFatalErr(ctx, logger, "shutting down api", err) } }() + // wait + sigCh := make(chan os.Signal) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh diff --git a/srv/mailinglist/mailer.go b/srv/mailinglist/mailer.go index 12fc398..b65ccb8 100644 --- a/srv/mailinglist/mailer.go +++ b/srv/mailinglist/mailer.go @@ -1,8 +1,14 @@ package mailinglist import ( + "context" + "errors" + "strings" + "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" + "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "github.com/mediocregopher/mediocre-go-lib/v2/mctx" ) // Mailer is used to deliver emails to arbitrary recipients. @@ -30,6 +36,39 @@ type MailerParams struct { SendAs string } +// SetupCfg implement the cfg.Cfger interface. +func (m *MailerParams) SetupCfg(cfg *cfg.Cfg) { + + cfg.StringVar(&m.SMTPAddr, "ml-smtp-addr", "", "Address of SMTP server to use for sending emails for the mailing list") + smtpAuthStr := cfg.String("ml-smtp-auth", "", "user:pass to use when authenticating with the mailing list SMTP server. The given user will also be used as the From address.") + + cfg.OnInit(func(ctx context.Context) error { + if m.SMTPAddr == "" { + return nil + } + + smtpAuthParts := strings.SplitN(*smtpAuthStr, ":", 2) + if len(smtpAuthParts) < 2 { + return errors.New("invalid -ml-smtp-auth") + } + + m.SMTPAuth = sasl.NewPlainClient("", smtpAuthParts[0], smtpAuthParts[1]) + m.SendAs = smtpAuthParts[0] + + return nil + }) +} + +// Annotate implements mctx.Annotator interface. +func (m *MailerParams) Annotate(a mctx.Annotations) { + if m.SMTPAddr == "" { + return + } + + a["smtpAddr"] = m.SMTPAddr + a["smtpSendAs"] = m.SendAs +} + type mailer struct { params MailerParams } diff --git a/srv/mailinglist/mailinglist.go b/srv/mailinglist/mailinglist.go index 2ebb952..60c1174 100644 --- a/srv/mailinglist/mailinglist.go +++ b/srv/mailinglist/mailinglist.go @@ -4,13 +4,17 @@ package mailinglist import ( "bytes" + "context" "errors" "fmt" "html/template" "io" + "net/url" "strings" "github.com/google/uuid" + "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "github.com/mediocregopher/mediocre-go-lib/v2/mctx" "github.com/tilinna/clock" ) @@ -42,13 +46,28 @@ type Params struct { Mailer Mailer Clock clock.Clock - // URL of the page which should be navigated to in order to finalize a - // subscription. - FinalizeSubURL string + // PublicURL is the base URL which site visitors can navigate to. + // MailingList will generate links based on this value. + PublicURL *url.URL +} + +// SetupCfg implement the cfg.Cfger interface. +func (p *Params) SetupCfg(cfg *cfg.Cfg) { + publicURLStr := cfg.String("public-url", "http://localhost:4000", "URL this service is accessible at") + + cfg.OnInit(func(ctx context.Context) error { + var err error + if p.PublicURL, err = url.Parse(*publicURLStr); err != nil { + return fmt.Errorf("parsing -public-url: %w", err) + } + + return nil + }) +} - // URL of the page which should be navigated to in order to remove a - // subscription. - UnsubURL string +// Annotate implements mctx.Annotator interface. +func (p *Params) Annotate(a mctx.Annotations) { + a["publicURL"] = p.PublicURL } // New initializes and returns a MailingList instance using the given Params. @@ -105,7 +124,11 @@ func (m *mailingList) BeginSubscription(email string) error { err = beginSubTpl.Execute(body, struct { SubLink string }{ - SubLink: fmt.Sprintf("%s?subToken=%s", m.params.FinalizeSubURL, emailRecord.SubToken), + SubLink: fmt.Sprintf( + "%s/mailinglist/finalize.html?subToken=%s", + m.params.PublicURL.String(), + emailRecord.SubToken, + ), }) if err != nil { @@ -217,7 +240,11 @@ func (m *mailingList) Publish(postTitle, postURL string) error { }{ PostTitle: postTitle, PostURL: postURL, - UnsubURL: fmt.Sprintf("%s?unsubToken=%s", m.params.UnsubURL, emailRecord.UnsubToken), + UnsubURL: fmt.Sprintf( + "%s/mailinglist/unsubscribe.html?unsubToken=%s", + m.params.PublicURL.String(), + emailRecord.UnsubToken, + ), }) if err != nil { diff --git a/srv/pow/pow.go b/srv/pow/pow.go index 8075103..ada8439 100644 --- a/srv/pow/pow.go +++ b/srv/pow/pow.go @@ -3,6 +3,7 @@ package pow import ( "bytes" + "context" "crypto/hmac" "crypto/md5" "crypto/rand" @@ -11,8 +12,11 @@ import ( "errors" "fmt" "hash" + "strconv" "time" + "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "github.com/mediocregopher/mediocre-go-lib/v2/mctx" "github.com/tilinna/clock" ) @@ -176,14 +180,42 @@ type ManagerParams struct { ChallengeTimeout time.Duration } -func (p ManagerParams) withDefaults() ManagerParams { +func (p *ManagerParams) setDefaults() { if p.Target == 0 { p.Target = 0x00FFFFFF } if p.ChallengeTimeout == 0 { p.ChallengeTimeout = 1 * time.Minute } - return p +} + +// SetupCfg implement the cfg.Cfger interface. +func (p *ManagerParams) SetupCfg(cfg *cfg.Cfg) { + powTargetStr := cfg.String("pow-target", "0x0000FFFF", "Proof-of-work target, lower is more difficult") + powSecretStr := cfg.String("pow-secret", "", "Secret used to sign proof-of-work challenge seeds") + + cfg.OnInit(func(ctx context.Context) error { + p.setDefaults() + + if *powSecretStr == "" { + return errors.New("-pow-secret is required") + } + + powTargetUint, err := strconv.ParseUint(*powTargetStr, 0, 32) + if err != nil { + return fmt.Errorf("parsing -pow-target: %w", err) + } + + p.Target = uint32(powTargetUint) + p.Secret = []byte(*powSecretStr) + + return nil + }) +} + +// Annotate implements mctx.Annotator interface. +func (p *ManagerParams) Annotate(a mctx.Annotations) { + a["powTarget"] = fmt.Sprintf("%x", p.Target) } type manager struct { @@ -193,7 +225,7 @@ type manager struct { // NewManager initializes and returns a Manager instance using the given // parameters. func NewManager(params ManagerParams) Manager { - params = params.withDefaults() + params.setDefaults() return &manager{ params: params, }