Since I do not do the social media thing, sharing photos to a limited subset of people is kind of a pain in the rear. Double so if the other people are random strangers.
The solution was to mass generate user/password groupings, print them on hard copy paper with a link and QR code, and then use some HAProxy configuration magic to prevent widespread sharing. Content is then published via a second instance of the static site generator used for this site to a dedicated sub domain.
genauth.sh:
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "$0 <count> [path]" >&2
echo " count number of credentials to generate" >&2
echo " path path appended to \$BASE_URL (default: /)" >&2
echo "Environment: AUTH_FILE, BASE_URL, HOST" >&2
exit 1
}
if [[ $# -lt 1 || $# -gt 2 ]]; then
usage
fi
COUNT="$1"
if ! [[ "$COUNT" =~ ^[1-9][0-9]*$ ]]; then
usage
fi
PATH_PART="${2:-/}"
[[ "$PATH_PART" == /* ]] || PATH_PART="/$PATH_PART"
HOST="${HOST:-photo.lustenberg.org}"
BASE_URL="${BASE_URL:-http://${HOST}}"
STAMP="$(date +%Y-%m-%d)"
AUTH_FILE="${AUTH_FILE:-./auth/${STAMP}}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# lighttpd mod_auth plain backend: user:password
umask 077
touch "$AUTH_FILE"
rand_token() {
diceware -n "$1"
}
# only this run's credentials get printed
BATCH_LIST=$(mktemp /tmp/authbatch.XXXXXX)
trap 'rm -f "$BATCH_LIST"' EXIT
for ((i = 0; i < COUNT; i++)); do
USER="$(rand_token 2)"
PASS="$(rand_token 3)"
printf '%s:%s\n' "$USER" "$PASS" >>"$BATCH_LIST"
done
cat "$BATCH_LIST" >>"$AUTH_FILE"
"$SCRIPT_DIR/printqr.sh" "$BATCH_LIST" "${BASE_URL}${PATH_PART}"
printqr.sh:
#!/usr/bin/env bash
set -euo pipefail
set -x
usage() {
echo "$0 <plain-userfile> <URL>" >&2
echo " plain-userfile lighttpd plain auth file ('user:password' lines)" >&2
echo " URL base URL; credentials are injected per entry" >&2
exit 1
}
if [[ $# -ne 2 ]]; then
usage
fi
AUTH_LIST="$1"
URL="$2"
if [[ ! -r "$AUTH_LIST" ]]; then
echo "$0: cannot read '$AUTH_LIST'" >&2
exit 1
fi
# Create secure temporary files for the image processing
QR_TMP=$(mktemp /tmp/qr_code.XXXXXX.png)
CARD_TMP=$(mktemp /tmp/qr_card.XXXXXX.png)
# Ensure temporary files are deleted automatically when the script exits or is interrupted
cleanup() {
rm -f "$QR_TMP" "$CARD_TMP"
}
trap cleanup EXIT
print_card() {
local url="$1"
local baseurl="$2"
local auth="$3"
local comment="\
${baseurl}
${auth}
Thank you for your time and smile!
Please freely download and repost your photos.
- No attribution preferred, enjoy! <3
Do not share your URL, QR, or other's photos.
- I'm trying to do something nice.
- Don't ruin it.
- Seriously.
- We have Dragons and Scorned Women.
Alex
"
qrencode -s 26 -o "$QR_TMP" "$url"
convert -size 1500x2400 xc:white \
-gravity center -draw "image over 0,-600 0,0 '$QR_TMP'" \
-pointsize 48 -fill black \
-family clean \
-gravity west \
-annotate +170+450 "$comment" \
"$CARD_TMP"
if true; then
lp -o media=Custom.5x8in -o InputSlot=Manual "$CARD_TMP"
else
display "$CARD_TMP"
fi
}
while IFS=: read -r user pass _; do
[[ -z "${user// /}" || "$user" == \#* ]] && continue
print_card "${URL/:\/\//://${user}:${pass}@}" "${URL}" "user: ${user} password: ${pass}"
done <"$AUTH_LIST"
The generated auth files are then rsync’d and cat | sort into a lighttpd authentication list. I’ll typically do a run of 25 prints, using water proof / resistant paper in 5x8. This will last me 2 to 4 days, YMMV.
HAProxy is used at the edge, tracking successful authorized requests vs IP count, as well as 404 count per client.
global
log /dev/log local0
log /dev/log local1 notice
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
chroot /var/lib/haproxy
user haproxy
group haproxy
daemon
# Modern SSL/TLS security defaults
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.3 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 10s
timeout server 10s
frontend http_in
bind *:80
http-request redirect scheme https unless { ssl_fc }
frontend tls_in
bind *:443 ssl crt /etc/haproxy/lustenberg.pem
# extract the username from the header
# this does allow a trival dos, but requires discovery, so im not worried about it too much
http-request set-var(txn.username) req.hdr(Authorization),regsub(^Basic\s+,,i),b64dec,field(1,:) if { req.hdr(Authorization) -m beg "Basic " }
http-request set-var-fmt(txn.user_ip) "%[var(txn.username)]:%[src]"
# increment username counter if this is a new successful ip
http-request track-sc0 var(txn.username) table stick_user_ip_count if !{ var(txn.user_ip),in_table(stick_user_ip) } { status 200 }
# track user:ip
http-request track-sc1 var(txn.user_ip) table stick_user_ip if { var(txn.username) -m found }
# track 404s
http-request track-sc2 src table stick_err_ip
http-response sc-inc-gpc0(2) if { status 404 }
# deny when more than 5 ip per user
http-request deny status 429 if { sc_conn_cnt(0) ge 5 }
# deny when more than 5 404s in hour
http-request deny status 429 if { sc_gpc_rate(0,2) ge 5 }
default_backend local_backend
backend stick_user_ip
stick-table type string len 64 size 10k expire 24h store http_req_rate(1h)
backend stick_user_ip_count
stick-table type string len 64 size 10k expire 24h store conn_cnt
backend stick_err_ip
stick-table type ip size 1m expire 1h store gpc_rate(1,1h)
backend local_backend
server app_server 127.0.0.1:8080 check