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 (
|
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)
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
@ -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
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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 (
|
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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user