parent
dce39b836a
commit
0197d9cd49
@ -0,0 +1,174 @@ |
||||
// Package api implements the HTTP-based api for the mediocre-blog.
|
||||
package api |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"net" |
||||
"net/http" |
||||
"net/http/httputil" |
||||
"net/url" |
||||
"os" |
||||
|
||||
"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" |
||||
"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 |
||||
MailingList mailinglist.MailingList |
||||
|
||||
// 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 |
||||
} |
||||
|
||||
// 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 |
||||
} |
||||
|
||||
// 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.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, fmt.Sprintf("%s: %v", "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) |
||||
} |
||||
|
||||
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()) |
||||
|
||||
apiHandler := logMiddleware(a.params.Logger, 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)) |
||||
|
||||
return mux |
||||
} |
@ -1,4 +1,4 @@ |
||||
package main |
||||
package api |
||||
|
||||
import ( |
||||
"net" |
@ -1,4 +1,4 @@ |
||||
package main |
||||
package api |
||||
|
||||
import ( |
||||
"context" |
@ -0,0 +1,52 @@ |
||||
// Package cfg implements a simple wrapper around go's flag package, in order to
|
||||
// implement initialization hooks.
|
||||
package cfg |
||||
|
||||
import ( |
||||
"context" |
||||
"flag" |
||||
"os" |
||||
) |
||||
|
||||
// Cfger is a component which can be used with Cfg to setup its initialization.
|
||||
type Cfger interface { |
||||
SetupCfg(*Cfg) |
||||
} |
||||
|
||||
// Cfg is a wrapper around the stdlib's FlagSet and a set of initialization
|
||||
// hooks.
|
||||
type Cfg struct { |
||||
*flag.FlagSet |
||||
|
||||
hooks []func(ctx context.Context) error |
||||
} |
||||
|
||||
// New initializes and returns a new instance of *Cfg.
|
||||
func New() *Cfg { |
||||
return &Cfg{ |
||||
FlagSet: flag.NewFlagSet("", flag.ExitOnError), |
||||
} |
||||
} |
||||
|
||||
// OnInit appends the given callback to the sequence of hooks which will run on
|
||||
// a call to Init.
|
||||
func (c *Cfg) OnInit(cb func(context.Context) error) { |
||||
c.hooks = append(c.hooks, cb) |
||||
} |
||||
|
||||
// Init runs all hooks registered using OnInit, in the same order OnInit was
|
||||
// called. If one returns an error that error is returned and no further hooks
|
||||
// are run.
|
||||
func (c *Cfg) Init(ctx context.Context) error { |
||||
if err := c.FlagSet.Parse(os.Args[1:]); err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, h := range c.hooks { |
||||
if err := h(ctx); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
Loading…
Reference in new issue