From 0bd8bd6f2366e60212dd0305a584427b7b1aab26 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Thu, 19 Jan 2023 18:03:20 +0100 Subject: Slight cleanup in http package --- src/http/api.go | 286 ------------------------------------------------------- src/http/http.go | 286 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+), 286 deletions(-) delete mode 100644 src/http/api.go create mode 100644 src/http/http.go diff --git a/src/http/api.go b/src/http/api.go deleted file mode 100644 index ffe0f25..0000000 --- a/src/http/api.go +++ /dev/null @@ -1,286 +0,0 @@ -// Package api implements the HTTP-based api for the mediocre-blog. -package http - -import ( - "context" - "embed" - "encoding/json" - "errors" - "fmt" - "html/template" - "net" - "net/http" - "net/url" - "os" - "strings" - "time" - - lru "github.com/hashicorp/golang-lru" - "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" - "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" -) - -//go:embed static -var staticFS embed.FS - -// 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 - - PostStore post.Store - PostAssetStore post.AssetStore - PostDraftStore post.DraftStore - - MailingList mailinglist.MailingList - - // PublicURL is the base URL which site visitors can navigate to. - PublicURL *url.URL - - // 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 - - // 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 - - // AuthRatelimit indicates how much time must pass between subsequent auth - // attempts. - AuthRatelimit time.Duration -} - -// SetupCfg implement the cfg.Cfger interface. -func (p *Params) SetupCfg(cfg *cfg.Cfg) { - - publicURLStr := cfg.String("http-public-url", "http://localhost:4000", "URL this service is accessible at") - - cfg.StringVar(&p.ListenProto, "http-listen-proto", "tcp", "Protocol to listen for HTTP requests with") - cfg.StringVar(&p.ListenAddr, "http-listen-addr", ":4000", "Address/path to listen for HTTP requests on") - - httpAuthUsersStr := cfg.String("http-auth-users", "{}", "JSON object with usernames as values and password hashes (produced by the hash-password binary) as values. Denotes users which are able to edit server-side data") - - httpAuthRatelimitStr := cfg.String("http-auth-ratelimit", "5s", "Minimum duration which must be waited between subsequent auth attempts") - - cfg.OnInit(func(context.Context) error { - - err := json.Unmarshal([]byte(*httpAuthUsersStr), &p.AuthUsers) - - if err != nil { - return fmt.Errorf("unmarshaling -http-auth-users: %w", err) - } - - if p.AuthRatelimit, err = time.ParseDuration(*httpAuthRatelimitStr); err != nil { - return fmt.Errorf("unmarshaling -http-auth-ratelimit: %w", err) - } - - *publicURLStr = strings.TrimSuffix(*publicURLStr, "/") - if p.PublicURL, err = url.Parse(*publicURLStr); err != nil { - return fmt.Errorf("parsing -http-public-url: %w", err) - } - - return nil - }) -} - -// Annotate implements mctx.Annotator interface. -func (p *Params) Annotate(a mctx.Annotations) { - a["httpPublicURL"] = p.PublicURL - a["httpListenProto"] = p.ListenProto - a["httpListenAddr"] = p.ListenAddr - a["httpAuthRatelimit"] = p.AuthRatelimit -} - -// 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 - auther Auther -} - -// 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, - auther: NewAuther(params.AuthUsers, params.AuthRatelimit), - } - - 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 { - defer a.auther.Close() - if err := a.srv.Shutdown(ctx); err != nil { - return err - } - - return nil -} - -func (a *api) apiHandler() http.Handler { - mux := http.NewServeMux() - - mux.Handle("/pow/challenge", a.newPowChallengeHandler()) - mux.Handle("/pow/check", - a.requirePowMiddleware( - http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}), - ), - ) - - mux.Handle("/mailinglist/subscribe", a.requirePowMiddleware(a.mailingListSubscribeHandler())) - mux.Handle("/mailinglist/finalize", a.mailingListFinalizeHandler()) - mux.Handle("/mailinglist/unsubscribe", a.mailingListUnsubscribeHandler()) - - return apiutil.MethodMux(map[string]http.Handler{ - "POST": mux, - }) -} - -func (a *api) blogHandler() http.Handler { - - cache, err := lru.New(5000) - - // instantiating the lru cache can't realistically fail - if err != nil { - panic(err) - } - - mux := http.NewServeMux() - - mux.Handle("/posts/", http.StripPrefix("/posts", - apiutil.MethodMux(map[string]http.Handler{ - "GET": a.getPostsHandler(), - "EDIT": a.editPostHandler(false), - "MANAGE": a.managePostsHandler(), - "POST": a.postPostHandler(), - "DELETE": a.deletePostHandler(false), - "PREVIEW": a.previewPostHandler(), - }), - )) - - mux.Handle("/assets/", http.StripPrefix("/assets", - apiutil.MethodMux(map[string]http.Handler{ - "GET": a.getPostAssetHandler(), - "MANAGE": a.managePostAssetsHandler(), - "POST": a.postPostAssetHandler(), - "DELETE": a.deletePostAssetHandler(), - }), - )) - - mux.Handle("/drafts/", http.StripPrefix("/drafts", - - // everything to do with drafts is protected - authMiddleware(a.auther)( - - apiutil.MethodMux(map[string]http.Handler{ - "EDIT": a.editPostHandler(true), - "MANAGE": a.manageDraftPostsHandler(), - "POST": a.postDraftPostHandler(), - "DELETE": a.deletePostHandler(true), - "PREVIEW": a.previewPostHandler(), - }), - ), - )) - - mux.Handle("/static/", http.FileServer(http.FS(staticFS))) - mux.Handle("/follow", a.renderDumbTplHandler("follow.html")) - mux.Handle("/admin", a.renderDumbTplHandler("admin.html")) - mux.Handle("/mailinglist/unsubscribe", a.renderDumbTplHandler("unsubscribe.html")) - mux.Handle("/mailinglist/finalize", a.renderDumbTplHandler("finalize.html")) - mux.Handle("/feed.xml", a.renderFeedHandler()) - mux.Handle("/", a.renderIndexHandler()) - - readOnlyMiddlewares := []middleware{ - logReqMiddleware, // only log GETs on cache miss - cacheMiddleware(cache), - } - - readWriteMiddlewares := []middleware{ - purgeCacheOnOKMiddleware(cache), - authMiddleware(a.auther), - } - - h := apiutil.MethodMux(map[string]http.Handler{ - "GET": applyMiddlewares(mux, readOnlyMiddlewares...), - "MANAGE": applyMiddlewares(mux, readOnlyMiddlewares...), - "EDIT": applyMiddlewares(mux, readOnlyMiddlewares...), - "*": applyMiddlewares(mux, readWriteMiddlewares...), - }) - - return h -} - -func (a *api) handler() http.Handler { - - mux := http.NewServeMux() - - mux.Handle("/api/", applyMiddlewares( - http.StripPrefix("/api", a.apiHandler()), - logReqMiddleware, - )) - - mux.Handle("/", a.blogHandler()) - - noCacheMiddleware := addResponseHeadersMiddleware(map[string]string{ - "Cache-Control": "no-store, max-age=0", - "Pragma": "no-cache", - "Expires": "0", - }) - - h := applyMiddlewares( - apiutil.MethodMux(map[string]http.Handler{ - "GET": applyMiddlewares(mux), - "MANAGE": applyMiddlewares(mux, noCacheMiddleware), - "EDIT": applyMiddlewares(mux, noCacheMiddleware), - "*": applyMiddlewares( - mux, - a.checkCSRFMiddleware, - noCacheMiddleware, - ), - }), - setLoggerMiddleware(a.params.Logger), - ) - - return h -} diff --git a/src/http/http.go b/src/http/http.go new file mode 100644 index 0000000..e5ca3f1 --- /dev/null +++ b/src/http/http.go @@ -0,0 +1,286 @@ +// Package http implements the HTTP-based api for the mediocre-blog. +package http + +import ( + "context" + "embed" + "encoding/json" + "errors" + "fmt" + "html/template" + "net" + "net/http" + "net/url" + "os" + "strings" + "time" + + lru "github.com/hashicorp/golang-lru" + "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "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" +) + +//go:embed static +var staticFS embed.FS + +// 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 + + PostStore post.Store + PostAssetStore post.AssetStore + PostDraftStore post.DraftStore + + MailingList mailinglist.MailingList + + // PublicURL is the base URL which site visitors can navigate to. + PublicURL *url.URL + + // 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 + + // 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 + + // AuthRatelimit indicates how much time must pass between subsequent auth + // attempts. + AuthRatelimit time.Duration +} + +// SetupCfg implement the cfg.Cfger interface. +func (p *Params) SetupCfg(cfg *cfg.Cfg) { + + publicURLStr := cfg.String("http-public-url", "http://localhost:4000", "URL this service is accessible at") + + cfg.StringVar(&p.ListenProto, "http-listen-proto", "tcp", "Protocol to listen for HTTP requests with") + cfg.StringVar(&p.ListenAddr, "http-listen-addr", ":4000", "Address/path to listen for HTTP requests on") + + httpAuthUsersStr := cfg.String("http-auth-users", "{}", "JSON object with usernames as values and password hashes (produced by the hash-password binary) as values. Denotes users which are able to edit server-side data") + + httpAuthRatelimitStr := cfg.String("http-auth-ratelimit", "5s", "Minimum duration which must be waited between subsequent auth attempts") + + cfg.OnInit(func(context.Context) error { + + err := json.Unmarshal([]byte(*httpAuthUsersStr), &p.AuthUsers) + + if err != nil { + return fmt.Errorf("unmarshaling -http-auth-users: %w", err) + } + + if p.AuthRatelimit, err = time.ParseDuration(*httpAuthRatelimitStr); err != nil { + return fmt.Errorf("unmarshaling -http-auth-ratelimit: %w", err) + } + + *publicURLStr = strings.TrimSuffix(*publicURLStr, "/") + if p.PublicURL, err = url.Parse(*publicURLStr); err != nil { + return fmt.Errorf("parsing -http-public-url: %w", err) + } + + return nil + }) +} + +// Annotate implements mctx.Annotator interface. +func (p *Params) Annotate(a mctx.Annotations) { + a["httpPublicURL"] = p.PublicURL + a["httpListenProto"] = p.ListenProto + a["httpListenAddr"] = p.ListenAddr + a["httpAuthRatelimit"] = p.AuthRatelimit +} + +// 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 + auther Auther +} + +// 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, + auther: NewAuther(params.AuthUsers, params.AuthRatelimit), + } + + 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 { + defer a.auther.Close() + if err := a.srv.Shutdown(ctx); err != nil { + return err + } + + return nil +} + +func (a *api) apiHandler() http.Handler { + mux := http.NewServeMux() + + mux.Handle("/pow/challenge", a.newPowChallengeHandler()) + mux.Handle("/pow/check", + a.requirePowMiddleware( + http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}), + ), + ) + + mux.Handle("/mailinglist/subscribe", a.requirePowMiddleware(a.mailingListSubscribeHandler())) + mux.Handle("/mailinglist/finalize", a.mailingListFinalizeHandler()) + mux.Handle("/mailinglist/unsubscribe", a.mailingListUnsubscribeHandler()) + + return apiutil.MethodMux(map[string]http.Handler{ + "POST": mux, + }) +} + +func (a *api) blogHandler() http.Handler { + + cache, err := lru.New(5000) + + // instantiating the lru cache can't realistically fail + if err != nil { + panic(err) + } + + mux := http.NewServeMux() + + mux.Handle("/posts/", http.StripPrefix("/posts", + apiutil.MethodMux(map[string]http.Handler{ + "GET": a.getPostsHandler(), + "EDIT": a.editPostHandler(false), + "MANAGE": a.managePostsHandler(), + "POST": a.postPostHandler(), + "DELETE": a.deletePostHandler(false), + "PREVIEW": a.previewPostHandler(), + }), + )) + + mux.Handle("/assets/", http.StripPrefix("/assets", + apiutil.MethodMux(map[string]http.Handler{ + "GET": a.getPostAssetHandler(), + "MANAGE": a.managePostAssetsHandler(), + "POST": a.postPostAssetHandler(), + "DELETE": a.deletePostAssetHandler(), + }), + )) + + mux.Handle("/drafts/", http.StripPrefix("/drafts", + + // everything to do with drafts is protected + authMiddleware(a.auther)( + + apiutil.MethodMux(map[string]http.Handler{ + "EDIT": a.editPostHandler(true), + "MANAGE": a.manageDraftPostsHandler(), + "POST": a.postDraftPostHandler(), + "DELETE": a.deletePostHandler(true), + "PREVIEW": a.previewPostHandler(), + }), + ), + )) + + mux.Handle("/static/", http.FileServer(http.FS(staticFS))) + mux.Handle("/follow", a.renderDumbTplHandler("follow.html")) + mux.Handle("/admin", a.renderDumbTplHandler("admin.html")) + mux.Handle("/mailinglist/unsubscribe", a.renderDumbTplHandler("unsubscribe.html")) + mux.Handle("/mailinglist/finalize", a.renderDumbTplHandler("finalize.html")) + mux.Handle("/feed.xml", a.renderFeedHandler()) + mux.Handle("/", a.renderIndexHandler()) + + readOnlyMiddlewares := []middleware{ + logReqMiddleware, // only log GETs on cache miss + cacheMiddleware(cache), + } + + readWriteMiddlewares := []middleware{ + purgeCacheOnOKMiddleware(cache), + authMiddleware(a.auther), + } + + h := apiutil.MethodMux(map[string]http.Handler{ + "GET": applyMiddlewares(mux, readOnlyMiddlewares...), + "MANAGE": applyMiddlewares(mux, readOnlyMiddlewares...), + "EDIT": applyMiddlewares(mux, readOnlyMiddlewares...), + "*": applyMiddlewares(mux, readWriteMiddlewares...), + }) + + return h +} + +func (a *api) handler() http.Handler { + + mux := http.NewServeMux() + + mux.Handle("/api/", applyMiddlewares( + http.StripPrefix("/api", a.apiHandler()), + logReqMiddleware, + )) + + mux.Handle("/", a.blogHandler()) + + noCacheMiddleware := addResponseHeadersMiddleware(map[string]string{ + "Cache-Control": "no-store, max-age=0", + "Pragma": "no-cache", + "Expires": "0", + }) + + h := applyMiddlewares( + apiutil.MethodMux(map[string]http.Handler{ + "GET": applyMiddlewares(mux), + "MANAGE": applyMiddlewares(mux, noCacheMiddleware), + "EDIT": applyMiddlewares(mux, noCacheMiddleware), + "*": applyMiddlewares( + mux, + a.checkCSRFMiddleware, + noCacheMiddleware, + ), + }), + setLoggerMiddleware(a.params.Logger), + ) + + return h +} -- cgit v1.2.3