From ec4aac24abc35fcf192c13a3fc9b2b65875c3444 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Tue, 3 Aug 2021 15:20:32 -0600 Subject: [PATCH] got pow working on the subscribe endpoint --- srv/cmd/mediocre-blog/mailinglist.go | 8 ++ srv/cmd/mediocre-blog/main.go | 37 ++++++-- srv/cmd/mediocre-blog/pow.go | 2 + srv/pow/pow.go | 9 +- static/default.nix | 2 +- static/src/assets/solvePow.js | 28 ++++++ static/src/follow.md | 123 +++++++++++++++++++++++++-- 7 files changed, 191 insertions(+), 18 deletions(-) create mode 100644 static/src/assets/solvePow.js diff --git a/srv/cmd/mediocre-blog/mailinglist.go b/srv/cmd/mediocre-blog/mailinglist.go index 75e5b6d..4a1ddce 100644 --- a/srv/cmd/mediocre-blog/mailinglist.go +++ b/srv/cmd/mediocre-blog/mailinglist.go @@ -16,6 +16,7 @@ func mailingListSubscribeHandler(ml mailinglist.MailingList) http.Handler { parts[1] == "" || len(email) >= 512 { badRequest(rw, r, errors.New("invalid email")) + return } if err := ml.BeginSubscription(email); errors.Is(err, mailinglist.ErrAlreadyVerified) { @@ -23,7 +24,10 @@ func mailingListSubscribeHandler(ml mailinglist.MailingList) http.Handler { // verification email was sent. } else if err != nil { internalServerError(rw, r, err) + return } + + jsonResult(rw, r, struct{}{}) }) } @@ -44,6 +48,8 @@ func mailingListFinalizeHandler(ml mailinglist.MailingList) http.Handler { internalServerError(rw, r, err) return } + + jsonResult(rw, r, struct{}{}) }) } @@ -63,5 +69,7 @@ func mailingListUnsubscribeHandler(ml mailinglist.MailingList) http.Handler { internalServerError(rw, r, err) return } + + jsonResult(rw, r, struct{}{}) }) } diff --git a/srv/cmd/mediocre-blog/main.go b/srv/cmd/mediocre-blog/main.go index 5a00a48..748e10b 100644 --- a/srv/cmd/mediocre-blog/main.go +++ b/srv/cmd/mediocre-blog/main.go @@ -5,6 +5,8 @@ import ( "flag" "fmt" "net/http" + "net/http/httputil" + "net/url" "path" "strconv" "strings" @@ -26,11 +28,13 @@ func main() { logger := mlog.NewLogger(nil) hostname := flag.String("hostname", "localhost:4000", "Hostname to advertise this server as") - staticDir := flag.String("static-dir", "", "Directory from which static files are served") listenAddr := flag.String("listen-addr", ":4000", "Address to listen for HTTP requests on") dataDir := flag.String("data-dir", ".", "Directory to use for long term storage") - powTargetStr := flag.String("pow-target", "0x000FFFF", "Proof-of-work target, lower is more difficult") + staticDir := flag.String("static-dir", "", "Directory from which static files are served (mutually exclusive with -static-proxy-url)") + staticProxyURLStr := flag.String("static-proxy-url", "", "HTTP address from which static files are served (mutually exclusive with -static-dir)") + + powTargetStr := flag.String("pow-target", "0x0000FFFF", "Proof-of-work target, lower is more difficult") powSecret := flag.String("pow-secret", "", "Secret used to sign proof-of-work challenge seeds") smtpAddr := flag.String("ml-smtp-addr", "", "Address of SMTP server to use for sending emails for the mailing list") @@ -41,8 +45,8 @@ func main() { flag.Parse() switch { - case *staticDir == "": - logger.Fatal(context.Background(), "-static-dir is required") + case *staticDir == "" && *staticProxyURLStr == "": + logger.Fatal(context.Background(), "-static-dir or -static-proxy-url is required") case *powSecret == "": logger.Fatal(context.Background(), "-pow-secret is required") case *smtpAddr == "": @@ -51,6 +55,14 @@ func main() { logger.Fatal(context.Background(), "-ml-smtp-auth is required") } + var staticProxyURL *url.URL + if *staticProxyURLStr != "" { + var err error + if staticProxyURL, err = url.Parse(*staticProxyURLStr); err != nil { + loggerFatalErr(context.Background(), logger, "parsing -static-proxy-url", err) + } + } + powTargetUint, err := strconv.ParseUint(*powTargetStr, 0, 32) if err != nil { loggerFatalErr(context.Background(), logger, "parsing -pow-target", err) @@ -68,7 +80,6 @@ func main() { ctx := mctx.Annotate(context.Background(), "hostname", *hostname, - "staticDir", *staticDir, "listenAddr", *listenAddr, "dataDir", *dataDir, "powTarget", fmt.Sprintf("%x", powTarget), @@ -76,6 +87,12 @@ func main() { "smtpSendAs", smtpSendAs, ) + if *staticDir != "" { + ctx = mctx.Annotate(ctx, "staticDir", *staticDir) + } else { + ctx = mctx.Annotate(ctx, "staticProxyURL", *staticProxyURLStr) + } + clock := clock.Realtime() powStore := pow.NewMemoryStore(clock) @@ -112,7 +129,15 @@ func main() { }) mux := http.NewServeMux() - mux.Handle("/", http.FileServer(http.Dir(*staticDir))) + + var staticHandler http.Handler + if *staticDir != "" { + staticHandler = http.FileServer(http.Dir(*staticDir)) + } else { + staticHandler = httputil.NewSingleHostReverseProxy(staticProxyURL) + } + + mux.Handle("/", staticHandler) apiMux := http.NewServeMux() apiMux.Handle("/pow/challenge", newPowChallengeHandler(powMgr)) diff --git a/srv/cmd/mediocre-blog/pow.go b/srv/cmd/mediocre-blog/pow.go index 8e64739..a505a64 100644 --- a/srv/cmd/mediocre-blog/pow.go +++ b/srv/cmd/mediocre-blog/pow.go @@ -11,6 +11,7 @@ import ( func newPowChallengeHandler(mgr pow.Manager) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + challenge := mgr.NewChallenge() jsonResult(rw, r, struct { @@ -25,6 +26,7 @@ func newPowChallengeHandler(mgr pow.Manager) http.Handler { func requirePowMiddleware(mgr pow.Manager, h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + seedHex := r.PostFormValue("powSeed") seed, err := hex.DecodeString(seedHex) if err != nil || len(seed) == 0 { diff --git a/srv/pow/pow.go b/srv/pow/pow.go index 3de1450..8075103 100644 --- a/srv/pow/pow.go +++ b/srv/pow/pow.go @@ -141,7 +141,7 @@ type Challenge struct { // Errors which may be produced by a Manager. var ( ErrInvalidSolution = errors.New("invalid solution") - ErrExpiredSolution = errors.New("expired solution") + ErrExpiredSeed = errors.New("expired seed") ) // Manager is used to both produce proof-of-work challenges and check their @@ -150,7 +150,7 @@ type Manager interface { NewChallenge() Challenge // Will produce ErrInvalidSolution if the solution is invalid, or - // ErrExpiredSolution if the solution has expired. + // ErrExpiredSeed if the seed has expired. CheckSolution(seed, solution []byte) error } @@ -193,6 +193,7 @@ type manager struct { // NewManager initializes and returns a Manager instance using the given // parameters. func NewManager(params ManagerParams) Manager { + params = params.withDefaults() return &manager{ params: params, } @@ -252,8 +253,8 @@ func (m *manager) CheckSolution(seed, solution []byte) error { if err != nil { return fmt.Errorf("parsing challenge parameters from seed: %w", err) - } else if c.ExpiresAt <= m.params.Clock.Now().Unix() { - return ErrExpiredSolution + } else if now := m.params.Clock.Now().Unix(); c.ExpiresAt <= now { + return ErrExpiredSeed } ok := (SolutionChecker{}).Check( diff --git a/static/default.nix b/static/default.nix index 1a146b0..74bd3bf 100644 --- a/static/default.nix +++ b/static/default.nix @@ -37,7 +37,7 @@ in name = "mediocre-blog-static-dev"; buildInputs = all_inputs; shellHook = '' - exec ${jekyll_env}/bin/jekyll serve -s ./src -d ./_site -w -I -D -H 0.0.0.0 + exec ${jekyll_env}/bin/jekyll serve -s ./src -d ./_site -w -I -D -H 0.0.0.0 -P 4001 ''; }; diff --git a/static/src/assets/solvePow.js b/static/src/assets/solvePow.js new file mode 100644 index 0000000..900400c --- /dev/null +++ b/static/src/assets/solvePow.js @@ -0,0 +1,28 @@ +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/static/src/follow.md b/static/src/follow.md index 8093267..4e949dd 100644 --- a/static/src/follow.md +++ b/static/src/follow.md @@ -6,7 +6,121 @@ nofollow: true Here's your options for receiving updates about new blog posts: -## Option 1: RSS +## Option 1: Email + +Email is by far my preferred option for notifying followers of new posts. + +The entire email list system for this blog, from storing subscriber email +addresses to the email server which sends the notifications out, has been +designed from scratch and is completely self-hosted in my living room. + +I solemnly swear that: + +* You will never receive an email from this blog except to notify of a new post. + +* Your email will never be provided or sold to anyone else for any reason. + +With all that said, if you'd like to receive an email everytime a new blog post +is published then input your email below and smash that subscribe button! + + + + + + + + + +## Option 2: RSS RSS is the classic way to follow any blog. It comes from a time before aggregators like reddit and twitter stole the show, when people felt capable to @@ -31,14 +145,9 @@ recommendations: iPhone/iPad/Mac devices, so I'm told. Their homepage description makes a much better sales pitch for RSS than I ever could. -## Option 2: Twitter +## Option 3: Twitter New posts are automatically published to [my Twitter](https://twitter.com/{{ site.twitter_username }}). Simply follow me there and pray the algorithm smiles upon my tweets enough to show them to you! :pray: :pray: :pray: -## Option 3: Email? - -I tried setting up an RSS-to-Email list thing on Mailchimp but it doesn't seem -to like my RSS feed. If anyone knows a better alternative please [email -me.](mailto:mediocregopher@gmail.com)