diff options
Diffstat (limited to 'srv/cmd')
-rw-r--r-- | srv/cmd/mediocre-blog/mailinglist.go | 85 | ||||
-rw-r--r-- | srv/cmd/mediocre-blog/main.go | 191 | ||||
-rw-r--r-- | srv/cmd/mediocre-blog/middleware.go | 78 | ||||
-rw-r--r-- | srv/cmd/mediocre-blog/pow.go | 51 | ||||
-rw-r--r-- | srv/cmd/mediocre-blog/utils.go | 60 |
5 files changed, 42 insertions, 423 deletions
diff --git a/srv/cmd/mediocre-blog/mailinglist.go b/srv/cmd/mediocre-blog/mailinglist.go deleted file mode 100644 index 39ab0d4..0000000 --- a/srv/cmd/mediocre-blog/mailinglist.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "errors" - "net/http" - "strings" - - "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" -) - -func mailingListSubscribeHandler(ml mailinglist.MailingList) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - email := r.PostFormValue("email") - if parts := strings.Split(email, "@"); len(parts) != 2 || - parts[0] == "" || - parts[1] == "" || - len(email) >= 512 { - badRequest(rw, r, errors.New("invalid email")) - return - } - - if err := ml.BeginSubscription(email); 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 { - internalServerError(rw, r, err) - return - } - - jsonResult(rw, r, struct{}{}) - }) -} - -func mailingListFinalizeHandler(ml mailinglist.MailingList) http.Handler { - var errInvalidSubToken = errors.New("invalid subToken") - - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - subToken := r.PostFormValue("subToken") - if l := len(subToken); l == 0 || l > 128 { - badRequest(rw, r, errInvalidSubToken) - return - } - - err := ml.FinalizeSubscription(subToken) - - if errors.Is(err, mailinglist.ErrNotFound) { - badRequest(rw, r, errInvalidSubToken) - return - - } else if errors.Is(err, mailinglist.ErrAlreadyVerified) { - // no problem - - } else if err != nil { - internalServerError(rw, r, err) - return - } - - jsonResult(rw, r, struct{}{}) - }) -} - -func mailingListUnsubscribeHandler(ml mailinglist.MailingList) http.Handler { - var errInvalidUnsubToken = errors.New("invalid unsubToken") - - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - unsubToken := r.PostFormValue("unsubToken") - if l := len(unsubToken); l == 0 || l > 128 { - badRequest(rw, r, errInvalidUnsubToken) - return - } - - err := ml.Unsubscribe(unsubToken) - - if errors.Is(err, mailinglist.ErrNotFound) { - badRequest(rw, r, errInvalidUnsubToken) - return - - } else if err != nil { - internalServerError(rw, r, err) - return - } - - jsonResult(rw, r, struct{}{}) - }) -} diff --git a/srv/cmd/mediocre-blog/main.go b/srv/cmd/mediocre-blog/main.go index 0a5f8b7..7d3f722 100644 --- a/srv/cmd/mediocre-blog/main.go +++ b/srv/cmd/mediocre-blog/main.go @@ -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,111 +25,59 @@ func loggerFatalErr(ctx context.Context, logger *mlog.Logger, descr string, err func main() { ctx := context.Background() + cfg := cfg.New() - 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") + dataDir := cfg.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)") + var powMgrParams pow.ManagerParams + powMgrParams.SetupCfg(cfg) + ctx = mctx.WithAnnotator(ctx, &powMgrParams) - 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") + var mailerParams mailinglist.MailerParams + mailerParams.SetupCfg(cfg) + ctx = mctx.WithAnnotator(ctx, &mailerParams) - 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.") + var mlParams mailinglist.Params + mlParams.SetupCfg(cfg) + ctx = mctx.WithAnnotator(ctx, &mlParams) - // parse config + var apiParams api.Params + apiParams.SetupCfg(cfg) + ctx = mctx.WithAnnotator(ctx, &apiParams) - 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") - } + // initialization + err := cfg.Init(ctx) - publicURL, err := url.Parse(*publicURLStr) - if err != nil { - loggerFatalErr(ctx, logger, "parsing -public-url", err) - } + logger := mlog.NewLogger(nil) + defer logger.Close() - 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) - } - } + logger.Info(ctx, "process started") + defer logger.Info(ctx, "process exiting") - 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", - }) - - 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)) + mlParams.Store = mlStore + mlParams.Mailer = mailer + mlParams.Clock = clock - 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) + ml := mailinglist.New(mlParams) - 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) - } - - if *listenProto == "unix" { - if err := os.Chmod(*listenAddr, 0777); err != nil { - loggerFatalErr(ctx, logger, "chmod-ing unix socket", err) - } + loggerFatalErr(ctx, logger, "initializing api", 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 diff --git a/srv/cmd/mediocre-blog/middleware.go b/srv/cmd/mediocre-blog/middleware.go deleted file mode 100644 index 165f82f..0000000 --- a/srv/cmd/mediocre-blog/middleware.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "net" - "net/http" - "time" - - "github.com/mediocregopher/mediocre-go-lib/v2/mctx" - "github.com/mediocregopher/mediocre-go-lib/v2/mlog" -) - -func addResponseHeaders(headers map[string]string, h http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - for k, v := range headers { - rw.Header().Set(k, v) - } - h.ServeHTTP(rw, r) - }) -} - -func annotateMiddleware(h http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - - type reqInfoKey string - - ip, _, _ := net.SplitHostPort(r.RemoteAddr) - - ctx := r.Context() - ctx = mctx.Annotate(ctx, - reqInfoKey("remote_ip"), ip, - reqInfoKey("url"), r.URL, - reqInfoKey("method"), r.Method, - ) - - r = r.WithContext(ctx) - h.ServeHTTP(rw, r) - }) -} - -type logResponseWriter struct { - http.ResponseWriter - statusCode int -} - -func newLogResponseWriter(rw http.ResponseWriter) *logResponseWriter { - return &logResponseWriter{ - ResponseWriter: rw, - statusCode: 200, - } -} - -func (lrw *logResponseWriter) WriteHeader(statusCode int) { - lrw.statusCode = statusCode - lrw.ResponseWriter.WriteHeader(statusCode) -} - -func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - - r = setRequestLogger(r, logger) - - lrw := newLogResponseWriter(rw) - - started := time.Now() - h.ServeHTTP(lrw, r) - took := time.Since(started) - - type logCtxKey string - - ctx := r.Context() - ctx = mctx.Annotate(ctx, - logCtxKey("took"), took.String(), - logCtxKey("response_code"), lrw.statusCode, - ) - - logger.Info(ctx, "handled HTTP request") - }) -} diff --git a/srv/cmd/mediocre-blog/pow.go b/srv/cmd/mediocre-blog/pow.go deleted file mode 100644 index a505a64..0000000 --- a/srv/cmd/mediocre-blog/pow.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "encoding/hex" - "errors" - "fmt" - "net/http" - - "github.com/mediocregopher/blog.mediocregopher.com/srv/pow" -) - -func newPowChallengeHandler(mgr pow.Manager) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - - challenge := mgr.NewChallenge() - - jsonResult(rw, r, struct { - Seed string `json:"seed"` - Target uint32 `json:"target"` - }{ - Seed: hex.EncodeToString(challenge.Seed), - Target: challenge.Target, - }) - }) -} - -func requirePowMiddleware(mgr pow.Manager, h http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - - seedHex := r.PostFormValue("powSeed") - seed, err := hex.DecodeString(seedHex) - if err != nil || len(seed) == 0 { - badRequest(rw, r, errors.New("invalid powSeed")) - return - } - - solutionHex := r.PostFormValue("powSolution") - solution, err := hex.DecodeString(solutionHex) - if err != nil || len(seed) == 0 { - badRequest(rw, r, errors.New("invalid powSolution")) - return - } - - if err := mgr.CheckSolution(seed, solution); err != nil { - badRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err)) - return - } - - h.ServeHTTP(rw, r) - }) -} diff --git a/srv/cmd/mediocre-blog/utils.go b/srv/cmd/mediocre-blog/utils.go deleted file mode 100644 index 1c9408c..0000000 --- a/srv/cmd/mediocre-blog/utils.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "net/http" - - "github.com/mediocregopher/mediocre-go-lib/v2/mlog" -) - -type loggerCtxKey int - -func setRequestLogger(r *http.Request, logger *mlog.Logger) *http.Request { - ctx := r.Context() - ctx = context.WithValue(ctx, loggerCtxKey(0), logger) - return r.WithContext(ctx) -} - -func getRequestLogger(r *http.Request) *mlog.Logger { - ctx := r.Context() - logger, _ := ctx.Value(loggerCtxKey(0)).(*mlog.Logger) - if logger == nil { - logger = mlog.Null - } - return logger -} - -func jsonResult(rw http.ResponseWriter, r *http.Request, v interface{}) { - b, err := json.Marshal(v) - if err != nil { - internalServerError(rw, r, err) - return - } - b = append(b, '\n') - - rw.Header().Set("Content-Type", "application/json") - rw.Write(b) -} - -func badRequest(rw http.ResponseWriter, r *http.Request, err error) { - getRequestLogger(r).Warn(r.Context(), "bad request", err) - - rw.WriteHeader(400) - jsonResult(rw, r, struct { - Error string `json:"error"` - }{ - Error: err.Error(), - }) -} - -func internalServerError(rw http.ResponseWriter, r *http.Request, err error) { - getRequestLogger(r).Error(r.Context(), "internal server error", err) - - rw.WriteHeader(500) - jsonResult(rw, r, struct { - Error string `json:"error"` - }{ - Error: "internal server error", - }) -} |