got pow working on the subscribe endpoint
This commit is contained in:
parent
20b9213010
commit
ec4aac24ab
@ -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{}{})
|
||||
})
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
'';
|
||||
};
|
||||
|
||||
|
28
static/src/assets/solvePow.js
Normal file
28
static/src/assets/solvePow.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user