diff options
author | Brian Picciano <mediocregopher@gmail.com> | 2023-08-25 21:04:59 +0200 |
---|---|---|
committer | Brian Picciano <mediocregopher@gmail.com> | 2023-08-25 21:12:57 +0200 |
commit | 78bbfa42fa1159bce12c2c1d29eeb0bb9a8a2f75 (patch) | |
tree | 041dd938346eddf0a4bcd098403c229555a654cb /src/http | |
parent | c4ec9064063f3b15aeb25feb85a3afaaa02008ba (diff) |
Remove mailinglist and proof-of-work functionality
Diffstat (limited to 'src/http')
-rw-r--r-- | src/http/http.go | 35 | ||||
-rw-r--r-- | src/http/mailinglist.go | 92 | ||||
-rw-r--r-- | src/http/posts.go | 13 | ||||
-rw-r--r-- | src/http/pow.go | 53 | ||||
-rw-r--r-- | src/http/static/api.js | 118 | ||||
-rw-r--r-- | src/http/static/solvePow.js | 28 | ||||
-rw-r--r-- | src/http/static/utils.js | 12 | ||||
-rw-r--r-- | src/http/tpl/finalize.html | 45 | ||||
-rw-r--r-- | src/http/tpl/follow.html | 108 | ||||
-rw-r--r-- | src/http/tpl/unsubscribe.html | 44 |
10 files changed, 5 insertions, 543 deletions
diff --git a/src/http/http.go b/src/http/http.go index ba81577..4b98d2b 100644 --- a/src/http/http.go +++ b/src/http/http.go @@ -18,10 +18,8 @@ import ( "github.com/mediocregopher/blog.mediocregopher.com/srv/cache" "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/post/asset" - "github.com/mediocregopher/blog.mediocregopher.com/srv/pow" "github.com/mediocregopher/mediocre-go-lib/v2/mctx" "github.com/mediocregopher/mediocre-go-lib/v2/mlog" ) @@ -32,17 +30,14 @@ 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 - Cache cache.Cache + Logger *mlog.Logger + Cache cache.Cache PostStore post.Store PostAssetStore asset.Store PostAssetLoader asset.Loader PostDraftStore post.DraftStore - MailingList mailinglist.MailingList - // PublicURL is the base URL which site visitors can navigate to. PublicURL *url.URL @@ -176,25 +171,6 @@ func (a *api) Shutdown(ctx context.Context) error { 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 { mux := http.NewServeMux() @@ -237,8 +213,6 @@ func (a *api) blogHandler() http.Handler { 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()) @@ -266,11 +240,6 @@ 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{ diff --git a/src/http/mailinglist.go b/src/http/mailinglist.go deleted file mode 100644 index eab2f51..0000000 --- a/src/http/mailinglist.go +++ /dev/null @@ -1,92 +0,0 @@ -package http - -import ( - "errors" - "net/http" - "strings" - - "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil" - "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" -) - -func (a *api) mailingListSubscribeHandler() http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - email := r.PostFormValue("email") - if parts := strings.Split(email, "@"); len(parts) != 2 || - parts[0] == "" || - parts[1] == "" || - len(email) >= 512 { - apiutil.BadRequest(rw, r, errors.New("invalid email")) - return - - } else if strings.ToLower(parts[1]) == "gmail.com" { - apiutil.BadRequest(rw, r, errors.New("gmail does not allow its users to receive email from me, sorry")) - return - } - - err := a.params.MailingList.BeginSubscription(email) - - if errors.Is(err, mailinglist.ErrAlreadyVerified) { - // just eat the error, make it look to the user like the - // verification email was sent. - } else if err != nil { - apiutil.InternalServerError(rw, r, err) - return - } - - apiutil.JSONResult(rw, r, struct{}{}) - }) -} - -func (a *api) mailingListFinalizeHandler() http.Handler { - var errInvalidSubToken = errors.New("invalid subToken") - - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - subToken := r.PostFormValue("subToken") - if l := len(subToken); l == 0 || l > 128 { - apiutil.BadRequest(rw, r, errInvalidSubToken) - return - } - - err := a.params.MailingList.FinalizeSubscription(subToken) - - if errors.Is(err, mailinglist.ErrNotFound) { - apiutil.BadRequest(rw, r, errInvalidSubToken) - return - - } else if errors.Is(err, mailinglist.ErrAlreadyVerified) { - // no problem - - } else if err != nil { - apiutil.InternalServerError(rw, r, err) - return - } - - apiutil.JSONResult(rw, r, struct{}{}) - }) -} - -func (a *api) mailingListUnsubscribeHandler() http.Handler { - var errInvalidUnsubToken = errors.New("invalid unsubToken") - - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - unsubToken := r.PostFormValue("unsubToken") - if l := len(unsubToken); l == 0 || l > 128 { - apiutil.BadRequest(rw, r, errInvalidUnsubToken) - return - } - - err := a.params.MailingList.Unsubscribe(unsubToken) - - if errors.Is(err, mailinglist.ErrNotFound) { - apiutil.BadRequest(rw, r, errInvalidUnsubToken) - return - - } else if err != nil { - apiutil.InternalServerError(rw, r, err) - return - } - - apiutil.JSONResult(rw, r, struct{}{}) - }) -} diff --git a/src/http/posts.go b/src/http/posts.go index 5c7ac25..b1dbc35 100644 --- a/src/http/posts.go +++ b/src/http/posts.go @@ -418,7 +418,7 @@ func postFromPostReq(r *http.Request) (post.Post, error) { return p, nil } -func (a *api) storeAndPublishPost(ctx context.Context, p post.Post) error { +func (a *api) publishPost(ctx context.Context, p post.Post) error { first, err := a.params.PostStore.Set(p, time.Now()) @@ -430,13 +430,6 @@ func (a *api) storeAndPublishPost(ctx context.Context, p post.Post) error { return nil } - a.params.Logger.Info(ctx, "publishing blog post to mailing list") - urlStr := a.postURL(p.ID, true) - - if err := a.params.MailingList.Publish(p.Title, urlStr); err != nil { - return fmt.Errorf("publishing post to mailing list: %w", err) - } - if err := a.params.PostDraftStore.Delete(p.ID); err != nil { return fmt.Errorf("deleting draft: %w", err) } @@ -458,9 +451,9 @@ func (a *api) postPostHandler() http.Handler { ctx = mctx.Annotate(ctx, "postID", p.ID) - if err := a.storeAndPublishPost(ctx, p); err != nil { + if err := a.publishPost(ctx, p); err != nil { apiutil.InternalServerError( - rw, r, fmt.Errorf("storing/publishing post with id %q: %w", p.ID, err), + rw, r, fmt.Errorf("publishing post with id %q: %w", p.ID, err), ) return } diff --git a/src/http/pow.go b/src/http/pow.go deleted file mode 100644 index 1bd5cb5..0000000 --- a/src/http/pow.go +++ /dev/null @@ -1,53 +0,0 @@ -package http - -import ( - "encoding/hex" - "errors" - "fmt" - "net/http" - - "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil" -) - -func (a *api) newPowChallengeHandler() http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - - challenge := a.params.PowManager.NewChallenge() - - apiutil.JSONResult(rw, r, struct { - Seed string `json:"seed"` - Target uint32 `json:"target"` - }{ - Seed: hex.EncodeToString(challenge.Seed), - Target: challenge.Target, - }) - }) -} - -func (a *api) requirePowMiddleware(h http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - - seedHex := r.FormValue("powSeed") - seed, err := hex.DecodeString(seedHex) - if err != nil || len(seed) == 0 { - apiutil.BadRequest(rw, r, errors.New("invalid powSeed")) - return - } - - solutionHex := r.FormValue("powSolution") - solution, err := hex.DecodeString(solutionHex) - if err != nil || len(seed) == 0 { - apiutil.BadRequest(rw, r, errors.New("invalid powSolution")) - return - } - - err = a.params.PowManager.CheckSolution(seed, solution) - - if err != nil { - apiutil.BadRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err)) - return - } - - h.ServeHTTP(rw, r) - }) -} diff --git a/src/http/static/api.js b/src/http/static/api.js deleted file mode 100644 index 55c9ecd..0000000 --- a/src/http/static/api.js +++ /dev/null @@ -1,118 +0,0 @@ -import * as utils from "/static/utils.js"; - -const doFetch = async (req) => { - let res, jsonRes; - try { - res = await fetch(req); - jsonRes = await res.json(); - - } catch (e) { - - if (e instanceof SyntaxError) - e = new Error(`status ${res.status}, empty (or invalid) response body`); - - console.error(`api call ${req.method} ${req.url}: unexpected error:`, e); - throw e; - } - - if (jsonRes.error) { - console.error( - `api call ${req.method} ${req.url}: application error:`, - res.status, - jsonRes.error, - ); - - throw jsonRes.error; - } - - return jsonRes; -} - -// may throw -const solvePow = async () => { - - const res = await call('/api/pow/challenge'); - - const worker = new Worker('/static/solvePow.js'); - - const p = new Promise((resolve, reject) => { - worker.postMessage({seedHex: res.seed, target: res.target}); - worker.onmessage = resolve; - }); - - const powSol = (await p).data; - worker.terminate(); - - return {seed: res.seed, solution: powSol}; -} - -const call = async (route, opts = {}) => { - const { - method = 'POST', - body = {}, - requiresPow = false, - } = opts; - - const reqOpts = { - method, - }; - - if (requiresPow) { - const {seed, solution} = await solvePow(); - body.powSeed = seed; - body.powSolution = solution; - } - - if (Object.keys(body).length > 0) { - const form = new FormData(); - for (const key in body) form.append(key, body[key]); - - reqOpts.body = form; - } - - const req = new Request(route, reqOpts); - return doFetch(req); -} - -const ws = async (route, opts = {}) => { - const { - requiresPow = false, - params = {}, - } = opts; - - const docURL = new URL(document.URL); - const protocol = docURL.protocol == "http:" ? "ws:" : "wss:"; - - const fullParams = new URLSearchParams(params); - - if (requiresPow) { - const {seed, solution} = await solvePow(); - fullParams.set("powSeed", seed); - fullParams.set("powSolution", solution); - } - - const rawConn = new WebSocket(`${protocol}//${docURL.host}${route}?${fullParams.toString()}`); - - const conn = { - next: () => new Promise((resolve, reject) => { - rawConn.onmessage = (m) => { - const mj = JSON.parse(m.data); - resolve(mj); - }; - rawConn.onerror = reject; - rawConn.onclose = reject; - }), - - close: rawConn.close, - }; - - return new Promise((resolve, reject) => { - rawConn.onopen = () => resolve(conn); - rawConn.onerror = reject; - }); -} - -export { - call, - ws -} diff --git a/src/http/static/solvePow.js b/src/http/static/solvePow.js deleted file mode 100644 index 900400c..0000000 --- a/src/http/static/solvePow.js +++ /dev/null @@ -1,28 +0,0 @@ -const fromHexString = hexString => - new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); - -const toHexString = bytes => - bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); - -onmessage = async (e) => { - const seed = fromHexString(e.data.seedHex); - const target = e.data.target; - - const fullBuf = new ArrayBuffer(seed.byteLength*2); - - const fullBufSeed = new Uint8Array(fullBuf, 0, seed.byteLength); - seed.forEach((v, i) => fullBufSeed[i] = v); - - const randBuf = new Uint8Array(fullBuf, seed.byteLength); - - while (true) { - crypto.getRandomValues(randBuf); - const digest = await crypto.subtle.digest('SHA-512', fullBuf); - const digestView = new DataView(digest); - if (digestView.getUint32(0) < target) { - postMessage(toHexString(randBuf)); - return; - } - } - -}; diff --git a/src/http/static/utils.js b/src/http/static/utils.js deleted file mode 100644 index 96a2950..0000000 --- a/src/http/static/utils.js +++ /dev/null @@ -1,12 +0,0 @@ -const cookies = {}; -const cookieKVs = document.cookie - .split(';') - .map(cookie => cookie.trim().split('=', 2)); - -for (const i in cookieKVs) { - cookies[cookieKVs[i][0]] = cookieKVs[i][1]; -} - -export { - cookies, -} diff --git a/src/http/tpl/finalize.html b/src/http/tpl/finalize.html deleted file mode 100644 index 8bdfceb..0000000 --- a/src/http/tpl/finalize.html +++ /dev/null @@ -1,45 +0,0 @@ -{{ define "body" }} - -<script async type="module" src="{{ StaticURL "api.js" }}"></script> - -<style> -#result.success { color: green; } -#result.fail { color: red; } -</style> - -<span id="result"></span> - -<script> - -(async () => { - - const resultSpan = document.getElementById("result"); - - try { - - const urlParams = new URLSearchParams(window.location.search); - const subToken = urlParams.get('subToken'); - - if (!subToken) throw "No subscription token provided"; - - const api = await import("{{ StaticURL "api.js" }}"); - - await api.call('/api/mailinglist/finalize', { - body: { subToken }, - }); - - resultSpan.className = "success"; - resultSpan.innerHTML = "Your email subscription has been finalized! Please go on about your day."; - - } catch (e) { - resultSpan.className = "fail"; - resultSpan.innerHTML = e; - } - -})(); - -</script> - -{{ end }} - -{{ template "base.html" . }} diff --git a/src/http/tpl/follow.html b/src/http/tpl/follow.html index 88fee46..1958f95 100644 --- a/src/http/tpl/follow.html +++ b/src/http/tpl/follow.html @@ -1,113 +1,5 @@ {{ define "body" }} -<script async type="module" src="{{ StaticURL "api.js" }}"></script> - -<p> - Here's your options for receiving updates about new posts: -</p> - -<h2>Option 1: Email</h2> - -<p> - Email is by far my preferred option for notifying followers of new posts. The - entire email list system for this site has been designed from scratch and is - completely self-hosted in my living room. -</p> - -<p> - I solemnly swear that: -</p> - -<ul> - - <li> - You will never receive an email from me except to notify of a new post. - </li> - - <li> - Your email will never be provided or sold to anyone else for any reason. - </li> - -</ul> - -<p> - So smash that subscribe button! -</p> - -<p> - You will need to verify your email, so be sure to check your spam folder to - complete the process if you don't immediately see anything in your inbox. -</p> - -<p style="color: var(--nc-lk-2);"> - Unfortunately Google considers all emails from my mail server to be spam, so - gmail emails are not allowed. Sorry (not sorry). -</p> - -<style> - -#emailStatus.success { - color: green; -} - -#emailStatus.fail { - color: red; -} - -</style> - -<form action="javascript:void(0);"> - <input type="email" placeholder="name@host.com" id="emailAddress" /> - <input class="button-primary" type="submit" value="Subscribe" id="emailSubscribe" /> - <span id="emailStatus"></span> -</form> - -<script> - -const emailAddress = document.getElementById("emailAddress"); -const emailSubscribe = document.getElementById("emailSubscribe"); -const emailSubscribeOrigValue = emailSubscribe.value; -const emailStatus = document.getElementById("emailStatus"); - -emailSubscribe.onclick = async () => { - - const api = await import("{{ StaticURL "api.js" }}"); - - emailSubscribe.disabled = true; - emailSubscribe.className = ""; - emailSubscribe.value = "Please hold..."; - emailStatus.innerHTML = ''; - - try { - - if (!window.isSecureContext) { - throw "The browser environment is not secure."; - } - - await api.call('/api/mailinglist/subscribe', { - body: { email: emailAddress.value }, - requiresPow: true, - }); - - emailStatus.className = "success"; - emailStatus.innerHTML = "Verification email sent (check your spam folder)"; - - } catch (e) { - emailStatus.className = "fail"; - emailStatus.innerHTML = e; - - } finally { - emailSubscribe.disabled = false; - emailSubscribe.className = "button-primary"; - emailSubscribe.value = emailSubscribeOrigValue; - } - -}; - -</script> - -<h2>Option 2: RSS</h2> - <p> RSS is the classic way to follow a site's updates, and we're bringing it back! Just give any RSS reader the following URL: diff --git a/src/http/tpl/unsubscribe.html b/src/http/tpl/unsubscribe.html deleted file mode 100644 index ad01735..0000000 --- a/src/http/tpl/unsubscribe.html +++ /dev/null @@ -1,44 +0,0 @@ -{{ define "body" }} - -<script async type="module" src="{{ StaticURL "api.js" }}"></script> - -<style> -#result.success { color: green; } -#result.fail { color: red; } -</style> - -<span id="result"></span> - -<script> - -(async () => { - - const resultSpan = document.getElementById("result"); - - try { - const urlParams = new URLSearchParams(window.location.search); - const unsubToken = urlParams.get('unsubToken'); - - if (!unsubToken) throw "No unsubscribe token provided"; - - const api = await import("{{ StaticURL "api.js" }}"); - - await api.call('/api/mailinglist/unsubscribe', { - body: { unsubToken }, - }); - - resultSpan.className = "success"; - resultSpan.innerHTML = "You have been unsubscribed! Please go on about your day."; - - } catch (e) { - resultSpan.className = "fail"; - resultSpan.innerHTML = e; - } - -})(); - -</script> - -{{ end }} - -{{ template "base.html" . }} |