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 }