You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
6.4 KiB
227 lines
6.4 KiB
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/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"
|
|
"github.com/tilinna/clock"
|
|
)
|
|
|
|
func loggerFatalErr(ctx context.Context, logger *mlog.Logger, descr string, err error) {
|
|
logger.Fatal(ctx, fmt.Sprintf("%s: %v", descr, err))
|
|
}
|
|
|
|
func main() {
|
|
|
|
ctx := context.Background()
|
|
|
|
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")
|
|
|
|
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,
|
|
)
|
|
}
|
|
|
|
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,
|
|
})
|
|
|
|
// sugar
|
|
requirePow := func(h http.Handler) http.Handler { return requirePowMiddleware(powMgr, h) }
|
|
|
|
var mailer mailinglist.Mailer
|
|
if *smtpAddr == "" {
|
|
logger.Info(ctx, "-smtp-addr not given, using NullMailer")
|
|
mailer = mailinglist.NullMailer
|
|
} else {
|
|
mailer = mailinglist.NewMailer(mailerCfg)
|
|
}
|
|
|
|
mlStore, err := mailinglist.NewStore(path.Join(*dataDir, "mailinglist.sqlite3"))
|
|
if err != nil {
|
|
loggerFatalErr(ctx, logger, "initializing mailing list storage", err)
|
|
}
|
|
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))
|
|
|
|
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")
|
|
|
|
l, err := net.Listen(*listenProto, *listenAddr)
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
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)
|
|
}
|
|
}()
|
|
|
|
sigCh := make(chan os.Signal)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
<-sigCh
|
|
|
|
// let the defers begin
|
|
}
|
|
|