From a94d7b7c247b97402186bfa295009dab8a013a7b Mon Sep 17 00:00:00 2001 From: sdomi Date: Sun, 21 Apr 2024 19:27:23 +0200 Subject: [PATCH 001/112] * fixes of some ugly sed hacks from 4 years ago --- src/server.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/server.sh b/src/server.sh index 215a1f3..d9a46fa 100755 --- a/src/server.sh +++ b/src/server.sh @@ -84,8 +84,8 @@ while read -r param; do data="$(sed -E 's/\?/��Lun4_iS_CuTe�/;s/^(.*)��Lun4_iS_CuTe�//' <<< "${r[url]}")" IFS='&' for i in $data; do - name="$(sed -E 's/\=(.*)$//' <<< "$i")" - value="$(sed "s/$name\=//" <<< "$i")" + name="${i/=*/}" + value="${i/*=/}" get_data[$name]="$value" done fi @@ -99,8 +99,8 @@ while read -r param; do data="$(sed -E 's/\?/��Lun4_iS_CuTe�/;s/^(.*)��Lun4_iS_CuTe�//' <<< "${r[url]}")" IFS='&' for i in $data; do - name="$(sed -E 's/\=(.*)$//' <<< "$i")" - value="$(sed "s/$name\=//" <<< "$i")" + name="${i/=*/}" + value="${i/*=/}" get_data[$name]="$value" done fi @@ -209,8 +209,8 @@ if [[ "${r[post]}" == true ]] && [[ "${r[status]}" == 200 || "${r[status]}" == IFS='&' for i in $(tr -d '\n' <<< "$data"); do - name="$(sed -E 's/\=(.*)$//' <<< "$i")" - param="$(sed "s/$name\=//" <<< "$i")" + name="${i/=*/}" + param="${i/*=/}" post_data[$name]="$param" done unset IFS From 231b52f171f4e21f58ec41230ba1a01ab70d6654 Mon Sep 17 00:00:00 2001 From: sdomi Date: Sun, 21 Apr 2024 21:54:06 +0200 Subject: [PATCH 002/112] * fix router parameter clobbering if a route contained a static string with the same name as one of the named params, said string would overwrite the payload from the previous named param. this commit adds a check for `:` in the template to prevent this --- src/server.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.sh b/src/server.sh index d9a46fa..2951633 100755 --- a/src/server.sh +++ b/src/server.sh @@ -133,7 +133,7 @@ if [[ ${r[status]} != 101 ]]; then url_=(${r[url]}) unset IFS for (( j=0; j<${#url[@]}; j++ )); do - if [[ ${url_[$j]} != '' ]]; then + if [[ ${url_[$j]} != '' && ${url[$j]} == ":"* ]]; then params[$(sed 's/://' <<< "${url[$j]}")]="${url_[$j]}" fi done From c459a405b2612d82b0173c549edaeb67a5cf8524 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Sun, 19 May 2024 16:21:40 +0100 Subject: [PATCH 003/112] Ignore query when parsing URL params --- src/server.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.sh b/src/server.sh index 2951633..73cb646 100755 --- a/src/server.sh +++ b/src/server.sh @@ -130,7 +130,7 @@ if [[ ${r[status]} != 101 ]]; then r[view]="${route[$((i+2))]}" IFS='/' url=(${route[$i]}) - url_=(${r[url]}) + url_=($(cut -d '?' -f 1 <<< "${r[url]}")) unset IFS for (( j=0; j<${#url[@]}; j++ )); do if [[ ${url_[$j]} != '' && ${url[$j]} == ":"* ]]; then From 10342035a41335aa2681a8a190a61fc2ac728f12 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Sun, 19 May 2024 19:52:10 +0100 Subject: [PATCH 004/112] Match headers on beginning of line --- src/server.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/server.sh b/src/server.sh index 73cb646..c82ae7c 100755 --- a/src/server.sh +++ b/src/server.sh @@ -30,10 +30,10 @@ while read -r param; do if [[ "$param_l" == $'\015' ]]; then break - elif [[ "$param_l" == *"content-length:"* ]]; then + elif [[ "$param_l" == "content-length:"* ]]; then r[content_length]="$(sed 's/Content-Length: //i;s/\r//' <<< "$param")" - elif [[ "$param_l" == *"content-type:"* ]]; then + elif [[ "$param_l" == "content-type:"* ]]; then r[content_type]="$(sed 's/Content-Type: //i;s/\r//' <<< "$param")" if [[ "${r[content_type]}" == *"multipart/form-data"* ]]; then tmpdir=$(mktemp -d) @@ -42,7 +42,7 @@ while read -r param; do r[content_boundary]="$(sed -E 's/(.*)boundary=//i;s/\r//;s/ //' <<< "${r[content_type]}")" fi - elif [[ "$param_l" == *"host:"* ]]; then + elif [[ "$param_l" == "host:"* ]]; then r[host]="$(sed 's/Host: //i;s/\r//;s/\\//g' <<< "$param")" r[host_portless]="$(sed -E 's/:(.*)$//' <<< "${r[host]}")" if [[ -f "config/$(basename -- ${r[host]})" ]]; then @@ -51,22 +51,22 @@ while read -r param; do source "config/$(basename -- ${r[host_portless]})" fi - elif [[ "$param_l" == *"user-agent:"* ]]; then + elif [[ "$param_l" == "user-agent:"* ]]; then r[user_agent]="$(sed 's/User-Agent: //i;s/\r//;s/\\//g' <<< "$param")" - elif [[ "$param_l" == *"upgrade:"* && $(sed 's/Upgrade: //i;s/\r//' <<< "$param") == "websocket" ]]; then + elif [[ "$param_l" == "upgrade:"* && $(sed 's/Upgrade: //i;s/\r//' <<< "$param") == "websocket" ]]; then r[status]=101 - elif [[ "$param_l" == *"sec-websocket-key:"* ]]; then + elif [[ "$param_l" == "sec-websocket-key:"* ]]; then r[websocket_key]="$(sed 's/Sec-WebSocket-Key: //i;s/\r//' <<< "$param")" - elif [[ "$param_l" == *"authorization: basic"* ]]; then + elif [[ "$param_l" == "authorization: basic"* ]]; then login_simple "$param" - elif [[ "$param_l" == *"authorization: bearer"* ]]; then + elif [[ "$param_l" == "authorization: bearer"* ]]; then r[authorization]="$(sed 's/Authorization: Bearer //i;s/\r//' <<< "$param")" - elif [[ "$param_l" == *"cookie: "* ]]; then + elif [[ "$param_l" == "cookie: "* ]]; then IFS=';' for i in $(IFS=' '; echo "$param" | sed -E 's/Cookie: //i;;s/%/\\x/g'); do name="$((grep -Poh "[^ ].*?(?==)" | head -1) <<< $i)" @@ -74,7 +74,7 @@ while read -r param; do cookies[$name]="$(echo -e $value)" done - elif [[ "$param_l" == *"range: bytes="* ]]; then + elif [[ "$param_l" == "range: bytes="* ]]; then r[range]="$(sed 's/Range: bytes=//;s/\r//' <<< "$param")" elif [[ "$param" == *"GET "* ]]; then From a65b600952d64365717f3bd3a6753d78f2e0bbc8 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Tue, 4 Jun 2024 00:17:53 +0100 Subject: [PATCH 005/112] Don't run ncat within background loop in the background too --- http.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http.sh b/http.sh index 482aeab..ac4333b 100755 --- a/http.sh +++ b/http.sh @@ -190,7 +190,7 @@ else done & else while true; do - ncat -i 600s -l -U "$socket" -c src/server.sh -k 2>> /dev/null & + ncat -i 600s -l -U "$socket" -c src/server.sh -k 2>> /dev/null done & fi socat TCP-LISTEN:${cfg[port]},fork,bind=${cfg[ip]} UNIX-CLIENT:$socket & From 4e6c5c0ba3af3d93a67961651db0906c6dfa686f Mon Sep 17 00:00:00 2001 From: sdomi Date: Wed, 17 Jul 2024 22:09:01 +0200 Subject: [PATCH 006/112] template: tpl includes with {{#PATH}} --- src/template.sh | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/template.sh b/src/template.sh index 9ef0969..a1164b5 100755 --- a/src/template.sh +++ b/src/template.sh @@ -12,9 +12,30 @@ function render() { local -n ref=$1 local tmp=$(mktemp) + # hack below! + # this whole code iterates over an array we control, but I want to add + # template includes. Hence, we have to pre-parse the template to + # find all of our include statements and add them to the roundup. + + while read elem; do + ref[#$elem]= + done <<< "$(grep -Poh '{{#.*?}}' <<< "$template" | sed 's/{{#//;s/}}$//')" + local key + IFS=$'\n' for key in ${!ref[@]}; do - if [[ "$key" == "_"* ]]; then # iter mode + if [[ "$key" == "#"* ]]; then # include mode + local file="${key/\#/}" + + # below check prevents the loop loading itself as a template. + # this is possibly not enough to prevent all recursions, but + # i see it as a last-ditch measure. so it'll do here. + if [[ "$file" == "$2" ]]; then + echo 's'$'\02''\{\{'"\\$key"'\}\}'$'\02''I cowardly refuse to endlessly recurse\!'$'\02''g;' >> "$tmp" + elif [[ -f "$file" ]]; then + echo 's'$'\02''\{\{'"\\$key"'\}\}'$'\02'"$(tr -d $'\01'$'\02' < "$file" | sed 's/\&/�UwU�/g')"$'\02''g;' >> "$tmp" + fi + elif [[ "$key" == "_"* ]]; then # iter mode local subtemplate=$(mktemp) echo "$template" | grep "{{start $key}}" -A99999 | grep "{{end $key}}" -B99999 | tr '\n' $'\01' > "$subtemplate" @@ -34,7 +55,7 @@ function render() { rm "$subtemplate" elif [[ "$key" == "@"* && "${ref[$key]}" != '' ]]; then local value="$(sed -E 's/\&/�UwU�/g' <<< "${ref[$key]}")" - echo 's'$'\02''\{\{\'"$key"'\}\}'$'\02'''"$value"''$'\02''g;' >> "$tmp" + echo 's'$'\02''\{\{\'"$key"'\}\}'$'\02'''"$value"''$'\02''g;' >> "$tmp" #' elif [[ "$key" == '?'* ]]; then local _key="\\?${key/?/}" @@ -55,6 +76,7 @@ function render() { echo 's'$'\02''\{\{\.'"$key"'\}\}'$'\02'$'\02''g;' >> "$tmp" fi done + unset IFS if [[ "$3" != true ]]; then # are we recursing? cat "$tmp" | tr '\n' $'\01' | sed -E 's/'$'\02'';'$'\01''/'$'\02'';/g;s/'$'\02''g;'$'\01''/'$'\02''g;/g' > "${tmp}_" From f5eebc109d3444ee04a208f769fa1d6009e923f3 Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 19 Jul 2024 00:58:13 +0200 Subject: [PATCH 007/112] account: added argon2id as a preferred (default) password hash --- http.sh | 3 +++ src/account.sh | 32 ++++++++++++++++++++++++++------ src/dependencies.optional | 1 + 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/http.sh b/http.sh index ac4333b..7d8f02f 100755 --- a/http.sh +++ b/http.sh @@ -46,6 +46,9 @@ cfg[mail_server]="" cfg[mail_password]="" cfg[mail_ssl]=true cfg[mail_ignore_bad_cert]=false + +# unset for legacy sha256sum hashing (not recommended) +cfg[hash]="argon2id" EOF fi diff --git a/src/account.sh b/src/account.sh index d423c57..1ba6cb7 100755 --- a/src/account.sh +++ b/src/account.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash # account.sh - account and session mgmt - # register(username, password) function register() { local username=$(echo -ne $(sed -E "s/ /_/g;s/\:/\-/g;s/\%/\\x/g" <<< "$1")) @@ -11,9 +10,16 @@ function register() { return 1 fi - local salt=$(dd if=/dev/urandom bs=256 count=1 | sha1sum | cut -c 1-16) - local hash=$(echo -n $2$salt | sha256sum | cut -c 1-64) - local token=$(dd if=/dev/urandom bs=32 count=1 | sha1sum | cut -c 1-40) + if [[ "${cfg[hash]}" == "argon2id" ]]; then + local salt=$(dd if=/dev/urandom bs=256 count=1 | sha1sum | cut -c 1-16) + local token=$(dd if=/dev/urandom bs=32 count=1 | sha1sum | cut -c 1-40) + local hash="$(echo -n "$2" | argon2 "$salt" -id -e)" + else + local salt=$(dd if=/dev/urandom bs=256 count=1 | sha1sum | cut -c 1-16) + local token=$(dd if=/dev/urandom bs=32 count=1 | sha1sum | cut -c 1-40) + local hash=$(echo -n $2$salt | sha256sum | cut -c 1-64) + fi + set_cookie_permanent "sh_session" $token set_cookie_permanent "username" $username @@ -26,7 +32,14 @@ function login() { IFS=':' local user=($(grep -P "$username:" secret/users.dat)) unset IFS - if [[ $(echo -n $2${user[2]} | sha256sum | cut -c 1-64 ) == "${user[1]}" ]]; then + + if [[ "${cfg[hash]}" == "argon2id" ]]; then + hash="$(echo -n "$2" | argon2 "${user[2]}" -id -e)" + else + hash="$(echo -n $2${user[2]} | sha256sum | cut -c 1-64 )" + fi + + if [[ "$hash" == "${user[1]}" ]]; then set_cookie_permanent "sh_session" "${user[3]}" set_cookie_permanent "username" "$username" return 0 @@ -47,7 +60,14 @@ function login_simple() { IFS=':' local user=($(grep "$login:" secret/users.dat)) unset IFS - if [[ $(echo -n $password${user[2]} | sha256sum | cut -c 1-64 ) == ${user[1]} ]]; then + + if [[ "${cfg[hash]}" == "argon2id" ]]; then + hash="$(echo -n "$password" | argon2 "$salt" -id -e)" + else + hash="$(echo -n $password${user[2]} | sha256sum | cut -c 1-64 )" + fi + + if [[ "$hash" == "${user[1]}" ]]; then r[authorized]=true else r[authorized]=false diff --git a/src/dependencies.optional b/src/dependencies.optional index 62629e1..5b5425d 100644 --- a/src/dependencies.optional +++ b/src/dependencies.optional @@ -2,3 +2,4 @@ sha1sum sha256sum curl iconv +argon2 From 61fea4b849871d12e568a41b065a03fcad6b0c8f Mon Sep 17 00:00:00 2001 From: famfo Date: Fri, 19 Jul 2024 02:25:33 +0200 Subject: [PATCH 008/112] Add option to show call trace, basic cli docs --- docs/running.md | 10 ++++++++++ http.sh | 11 ++++++++--- src/server.sh | 6 ++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 docs/running.md diff --git a/docs/running.md b/docs/running.md new file mode 100644 index 0000000..0f0d1d9 --- /dev/null +++ b/docs/running.md @@ -0,0 +1,10 @@ +# Running http.sh + +## cli args + +The only args checked are `$1`. + +- `init` create an app skeleton +- `debug` show stderr +- `debuggier` show stderr and calltrace + diff --git a/http.sh b/http.sh index 7d8f02f..af82dfa 100755 --- a/http.sh +++ b/http.sh @@ -6,7 +6,7 @@ if [[ ! -f "config/master.sh" ]]; then cat < "config/master.sh" declare -A cfg -cfg[ip]=0.0.0.0 # IP address to bind to - use 0.0.0.0 to bind to all +cfg[ip]=[::] # IP address to bind to - use [::] to bind to all cfg[http]=true # enables/disables listening on HTTP cfg[port]=1337 # HTTP port @@ -161,6 +161,11 @@ EOF if [[ "$1" == "debug" ]]; then cfg[dbg]=true echo "[DEBUG] Activated debug mode - stderr will be shown" +elif [[ "$1" == "debuggier" ]]; then + cfg[dbg]=true + cfg[debuggier]=true + echo "[DEBUG] Activated debuggier mode - stderr and call trace will be shown" + set -x fi source src/worker.sh @@ -173,7 +178,7 @@ if [[ ${cfg[socat_only]} == true ]]; then echo "[INFO] listening directly via socat, assuming no ncat available" echo "[HTTP] listening on ${cfg[ip]}:${cfg[port]}" if [[ ${cfg[dbg]} == true ]]; then - socat tcp-listen:${cfg[port]},bind=${cfg[ip]},fork "exec:bash -c src/server.sh" + socat tcp-listen:${cfg[port]},bind=${cfg[ip]},fork "exec:bash -c \'src/server.sh ${cfg[debuggier]}\'" else socat tcp-listen:${cfg[port]},bind=${cfg[ip]},fork "exec:bash -c src/server.sh" 2>> /dev/null if [[ $? != 0 ]]; then @@ -189,7 +194,7 @@ else # to quit after the first time-outed connection, ignoring the # "broker" (-k) mode. This is a workaround for this. while true; do - ncat -i 600s -l -U "$socket" -c src/server.sh -k + ncat -i 600s -l -U "$socket" -c "src/server.sh ${cfg[debuggier]}" -k done & else while true; do diff --git a/src/server.sh b/src/server.sh index c82ae7c..cc0f17d 100755 --- a/src/server.sh +++ b/src/server.sh @@ -1,4 +1,10 @@ #!/usr/bin/env bash + +# If $1 is set to true, enable the call trace +if [[ "$1" == true ]]; then + set -x +fi + source config/master.sh source src/mime.sh source src/misc.sh From b28e1d9fcd9a1f4f648ba6ca03044661b0f27b3d Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 19 Jul 2024 17:23:10 +0200 Subject: [PATCH 009/112] account: fix poor checking that could lead to privilege escalation --- src/account.sh | 70 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/account.sh b/src/account.sh index 1ba6cb7..b73cd78 100755 --- a/src/account.sh +++ b/src/account.sh @@ -5,10 +5,15 @@ function register() { local username=$(echo -ne $(sed -E "s/ /_/g;s/\:/\-/g;s/\%/\\x/g" <<< "$1")) - if [[ $(grep "$username:" secret/users.dat) != '' ]]; then - reason="This user already exists!" - return 1 - fi + IFS=$'\n' + while read user; do + IFS=: a=($user) + if [[ "${a[0]}" == "$username" ]]; then + reason="This user already exists!" + return 1 + fi + done < secret/users.dat + unset IFS if [[ "${cfg[hash]}" == "argon2id" ]]; then local salt=$(dd if=/dev/urandom bs=256 count=1 | sha1sum | cut -c 1-16) @@ -29,9 +34,17 @@ function register() { # login(username, password) function login() { local username=$(echo -ne $(sed -E 's/%/\\x/g' <<< "$1")) - IFS=':' - local user=($(grep -P "$username:" secret/users.dat)) - unset IFS + IFS=$'\n' + while read a; do + IFS=: user=($a) + if [[ "${user[0]}" == "$username" ]]; then + break + fi + done < secret/users.dat + if [[ "${user[0]}" == '' ]]; then + reason="Bad credentials" + return 1 + fi if [[ "${cfg[hash]}" == "argon2id" ]]; then hash="$(echo -n "$2" | argon2 "${user[2]}" -id -e)" @@ -46,7 +59,7 @@ function login() { else remove_cookie "sh_session" remove_cookie "username" - reason="Invalid credentials!!11" + reason="Bad credentials" return 1 fi } @@ -56,13 +69,16 @@ function login_simple() { local data=$(base64 -d <<< "$3") local password=$(sed -E 's/^(.*)\://' <<< "$data") local login=$(sed -E 's/\:(.*)$//' <<< "$data") - - IFS=':' - local user=($(grep "$login:" secret/users.dat)) + + IFS=$'\n' + while read a; do + IFS=':' user=($a) + [[ ${user[0]} == "$login" ]] && break + done < secret/users.dat unset IFS if [[ "${cfg[hash]}" == "argon2id" ]]; then - hash="$(echo -n "$password" | argon2 "$salt" -id -e)" + hash="$(echo -n "$password" | argon2 "${user[2]}" -id -e)" else hash="$(echo -n $password${user[2]} | sha256sum | cut -c 1-64 )" fi @@ -82,26 +98,34 @@ function logout() { # session_verify(session) function session_verify() { - if [[ $(grep ":$1" secret/users.dat) != '' && $1 != '' ]]; then - return 0 - else - return 1 - fi + IFS=$'\n' + while read user; do + IFS=: a=($user) + if [[ "${a[3]}" == "$1" && "$1" != '' ]]; then + return 0 + fi + done < secret/users.dat + return 1 } # session_get_username(session) function session_get_username() { - [[ "$1" == "" ]] && return + [[ "$1" == "" ]] && return 1 - IFS=':' - local data=($(grep ":$1$" secret/users.dat)) - unset IFS - echo ${data[0]} + IFS=$'\n' + while read user; do + IFS=':' a=$($user) + if [[ "${a[3]}" == "$1" ]]; then + echo "${a[0]}" + break + fi + done < secret/users.dat + return 1 } # THIS FUNCTION IS DANGEROUS # delete_account(username) function delete_account() { - [[ "$1" == "" ]] && return + [[ "$1" == "" ]] && return 1 sed -i "s/^$1:.*//;/^$/d" secret/users.dat } From 1059fcf1774beab6a73bfc434b7c6e93871d139e Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 19 Jul 2024 17:35:11 +0200 Subject: [PATCH 010/112] template: fix edge case with newline splitting sed arguments --- src/template.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/template.sh b/src/template.sh index a1164b5..10feacd 100755 --- a/src/template.sh +++ b/src/template.sh @@ -69,7 +69,7 @@ function render() { if [[ "$3" != true ]]; then local value="$(html_encode <<< "${ref[$key]}" | sed -E 's/\&/�UwU�/g')" else - local value="$(sed -E 's/\\\\/�OwO�/g;s/\\//g;s/�OwO�/\\/g' <<< "${ref[$key]}" | html_encode | sed -E 's/\&/�UwU�/g')" + local value="$(echo -n "${ref[$key]}" | tr -d $'\01'$'\02' | tr $'\n' $'\01' | sed -E 's/\\\\/�OwO�/g;s/\\//g;s/�OwO�/\\/g' | html_encode | sed -E 's/\&/�UwU�/g')" fi echo 's'$'\02''\{\{\.'"$key"'\}\}'$'\02'''"$value"''$'\02''g;' >> "$tmp" else From ccc1ce327338fba75de8bec72ffeff78bd577cc1 Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 19 Jul 2024 18:03:27 +0200 Subject: [PATCH 011/112] account: typo fix + minor flow changes --- src/account.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/account.sh b/src/account.sh index b73cd78..4fe8059 100755 --- a/src/account.sh +++ b/src/account.sh @@ -98,10 +98,11 @@ function logout() { # session_verify(session) function session_verify() { + [[ "$1" == '' ]] && return 1 IFS=$'\n' while read user; do IFS=: a=($user) - if [[ "${a[3]}" == "$1" && "$1" != '' ]]; then + if [[ "${a[3]}" == "$1" ]]; then return 0 fi done < secret/users.dat @@ -114,10 +115,10 @@ function session_get_username() { IFS=$'\n' while read user; do - IFS=':' a=$($user) + IFS=':' a=($user) if [[ "${a[3]}" == "$1" ]]; then echo "${a[0]}" - break + return 0 fi done < secret/users.dat return 1 From ac89f028d07e7eefd3a57d1ee99ef2d6e671a6f7 Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 19 Jul 2024 22:24:51 +0200 Subject: [PATCH 012/112] template: fix misrenders due to unsorted key array --- src/template.sh | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/template.sh b/src/template.sh index 10feacd..0f01bda 100755 --- a/src/template.sh +++ b/src/template.sh @@ -12,30 +12,10 @@ function render() { local -n ref=$1 local tmp=$(mktemp) - # hack below! - # this whole code iterates over an array we control, but I want to add - # template includes. Hence, we have to pre-parse the template to - # find all of our include statements and add them to the roundup. - - while read elem; do - ref[#$elem]= - done <<< "$(grep -Poh '{{#.*?}}' <<< "$template" | sed 's/{{#//;s/}}$//')" - local key IFS=$'\n' for key in ${!ref[@]}; do - if [[ "$key" == "#"* ]]; then # include mode - local file="${key/\#/}" - - # below check prevents the loop loading itself as a template. - # this is possibly not enough to prevent all recursions, but - # i see it as a last-ditch measure. so it'll do here. - if [[ "$file" == "$2" ]]; then - echo 's'$'\02''\{\{'"\\$key"'\}\}'$'\02''I cowardly refuse to endlessly recurse\!'$'\02''g;' >> "$tmp" - elif [[ -f "$file" ]]; then - echo 's'$'\02''\{\{'"\\$key"'\}\}'$'\02'"$(tr -d $'\01'$'\02' < "$file" | sed 's/\&/�UwU�/g')"$'\02''g;' >> "$tmp" - fi - elif [[ "$key" == "_"* ]]; then # iter mode + if [[ "$key" == "_"* ]]; then # iter mode local subtemplate=$(mktemp) echo "$template" | grep "{{start $key}}" -A99999 | grep "{{end $key}}" -B99999 | tr '\n' $'\01' > "$subtemplate" @@ -61,6 +41,12 @@ function render() { local subtemplate=$(mktemp) echo 's'$'\02''\{\{start '"$_key"'\}\}((.*)\{\{else '"$_key"'\}\}.*\{\{end '"$_key"'\}\}|(.*)\{\{end '"$_key"'\}\})'$'\02''\2\3'$'\02'';' >> "$subtemplate" + + # TODO: check if this is needed? + # the code below makes sure to resolve the conditional blocks + # *before* anything else. I can't think of *why* this is needed + # right now, but I definitely had a reason in this. Question is, what reason. + cat <<< $(cat "$subtemplate" "$tmp") > "$tmp" # call that cat abuse rm "$subtemplate" @@ -78,6 +64,26 @@ function render() { done unset IFS + # process file includes; + # achtung: even though this is *after* the main loop, it actually executes sed reaplces *before* it; + # recursion is currently unsupported here, i feel like it may break things? + if [[ "$template" == *'{{#'* && "$3" != true ]]; then + local subtemplate=$(mktemp) + while read key; do + # below check prevents the loop loading itself as a template. + # this is possibly not enough to prevent all recursions, but + # i see it as a last-ditch measure. so it'll do here. + if [[ "$file" == "$2" ]]; then + echo 's'$'\02''\{\{\#'"$key"'\}\}'$'\02''I cowardly refuse to endlessly recurse\!'$'\02''g;' >> "$subtemplate" + elif [[ -f "$key" ]]; then + echo 's'$'\02''\{\{\#'"$key"'\}\}'$'\02'"$(tr -d $'\01'$'\02' < "$key" | tr $'\n' $'\01' | sed 's/\&/�UwU�/g')"$'\02''g;' >> "$subtemplate" + fi + done <<< "$(grep -Poh '{{#.*?}}' <<< "$template" | sed 's/{{#//;s/}}$//')" + + cat <<< $(cat "$subtemplate" "$tmp") > "$tmp" + rm "$subtemplate" + fi + if [[ "$3" != true ]]; then # are we recursing? cat "$tmp" | tr '\n' $'\01' | sed -E 's/'$'\02'';'$'\01''/'$'\02'';/g;s/'$'\02''g;'$'\01''/'$'\02''g;/g' > "${tmp}_" From 00f9432b295f9ddee6035682390222d2b2019e77 Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 19 Jul 2024 23:11:27 +0200 Subject: [PATCH 013/112] misc: fix decoding spaces in url_decode (oops?) --- src/misc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/misc.sh b/src/misc.sh index 4da8b06..7f7b1e0 100755 --- a/src/misc.sh +++ b/src/misc.sh @@ -53,7 +53,7 @@ function url_encode() { # url_decode(string) function url_decode() { - echo -ne "$(sed -E 's/%[0-1][0-9a-f]//g;s/%/\\x/g' <<< "$1")" + echo -ne "$(sed -E 's/%[0-1][0-9a-f]//g;s/%/\\x/g;s/\+/ /g' <<< "$1")" } # bogus function! From efbeca0498ddf8e598b3c5b609f06777967fa080 Mon Sep 17 00:00:00 2001 From: sdomi Date: Wed, 24 Jul 2024 03:01:43 +0200 Subject: [PATCH 014/112] template: normalize IFS --- src/template.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/src/template.sh b/src/template.sh index 0f01bda..af38357 100755 --- a/src/template.sh +++ b/src/template.sh @@ -140,6 +140,7 @@ function nested_add() { declare -g -A _$nested_id # poor man's array copy + IFS=' ' for k in ${!nested_ref[@]}; do declare -g -A _$nested_id[$k]="${nested_ref[$k]}" done From 1df5fb17cada14cac80d79280f1e37d7d34873d0 Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 26 Jul 2024 02:51:34 +0200 Subject: [PATCH 015/112] notORM: new generic data I/O interface, currently backed by CSV-esque files --- src/notORM.sh | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/server.sh | 1 + 2 files changed, 166 insertions(+) create mode 100755 src/notORM.sh diff --git a/src/notORM.sh b/src/notORM.sh new file mode 100755 index 0000000..cc534ad --- /dev/null +++ b/src/notORM.sh @@ -0,0 +1,165 @@ +#!/bin/bash +## notORM.sh - clearly, not an ORM. +# basic interface for saving semi-arbitrary data organized in "tables". + +## limitations: +# - only for strings (we trim some bytes; see `reserved values` below) +# - currently only supports saving to CSV-with-extra-steps + +## function return values: +# +# 0 - success +# 1 - general failure +# 2 - entry not found +# 3 - locked, try again later +# 4 - file not found + +## data reserved values: +# +# \x00 - bash yeets it out of existence +# \x01 - delimeter +# \x02 - newline +# \x03 - control chr for sed +delim=$'\01' +newline=$'\02' +ctrl=$'\03' + +repeat() { + local IFS=' ' + printf -- "$2%.0s" $(seq 1 $1) +} + +# adds a flat `array` to the `store`. +# a store can be any file, as long as we have r/w access to it and the +# adjacent directory. +# +# this function will create some helper files if they don't exist. those +# shouldn't be removed, as other functions may use them for data mangling. +# +# data_add(store, array) +data_add() { + [[ ! -v "$2" ]] && return 1 + local -n ref="$2" + [[ ! -f "$1" ]] && echo "${#ref[@]}" > "${1}.cols" + local res= + local IFS=$'\n' + + for i in "${ref[@]}"; do + res+="$(echo -n "$i" | tr -d '\01\02\03' | tr '\n' '\02')"$delim + done + + echo "$res" >> "$1" # TODO: some locking +} + +# get one entry from store, filtering by search. exit after first result. +# by default uses the 0th column. override with optional `column`. +# returns the data to $res. override with optional `res` +# +# data_get(store, search, [column], [res]) -> $res / ${!4} +data_get() { + [[ ! "$2" ]] && return 1 + [[ ! -f "$1" ]] && return 4 + local column=${3:-0} + local -n ref=${4:-res} + local IFS=$'\n' + + while read line; do + local IFS=$delim + ref=($(tr '\02' '\n' <<< "$line")) + [[ "${ref[$column]}" == "$2" ]] && return 0 + done < "$1" + + return 2 +} + +# run `callback` on all entries from `store` that match `search`. +# by default uses the 0th column. override with optional `column` +# +# data_iter(store, search, callback, [column]) -> $data +data_iter() { + [[ ! "$3" ]] && return 1 + local column=${4:-0} + local IFS=$'\n' + + while read line; do + local IFS=$delim + data=($(tr '\02' '\n' <<< "$line")) + [[ "${data[$column]}" == "$2" || ! "$2" ]] && "$3" + done < "$1" +} + +# replace a value in `store` with `array`, filtering by `search`. +# by default uses the 0th column. override with optional `column` +# +# `value` is any string, which will directly replace `search` +# +# data_replace(store, search, value, [column]) +data_replace_value() { + [[ ! "$3" ]] && return 1 + [[ ! -f "$1" ]] && return 4 + local column=${4:-0} + local IFS=' ' + + if [[ $column == 0 ]]; then + local expr="s$ctrl^$(_sed_sanitize "$2")(${delim}.*)$ctrl$(_sed_sanitize "$3")\1$ctrl" + else + local expr="s$ctrl^($(repeat $column ".*$delim"))$(_sed_sanitize "$2")($delim$(repeat $(( $(cat "${1}.cols") - column - 1 )) ".*$delim"))"'$'"$ctrl\1$(_sed_sanitize "$3")\2$ctrl" + fi + + sed -E -i "$expr" "$1" +} + +# replace an entire entry in `store` with `array`, filtering by `search`. +# by default uses the 0th column. override with optional `column` +# +# pass `array` without expanding (`arr`, not `$arr`). +# +# data_replace(store, search, array, [column]) +data_replace() { + [[ ! "$3" ]] && return 1 + [[ ! -f "$1" ]] && return 4 + local column=${4:-0} + local -n ref="$3" + local output= + local IFS=' ' + + for i in "${ref[@]}"; do + output+="$(echo -n "$i" | tr -d '\01\02\03' | tr '\n' '\02')$delim" + done + + if [[ $column == 0 ]]; then + local expr="s$ctrl^$(_sed_sanitize "$2")${delim}.*$ctrl$(_sed_sanitize_array "$output")$ctrl" + else + local expr="s$ctrl^$(repeat $column ".*$delim")$(_sed_sanitize "$2")$delim$(repeat $(( $(cat "${1}.cols") - column - 1 )) ".*$delim")"'$'"$ctrl$(_sed_sanitize_array "$output")$ctrl" + fi + + sed -E -i "$expr" "$1" +} + +# deletes entries from the `store` using `search`. +# by default uses the 0th column. override with optional `column` +# +# data_yeet(store, search, [column]) +data_yeet() { + [[ ! "$2" ]] && return 1 + [[ ! -f "$1" ]] && return 4 + local column=${3:-0} + local IFS=' ' + + if [[ $column == 0 ]]; then + local expr="/^$(_sed_sanitize "$2")${delim}.*/d" + else + local expr="/^$(repeat $column ".*$delim")$(_sed_sanitize "$2")$delim$(repeat $(( $(cat "${1}.cols") - column - 1 )) ".*$delim")"'$'"/d" + fi + + sed -E -i "$expr" "$1" +} + +_sed_sanitize() { + echo -n "$1" | tr -d '\01\02\03' | tr '\n' '\02' | xxd -p | tr -d '\n' | sed -E 's/../\\x&/g' +} + +_sed_sanitize_array() { + echo -n "$1" | xxd -p | tr -d '\n' | sed -E 's/../\\x&/g' +} + diff --git a/src/server.sh b/src/server.sh index cc0f17d..1cc25f2 100755 --- a/src/server.sh +++ b/src/server.sh @@ -12,6 +12,7 @@ source src/account.sh source src/mail.sh source src/route.sh source src/template.sh +source src/notORM.sh # to be split off HTTP.sh at some point :^) [[ -f "${cfg[namespace]}/config.sh" ]] && source "${cfg[namespace]}/config.sh" declare -A r # current request / response From bb8526a75228815471a1234e7414d40fef03b46f Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 26 Jul 2024 03:20:50 +0200 Subject: [PATCH 016/112] account: migrate to notORM for data storage --- src/account.sh | 65 +++++++++++++++++--------------------------------- 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/src/account.sh b/src/account.sh index 4fe8059..3b29021 100755 --- a/src/account.sh +++ b/src/account.sh @@ -3,17 +3,13 @@ # register(username, password) function register() { - local username=$(echo -ne $(sed -E "s/ /_/g;s/\:/\-/g;s/\%/\\x/g" <<< "$1")) + local username=$(url_decode "$1") - IFS=$'\n' - while read user; do - IFS=: a=($user) - if [[ "${a[0]}" == "$username" ]]; then - reason="This user already exists!" - return 1 - fi - done < secret/users.dat - unset IFS + data_get secret/users.dat "$username" + if [[ $? != 2 && $? != 4 ]]; then # entry not found / file not found + reason="This user already exists!" + return 1 + fi if [[ "${cfg[hash]}" == "argon2id" ]]; then local salt=$(dd if=/dev/urandom bs=256 count=1 | sha1sum | cut -c 1-16) @@ -27,21 +23,16 @@ function register() { set_cookie_permanent "sh_session" $token set_cookie_permanent "username" $username - - echo "$username:$hash:$salt:$token" >> secret/users.dat + + out=("$username" "$hash" "$salt" "$token") + data_add secret/users.dat out } # login(username, password) function login() { - local username=$(echo -ne $(sed -E 's/%/\\x/g' <<< "$1")) - IFS=$'\n' - while read a; do - IFS=: user=($a) - if [[ "${user[0]}" == "$username" ]]; then - break - fi - done < secret/users.dat - if [[ "${user[0]}" == '' ]]; then + local username=$(url_decode "$1") + + if ! data_get secret/users.dat "$username" 0 user; then reason="Bad credentials" return 1 fi @@ -70,12 +61,7 @@ function login_simple() { local password=$(sed -E 's/^(.*)\://' <<< "$data") local login=$(sed -E 's/\:(.*)$//' <<< "$data") - IFS=$'\n' - while read a; do - IFS=':' user=($a) - [[ ${user[0]} == "$login" ]] && break - done < secret/users.dat - unset IFS + data_get secret/users.dat "$login" 0 user if [[ "${cfg[hash]}" == "argon2id" ]]; then hash="$(echo -n "$password" | argon2 "${user[2]}" -id -e)" @@ -99,13 +85,10 @@ function logout() { # session_verify(session) function session_verify() { [[ "$1" == '' ]] && return 1 - IFS=$'\n' - while read user; do - IFS=: a=($user) - if [[ "${a[3]}" == "$1" ]]; then - return 0 - fi - done < secret/users.dat + + if data_get secret/users.dat "$1" 3; then + return 0 + fi return 1 } @@ -113,14 +96,10 @@ function session_verify() { function session_get_username() { [[ "$1" == "" ]] && return 1 - IFS=$'\n' - while read user; do - IFS=':' a=($user) - if [[ "${a[3]}" == "$1" ]]; then - echo "${a[0]}" - return 0 - fi - done < secret/users.dat + if data_get secret/users.dat "$1" 3 user; then + echo "${user[0]}" + return 0 + fi return 1 } @@ -128,5 +107,5 @@ function session_get_username() { # delete_account(username) function delete_account() { [[ "$1" == "" ]] && return 1 - sed -i "s/^$1:.*//;/^$/d" secret/users.dat + data_yeet secret/users.dat "$1" } From 2c1dfa20f171b76e5d06a17abe7acf07ea15972a Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 26 Jul 2024 03:24:14 +0200 Subject: [PATCH 017/112] meta: version bump, since i'm breaking compat anyways --- config/master_example.sh | 4 ++-- http.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/master_example.sh b/config/master_example.sh index 17ede6f..e080e14 100644 --- a/config/master_example.sh +++ b/config/master_example.sh @@ -21,9 +21,9 @@ cfg[ssl_cert]='' cfg[ssl_key]='' cfg[extension]='shs' -cfg[extra_headers]='server: HTTP.sh/0.95 (devel)' +cfg[extra_headers]='server: HTTP.sh/0.96 (devel)' -cfg[title]='HTTP.sh 0.95' +cfg[title]='HTTP.sh 0.96' cfg[php_enabled]=false # enable PHP script evalutaion (requires PHP) cfg[python_enabled]=false # enable Python script evalutaion (requires Python) diff --git a/http.sh b/http.sh index af82dfa..96d8817 100755 --- a/http.sh +++ b/http.sh @@ -27,9 +27,9 @@ cfg[ssl_cert]='' cfg[ssl_key]='' cfg[extension]='shs' -cfg[extra_headers]='server: HTTP.sh/0.95 (devel)' +cfg[extra_headers]='server: HTTP.sh/0.96 (devel)' -cfg[title]='HTTP.sh 0.95' +cfg[title]='HTTP.sh 0.96' cfg[php_enabled]=false # enable PHP script evalutaion (requires PHP) cfg[python_enabled]=false # enable Python script evalutaion (requires Python) From ee1a5401206a5cf812b91b2ee0ad9ab083a6a1b4 Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 26 Jul 2024 20:52:42 +0200 Subject: [PATCH 018/112] server: remove some of the parsing crimes --- src/server.sh | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/server.sh b/src/server.sh index 1cc25f2..c682011 100755 --- a/src/server.sh +++ b/src/server.sh @@ -33,6 +33,8 @@ while read -r param; do value='' data='' unset IFS + + # TODO: think about refactoring those ifs; maybe we *don't* need to have a header "allowlist" afterall... if [[ "$param_l" == $'\015' ]]; then break @@ -84,36 +86,23 @@ while read -r param; do elif [[ "$param_l" == "range: bytes="* ]]; then r[range]="$(sed 's/Range: bytes=//;s/\r//' <<< "$param")" - elif [[ "$param" == *"GET "* ]]; then - r[url]="$(echo -ne "$(url_decode "$(sed -E 's/GET //;s/HTTP\/[0-9]+\.[0-9]+//;s/ //g;s/\/*\r//g;s/\/\/*/\//g' <<< "$param")")")" - data="$(sed -E 's/\?/��Lun4_iS_CuTe�/;s/^(.*)��Lun4_iS_CuTe�//;s/\&/ /g' <<< "${r[url]}")" - if [[ "$data" != "${r[url]}" ]]; then - data="$(sed -E 's/\?/��Lun4_iS_CuTe�/;s/^(.*)��Lun4_iS_CuTe�//' <<< "${r[url]}")" - IFS='&' - for i in $data; do - name="${i/=*/}" - value="${i/*=/}" - get_data[$name]="$value" - done - fi - - elif [[ "$param" == *"POST "* ]]; then - r[url]="$(echo -ne "$(url_decode "$(sed -E 's/POST //;s/HTTP\/[0-9]+\.[0-9]+//;s/ //g;s/\/*\r//g;s/\/\/*/\//g' <<< "$param")")")" - r[post]=true - # below shamelessly copied from GET, should be moved to a function - data="$(sed -E 's/\?/��Lun4_iS_CuTe�/;s/^(.*)��Lun4_iS_CuTe�//;s/\&/ /g' <<< "${r[url]}")" - if [[ "$data" != "${r[url]}" ]]; then - data="$(sed -E 's/\?/��Lun4_iS_CuTe�/;s/^(.*)��Lun4_iS_CuTe�//' <<< "${r[url]}")" - IFS='&' - for i in $data; do - name="${i/=*/}" - value="${i/*=/}" - get_data[$name]="$value" - done - fi + elif [[ "$param" =~ ^(GET|POST|PATCH|PUT|DELETE|MEOW) ]]; then # TODO: OPTIONS, HEAD + r[method]="${param%% *}" + [[ "${r[meow],,}" != "get" ]] && r[post]=true + r[url]="$(sed -E 's/^[a-zA-Z]* //;s/HTTP\/[0-9]+\.[0-9]+//;s/ //g;s/\/*\r//g;s/\/\/*/\//g' <<< "$param")" + + IFS='&' + for i in ${r[url]#*\?}; do + name="$(url_decode "${i%=*}")" + value="$(url_decode "${i#*=}")" + get_data[$name]="$value" + done + unset IFS fi done +r[url]="$(url_decode "${r[url]}")" # doing this here for.. reasons + r[uri]="$(realpath "${cfg[namespace]}/${cfg[root]}$(sed -E 's/\?(.*)$//' <<< "${r[url]}")")" [[ -d "${r[uri]}/" ]] && pwd="${r[uri]}" || pwd=$(dirname "${r[uri]}") From b0f23c01e53a49a59de507e5a35cb91233fc6e0d Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 26 Jul 2024 20:52:52 +0200 Subject: [PATCH 019/112] notORM: fix typo --- src/notORM.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notORM.sh b/src/notORM.sh index cc534ad..b30ea0c 100755 --- a/src/notORM.sh +++ b/src/notORM.sh @@ -93,7 +93,7 @@ data_iter() { # # `value` is any string, which will directly replace `search` # -# data_replace(store, search, value, [column]) +# data_replace_value(store, search, value, [column]) data_replace_value() { [[ ! "$3" ]] && return 1 [[ ! -f "$1" ]] && return 4 From 7a6e6c2f3831a98d685b34de81aa2de048177dba Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 26 Jul 2024 20:56:23 +0200 Subject: [PATCH 020/112] server: fix "typo" --- src/server.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.sh b/src/server.sh index c682011..3553235 100755 --- a/src/server.sh +++ b/src/server.sh @@ -88,7 +88,7 @@ while read -r param; do elif [[ "$param" =~ ^(GET|POST|PATCH|PUT|DELETE|MEOW) ]]; then # TODO: OPTIONS, HEAD r[method]="${param%% *}" - [[ "${r[meow],,}" != "get" ]] && r[post]=true + [[ "${r[method],,}" != "get" ]] && r[post]=true r[url]="$(sed -E 's/^[a-zA-Z]* //;s/HTTP\/[0-9]+\.[0-9]+//;s/ //g;s/\/*\r//g;s/\/\/*/\//g' <<< "$param")" IFS='&' From 0dbd85f9ecc3fbecdcbf74f03a64655e68ef2b92 Mon Sep 17 00:00:00 2001 From: sdomi Date: Fri, 26 Jul 2024 23:11:49 +0200 Subject: [PATCH 021/112] server: better handle custom statuses, fix some string escapes --- src/account.sh | 4 ++-- src/mime.sh | 24 ++++++++++++++---------- src/response/200.sh | 8 ++++++-- src/server.sh | 4 ++-- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/account.sh b/src/account.sh index 3b29021..607917b 100755 --- a/src/account.sh +++ b/src/account.sh @@ -21,8 +21,8 @@ function register() { local hash=$(echo -n $2$salt | sha256sum | cut -c 1-64) fi - set_cookie_permanent "sh_session" $token - set_cookie_permanent "username" $username + set_cookie_permanent "sh_session" "$token" + set_cookie_permanent "username" "$username" out=("$username" "$hash" "$salt" "$token") data_add secret/users.dat out diff --git a/src/mime.sh b/src/mime.sh index 566706f..6cde775 100755 --- a/src/mime.sh +++ b/src/mime.sh @@ -14,16 +14,20 @@ function get_mime() { local file="$@" - local mime="$(file --mime-type -b "$file")" - if [[ $file == *".htm" || $file == *".html" ]]; then - mimetype="text/html" - elif [[ $file == *".shs" || $file == *".py" || $file == *".php" ]]; then - mimetype="" - elif [[ $file == *".css" ]]; then - mimetype="text/css" - elif [[ $mime == "text/"* && $mime != "text/xml" ]]; then - mimetype="text/plain" + if [[ -f "$file" ]]; then + local mime="$(file --mime-type -b "$file")" + if [[ $file == *".htm" || $file == *".html" ]]; then + mimetype="text/html" + elif [[ $file == *".shs" || $file == *".py" || $file == *".php" ]]; then + mimetype="" + elif [[ $file == *".css" ]]; then + mimetype="text/css" + elif [[ $mime == "text/"* && $mime != "text/xml" ]]; then + mimetype="text/plain" + else + mimetype="$mime" + fi else - mimetype="$mime" + mimetype="" fi } diff --git a/src/response/200.sh b/src/response/200.sh index f3dc8da..4b85f93 100755 --- a/src/response/200.sh +++ b/src/response/200.sh @@ -1,9 +1,13 @@ +# TODO: move parts of this into server.sh, or rename the file appropriately + function __headers() { if [[ "${cfg[unbuffered]}" != true ]]; then - if [[ "${r[headers]}" == *'Location'* ]]; then + if [[ "${r[headers]}" == *'Location'* ]]; then # override for redirects printf "HTTP/1.0 302 aaaaa\r\n" - else + elif [[ "${r[status]}" == '200' || "${r[status]}" == '212' ]]; then # normal or router, should just return 200 printf "HTTP/1.0 200 OK\r\n" + else # changed by the user in the meantime :) + printf "HTTP/1.0 ${r[status]} meow\r\n" fi [[ "${r[headers]}" != '' ]] && printf "${r[headers]}" printf "${cfg[extra_headers]}\r\n" diff --git a/src/server.sh b/src/server.sh index 3553235..f8d7ff4 100755 --- a/src/server.sh +++ b/src/server.sh @@ -187,7 +187,7 @@ if [[ "${r[post]}" == true ]] && [[ "${r[status]}" == 200 || "${r[status]}" == for i in $boundaries_list; do tmpout=$(mktemp -p $tmpdir) dd iflag=fullblock if=$tmpfile ibs=$(($i+$delimeter_len)) obs=1M skip=1 | while true; do - read line + read -r line if [[ $line == $'\015' ]]; then cat - > $tmpout break @@ -201,7 +201,7 @@ if [[ "${r[post]}" == true ]] && [[ "${r[status]}" == 200 || "${r[status]}" == done rm $tmpfile else - read -N "${r[content_length]}" data + read -r -N "${r[content_length]}" data IFS='&' for i in $(tr -d '\n' <<< "$data"); do From eb80d427116735447396def3bb5b2d25fdbbd4aa Mon Sep 17 00:00:00 2001 From: sdomi Date: Tue, 30 Jul 2024 16:48:30 +0200 Subject: [PATCH 022/112] server: fix slight parameter decode mangling --- src/server.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server.sh b/src/server.sh index f8d7ff4..c237dbf 100755 --- a/src/server.sh +++ b/src/server.sh @@ -86,14 +86,14 @@ while read -r param; do elif [[ "$param_l" == "range: bytes="* ]]; then r[range]="$(sed 's/Range: bytes=//;s/\r//' <<< "$param")" - elif [[ "$param" =~ ^(GET|POST|PATCH|PUT|DELETE|MEOW) ]]; then # TODO: OPTIONS, HEAD + elif [[ "$param_l" =~ ^(get|post|patch|put|delete|meow) ]]; then # TODO: OPTIONS, HEAD r[method]="${param%% *}" [[ "${r[method],,}" != "get" ]] && r[post]=true r[url]="$(sed -E 's/^[a-zA-Z]* //;s/HTTP\/[0-9]+\.[0-9]+//;s/ //g;s/\/*\r//g;s/\/\/*/\//g' <<< "$param")" IFS='&' for i in ${r[url]#*\?}; do - name="$(url_decode "${i%=*}")" + name="$(url_decode "${i%%=*}")" value="$(url_decode "${i#*=}")" get_data[$name]="$value" done From 640baa8e7b308c6d26aa045fc27435234b648b89 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Sun, 28 Jul 2024 17:40:34 +0100 Subject: [PATCH 023/112] docker: bump to alpine:3.20 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5badeb3..4aab686 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.14 +FROM alpine:3.20 RUN apk update \ && apk add sed xxd grep findutils file nmap-ncat socat jq bash file curl From cb2acacc3208df741ee2265f1576c03169c281a8 Mon Sep 17 00:00:00 2001 From: famfo Date: Wed, 31 Jul 2024 19:38:54 +0200 Subject: [PATCH 024/112] Set content type when running in buffered mode --- src/mime.sh | 17 +++++++++++++++++ src/response/200.sh | 10 ++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/mime.sh b/src/mime.sh index 6cde775..f9d05c6 100755 --- a/src/mime.sh +++ b/src/mime.sh @@ -31,3 +31,20 @@ function get_mime() { mimetype="" fi } + +# get_mime_contents(path) -> $mimetype +# Sets an empty mimetype if nothing known is matched +# Limitations: +# - CSS doesn't get matched (curse you `file`) +# TODO: add more types +function get_mime_contents() { + local ftype=$(file "$1") + mimetype="" + case "$ftype" in + *"HTML document"*) mimetype="text/html";; + *"SVG Scalable Vector Graphics image"*) mimetype="image/svg+xml";; + *"PNG image data"*) mimetype="image/png";; + *"JPEG image data"*) mimetype="image/jpeg";; + esac +} + diff --git a/src/response/200.sh b/src/response/200.sh index 4b85f93..fe28ae6 100755 --- a/src/response/200.sh +++ b/src/response/200.sh @@ -1,5 +1,7 @@ # TODO: move parts of this into server.sh, or rename the file appropriately +# __headers(end) +# Sets the header and terminates the header block if end is NOT set to false function __headers() { if [[ "${cfg[unbuffered]}" != true ]]; then if [[ "${r[headers]}" == *'Location'* ]]; then # override for redirects @@ -19,7 +21,8 @@ function __headers() { get_mime "${r[uri]}" [[ "$mimetype" != '' ]] && printf "content-type: $mimetype\r\n" fi - printf "\r\n" + + [[ "$1" != false ]] && printf "\r\n" } if [[ ${r[status]} == 212 ]]; then @@ -28,7 +31,10 @@ if [[ ${r[status]} == 212 ]]; then else temp=$(mktemp) source "${r[view]}" > $temp - __headers + __headers false + get_mime_contents "$temp" + [[ "$mimetype" != '' ]] && printf "content-type: $mimetype\r\n" + printf "\r\n" cat $temp rm $temp fi From 9adc827018f51a5c168f9d3bdf264f483da820db Mon Sep 17 00:00:00 2001 From: famfo Date: Wed, 31 Jul 2024 19:51:59 +0200 Subject: [PATCH 025/112] Set content type for 403 and 404 pages --- src/response/403.sh | 1 + src/response/404.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/src/response/403.sh b/src/response/403.sh index 44a2efb..3b12227 100755 --- a/src/response/403.sh +++ b/src/response/403.sh @@ -1,4 +1,5 @@ printf "HTTP/1.0 403 Forbidden +content-type: text/html ${cfg[extra_headers]}\r\n\r\n" source templates/head.sh echo "

403: You've been naughty

" diff --git a/src/response/404.sh b/src/response/404.sh index b60b500..2dedb34 100755 --- a/src/response/404.sh +++ b/src/response/404.sh @@ -1,4 +1,5 @@ printf "HTTP/1.0 404 Not Found +content-type: text/html ${cfg[extra_headers]}\r\n\r\n" source templates/head.sh echo "

404 Not Found

" From 317c827a1dfd6946a57d4e2ce50139c4fa8deef4 Mon Sep 17 00:00:00 2001 From: famfo Date: Wed, 31 Jul 2024 19:55:32 +0200 Subject: [PATCH 026/112] Set content type for listing --- src/response/listing.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/src/response/listing.sh b/src/response/listing.sh index ea7f6a2..b94ff02 100755 --- a/src/response/listing.sh +++ b/src/response/listing.sh @@ -1,4 +1,5 @@ printf "HTTP/1.0 200 OK +content-type: text/html ${cfg[extra_headers]}\r\n\r\n" source templates/head.sh From d93323597d719b261c55f5dad464d48e417d5cce Mon Sep 17 00:00:00 2001 From: famfo Date: Wed, 31 Jul 2024 20:17:47 +0200 Subject: [PATCH 027/112] Use file diretcly to set mime type --- src/mime.sh | 16 ---------------- src/response/200.sh | 5 +++-- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/mime.sh b/src/mime.sh index f9d05c6..035ba74 100755 --- a/src/mime.sh +++ b/src/mime.sh @@ -32,19 +32,3 @@ function get_mime() { fi } -# get_mime_contents(path) -> $mimetype -# Sets an empty mimetype if nothing known is matched -# Limitations: -# - CSS doesn't get matched (curse you `file`) -# TODO: add more types -function get_mime_contents() { - local ftype=$(file "$1") - mimetype="" - case "$ftype" in - *"HTML document"*) mimetype="text/html";; - *"SVG Scalable Vector Graphics image"*) mimetype="image/svg+xml";; - *"PNG image data"*) mimetype="image/png";; - *"JPEG image data"*) mimetype="image/jpeg";; - esac -} - diff --git a/src/response/200.sh b/src/response/200.sh index fe28ae6..2fdf61b 100755 --- a/src/response/200.sh +++ b/src/response/200.sh @@ -32,8 +32,9 @@ if [[ ${r[status]} == 212 ]]; then temp=$(mktemp) source "${r[view]}" > $temp __headers false - get_mime_contents "$temp" - [[ "$mimetype" != '' ]] && printf "content-type: $mimetype\r\n" + mimetype=$(file --mime-type -b "$temp") + # Defaults to text/plain for things it doesn't know, eg. CSS + [[ "$mimetype" != 'text/plain' ]] && printf "content-type: $mimetype\r\n" printf "\r\n" cat $temp rm $temp From 343da427a1e38f35d1d3fc1075304bee1c451fdb Mon Sep 17 00:00:00 2001 From: famfo Date: Wed, 31 Jul 2024 20:32:07 +0200 Subject: [PATCH 028/112] Modify get_mime to allow text/html --- src/mime.sh | 2 +- src/response/200.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mime.sh b/src/mime.sh index 035ba74..df3377a 100755 --- a/src/mime.sh +++ b/src/mime.sh @@ -16,7 +16,7 @@ function get_mime() { local file="$@" if [[ -f "$file" ]]; then local mime="$(file --mime-type -b "$file")" - if [[ $file == *".htm" || $file == *".html" ]]; then + if [[ $file == *".htm" || $file == *".html" || $mime == "text/html" ]]; then mimetype="text/html" elif [[ $file == *".shs" || $file == *".py" || $file == *".php" ]]; then mimetype="" diff --git a/src/response/200.sh b/src/response/200.sh index 2fdf61b..b6ecd99 100755 --- a/src/response/200.sh +++ b/src/response/200.sh @@ -32,7 +32,7 @@ if [[ ${r[status]} == 212 ]]; then temp=$(mktemp) source "${r[view]}" > $temp __headers false - mimetype=$(file --mime-type -b "$temp") + get_mime "$temp" # Defaults to text/plain for things it doesn't know, eg. CSS [[ "$mimetype" != 'text/plain' ]] && printf "content-type: $mimetype\r\n" printf "\r\n" From 3e50cc873789a223332b9691b8e71f84b1c53db1 Mon Sep 17 00:00:00 2001 From: sdomi Date: Sat, 3 Aug 2024 20:13:27 +0200 Subject: [PATCH 029/112] account: optional extra fields --- src/account.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/account.sh b/src/account.sh index 607917b..c1da649 100755 --- a/src/account.sh +++ b/src/account.sh @@ -1,9 +1,14 @@ #!/usr/bin/env bash # account.sh - account and session mgmt -# register(username, password) +# registers a new user. +# first two params are strings; third is a reference to an array with +# optional extra data (email, OTP...) +# +# register(username, password, [extra]) function register() { local username=$(url_decode "$1") + unset IFS data_get secret/users.dat "$username" if [[ $? != 2 && $? != 4 ]]; then # entry not found / file not found @@ -24,13 +29,14 @@ function register() { set_cookie_permanent "sh_session" "$token" set_cookie_permanent "username" "$username" - out=("$username" "$hash" "$salt" "$token") + out=("$username" "$hash" "$salt" "$token" "${extra[@]}") data_add secret/users.dat out } -# login(username, password) +# login(username, password) -> [res] function login() { local username=$(url_decode "$1") + unset IFS if ! data_get secret/users.dat "$username" 0 user; then reason="Bad credentials" @@ -46,6 +52,9 @@ function login() { if [[ "$hash" == "${user[1]}" ]]; then set_cookie_permanent "sh_session" "${user[3]}" set_cookie_permanent "username" "$username" + + declare -ga res=("${user[@]:4}") + return 0 else remove_cookie "sh_session" @@ -82,11 +91,13 @@ function logout() { remove_cookie "username" } -# session_verify(session) +# session_verify(session) -> [res] function session_verify() { [[ "$1" == '' ]] && return 1 + unset IFS if data_get secret/users.dat "$1" 3; then + declare -ga res=("${user[@]:4}") return 0 fi return 1 @@ -95,6 +106,7 @@ function session_verify() { # session_get_username(session) function session_get_username() { [[ "$1" == "" ]] && return 1 + unset IFS if data_get secret/users.dat "$1" 3 user; then echo "${user[0]}" From 5ef931ca9d6d983ebceb2d46137af8ce80615fcf Mon Sep 17 00:00:00 2001 From: sdomi Date: Sun, 4 Aug 2024 00:19:47 +0200 Subject: [PATCH 030/112] account: finally, proper sessions --- src/account.sh | 51 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/src/account.sh b/src/account.sh index c1da649..f5af493 100755 --- a/src/account.sh +++ b/src/account.sh @@ -17,20 +17,22 @@ function register() { fi if [[ "${cfg[hash]}" == "argon2id" ]]; then - local salt=$(dd if=/dev/urandom bs=256 count=1 | sha1sum | cut -c 1-16) - local token=$(dd if=/dev/urandom bs=32 count=1 | sha1sum | cut -c 1-40) + local salt=$(dd if=/dev/urandom bs=16 count=1 status=none | xxd -p) + local token=$(dd if=/dev/urandom bs=20 count=1 status=none | xxd -p) # TODO: fully remove local hash="$(echo -n "$2" | argon2 "$salt" -id -e)" else - local salt=$(dd if=/dev/urandom bs=256 count=1 | sha1sum | cut -c 1-16) - local token=$(dd if=/dev/urandom bs=32 count=1 | sha1sum | cut -c 1-40) + local salt=$(dd if=/dev/urandom bs=16 count=1 status=none | xxd -p) + local token=$(dd if=/dev/urandom bs=20 count=1 status=none | xxd -p) local hash=$(echo -n $2$salt | sha256sum | cut -c 1-64) fi - - set_cookie_permanent "sh_session" "$token" - set_cookie_permanent "username" "$username" - out=("$username" "$hash" "$salt" "$token" "${extra[@]}") + local out=("$username" "$hash" "$salt" "$token" "${extra[@]}") data_add secret/users.dat out + + _new_session "$username" + + set_cookie_permanent "sh_session" "${session[2]}" + set_cookie_permanent "username" "$username" } # login(username, password) -> [res] @@ -50,7 +52,9 @@ function login() { fi if [[ "$hash" == "${user[1]}" ]]; then - set_cookie_permanent "sh_session" "${user[3]}" + _new_session "$username" + + set_cookie_permanent "sh_session" "${session[2]}" set_cookie_permanent "username" "$username" declare -ga res=("${user[@]:4}") @@ -87,6 +91,10 @@ function login_simple() { # logout() function logout() { + log "${cookies[sh_session]}" + if [[ "${cookies[sh_session]}" ]]; then + data_yeet secret/sessions.dat "${cookies[sh_session]}" 2 + fi remove_cookie "sh_session" remove_cookie "username" } @@ -95,10 +103,13 @@ function logout() { function session_verify() { [[ "$1" == '' ]] && return 1 unset IFS + local user - if data_get secret/users.dat "$1" 3; then - declare -ga res=("${user[@]:4}") - return 0 + if data_get secret/sessions.dat "$1" 2 session; then + if data_get secret/users.dat "${session[0]}" 0 user; then # double-check if tables agree + declare -ga res=("${user[@]:4}") + return 0 + fi fi return 1 } @@ -107,10 +118,13 @@ function session_verify() { function session_get_username() { [[ "$1" == "" ]] && return 1 unset IFS + local session - if data_get secret/users.dat "$1" 3 user; then - echo "${user[0]}" - return 0 + if data_get secret/sessions.dat "$1" 2 session; then + if data_get secret/users.dat "${session[0]}" 0 user; then # double-check if tables agree + echo "${user[0]}" + return 0 + fi fi return 1 } @@ -121,3 +135,10 @@ function delete_account() { [[ "$1" == "" ]] && return 1 data_yeet secret/users.dat "$1" } + +# _new_session(username) -> $session +_new_session() { + [[ ! "$1" ]] && return 1 + session=("$1" "$(date '+%s')" "$(dd if=/dev/urandom bs=24 count=1 status=none | xxd -p)") + data_add secret/sessions.dat session +} From f16005fa0b8959ecabbfab9483a3b34780a91c95 Mon Sep 17 00:00:00 2001 From: sdomi Date: Sun, 4 Aug 2024 00:20:25 +0200 Subject: [PATCH 031/112] notORM: fix the repeat function --- src/notORM.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notORM.sh b/src/notORM.sh index b30ea0c..7cf7b8b 100755 --- a/src/notORM.sh +++ b/src/notORM.sh @@ -25,8 +25,8 @@ newline=$'\02' ctrl=$'\03' repeat() { - local IFS=' ' - printf -- "$2%.0s" $(seq 1 $1) + local IFS=$'\n' + [[ "$1" -gt 0 ]] && printf -- "$2%.0s" $(seq 1 $1) } # adds a flat `array` to the `store`. From 93d02b4295f810ef5ffc28f0ea4d09a722060a93 Mon Sep 17 00:00:00 2001 From: sdomi Date: Sun, 4 Aug 2024 01:33:10 +0200 Subject: [PATCH 032/112] account: more middleware functions --- src/account.sh | 116 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 21 deletions(-) diff --git a/src/account.sh b/src/account.sh index f5af493..c79f4eb 100755 --- a/src/account.sh +++ b/src/account.sh @@ -15,16 +15,11 @@ function register() { reason="This user already exists!" return 1 fi - - if [[ "${cfg[hash]}" == "argon2id" ]]; then - local salt=$(dd if=/dev/urandom bs=16 count=1 status=none | xxd -p) - local token=$(dd if=/dev/urandom bs=20 count=1 status=none | xxd -p) # TODO: fully remove - local hash="$(echo -n "$2" | argon2 "$salt" -id -e)" - else - local salt=$(dd if=/dev/urandom bs=16 count=1 status=none | xxd -p) - local token=$(dd if=/dev/urandom bs=20 count=1 status=none | xxd -p) - local hash=$(echo -n $2$salt | sha256sum | cut -c 1-64) - fi + + local salt=$(dd if=/dev/urandom bs=16 count=1 status=none | xxd -p) + user_gen_reset_token + + _password_hash "$2" "$salt" local out=("$username" "$hash" "$salt" "$token" "${extra[@]}") data_add secret/users.dat out @@ -33,6 +28,8 @@ function register() { set_cookie_permanent "sh_session" "${session[2]}" set_cookie_permanent "username" "$username" + + unset hash } # login(username, password) -> [res] @@ -45,11 +42,7 @@ function login() { return 1 fi - if [[ "${cfg[hash]}" == "argon2id" ]]; then - hash="$(echo -n "$2" | argon2 "${user[2]}" -id -e)" - else - hash="$(echo -n $2${user[2]} | sha256sum | cut -c 1-64 )" - fi + _password_hash "$2" "${user[2]}" if [[ "$hash" == "${user[1]}" ]]; then _new_session "$username" @@ -58,12 +51,15 @@ function login() { set_cookie_permanent "username" "$username" declare -ga res=("${user[@]:4}") - + + unset hash return 0 else remove_cookie "sh_session" remove_cookie "username" reason="Bad credentials" + + unset hash return 1 fi } @@ -76,17 +72,15 @@ function login_simple() { data_get secret/users.dat "$login" 0 user - if [[ "${cfg[hash]}" == "argon2id" ]]; then - hash="$(echo -n "$password" | argon2 "${user[2]}" -id -e)" - else - hash="$(echo -n $password${user[2]} | sha256sum | cut -c 1-64 )" - fi + _password_hash "$password" "${user[2]}" if [[ "$hash" == "${user[1]}" ]]; then r[authorized]=true else r[authorized]=false fi + + unset hash } # logout() @@ -136,9 +130,89 @@ function delete_account() { data_yeet secret/users.dat "$1" } +# user_reset_password(username, token, new_password) +user_reset_password() { + [[ ! "$1" ]] && return 1 # sensitive function, so we're checking all three + [[ ! "$2" ]] && return 1 # there's probably a better way, + [[ ! "$3" ]] && return 1 # but i don't care. + if data_get secret/users.dat "$1" 0 user; then + + if [[ "$2" == "${user[3]}" ]]; then + _password_hash "$3" "${user[2]}" + user[1]="$hash" + user[3]='' + + data_replace secret/users.dat "$1" user + + session_purge "$1" + + unset hash token + return 0 + fi + fi + return 1 +} + +# user_change_password(username, old_password, new_password) +user_change_password() { + [[ ! "$1" ]] && return 1 + [[ ! "$2" ]] && return 1 + [[ ! "$3" ]] && return 1 + if data_get secret/users.dat "$1" 0 user; then + + _password_hash "$2" "${user[2]}" + + if [[ "$hash" == "${user[1]}" ]]; then + _password_hash "$3" "${user[2]}" + [[ ! "$hash" ]] && return + user[1]="$hash" + user[3]='' + data_replace secret/users.dat "$1" user + + session_purge "$1" + + unset hash token + return 0 + fi + fi + + unset hash + return 1 +} + +# user_gen_reset_token(username) -> $token +user_gen_reset_token() { + [[ ! "$1" ]] && return 1 + if data_get secret/users.dat "$1" 0 user; then + user[3]="$(dd if=/dev/urandom bs=20 count=1 status=none | xxd -p)" + data_replace secret/users.dat "$1" user + token="${user[3]}" + else + return 1 + fi +} + +# logs out ALL sessions for user +# +# session_purge(username) +session_purge() { + data_yeet secret/sessions.dat "$1" +} + # _new_session(username) -> $session _new_session() { [[ ! "$1" ]] && return 1 session=("$1" "$(date '+%s')" "$(dd if=/dev/urandom bs=24 count=1 status=none | xxd -p)") data_add secret/sessions.dat session } + +_password_hash() { + [[ ! "$1" ]] && return 1 + [[ ! "$2" ]] && return 1 + + if [[ "${cfg[hash]}" == "argon2id" ]]; then + hash="$(echo -n "$1" | argon2 "$2" -id -e)" + else + hash=$(echo -n $1$2 | sha256sum | cut -c 1-64) + fi +} From 5b8d4928987033a96a0499e0918737b8927b5f41 Mon Sep 17 00:00:00 2001 From: sdomi Date: Sun, 4 Aug 2024 01:57:12 +0200 Subject: [PATCH 033/112] account: remember me flag --- src/account.sh | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/account.sh b/src/account.sh index c79f4eb..a49aecc 100755 --- a/src/account.sh +++ b/src/account.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash # account.sh - account and session mgmt +# TODO: add stricter argument checks for all the funcs # registers a new user. # first two params are strings; third is a reference to an array with # optional extra data (email, OTP...) # -# register(username, password, [extra]) +# [extra=()] register(username, password) function register() { local username=$(url_decode "$1") unset IFS @@ -17,11 +18,10 @@ function register() { fi local salt=$(dd if=/dev/urandom bs=16 count=1 status=none | xxd -p) - user_gen_reset_token _password_hash "$2" "$salt" - local out=("$username" "$hash" "$salt" "$token" "${extra[@]}") + local out=("$username" "$hash" "$salt" "" "${extra[@]}") data_add secret/users.dat out _new_session "$username" @@ -32,9 +32,10 @@ function register() { unset hash } -# login(username, password) -> [res] +# login(username, password, [forever]) -> [res] function login() { local username=$(url_decode "$1") + [[ "$3" ]] && local forever=true unset IFS if ! data_get secret/users.dat "$username" 0 user; then @@ -45,14 +46,19 @@ function login() { _password_hash "$2" "${user[2]}" if [[ "$hash" == "${user[1]}" ]]; then - _new_session "$username" - - set_cookie_permanent "sh_session" "${session[2]}" - set_cookie_permanent "username" "$username" + _new_session "$username" "$forever" + + if [[ "$forever" == true ]]; then + set_cookie_permanent "sh_session" "${session[2]}" + set_cookie_permanent "username" "$username" + else + set_cookie "sh_session" "${session[2]}" + set_cookie "username" "$username" + fi declare -ga res=("${user[@]:4}") - unset hash + unset hash return 0 else remove_cookie "sh_session" @@ -199,10 +205,11 @@ session_purge() { data_yeet secret/sessions.dat "$1" } -# _new_session(username) -> $session +# _new_session(username, forever) -> $session _new_session() { [[ ! "$1" ]] && return 1 - session=("$1" "$(date '+%s')" "$(dd if=/dev/urandom bs=24 count=1 status=none | xxd -p)") + [[ "$2" == true ]] && local forever=true || local forever=false + session=("$1" "$(date '+%s')" "$(dd if=/dev/urandom bs=24 count=1 status=none | xxd -p)" "$forever") data_add secret/sessions.dat session } From 332c256d6c1554ac86a0735ff592018ba6210264 Mon Sep 17 00:00:00 2001 From: sdomi Date: Sun, 4 Aug 2024 21:37:51 +0200 Subject: [PATCH 034/112] tests: crude testing framework --- tests/00-prepare.sh | 17 +++++++ tests/01-http-basic.sh | 66 ++++++++++++++++++++++++ tst.sh | 111 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 tests/00-prepare.sh create mode 100644 tests/01-http-basic.sh create mode 100755 tst.sh diff --git a/tests/00-prepare.sh b/tests/00-prepare.sh new file mode 100644 index 0000000..fcffa7b --- /dev/null +++ b/tests/00-prepare.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +prepare() { + [[ ! -d app ]] && ./http.sh init + ./http.sh >/dev/null & +} + +tst() { + for i in {1..10}; do + if [[ "$(ss -tulnap | grep LISTEN | grep 1337)" ]]; then + return 0 + fi + sleep 0.5 + done + + return 255 +} diff --git a/tests/01-http-basic.sh b/tests/01-http-basic.sh new file mode 100644 index 0000000..68a73c0 --- /dev/null +++ b/tests/01-http-basic.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +test_server_header() { + tst() { + curl -s -I localhost:1337 + } + + match_sub="HTTP.sh" +} + +test_server_output() { + prepare() { + cat <<"EOF" > app/webroot/meow.shs +#!/bin/bash +echo meow +EOF + } + + cleanup() { + rm app/webroot/meow.shs + } + + tst() { + curl -s localhost:1337/meow.shs + } + + match="meow" +} + +test_server_get_param() { + prepare() { + cat <<"EOF" > app/webroot/meow.shs +#!/bin/bash +echo "${get_data[meow]}" +EOF + } + + tst() { + curl -s "localhost:1337/meow.shs?meow=nyaa" + } + + match="nyaa" +} + +test_server_post_param() { + prepare() { + cat <<"EOF" > app/webroot/meow.shs +#!/bin/bash +echo "${post_data[meow]}" +EOF + } + + tst() { + curl -s "localhost:1337/meow.shs" -d 'meow=nyaa' + } + + match="nyaa" +} + + +subtest_list=( + test_server_header + test_server_output + test_server_get_param + test_server_post_param +) diff --git a/tst.sh b/tst.sh new file mode 100755 index 0000000..50e7267 --- /dev/null +++ b/tst.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +tst() { + echo "dummy test!" +} + +match="" +match_begin="" +match_end="" +match_sub="" + +prepare() { + : +} + +cleanup () { + : +} + +on_error() { + on_error_default +} + +on_success() { + on_success_default +} + +on_success_default() { + echo "OK: $test_name" + (( ok_count++ )) + return 0 # surprisingly load-bearing +} + +on_error_default() { + echo "FAIL: $test_name" + echo "(res: $res)" + (( fail_count++ )) + return 0 +} + +on_fatal() { + echo "FATAL: $test_name" + _final_cleanup + exit 1 +} + +if [[ ! -f "$1" ]]; then + echo -e "$0 - basic test framework\n\nusage: $0 [test] [...]" + exit 1 +fi + +ok_count=0 +fail_count=0 + +_a() { + [[ "$res_code" == 255 ]] && on_fatal + if [[ "$match" ]]; then + [[ "$res" == "$match" ]] && on_success || on_error + elif [[ "$match_sub" ]]; then + [[ "$res" == *"$match_sub"* ]] && on_success || on_error + elif [[ "$match_begin" ]]; then + [[ "$res" == "$match_begin"* ]] && on_success || on_error + elif [[ "$match_end" ]]; then + [[ "$res" == *"$match_end" ]] && on_success || on_error + else + [[ "$res_code" == 0 ]] && on_success || on_error + fi + unset match match_sub match_begin match_end + prepare() { :; } +} + +_final_cleanup() { + # handle spawned processes + for i in $(jobs -p); do + pkill -P $i + done + sleep 2 + for i in $(jobs -p); do + pkill -9 -P $i + done + pkill -P $$ +} + +for j in "$@"; do + source "$j" + if [[ "${#subtest_list[@]}" == 0 ]]; then + test_name="$j" + prepare + res="$(tst)" + res_code=$? + cleanup + _a + else + echo "--- $j ---" + for i in "${subtest_list[@]}"; do + test_name="$i" + "$i" + prepare + res="$(tst)" + res_code=$? + cleanup + _a + done + fi +done + +_final_cleanup + +echo -e "\n\nTesting done! +OK: $ok_count +FAIL: $fail_count" From 1c48d95d414a6f85785ff396dc3553a58a0a9193 Mon Sep 17 00:00:00 2001 From: sdomi Date: Mon, 5 Aug 2024 18:13:03 +0200 Subject: [PATCH 035/112] docs: wrote some info about tst.sh --- docs/tests.md | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/tests.md diff --git a/docs/tests.md b/docs/tests.md new file mode 100644 index 0000000..6b62aca --- /dev/null +++ b/docs/tests.md @@ -0,0 +1,147 @@ +# the test framework + +We have a small test harness! It lives in `./tst.sh` in the root of the HTTP.sh repo. It's inspired +by some init systems, and a bit influenced by how APKBUILD/PKGBUILDs are structured. A very basic +test is attached below: + +``` +tst() { + return 0 +} +``` + +A `tst()` function is all you need in a test. Running the test can be done like so: + +``` +$ ./tst.sh tests/example.sh +OK: tests/example.sh + + +Testing done! +OK: 1 +FAIL: 0 +``` + +If running multiple tests is desired, I recommend calling `./tst.sh tests/*`, and prepending the +filenames with numbers to make sure they run in the correct sequence. + +You can also contain multiple tests in a file by grouping them into a function, and then adding the +function names to an array: + +``` +a() { + tst() { + return 0 + } +} +b() { + tst() { + return 1 + } +} + +subtest_list=( + a + b +) +``` + +This will yield the following result *(output subject to change)*: + +``` +--- tests/example.sh --- +OK: a +FAIL: b +(res: ) + + +Testing done! +OK: 1 +FAIL: 1 +``` + +Of note: `tst.sh` is designed in a way where *most* functions will fall through; If you'd like to +run the same test against a different set of checks (see below) then you *don't* need to redefine +the `tst()` function, just changing the checks is enough. + +--- + +## return codes + +The following return codes are defined: + +- 0 as success +- 1 as error (test execution continues) +- 255 as fatal error (cleans up and exits immediately) + +## determining success / failure + +Besides very simple return-code based matching, `tst.sh` also supports stdout matching with the +following strings: + +- `match` (matches the whole string) +- `match_sub` (matches a substring) +- `match_begin` (matches the beginning) +- `match_end` (matches the end) + +If any of those is defined, all except fatal return codes are ignored. If more than one of those +is defined, it checks the list above top-to-bottom and picks the first one that is set, ignoring +all others. + +## special functions + +The framework defines two special functions, plus a few callbacks that can be overriden: + +### prepare + +`prepare` runs **once** after definition, right before the test itself. As of now, it's the only +function that gets cleaned up after each run (by design); With this, one can ensure a consistent +state for a group of tests. + +By default (undefined state), `prepare` does nothing. + +``` +prepare() { + echo 'echo meow' > app/webroot/test.shs +} + +tst() { + curl localhost:1337/test.shs +} + +match="meow" +``` + +*(note: this test requires tst.sh to be used with http.sh, and for http.sh to be running)* + +### cleanup + +`cleanup` runs after every test. The name should be self-explanatory. Define as `cleanup() { :; }` +to disable behavior from previous tests. + +By default (undefined state), `cleanup` does nothing. + +``` +prepare() { + echo 'echo meow' > app/webroot/test.shs +} + +tst() { + curl localhost:1337/test.shs +} + +cleanup() { + rm app/webroot/test.shs +} + +match="meow" +``` + +*(note: same thing as above)* + +### on_success, on_error, on_fatal + +Called on every success, failure and fatal error. First two call `on_{success,error}_default`, +which increments the counter and outputs the OK/FAIL message. The third one just logs the FATAL, +cleans up and exits. Overloading `on_fatal` is not recommended; While overloading the other two, +make sure to add a call to the `_default` function, or handle the numbers gracefully by yourself. From 46530b9f1730468b3b919d4953f33fd03bbf34a6 Mon Sep 17 00:00:00 2001 From: sdomi Date: Mon, 5 Aug 2024 19:17:45 +0200 Subject: [PATCH 036/112] docs: quick start --- docs/directory-structure.md | 49 +++++++++++++++++ docs/quick-start.md | 101 ++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 docs/directory-structure.md create mode 100644 docs/quick-start.md diff --git a/docs/directory-structure.md b/docs/directory-structure.md new file mode 100644 index 0000000..ba108f0 --- /dev/null +++ b/docs/directory-structure.md @@ -0,0 +1,49 @@ +# File / Directory structure + +(alphabetical order; state for 2024-08-05) + +- `config` contains per-vhost configuration settings. `config/master.sh` gets loaded by default, + `config/[:port]` gets loaded based on the `Host` header. +- `docs` is what you're reading now. Hi!! +- `secret` is where user data gets stored. think: user accounts and sessions. +- `src` contains the majority of HTTP.sh's code. + - `response/*` are files executed based on computed return code. `response/200.sh` is a bit + special, because it handles the general "success" path. Refactor pending. + - `account.sh` is the middleware for user account management + - `dependencies.*` store the list of required and optional deps, newline delimetered + - `mail.sh` has some crude SMTP code, for sending out mails + - `mime.sh` contains a glue function for handling special cases where `file` command doesn't + return the proper mimetype + - `misc.sh` consists of functions that didn't really fit anywhere else. Of note, `html_encode`, + `url_encode`, `url_decode`, `header` and various cookie functions all live there for now. + - `notORM.sh` is, as I said, not an [ORM](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) + - `route.sh` defines a small function for handling adding the routes + - `server.sh` is where most of the demons live + - `template.sh` is where the rest of the demons live + - `worker.sh` is the literal embodiment of "we have cron at home"; workers are just background + jobs that run every n minutes; you can also start and stop them on will! *fancy* + - `ws.sh` is an incomplete WebSocket implementation +- `storage` is like `secret`, but you can generally use it for whatever +- `templates` will be moved/removed soon (`head.sh` has *nothing* to do with the current templating + system; it has some handlers for remaking things you put into `meta[]` array into HTML `` + fields. Should not be used, at least not in its current form.) +- `tests` is where all the tests live! + +The actually important files are: +- `http.sh` - run this and see what happens +- `tst.sh` - [the test suite](tests.md) + +## suggested skeleton structure in `app/` + +FYI: this is merely a suggestion. `./http.sh init` will create some of those directories for you, +but it's fine to move things around. A lot of it can be changed within `config/master.sh`, even the +directory name itself! + +- `src` for various backend code +- `templates` for HTML in our special templating language +- `views` for individual pages / endpoints +- `webroot` for static files, or .shs scripts that don't use the router +- `config.sh` has some general, always-included stuff +- `routes.sh` configures the router; entries should point into `views/` +- `localcfg.sh` may be sourced from `config.sh` and contain only local config (useful for developing + stuff with others through git, for instance; `localcfg.sh` should then be added to `.gitignore`) diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..ad91d2d --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,101 @@ +# HTTP.sh: quick start + +Welcome to the exciting world of Bash witchery! I'll be your guide on this webdev adventure today. + +## about HTTP.sh + +HTTP.sh is a very extensive web framework. I use it for quick and dirty hacks, and I "designed" it +in a way where you don't need to write a lot of code to do some basic stuff. I'm also gradually +adding middleware that helps you do more advanced stuff. With some regards, there are already +multiple ways one could implement a web app in HTTP.sh; Thus, I feel like I need this to be heard: + +**There are no bad ways to write code here.** You can still write *bad code*, but this is a safe +space where nobody shall tell you "Y is garbage, you should use X instead!"; + +This strongly applies to specific features of the framework: You can use the templating engine, or +you can just `echo` a bunch of stuff directly from your script. You can use the URL router, or you +could just name your scripts under the webroot in a fancy way. **As long as it works, it's good :3** + +## Getting started + +First, clone the repository. I'm sure you know how to do that; Afterwards, try running: + +``` +./http.sh init +./http.sh +``` + +`init` will lay out some directories, and running it w/o any params will just start the server. +If you're missing any dependencies, you should now see a list of them. + +By default, http.sh starts on port 1337; Try going to http://localhost:1337/ - if you see a welcome +page, it's working!! + +We have a "debug mode" under `./http.sh debug`. Check [running.md](running.md) for more options. + +## Basic scripting + +By default, your application lives in `app/`. See [directory-structure.md](directory-structure.md) +for more info on what goes where. For now, go into `app/webroot/` and remove `index.shs`. That +should bring you to an empty directory listing; Static files can be put as-is into `app/webroot/` +and they'll be visible within the directory! + +To create a script, make a new file with `.shs` extension, and start writing a script like normal. +All of your `stdout` (aka: everything you `echo`) goes directly to the output. Everything sent to +`stderr` will be shown in the `./http.sh debug` output. + +## Parameters + +There are a few ways of receiving input; The most basic ones are `get_data` and `post_data`, which +are associative arrays that handle GET params and POST (body) params, respectively. Consider the +following example: + +``` +#!/bin/bash +echo '' + +if [[ ! "${get_data[example]}" ]]; then + echo '
+ + +
' +else + echo "

you sent: $(html_encode "${get_data[example]}")

" +fi + +echo '' +``` + +When opened in a browser, this example looks like so: + +![screenshot of a simple web page. there's a text box, and a button saying Submit Query](https://f.sakamoto.pl/IwIalnWw.png) + +... and after submitting data, it looks like that: + +![screenshot of another page. it says "you sent: meow!"](https://f.sakamoto.pl/IwIy0thg.png) + +## Security + +Remember to use sufficient quotes in your scripts, and escape untrusted data (read: ALL data you +didn't write/create yourself. This is especially important when parameter splitting may occur; +For instance, consider: + +``` +rm storage/${get_data[file]} +``` + +vs + +``` +rm -- "storage/$(basename "${get_data[file]}")" +``` + +The first one can fail due to: +- spaces (if `?file=a+b+c+d`, then it will remove `storage/a`, `b`, `c` and `d`). Hence, you get + arbitrary file deletion. +- unescaped filename (param containing `../` leads to path traversal) +- unterminated parameter expansion (`--` in `rm --` terminates switches; after this point, only + file names can occur) + +Furthermore, if you're displaying user-controlled data in your app, remember to use `html_encode` +to prevent cross-site scripting attacks. From 358a8737ab69a326c8c90f22d74061221ba4d3be Mon Sep 17 00:00:00 2001 From: sdomi Date: Tue, 6 Aug 2024 22:50:57 +0200 Subject: [PATCH 037/112] tests: basic template tests --- tests/02-template.sh | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/02-template.sh diff --git a/tests/02-template.sh b/tests/02-template.sh new file mode 100644 index 0000000..55be91f --- /dev/null +++ b/tests/02-template.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +tpl_basic() { + prepare() { + source src/misc.sh + source src/template.sh + } + tst() { + declare -A meow + meow[asdf]="$value" + + render meow <(echo "value: {{.asdf}}") + } + + value="A quick brown fox jumped over the lazy dog" + match="value: $value" +} + +tpl_basic_specialchars() { + value="&#$%^&*() <-- look at me go" + match="value: $(html_encode "$value")" +} + +tpl_basic_newline() { + value=$'\n'a$'\n' + match="value: $(html_encode "$value")" +} + +subtest_list=( + tpl_basic + tpl_basic_specialchars + tpl_basic_newline +) From d6f46b949de796fb6af35fcc386834299903a1a0 Mon Sep 17 00:00:00 2001 From: sdomi Date: Tue, 6 Aug 2024 23:42:44 +0200 Subject: [PATCH 038/112] tests: add match_not; add html/url encode tests --- docs/tests.md | 30 ++++++++++++-- tests/03-misc.sh | 100 +++++++++++++++++++++++++++++++++++++++++++++++ tst.sh | 59 ++++++++++++++++++++++------ 3 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 tests/03-misc.sh diff --git a/docs/tests.md b/docs/tests.md index 6b62aca..60ec958 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -77,14 +77,15 @@ The following return codes are defined: ## determining success / failure Besides very simple return-code based matching, `tst.sh` also supports stdout matching with the -following strings: +following variables: - `match` (matches the whole string) - `match_sub` (matches a substring) - `match_begin` (matches the beginning) - `match_end` (matches the end) +- `match_not` (inverse substring match) -If any of those is defined, all except fatal return codes are ignored. If more than one of those +If any of those are defined, all except fatal return codes are ignored. If more than one of those is defined, it checks the list above top-to-bottom and picks the first one that is set, ignoring all others. @@ -95,8 +96,7 @@ The framework defines two special functions, plus a few callbacks that can be ov ### prepare `prepare` runs **once** after definition, right before the test itself. As of now, it's the only -function that gets cleaned up after each run (by design); With this, one can ensure a consistent -state for a group of tests. +function that gets cleaned up after each run (by design; see section `statefullness` below) By default (undefined state), `prepare` does nothing. @@ -145,3 +145,25 @@ Called on every success, failure and fatal error. First two call `on_{success,er which increments the counter and outputs the OK/FAIL message. The third one just logs the FATAL, cleans up and exits. Overloading `on_fatal` is not recommended; While overloading the other two, make sure to add a call to the `_default` function, or handle the numbers gracefully by yourself. + +## statefullness + +This framework is designed in a way where a lot of the state is inherited from previous tests. This +is by-design, to make sure that there's less repetition in the tests themselves. It is up to the +author of the tests to remember about cleaning up variables and other state that could affect any +further tests in the chain. + +Currently, state is cleaned up under the following circumstances: +- all `match` variables get cleaned up after every test +- `prepare()` function is reset after every test (so, each definition of `prepare` will run + exactly *once*) +- upon switching files, `tst()` and `cleanup()` get reset to initial values. Of note, those two + **do** get inherited between subtests in a single file! +- upon termination of the test harness, it tries to kill all child processes + +The following state **is not** cleaned up: +- `tst()` and `cleanup()` between subtests in a single file +- `on_error()`, `on_success()` functions +- any global user-defined variables, also between files +- any started processes +- any modified files (we don't have a way to track those atm, although I may look into this) diff --git a/tests/03-misc.sh b/tests/03-misc.sh new file mode 100644 index 0000000..4aaaa99 --- /dev/null +++ b/tests/03-misc.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +misc_html_escape_basic() { + prepare() { + source src/misc.sh + } + tst() { + html_encode "$value" + } + value="meow" + match="meow" +} + +misc_html_escape_special() { + value="