initial commit
This commit is contained in:
commit
8389bd0a79
8 changed files with 327 additions and 0 deletions
17
blogin.css
Normal file
17
blogin.css
Normal file
|
@ -0,0 +1,17 @@
|
|||
.error {
|
||||
border: 2px solid red;
|
||||
border-radius: 4px;
|
||||
|
||||
padding: 1em;
|
||||
background-color: darkred;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.info {
|
||||
border: 2px solid darkgreen;
|
||||
border-radius: 4px;
|
||||
|
||||
padding: 1em;
|
||||
background-color: limegreen;
|
||||
color: black;
|
||||
}
|
159
blogin.sh
Normal file
159
blogin.sh
Normal file
|
@ -0,0 +1,159 @@
|
|||
#shellcheck disable=SC2034
|
||||
|
||||
set -e
|
||||
set -o pipefail
|
||||
# set -x
|
||||
|
||||
cd "$(dirname "$(readlink -f "$path")")" >&2
|
||||
|
||||
. util.sh
|
||||
|
||||
. ~/.bloginrc
|
||||
. "$CASH_PATH"
|
||||
|
||||
ALLOWED_SIGNERS=$(cat <<EOF
|
||||
blogin $(cat "$SIGN_PUB")
|
||||
EOF
|
||||
)
|
||||
|
||||
SESSION_TIME=${SESSION_TIME:-1 hour}
|
||||
|
||||
TITLE="blogin"
|
||||
|
||||
DESCRIPTION="the worst member of the logiverse"
|
||||
AUTHOR="$USER"
|
||||
TAGS=(meta interactive tadiweb)
|
||||
|
||||
action=$(jq -r '.action // ""' <<< "$BLAG_QUERY" || :)
|
||||
if [[ "${action:+x}" == "x" ]]; then
|
||||
DONT_CACHE=1
|
||||
fi
|
||||
|
||||
if [[ "$action" == "verify_token" ]]; then
|
||||
RAW=application/json
|
||||
metadata
|
||||
|
||||
#shellcheck disable=SC2266
|
||||
{
|
||||
jq -r '.token // ""' <<< "$BLAG_QUERY" |
|
||||
bwt_verify |
|
||||
[[ "$(jq -r ".till > now")" == "true" ]]
|
||||
} || echo false
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$action" == "posts" ]]; then
|
||||
RAW=application/json
|
||||
metadata
|
||||
|
||||
db_rows "SELECT * FROM posts" |
|
||||
jq -r 'map([.contents, .username, .ts, null, 0]) | select(. != [])'
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
metadata
|
||||
|
||||
echo "<style>"
|
||||
cat blogin.css
|
||||
echo "</style>"
|
||||
|
||||
header 1 <<< "$TITLE"
|
||||
paragraph <<< "$DESCRIPTION"
|
||||
|
||||
link "blogin.sh" <<< "view source" | paragraph
|
||||
|
||||
token=$(jq -r '.token // ""' <<< "$BLAG_QUERY" || :)
|
||||
|
||||
logged_in=0
|
||||
if [[ "$action" == "login" ]]; then
|
||||
username=$(jq -r '.username' <<< "$BLAG_QUERY" || :)
|
||||
password=$(jq -r '.password' <<< "$BLAG_QUERY" || :)
|
||||
|
||||
if ! rg -Uq '^[a-zA-Z0-9_]{3,24}$' <<< "$username"; then
|
||||
error_box "username must be between 3 to 24 characters, and must consist of alphanumerical characters plus underscore"
|
||||
elif ! rg -Uq '^.{8,108}$' <<< "$password"; then
|
||||
error_box "password must be between 8 and 108 characters"
|
||||
else
|
||||
{
|
||||
query="SELECT password FROM users WHERE username=$(escape_sql_str "$username")"
|
||||
row=$(db_row "$query")
|
||||
if [[ "$row" == "null" ]]; then
|
||||
hash=$(printf '%s' "$password" | argon2_hash)
|
||||
echo "registering user $username" >&2
|
||||
unset password
|
||||
db_query "INSERT INTO users (username, password) VALUES ($(escape_sql_str "$username"), $(escape_sql_str "$hash"))" >&2
|
||||
token=$(printf '{"till":%s,"user":%s}' "$(date -d "+ $SESSION_TIME" '+%s')" "$(jq -Rr @json <<< "$username")" | bwt_sign)
|
||||
logged_in=1
|
||||
info_box "account created!"
|
||||
else
|
||||
hash=$(jq -r '.password' <<< "$row")
|
||||
if argon2_verify "$hash" <<< "$password" ; then
|
||||
token=$(printf '{"till":%s,"user":%s}' "$(date -d "+ $SESSION_TIME" '+%s')" "$(jq -Rr @json <<< "$username")" | bwt_sign)
|
||||
logged_in=1
|
||||
info_box "logged in!"
|
||||
else
|
||||
error_box "invalid password"
|
||||
fi
|
||||
fi
|
||||
} || {
|
||||
error_box "failed to log in"
|
||||
}
|
||||
fi
|
||||
elif [[ "${token:+x}" == "x" ]]; then
|
||||
if { verified=$(bwt_verify <<< "$token"); } then
|
||||
till=$(jq -r '.till' <<< "$verified")
|
||||
now=$(date '+%s')
|
||||
if (( till > now )); then
|
||||
user=$(jq -r '.user' <<< "$verified")
|
||||
logged_in=1
|
||||
else
|
||||
# token expired
|
||||
error_box "session expired, please log in again"
|
||||
fi
|
||||
else
|
||||
echo "warning: somebody tried something funny" >&2
|
||||
error_box "bwt failed to validate"
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( logged_in == 1 )); then
|
||||
escape <<< "hello, $user" | header 2
|
||||
|
||||
if [[ "$action" == "submit" ]]; then
|
||||
{
|
||||
status=$(jq -r .status <<< "$BLAG_QUERY")
|
||||
chars=${#status}
|
||||
|
||||
if (( chars == 0 || chars > 200 )); then
|
||||
error_box "status should be between 1 and 200 characters"
|
||||
fi
|
||||
|
||||
db_query "INSERT INTO posts (username, contents, ts) VALUES (
|
||||
$(escape_sql_str "$user"),
|
||||
$(escape_sql_str "$status"),
|
||||
$(escape_sql_str "$(date --rfc-3339=seconds)")::timestamp
|
||||
) ON CONFLICT (username) DO UPDATE
|
||||
SET contents=excluded.contents, ts=excluded.ts" >&2
|
||||
info_box "updated status :D"
|
||||
} || error_box "failed to update status :("
|
||||
fi
|
||||
|
||||
render partials/submit_form.sh "$token"
|
||||
else
|
||||
header 2 <<< "(b)login"
|
||||
render partials/login_form.sh
|
||||
fi
|
||||
|
||||
header 2 <<< "posts"
|
||||
|
||||
#shellcheck disable=SC2162
|
||||
while read POST; do {
|
||||
if [[ "${POST:+x}" == "x" ]]; then
|
||||
declare -A post="$POST"
|
||||
render partials/post.sh "${post[username]}" "${post[instance]}" "${post[timestamp]}" "${post[contents]}"
|
||||
fi
|
||||
} & done < <(parse_posts "${INSTANCES[@]}" || :)
|
||||
|
||||
wait
|
4
migrations/0.sql
Normal file
4
migrations/0.sql
Normal file
|
@ -0,0 +1,4 @@
|
|||
CREATE TABLE users (
|
||||
username TEXT PRIMARY KEY NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
);
|
5
migrations/1.sql
Normal file
5
migrations/1.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE posts (
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
contents TEXT NOT NULL,
|
||||
ts TIMESTAMP NOT NULL
|
||||
);
|
24
partials/login_form.sh
Normal file
24
partials/login_form.sh
Normal file
|
@ -0,0 +1,24 @@
|
|||
cat <<EOF
|
||||
<form action="/posts/blogin">
|
||||
<input type="hidden" name="action" value="login" />
|
||||
<input
|
||||
type="text"
|
||||
minlength="3"
|
||||
maxlength="108"
|
||||
placeholder="username"
|
||||
name="username"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
<input
|
||||
type="password"
|
||||
minlength="3"
|
||||
maxlength="108"
|
||||
placeholder="password"
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
<input type="submit" value="log in" />
|
||||
</form>
|
||||
EOF
|
12
partials/post.sh
Normal file
12
partials/post.sh
Normal file
|
@ -0,0 +1,12 @@
|
|||
cat <<EOF
|
||||
<blockquote class="post">
|
||||
<div class="meta">
|
||||
<span class="username">$1</span
|
||||
><span class="at">@</span
|
||||
><span class="instance"><a href="https://$2/" target="_blank">$2</a></span>
|
||||
at
|
||||
<span class="timestamp date date-rfc3339">$3</span>
|
||||
</div>
|
||||
<div class="contents">$4</div>
|
||||
</blockquote>
|
||||
EOF
|
10
partials/submit_form.sh
Normal file
10
partials/submit_form.sh
Normal file
|
@ -0,0 +1,10 @@
|
|||
cat <<EOF
|
||||
<form action="/posts/blogin" method="GET">
|
||||
<input type="hidden" name="action" value="submit" />
|
||||
<input type="hidden" name="token" value="$(escape <<< "$1")" />
|
||||
<input type="text" maxlength="200" name="status" required />
|
||||
<br />
|
||||
<input type="submit" value="update blatus" />
|
||||
</form>
|
||||
EOF
|
||||
|
96
util.sh
Normal file
96
util.sh
Normal file
|
@ -0,0 +1,96 @@
|
|||
# BWT: bash web token
|
||||
bwt_sign() {
|
||||
data=$(</dev/stdin)
|
||||
signature=$(ssh-keygen -Y sign -f "$SIGN_KEY" -n blogin - <<< "$data")
|
||||
printf '{"d":%s,"s":%s}' \
|
||||
"$(jq -Rsr @json <<< "$data")" \
|
||||
"$(jq -Rsr @json <<< "$signature")" |
|
||||
zstd -1 | base64 -w0 |
|
||||
sed 's,=,_E_,g;s,/,_S_,g;s,+,_P_,g' # urlsafe base64 but evil
|
||||
}
|
||||
|
||||
bwt_verify() {
|
||||
json=$(sed 's,_E_,=,g;s,_S_,/,g;s,_P_,+,g' | base64 -d | zstd -d)
|
||||
data=$(jq -r '.d' <<< "$json")
|
||||
signature=$(jq -r '.s' <<< "$json")
|
||||
|
||||
if [[ "${data:-x}" == "x" || "${signature:-x}" == "x" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
ssh-keygen -q -Y verify -I 'blogin' -f <(cat <<< "$ALLOWED_SIGNERS") -n blogin \
|
||||
-s <(cat <<< "$signature") <<< "$data" &&
|
||||
printf '%s' "$data" || return $?
|
||||
}
|
||||
|
||||
db_query() {
|
||||
psql "${PSQL_OPTS[@]}" --csv -c "${1?need query}"
|
||||
}
|
||||
|
||||
db_rows() {
|
||||
db_query "${1?need query}" | yq -rp csv '@json' -
|
||||
}
|
||||
|
||||
db_row() {
|
||||
db_query "${1?need query}" | yq -rp csv '.0 | @json' -
|
||||
}
|
||||
|
||||
escape_sql_str() {
|
||||
printf "convert_from(decode('%s', 'base64'), 'UTF-8')" "$(printf '%s' "${1?}" | base64 -w0)"
|
||||
}
|
||||
|
||||
argon2_hash() {
|
||||
argon2 "$(openssl rand 16 | base64)" -id -e
|
||||
}
|
||||
|
||||
argon2_verify() {
|
||||
$(cash -O2 -pipe -largon2 <<EOF
|
||||
#include <argon2.h>
|
||||
#include <errno.h>
|
||||
#include <error.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) error(1, EINVAL, "expected exactly 2 arguments, got %d", argc - 1);
|
||||
|
||||
int result = argon2id_verify(argv[1], argv[2], strlen(argv[2]));
|
||||
|
||||
if (result == ARGON2_OK) return 0;
|
||||
else return 1;
|
||||
}
|
||||
EOF
|
||||
) "${1?need hash}" "$(</dev/stdin)"
|
||||
}
|
||||
|
||||
error_box() {
|
||||
printf '<div class="error">%s</div>' "$(escape <<< "${1?}")"
|
||||
}
|
||||
|
||||
info_box() {
|
||||
printf '<div class="info">%s</div>' "$(escape <<< "${1?}")"
|
||||
}
|
||||
|
||||
parse_posts() {
|
||||
pipe=$(mktemp -u)
|
||||
mkfifo "$pipe"
|
||||
|
||||
for instance in "$@"; do {
|
||||
name=$(jq -r .name <<< "$instance")
|
||||
endpoint=$(jq -r .feed <<< "$instance")
|
||||
echo "requesting feed from instance ${name@Q}" >&2
|
||||
curl -fsSL "$endpoint" | jq -r ".[] | .[5] |= $(jq -Rr @json <<< "$name") | @json" >> "$pipe"
|
||||
echo "feed from ${name@Q} consumed" >&2
|
||||
} & done
|
||||
|
||||
jq -sr 'sort_by(.[2] | sub(" "; "T") | sub("$"; "Z") | fromdate | -.) |
|
||||
.[] |
|
||||
"([username]=" + (.[1] | @html | @sh) +
|
||||
" [timestamp]=" + (.[2] | @html | @sh) +
|
||||
" [contents]=" + (.[0] | @html | @sh) +
|
||||
" [instance]=" + (.[5] | @html | @sh) +
|
||||
")"' < "$pipe"
|
||||
|
||||
wait || :
|
||||
|
||||
rm "$pipe"
|
||||
}
|
Loading…
Reference in a new issue