diff options
Diffstat (limited to 'srv/src/http/api.go')
-rw-r--r-- | srv/src/http/api.go | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/srv/src/http/api.go b/srv/src/http/api.go new file mode 100644 index 0000000..bbf4419 --- /dev/null +++ b/srv/src/http/api.go @@ -0,0 +1,249 @@ +// Package api implements the HTTP-based api for the mediocre-blog. +package http + +import ( + "context" + "errors" + "fmt" + "html/template" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + + "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "github.com/mediocregopher/blog.mediocregopher.com/srv/chat" + "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil" + "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" + "github.com/mediocregopher/blog.mediocregopher.com/srv/post" + "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 + + // PathPrefix, if given, will be prefixed to all url paths which are + // rendered by the API's templating system. + PathPrefix string + + PostStore post.Store + PostAssetStore post.AssetStore + + MailingList mailinglist.MailingList + + GlobalRoom chat.Room + UserIDCalculator *chat.UserIDCalculator + + // 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 + + // AuthUsers keys are usernames which are allowed to edit server-side data, + // and the values are the password hash which accompanies those users. The + // password hash must have been produced by NewPasswordHash. + AuthUsers map[string]string +} + +// 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 + + redirectTpl *template.Template +} + +// 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.redirectTpl = a.mustParseTpl("redirect.html") + + 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, "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) + } + + formMiddleware := func(h http.Handler) http.Handler { + h = checkCSRFMiddleware(h) + h = disallowGetMiddleware(h) + h = logReqMiddleware(h) + h = addResponseHeaders(map[string]string{ + "Cache-Control": "no-store, max-age=0", + "Pragma": "no-cache", + "Expires": "0", + }, h) + return h + } + + auther := NewAuther(a.params.AuthUsers) + + 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()) + + apiMux.Handle("/chat/global/", http.StripPrefix("/chat/global", newChatHandler( + a.params.GlobalRoom, + a.params.UserIDCalculator, + a.requirePowMiddleware, + ))) + + mux.Handle("/api/", http.StripPrefix("/api", formMiddleware(apiMux))) + } + + { + v2Mux := http.NewServeMux() + v2Mux.Handle("/follow.html", a.renderDumbTplHandler("follow.html")) + v2Mux.Handle("/posts/", http.StripPrefix("/posts", + apiutil.MethodMux(map[string]http.Handler{ + "GET": a.renderPostHandler(), + "EDIT": a.editPostHandler(), + "POST": authMiddleware(auther, + formMiddleware(a.postPostHandler()), + ), + "DELETE": authMiddleware(auther, + formMiddleware(a.deletePostHandler()), + ), + "PREVIEW": formMiddleware(a.previewPostHandler()), + }), + )) + v2Mux.Handle("/assets/", http.StripPrefix("/assets", + apiutil.MethodMux(map[string]http.Handler{ + "GET": a.getPostAssetHandler(), + "POST": authMiddleware(auther, + formMiddleware(a.postPostAssetHandler()), + ), + "DELETE": authMiddleware(auther, + formMiddleware(a.deletePostAssetHandler()), + ), + }), + )) + v2Mux.Handle("/", a.renderIndexHandler()) + + mux.Handle("/v2/", http.StripPrefix("/v2", v2Mux)) + } + + var globalHandler http.Handler = mux + globalHandler = setCSRFMiddleware(globalHandler) + globalHandler = setLoggerMiddleware(a.params.Logger, globalHandler) + + return globalHandler +} |