got pow working on the subscribe endpoint

This commit is contained in:
Brian Picciano 2021-08-03 15:20:32 -06:00
parent 20b9213010
commit ec4aac24ab
7 changed files with 191 additions and 18 deletions

View File

@ -16,6 +16,7 @@ func mailingListSubscribeHandler(ml mailinglist.MailingList) http.Handler {
parts[1] == "" || parts[1] == "" ||
len(email) >= 512 { len(email) >= 512 {
badRequest(rw, r, errors.New("invalid email")) badRequest(rw, r, errors.New("invalid email"))
return
} }
if err := ml.BeginSubscription(email); errors.Is(err, mailinglist.ErrAlreadyVerified) { 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. // verification email was sent.
} else if err != nil { } else if err != nil {
internalServerError(rw, r, err) internalServerError(rw, r, err)
return
} }
jsonResult(rw, r, struct{}{})
}) })
} }
@ -44,6 +48,8 @@ func mailingListFinalizeHandler(ml mailinglist.MailingList) http.Handler {
internalServerError(rw, r, err) internalServerError(rw, r, err)
return return
} }
jsonResult(rw, r, struct{}{})
}) })
} }
@ -63,5 +69,7 @@ func mailingListUnsubscribeHandler(ml mailinglist.MailingList) http.Handler {
internalServerError(rw, r, err) internalServerError(rw, r, err)
return return
} }
jsonResult(rw, r, struct{}{})
}) })
} }

View File

@ -5,6 +5,8 @@ import (
"flag" "flag"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httputil"
"net/url"
"path" "path"
"strconv" "strconv"
"strings" "strings"
@ -26,11 +28,13 @@ func main() {
logger := mlog.NewLogger(nil) logger := mlog.NewLogger(nil)
hostname := flag.String("hostname", "localhost:4000", "Hostname to advertise this server as") 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") 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") 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") 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") 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() flag.Parse()
switch { switch {
case *staticDir == "": case *staticDir == "" && *staticProxyURLStr == "":
logger.Fatal(context.Background(), "-static-dir is required") logger.Fatal(context.Background(), "-static-dir or -static-proxy-url is required")
case *powSecret == "": case *powSecret == "":
logger.Fatal(context.Background(), "-pow-secret is required") logger.Fatal(context.Background(), "-pow-secret is required")
case *smtpAddr == "": case *smtpAddr == "":
@ -51,6 +55,14 @@ func main() {
logger.Fatal(context.Background(), "-ml-smtp-auth is required") 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) powTargetUint, err := strconv.ParseUint(*powTargetStr, 0, 32)
if err != nil { if err != nil {
loggerFatalErr(context.Background(), logger, "parsing -pow-target", err) loggerFatalErr(context.Background(), logger, "parsing -pow-target", err)
@ -68,7 +80,6 @@ func main() {
ctx := mctx.Annotate(context.Background(), ctx := mctx.Annotate(context.Background(),
"hostname", *hostname, "hostname", *hostname,
"staticDir", *staticDir,
"listenAddr", *listenAddr, "listenAddr", *listenAddr,
"dataDir", *dataDir, "dataDir", *dataDir,
"powTarget", fmt.Sprintf("%x", powTarget), "powTarget", fmt.Sprintf("%x", powTarget),
@ -76,6 +87,12 @@ func main() {
"smtpSendAs", smtpSendAs, "smtpSendAs", smtpSendAs,
) )
if *staticDir != "" {
ctx = mctx.Annotate(ctx, "staticDir", *staticDir)
} else {
ctx = mctx.Annotate(ctx, "staticProxyURL", *staticProxyURLStr)
}
clock := clock.Realtime() clock := clock.Realtime()
powStore := pow.NewMemoryStore(clock) powStore := pow.NewMemoryStore(clock)
@ -112,7 +129,15 @@ func main() {
}) })
mux := http.NewServeMux() 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 := http.NewServeMux()
apiMux.Handle("/pow/challenge", newPowChallengeHandler(powMgr)) apiMux.Handle("/pow/challenge", newPowChallengeHandler(powMgr))

View File

@ -11,6 +11,7 @@ import (
func newPowChallengeHandler(mgr pow.Manager) http.Handler { func newPowChallengeHandler(mgr pow.Manager) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
challenge := mgr.NewChallenge() challenge := mgr.NewChallenge()
jsonResult(rw, r, struct { 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 { func requirePowMiddleware(mgr pow.Manager, h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
seedHex := r.PostFormValue("powSeed") seedHex := r.PostFormValue("powSeed")
seed, err := hex.DecodeString(seedHex) seed, err := hex.DecodeString(seedHex)
if err != nil || len(seed) == 0 { if err != nil || len(seed) == 0 {

View File

@ -141,7 +141,7 @@ type Challenge struct {
// Errors which may be produced by a Manager. // Errors which may be produced by a Manager.
var ( var (
ErrInvalidSolution = errors.New("invalid solution") 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 // Manager is used to both produce proof-of-work challenges and check their
@ -150,7 +150,7 @@ type Manager interface {
NewChallenge() Challenge NewChallenge() Challenge
// Will produce ErrInvalidSolution if the solution is invalid, or // 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 CheckSolution(seed, solution []byte) error
} }
@ -193,6 +193,7 @@ type manager struct {
// NewManager initializes and returns a Manager instance using the given // NewManager initializes and returns a Manager instance using the given
// parameters. // parameters.
func NewManager(params ManagerParams) Manager { func NewManager(params ManagerParams) Manager {
params = params.withDefaults()
return &manager{ return &manager{
params: params, params: params,
} }
@ -252,8 +253,8 @@ func (m *manager) CheckSolution(seed, solution []byte) error {
if err != nil { if err != nil {
return fmt.Errorf("parsing challenge parameters from seed: %w", err) return fmt.Errorf("parsing challenge parameters from seed: %w", err)
} else if c.ExpiresAt <= m.params.Clock.Now().Unix() { } else if now := m.params.Clock.Now().Unix(); c.ExpiresAt <= now {
return ErrExpiredSolution return ErrExpiredSeed
} }
ok := (SolutionChecker{}).Check( ok := (SolutionChecker{}).Check(

View File

@ -37,7 +37,7 @@ in
name = "mediocre-blog-static-dev"; name = "mediocre-blog-static-dev";
buildInputs = all_inputs; buildInputs = all_inputs;
shellHook = '' 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
''; '';
}; };

View File

@ -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;
}
}
};

View File

@ -6,7 +6,121 @@ nofollow: true
Here's your options for receiving updates about new blog posts: 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!
<style>
#emailStatus.success {
color: green;
}
#emailStatus.fail {
color: red;
}
</style>
<input type="email" placeholder="name@host.com" id="emailAddress" />
<input class="button-primary" type="submit" value="Subscribe" id="emailSubscribe" />
<span id="emailStatus"></span>
<script>
const emailAddress = document.getElementById("emailAddress");
const emailSubscribe = document.getElementById("emailSubscribe");
const emailSubscribeOrigValue = emailSubscribe.value;
const emailStatus = document.getElementById("emailStatus");
const solvePow = async (seedHex, target) => {
const worker = new Worker('/assets/solvePow.js');
const p = new Promise((resolve, reject) => {
worker.postMessage({seedHex, target});
worker.onmessage = resolve;
});
const solutionHex = (await p).data;
worker.terminate();
return solutionHex;
}
emailSubscribe.onclick = async () => {
emailSubscribe.disabled = true;
emailSubscribe.className = "";
emailSubscribe.value = "Please hold...";
await (async () => {
setErr = (errStr, retry) => {
emailStatus.className = "fail";
emailStatus.innerHTML = errStr
if (retry) emailStatus.innerHTML += " (please try again)";
};
if (!window.isSecureContext) {
setErr("The browser environment is not secure.", false);
return;
}
const getPowReq = new Request('/api/pow/challenge');
const res = await fetch(getPowReq)
.then(response => response.json())
if (res.error) {
setErr(res.error, true);
return;
}
const powSol = await solvePow(res.seed, res.target);
const subscribeForm = new FormData();
subscribeForm.append('powSeed', res.seed);
subscribeForm.append('powSolution', powSol);
subscribeForm.append('email', emailAddress.value);
const subscribeReq = new Request('/api/mailinglist/subscribe', {
method: 'POST',
body: subscribeForm,
});
const subRes = await fetch(subscribeReq)
.then(response => response.json());
if (subRes.error) {
setErr(subRes.error, true);
return;
}
emailStatus.className = "success";
emailStatus.innerHTML = "Verification email sent (check your spam folder)";
})();
emailSubscribe.disabled = false;
emailSubscribe.className = "button-primary";
emailSubscribe.value = emailSubscribeOrigValue;
};
</script>
## Option 2: RSS
RSS is the classic way to follow any blog. It comes from a time before 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 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 iPhone/iPad/Mac devices, so I'm told. Their homepage description makes a much
better sales pitch for RSS than I ever could. 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/{{ New posts are automatically published to [my Twitter](https://twitter.com/{{
site.twitter_username }}). Simply follow me there and pray the algorithm smiles site.twitter_username }}). Simply follow me there and pray the algorithm smiles
upon my tweets enough to show them to you! :pray: :pray: :pray: 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)