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] == "" ||
|
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{}{})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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 {
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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:
|
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)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user