split configuration parsing out into separate packages, split api out as well

This commit is contained in:
Brian Picciano 2021-08-07 20:38:37 -06:00
parent dce39b836a
commit 0197d9cd49
10 changed files with 397 additions and 178 deletions

174
srv/api/api.go Normal file
View File

@ -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
}

View File

@ -1,4 +1,4 @@
package main package api
import ( import (
"errors" "errors"
@ -8,7 +8,7 @@ import (
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" "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) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
email := r.PostFormValue("email") email := r.PostFormValue("email")
if parts := strings.Split(email, "@"); len(parts) != 2 || if parts := strings.Split(email, "@"); len(parts) != 2 ||
@ -19,7 +19,9 @@ func mailingListSubscribeHandler(ml mailinglist.MailingList) http.Handler {
return 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 // just eat the error, make it look to the user like the
// verification email was sent. // verification email was sent.
} else if err != nil { } 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") var errInvalidSubToken = errors.New("invalid subToken")
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
@ -41,7 +43,7 @@ func mailingListFinalizeHandler(ml mailinglist.MailingList) http.Handler {
return return
} }
err := ml.FinalizeSubscription(subToken) err := a.params.MailingList.FinalizeSubscription(subToken)
if errors.Is(err, mailinglist.ErrNotFound) { if errors.Is(err, mailinglist.ErrNotFound) {
badRequest(rw, r, errInvalidSubToken) 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") var errInvalidUnsubToken = errors.New("invalid unsubToken")
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
@ -69,7 +71,7 @@ func mailingListUnsubscribeHandler(ml mailinglist.MailingList) http.Handler {
return return
} }
err := ml.Unsubscribe(unsubToken) err := a.params.MailingList.Unsubscribe(unsubToken)
if errors.Is(err, mailinglist.ErrNotFound) { if errors.Is(err, mailinglist.ErrNotFound) {
badRequest(rw, r, errInvalidUnsubToken) badRequest(rw, r, errInvalidUnsubToken)

View File

@ -1,4 +1,4 @@
package main package api
import ( import (
"net" "net"

View File

@ -1,18 +1,16 @@
package main package api
import ( import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"net/http" "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) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
challenge := mgr.NewChallenge() challenge := a.params.PowManager.NewChallenge()
jsonResult(rw, r, struct { jsonResult(rw, r, struct {
Seed string `json:"seed"` 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) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
seedHex := r.PostFormValue("powSeed") seedHex := r.PostFormValue("powSeed")
@ -41,7 +39,9 @@ func requirePowMiddleware(mgr pow.Manager, h http.Handler) http.Handler {
return 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)) badRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err))
return return
} }

View File

@ -1,4 +1,4 @@
package main package api
import ( import (
"context" "context"

52
srv/cfg/cfg.go Normal file
View File

@ -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
}

View File

@ -2,22 +2,15 @@ package main
import ( import (
"context" "context"
"errors"
"flag"
"fmt" "fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os" "os"
"os/signal" "os/signal"
"path" "path"
"strconv"
"strings"
"syscall" "syscall"
"time" "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/mailinglist"
"github.com/mediocregopher/blog.mediocregopher.com/srv/pow" "github.com/mediocregopher/blog.mediocregopher.com/srv/pow"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx" "github.com/mediocregopher/mediocre-go-lib/v2/mctx"
@ -32,6 +25,28 @@ func loggerFatalErr(ctx context.Context, logger *mlog.Logger, descr string, err
func main() { func main() {
ctx := context.Background() ctx := context.Background()
cfg := cfg.New()
dataDir := cfg.String("data-dir", ".", "Directory to use for long term storage")
var powMgrParams pow.ManagerParams
powMgrParams.SetupCfg(cfg)
ctx = mctx.WithAnnotator(ctx, &powMgrParams)
var mailerParams mailinglist.MailerParams
mailerParams.SetupCfg(cfg)
ctx = mctx.WithAnnotator(ctx, &mailerParams)
var mlParams mailinglist.Params
mlParams.SetupCfg(cfg)
ctx = mctx.WithAnnotator(ctx, &mlParams)
var apiParams api.Params
apiParams.SetupCfg(cfg)
ctx = mctx.WithAnnotator(ctx, &apiParams)
// initialization
err := cfg.Init(ctx)
logger := mlog.NewLogger(nil) logger := mlog.NewLogger(nil)
defer logger.Close() defer logger.Close()
@ -39,104 +54,30 @@ func main() {
logger.Info(ctx, "process started") logger.Info(ctx, "process started")
defer logger.Info(ctx, "process exiting") 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")
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)")
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")
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.")
// parse config
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")
}
publicURL, err := url.Parse(*publicURLStr)
if err != nil { if err != nil {
loggerFatalErr(ctx, logger, "parsing -public-url", err) loggerFatalErr(ctx, logger, "initializing", err)
}
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)
}
}
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,
)
} }
ctx = mctx.Annotate(ctx, ctx = mctx.Annotate(ctx,
"publicURL", publicURL.String(),
"listenProto", *listenProto,
"listenAddr", *listenAddr,
"dataDir", *dataDir, "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() clock := clock.Realtime()
powStore := pow.NewMemoryStore(clock) powStore := pow.NewMemoryStore(clock)
defer powStore.Close() defer powStore.Close()
powMgr := pow.NewManager(pow.ManagerParams{ powMgrParams.Store = powStore
Clock: clock, powMgrParams.Clock = clock
Store: powStore,
Secret: []byte(*powSecret),
Target: powTarget,
})
// sugar powMgr := pow.NewManager(powMgrParams)
requirePow := func(h http.Handler) http.Handler { return requirePowMiddleware(powMgr, h) }
var mailer mailinglist.Mailer var mailer mailinglist.Mailer
if *smtpAddr == "" { if mailerParams.SMTPAddr == "" {
logger.Info(ctx, "-smtp-addr not given, using NullMailer") logger.Info(ctx, "-smtp-addr not given, using NullMailer")
mailer = mailinglist.NullMailer mailer = mailinglist.NullMailer
} else { } else {
mailer = mailinglist.NewMailer(mailerCfg) mailer = mailinglist.NewMailer(mailerParams)
} }
mlStore, err := mailinglist.NewStore(path.Join(*dataDir, "mailinglist.sqlite3")) mlStore, err := mailinglist.NewStore(path.Join(*dataDir, "mailinglist.sqlite3"))
@ -145,80 +86,32 @@ func main() {
} }
defer mlStore.Close() defer mlStore.Close()
ml := mailinglist.New(mailinglist.Params{ mlParams.Store = mlStore
Store: mlStore, mlParams.Mailer = mailer
Mailer: mailer, mlParams.Clock = clock
Clock: clock,
FinalizeSubURL: publicURL.String() + "/mailinglist/finalize.html",
UnsubURL: publicURL.String() + "/mailinglist/unsubscribe.html",
})
mux := http.NewServeMux() ml := mailinglist.New(mlParams)
var staticHandler http.Handler apiParams.Logger = logger.WithNamespace("api")
if *staticDir != "" { apiParams.PowManager = powMgr
staticHandler = http.FileServer(http.Dir(*staticDir)) apiParams.MailingList = ml
} 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))
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)
mux.Handle("/api/", http.StripPrefix("/api", apiHandler))
// run
logger.Info(ctx, "listening") logger.Info(ctx, "listening")
a, err := api.New(apiParams)
l, err := net.Listen(*listenProto, *listenAddr)
if err != nil { if err != nil {
loggerFatalErr(ctx, logger, "creating listen socket", err) loggerFatalErr(ctx, logger, "initializing api", err)
} }
if *listenProto == "unix" {
if err := os.Chmod(*listenAddr, 0777); err != nil {
loggerFatalErr(ctx, logger, "chmod-ing unix socket", 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() { defer func() {
closeCtx, cancel := context.WithTimeout(ctx, 5*time.Second) shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() defer cancel()
logger.Info(ctx, "beginning graceful shutdown of http server") if err := a.Shutdown(shutdownCtx); err != nil {
loggerFatalErr(ctx, logger, "shutting down api", err)
if err := srv.Shutdown(closeCtx); err != nil {
loggerFatalErr(ctx, logger, "gracefully shutting down http server", err)
} }
}() }()
// wait
sigCh := make(chan os.Signal) sigCh := make(chan os.Signal)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh <-sigCh

View File

@ -1,8 +1,14 @@
package mailinglist package mailinglist
import ( import (
"context"
"errors"
"strings"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/emersion/go-smtp" "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. // Mailer is used to deliver emails to arbitrary recipients.
@ -30,6 +36,39 @@ type MailerParams struct {
SendAs string 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 { type mailer struct {
params MailerParams params MailerParams
} }

View File

@ -4,13 +4,17 @@ package mailinglist
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"net/url"
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/tilinna/clock" "github.com/tilinna/clock"
) )
@ -42,13 +46,28 @@ type Params struct {
Mailer Mailer Mailer Mailer
Clock clock.Clock Clock clock.Clock
// URL of the page which should be navigated to in order to finalize a // PublicURL is the base URL which site visitors can navigate to.
// subscription. // MailingList will generate links based on this value.
FinalizeSubURL string PublicURL *url.URL
}
// URL of the page which should be navigated to in order to remove a // SetupCfg implement the cfg.Cfger interface.
// subscription. func (p *Params) SetupCfg(cfg *cfg.Cfg) {
UnsubURL string 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
})
}
// 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. // 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 { err = beginSubTpl.Execute(body, struct {
SubLink string 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 { if err != nil {
@ -217,7 +240,11 @@ func (m *mailingList) Publish(postTitle, postURL string) error {
}{ }{
PostTitle: postTitle, PostTitle: postTitle,
PostURL: postURL, 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 { if err != nil {

View File

@ -3,6 +3,7 @@ package pow
import ( import (
"bytes" "bytes"
"context"
"crypto/hmac" "crypto/hmac"
"crypto/md5" "crypto/md5"
"crypto/rand" "crypto/rand"
@ -11,8 +12,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"hash" "hash"
"strconv"
"time" "time"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/tilinna/clock" "github.com/tilinna/clock"
) )
@ -176,14 +180,42 @@ type ManagerParams struct {
ChallengeTimeout time.Duration ChallengeTimeout time.Duration
} }
func (p ManagerParams) withDefaults() ManagerParams { func (p *ManagerParams) setDefaults() {
if p.Target == 0 { if p.Target == 0 {
p.Target = 0x00FFFFFF p.Target = 0x00FFFFFF
} }
if p.ChallengeTimeout == 0 { if p.ChallengeTimeout == 0 {
p.ChallengeTimeout = 1 * time.Minute 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 { type manager struct {
@ -193,7 +225,7 @@ type manager struct {
// NewManager initializes and returns a Manager instance using the given // NewManager initializes and returns a Manager instance using the given
// parameters. // parameters.
func NewManager(params ManagerParams) Manager { func NewManager(params ManagerParams) Manager {
params = params.withDefaults() params.setDefaults()
return &manager{ return &manager{
params: params, params: params,
} }