summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Picciano <mediocregopher@gmail.com>2021-08-07 20:38:37 -0600
committerBrian Picciano <mediocregopher@gmail.com>2021-08-07 20:38:37 -0600
commit0197d9cd493b5785bca05f476856540ec64da64a (patch)
treedb19ac4bfa602b1e0b001769c57d6b7c37d96fc4
parentdce39b836a0fd6e37ab2499c2e0e232572c17ad6 (diff)
split configuration parsing out into separate packages, split api out as well
-rw-r--r--srv/api/api.go174
-rw-r--r--srv/api/mailinglist.go (renamed from srv/cmd/mediocre-blog/mailinglist.go)16
-rw-r--r--srv/api/middleware.go (renamed from srv/cmd/mediocre-blog/middleware.go)2
-rw-r--r--srv/api/pow.go (renamed from srv/cmd/mediocre-blog/pow.go)14
-rw-r--r--srv/api/utils.go (renamed from srv/cmd/mediocre-blog/utils.go)2
-rw-r--r--srv/cfg/cfg.go52
-rw-r--r--srv/cmd/mediocre-blog/main.go191
-rw-r--r--srv/mailinglist/mailer.go39
-rw-r--r--srv/mailinglist/mailinglist.go43
-rw-r--r--srv/pow/pow.go38
10 files changed, 395 insertions, 176 deletions
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
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
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
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
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,
}