split configuration parsing out into separate packages, split api out as well
This commit is contained in:
parent
dce39b836a
commit
0197d9cd49
174
srv/api/api.go
Normal file
174
srv/api/api.go
Normal 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
|
||||
}
|
@ -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)
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package api
|
||||
|
||||
import (
|
||||
"net"
|
@ -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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
52
srv/cfg/cfg.go
Normal file
52
srv/cfg/cfg.go
Normal 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
|
||||
}
|
@ -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,6 +25,28 @@ func loggerFatalErr(ctx context.Context, logger *mlog.Logger, descr string, err
|
||||
func main() {
|
||||
|
||||
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)
|
||||
defer logger.Close()
|
||||
@ -39,104 +54,30 @@ func main() {
|
||||
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")
|
||||
|
||||
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 {
|
||||
loggerFatalErr(ctx, logger, "parsing -public-url", 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,
|
||||
)
|
||||
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",
|
||||
})
|
||||
mlParams.Store = mlStore
|
||||
mlParams.Mailer = mailer
|
||||
mlParams.Clock = clock
|
||||
|
||||
mux := http.NewServeMux()
|
||||
ml := mailinglist.New(mlParams)
|
||||
|
||||
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))
|
||||
|
||||
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
|
||||
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)
|
||||
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() {
|
||||
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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
// URL of the page which should be navigated to in order to remove a
|
||||
// subscription.
|
||||
UnsubURL string
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user