You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
272 lines
7.3 KiB
272 lines
7.3 KiB
// 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.renderPostHandler(),
|
|
"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(),
|
|
"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{
|
|
"GET": a.renderDraftPostHandler(),
|
|
"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())
|
|
|
|
h := apiutil.MethodMux(map[string]http.Handler{
|
|
"GET": applyMiddlewares(
|
|
mux,
|
|
logReqMiddleware, // only log GETs on cache miss
|
|
cacheMiddleware(cache),
|
|
),
|
|
"*": applyMiddlewares(
|
|
mux,
|
|
purgeCacheOnOKMiddleware(cache),
|
|
authMiddleware(a.auther),
|
|
),
|
|
})
|
|
|
|
return h
|
|
}
|
|
|
|
func (a *api) handler() http.Handler {
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.Handle("/api/", http.StripPrefix("/api", a.apiHandler()))
|
|
mux.Handle("/", a.blogHandler())
|
|
|
|
h := apiutil.MethodMux(map[string]http.Handler{
|
|
"GET": applyMiddlewares(
|
|
mux,
|
|
),
|
|
"*": applyMiddlewares(
|
|
mux,
|
|
a.checkCSRFMiddleware,
|
|
addResponseHeadersMiddleware(map[string]string{
|
|
"Cache-Control": "no-store, max-age=0",
|
|
"Pragma": "no-cache",
|
|
"Expires": "0",
|
|
}),
|
|
logReqMiddleware,
|
|
),
|
|
})
|
|
|
|
h = setLoggerMiddleware(a.params.Logger)(h)
|
|
|
|
return h
|
|
}
|
|
|