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] == "" ||
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{}{})
})
}

View File

@ -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))

View File

@ -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 {

View File

@ -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(

View File

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

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:
## 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
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)