summaryrefslogtreecommitdiff
path: root/srv/src/http/api.go
diff options
context:
space:
mode:
Diffstat (limited to 'srv/src/http/api.go')
-rw-r--r--srv/src/http/api.go249
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
+}