From 8389bd0a794ab000edb106e7f94de002df0b0115 Mon Sep 17 00:00:00 2001 From: slonkazoid Date: Sun, 29 Dec 2024 01:07:20 +0300 Subject: [PATCH] initial commit --- blogin.css | 17 +++++ blogin.sh | 159 ++++++++++++++++++++++++++++++++++++++++ migrations/0.sql | 4 + migrations/1.sql | 5 ++ partials/login_form.sh | 24 ++++++ partials/post.sh | 12 +++ partials/submit_form.sh | 10 +++ util.sh | 96 ++++++++++++++++++++++++ 8 files changed, 327 insertions(+) create mode 100644 blogin.css create mode 100644 blogin.sh create mode 100644 migrations/0.sql create mode 100644 migrations/1.sql create mode 100644 partials/login_form.sh create mode 100644 partials/post.sh create mode 100644 partials/submit_form.sh create mode 100644 util.sh diff --git a/blogin.css b/blogin.css new file mode 100644 index 0000000..ae15ca4 --- /dev/null +++ b/blogin.css @@ -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; +} diff --git a/blogin.sh b/blogin.sh new file mode 100644 index 0000000..fdf47a4 --- /dev/null +++ b/blogin.sh @@ -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 < 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 "" + +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 diff --git a/migrations/0.sql b/migrations/0.sql new file mode 100644 index 0000000..fc27422 --- /dev/null +++ b/migrations/0.sql @@ -0,0 +1,4 @@ +CREATE TABLE users ( + username TEXT PRIMARY KEY NOT NULL, + password TEXT NOT NULL +); diff --git a/migrations/1.sql b/migrations/1.sql new file mode 100644 index 0000000..4be9fa8 --- /dev/null +++ b/migrations/1.sql @@ -0,0 +1,5 @@ +CREATE TABLE posts ( + username TEXT UNIQUE NOT NULL, + contents TEXT NOT NULL, + ts TIMESTAMP NOT NULL +); diff --git a/partials/login_form.sh b/partials/login_form.sh new file mode 100644 index 0000000..93442c2 --- /dev/null +++ b/partials/login_form.sh @@ -0,0 +1,24 @@ +cat < + + +
+ +
+ + +EOF diff --git a/partials/post.sh b/partials/post.sh new file mode 100644 index 0000000..8c870fe --- /dev/null +++ b/partials/post.sh @@ -0,0 +1,12 @@ +cat < +
+ $1@$2 + at + $3 +
+
$4
+ +EOF diff --git a/partials/submit_form.sh b/partials/submit_form.sh new file mode 100644 index 0000000..7c0229c --- /dev/null +++ b/partials/submit_form.sh @@ -0,0 +1,10 @@ +cat < + + + +
+ + +EOF + diff --git a/util.sh b/util.sh new file mode 100644 index 0000000..e763b85 --- /dev/null +++ b/util.sh @@ -0,0 +1,96 @@ +# BWT: bash web token +bwt_sign() { + data=$( +#include +#include +#include + +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}" "$(%s' "$(escape <<< "${1?}")" +} + +info_box() { + printf '
%s
' "$(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" +}