#!/bin/sh -e # # https://www.romanzolotarev.com/bin/n # Copyright 2018-2019 Roman Zolotarev # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # main() { BASE_URL='n' : "${PAGES:=/htdocs/rgz}" : "${DB:=/db/rgz}" : "${ROOT:=}" HEADER='/htdocs/rgz/raw/_header.html' FOOTER='/htdocs/rgz/raw/_footer.html' URI=${REQUEST_URI##/$BASE_URL} case "$REQUEST_METHOD$URI" in POST/?send_magic_link) send_magic_link;; GET/*?opt_in) opt_in;; GET/?wait_for_magic_link*) wait_for_magic_link;; GET/*?opt_out) opt_out;; GET/?opt_out_email=*) opt_out_email;; GET/?magic=*) verify_magic_link;; GET/*) render_page;; *) full_stop 'Wat?';; esac } ############################################################################## verify_magic_link() { magic=$(get_value "$QUERY_STRING" 'magic') test -n "$magic" || full_stop 'This magic link is broken' f="$DB/tokens/n-$magic" test -f "$f" || full_stop 'This magic link is expired' e=$(cat "$f") test -n "$e" || full_stop 'Email is missing' rm "$f" e_hash=$(echo "$e" | sha256) m_id=$(get_member_id "$e_hash") d="$(date +%s)" if test -n "$m_id" then f_n="$DB/members/$m_id/newsletter" test -f "$f_n" || make_file "$f_n" "$d" else m_id=$(random_str 20) f_n="$DB/members/$m_id/newsletter" make_file "$DB/members/$m_id/created_at" "$d" make_file "$DB/members/$m_id/email" "$e" make_file "$DB/members/$m_id/emails/$e_hash" "$e" make_file "$f_n" "$d" fi s_key=$(create_session "$m_id") http303 '/n/' \ 'Content-Type: text/html; charset=utf-8 Set-Cookie: m_id='"$m_id"'; Path=/; Secure; HttpOnly Set-Cookie: s_key='"$s_key"'; Path=/; Secure; HttpOnly Set-Cookie: key_a=; Path=/; Secure; HttpOnly Set-Cookie: e_hash=; Path=/; Secure; HttpOnly' } create_session() { m_id="$1" s_key=$(random_str 20) make_file "$DB/members/$m_id/sessions/$s_key" echo "$s_key" } wait_for_magic_link() { e=$(get_value "$QUERY_STRING" 'email') m_id=$(get_cookie 'm_id') s_key=$(get_cookie 's_key') f="$DB/members/$m_id/sessions/$s_key" test -f "$f" && http303 '/n/' http200_wrapped '

Sending a magic link to you in a minute.
Please check your mailbox.

From: hi@romanzolotarev.com
Subject: [RGZ.EE] Confirm subscription
To: '"$e"'

...

The link will expire in an hour. Didn'\''t get an email? Try again

' } send_magic_link() { query=$(read_query_string_post) || full_stop 'Invalid request' e=$(get_value "$query" 'email' | tr '[:upper:]' '[:lower:]') e_hash=$(echo "$e" | sha256) e_e=$(encode_value "$e") magic=$(random_str 20) make_file "$DB/tokens/n-$magic" "$e" make_file "$DB/mail/n-$e_hash" 'To: '"$e"' MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 7bit Subject: [RGZ.EE] Confirm subscription Hello, you are about to subscribe to my newsletter, please open this link to continue: '"https://$SERVER_NAME/n/?magic=$magic"' -- The link will expire in an hour. Sent to: '"$e" http303 "/n/?wait_for_magic_link&email=$e_e" } opt_out_email() { e=$(get_value "$QUERY_STRING" 'opt_out_email') test -n "$e" || full_stop "$e is not subscribed" e_hash=$(echo "$e" | sha256) m_id=$(get_member_id "$e_hash") test -n "$m_id" || full_stop "$e is not subscribed" f_n="$DB/members/$m_id/newsletter" test -f "$f_n" || full_stop "$e is not subscribed" rm "$f_n" http200_wrapped '

Unsubscribed

'"$e"' has been unsubscribed for the newsletter.

Find me on Twitter or elsewhere.
Let me know if I can help you.

' } opt_out() { m_id=$(get_cookie 'm_id') s_key=$(get_cookie 's_key') test -f "$DB/members/$m_id/sessions/$s_key" || full_stop 'You are not logged in' f_n="$DB/members/$m_id/newsletter" test -f "$f_n" && rm "$f_n" http303 "${REQUEST_URI%%\?*}" } opt_in() { m_id=$(get_cookie 'm_id') s_key=$(get_cookie 's_key') test -f "$DB/members/$m_id/sessions/$s_key" || full_stop 'You are not logged in' f_n="$DB/members/$m_id/newsletter" test -f "$f_n" || make_file "$f_n" "$(date +%s)" http303 "${REQUEST_URI%%\?*}" } render_page() { p_uri="${PAGES}${REQUEST_URI%%\?*}" if test "${p_uri%%/}" = "$p_uri" then f_page="$p_uri" else f_page="${p_uri}index.html" fi test -f "$f_page" || http404 # logged in && opted in && no email # => set_fill_email_form ###################################################################### # # logged out # => opt_in_email_form # # logged in && opted in # => opt_out_form # # logged in && opted out && no email # => set_opt_in_form # # logged in && opted out # => set_opt_in_form m_id=$(get_cookie 'm_id') s_key=$(get_cookie 's_key') f_m="$DB/members/$m_id/sessions/$s_key" test -f "$f_m" || opt_in_email_form "$f_page" f_email="$DB/members/$m_id/email" email=$(cat "$f_email") test -n "$email" || http500 fill_email_form set_user_profile_link "$email" f_n="$DB/members/$m_id/newsletter" if test -f "$f_n" then d=$(date -j -r "$(cat "$f_n")" '+%e %b %Y at %H:%M:%S UTC') opt_out_form "$f_page" "$email" "$d" else opt_in_form "$f_page" "$email" fi } ############################################################################## set_user_profile_link() { e="$1" export USER_PROFILE_LINK='
'"$e"'
' } opt_out_form() { f="$1" e="$2" d="$3" export SUBSCRIPTION_FORM='

👌
Thanks for subscribing to my newsletter.
Expect monthly updates sent to '"$e"'.

Unsubscribe

' http200 "$(render_template < "$f")" } opt_in_form() { f="$1" e="$2" export SUBSCRIPTION_FORM='

📨
You are not subscribed at the moment.
Want to get monthly updates to '"$e"'?
Join 500+ subscribers.

Subscribe

' http200 "$(render_template < "$f")" } opt_in_email_form() { f="$1" export SUBSCRIPTION_FORM='

📨
Want to get my monthly updates via email?
Join 500+ subscribers.


' http200 "$(render_template < "$f")" } ############################################################################## get_member_id() { e_hash="$1" d="$DB/members" test -d "$d" || { echo; return; } f=$(find "$d" -name "$e_hash" -path '*/emails/*' -type f | head -1) m="${f##$d/}" echo "${m%%/*}" } random_str() { jot -rcs '' "$1" 97 122 } encode_value() { test -n "$1" || { echo; return; } echo "$1" | awk ' BEGIN { a = "" for (n = 0; n < 256; n++) pack[sprintf("%c", n)] = sprintf("%%%02x", n) } { sline = "" slen = length($0) for (n = 1; n <= slen; n++) { char = substr($0, n, 1) if ( char !~ /^[[:alnum:]_]$/ ) char = pack[char] sline = sline char } a = a ( a ? "%0a" : "" ) sline } END { print a }' } get_value() { # h/t Devin Teske test -n "$1" || { echo; return; } x="${1##*$2=}" test "$x" = "$1" && { echo; return; } # shellcheck disable=1004 echo "${x%%&*}" | awk ' BEGIN { for (n = 0; n < 256; n++) chr[n] = sprintf("%c",n) } { t = $0 a = "" gsub(/\+/, " ", t) while( match(t, /%[[:xdigit:]][[:xdigit:]]/) ) { a = a substr(t, 1, RSTART-1)\ chr[ sprintf("%u", "0x" substr(t, RSTART+1, 2))] t = substr(t, RSTART+RLENGTH) } a = a t gsub(//,"\\>",a) print a }' } read_query_string_post() { test -n "$CONTENT_LENGTH" || full_stop 'Invalid content' dd bs=1 count="$CONTENT_LENGTH" status=none } make_file() { mkdir -p "${1%/*}" chmod 0770 "${1%/*}" echo "$2" > "$1" chmod 0660 "$1" } render_template() { # h/t Devin Teske awk ' BEGIN { w = "[a-zA-Z_][a-zA-Z0-9_]*" var = sprintf("\\$(%s|{%s})", w, w) } { str = "" tail = $0 while (match(tail, var)) { head = substr(tail, 1, RSTART - 1) repl = substr(tail, RSTART, RLENGTH) tail = substr(tail, RSTART + RLENGTH) if ((match(head, /\\+/) ? RLENGTH + 1 : 1) % 2 == 1) { sub(/^\$/, "", repl) gsub(/(^{|}$)/, "", repl) repl = ENVIRON[repl] } str = str head repl } str = str tail print str }' } get_cookie() { c="${HTTP_COOKIE##*$1=}" test -z "$c" || test "$HTTP_COOKIE" = "$c" && echo '' && return echo "${c%%;*}" } full_stop() { err="$1" fb='/feedback.html?comment='"$(encode_value "\"$err\"")" u="${REQUEST_URI%%\?*}" http200_wrapped '

Oops...

'"$err"'

Something went wrong, please let me know or try again a bit later.

' } http500() { echo 'Status: 500 Internal Server Error' echo 'Content-Type: text/html; charset=utf-8' echo echo "
$*
" exit 1 } http404() { echo 'Status: 404 Not Found' echo 'Content-Type: text/html; charset=utf-8' echo cat "$HEADER" echo "

Oops...

/$BASE_URL$URI not found

" cat "$FOOTER" exit 0 } http303() { echo 'Status: 303 See Other' echo 'Content-Type: text/html; charset=utf-8' echo "Location: $1" test -n "$2" && echo "$2" echo exit 0 } http200() { echo 'Status: 200 OK' echo 'Content-Type: text/html; charset=utf-8' echo echo "$1" exit 0 } http200_wrapped() { echo 'Status: 200 OK' echo 'Content-Type: text/html; charset=utf-8' echo cat "$HEADER" echo "$1" cat "$FOOTER" exit 0 } ############################################################################## main