#!/bin/sh -e # # https://www.romanzolotarev.com/bin/form # Copyright 2018 Roman Zolotarev # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # main() { test_bin test -n "$DB" || no_def 'DB' MAIL_QUEUE="$DB/mail" TOKENS="$DB/tokens" mkdir -p "$MAIL_QUEUE" || no_dir "$MAIL_QUEUE" mkdir -p "$TOKENS" || no_dir "$TOKENS" chmod 0770 "$DB" "$MAIL_QUEUE" "$TOKENS" test -n "$MAIL_TO" || no_def 'MAIL_TO' test -n "$MAIL_SUBJECT" || no_def 'MAIL_SUBJECT' # shellcheck disable=SC2153 test -n "$ERR_EXPIRED" || no_def 'ERR_EXPIRED' test -n "$ERR_FORMAT" || no_def 'ERR_FORMAT' test -n "$ERR_INVALID" || no_def 'ERR_INVALID' test -n "$EXP_TIME" || no_def 'EXP_TIME' test -n "$FIELDS" || no_def 'FIELDS' test -n "$SUCCESS_URL" || no_def 'SUCCESS_URL' test -n "$MAIL_QUEUE" || no_def 'MAIL_QUEUE' test -n "$TOKENS" || no_def 'TOKENS' test -d "$MAIL_QUEUE" || no_dir "$MAIL_QUEUE" test -d "$TOKENS" || no_dir "$TOKENS" test -n "$TEMPLATE" || no_def 'TEMPLATE' test -f "$TEMPLATE" || no_file "$TEMPLATE" case "$REQUEST_METHOD" in POST) post;; GET) get;; *) fail 'invalid';; esac } get() { token=$(create_token) && export token err=$(render_err) && export err params=$(get_params) && eval "$params" http200 "$(render_template < "$TEMPLATE")" } post() { query=$(read_query_string_post) || fail 'invalid' field_r=$(validate_query "$query") || fail "$field_r" "$query" token_r=$(validate_token "$query") || fail "$token_r" "$query" file="$MAIL_QUEUE/form-$(random_str)" case "$MAIL_HTML" in yes) render_message_html "$query" > "$file";; *) render_message_plain "$query" > "$file";; esac chmod 0660 "$file" http301 "$SUCCESS_URL" } validate_token() { token=$(get_value 'token' "$1") test -n "$token" || { echo 'invalid'; exit 1; } test -f "$TOKENS/f-${token:?}" || { echo 'invalid'; exit 1; } test "$(cat "$TOKENS/f-$token")" = "$REMOTE_ADDR" || { echo 'invalid'; exit 1; } delta=$(( $(date +%s) - $(stat -f%m "$TOKENS/f-$token") )) rm "$TOKENS/f-$token" test "$delta" -lt "$EXP_TIME" || { echo 'expired'; exit 1; } } validate_query() { echo "$FIELDS" | grep . | while IFS=, read -r field min max __ do value=$(get_value "$field" "$1") test ${#value} -ge "$min" || { echo "$field"; exit 1; } test ${#value} -le "$max" || { echo "$field"; exit 1; } done } read_query_string_post() { test -n "$CONTENT_LENGTH" || exit 1 test "$CONTENT_LENGTH" -le "$(get_max_length)" || exit 1 dd bs=1 count="$CONTENT_LENGTH" status=none } get_max_length() { echo "$FIELDS" | grep . | awk -F, '{i=i+2+length($1)+($3*3)}END{print i+27}' } get_params() { test -n "$QUERY_STRING" || return # shellcheck disable=SC2034 echo "$FIELDS" | grep . | while IFS=, read -r field __ do echo "export $field='$(get_value "$field" "$QUERY_STRING")'" done } create_token() { token=$(random_str) echo "$REMOTE_ADDR" > "$TOKENS/f-$token" chmod 0660 "$TOKENS/f-$token" echo "$token" } random_str() { jot -rcs '' 20 97 122 } render_err() { test -n "$QUERY_STRING" || return err=$(get_value 'err' "$QUERY_STRING") case "$err" in invalid) echo "$ERR_INVALID";; expired) echo "$ERR_EXPIRED";; '') ;; *) render_field_err "$err";; esac } render_field_err() { echo "$FIELDS" | grep . | awk -F, ' /^'"$1"'/ { gsub(/^ /, "", $2) gsub(/^ /, "", $3) gsub(/^ /, "", $4) printf("'"$ERR_FORMAT"'",$4,$2,$3) }' } no_def() { http500 "$1: Not defined"; } no_w() { http500 "$1: Not writable"; } no_dir() { http500 "$1: No such directory"; } no_file() { http500 "$1: No such file"; } fail() { post=$(echo "$2" | tr '&' '\n' | grep -vE '^token=|^send=|^err=' | tr '\n' '&') http301 "?${post}err=$1" } http200() { echo 'Status: 200 OK' echo 'Content-Type: text/html; charset=utf-8' echo echo "$1" exit 0 } http301() { echo 'Status: 301 Moved Permanently' echo 'Content-Type: text/html; charset=utf-8' echo "Location: $1" echo exit 0 } http500() { echo 'Status: 500 Internal Server Error' echo 'Content-Type: text/html; charset=utf-8' echo ' 500 Internal Server Error

500 Internal Server Error

'"${0##*/}: $1"'
OpenBSD httpd
' exit 1 } get_value() { echo "$2" | tr '&' '\n' | awk -F= '/^'"$1"'/{print$2}' | decode_url } decode_url() { # h/t Devin Teske # shellcheck disable=1004 awk ' BEGIN { for (n = 0; n < 256; n++) chr[n] = sprintf("%c",n) } { t = $0 a = "" gsub(/\+/, " ", t) while( match(t, /%[[:xdigit:]][[:xdigit:]]/) ) { a = a substr(t, 1, RSTART-1)\ chr[ sprintf("%u", "0x" substr(t, RSTART+1, 2))] t = substr(t, RSTART+RLENGTH) } a = a t gsub(//,"\\>",a) print a }' } get_all_values() { # shellcheck disable=SC2034 echo "$FIELDS" | grep . | while IFS=, read -r field __ __ label do echo "$label: $(get_value "$field" "$1")" done | sed 's/^ //g;s//\>/g' } render_message_plain() { values=$(get_all_values "$1") meta="$SERVER_NAME $REMOTE_ADDR $(date '+%d/%b/%Y:%H:%M:%S %z')" echo "To: $MAIL_TO" echo 'MIME-Version: 1.0' echo 'Content-Type: text/plain; charset=utf-8' echo 'Content-Transfer-Encoding: 7bit' echo "Subject: $(base64 "$MAIL_SUBJECT")" echo echo "$values" echo echo --- echo echo "$meta" } render_message_html() { values=$(get_all_values "$1") meta="$SERVER_NAME $REMOTE_ADDR $(date '+%d/%b/%Y:%H:%M:%S %z')" boundary="$(random_str)" echo "To: $MAIL_TO" echo 'MIME-Version: 1.0' echo 'Content-Type: multipart/alternative; boundary="'"$boundary"'"' echo "Subject: $(base64 "$MAIL_SUBJECT")" echo echo "--$boundary" echo 'Content-Type: text/plain; charset=utf-8' echo 'Content-Transfer-Encoding: 7bit' echo echo "$values" echo echo --- echo echo "$meta" echo echo "--$boundary" echo 'Content-Type: text/html; charset=utf-8' echo 'Content-Transfer-Encoding: 7bit' echo echo '
'
	echo "$values"
	echo '
' echo '
' echo '

' echo "$meta" echo '

' echo echo "--$boundary--" } base64() { echo "=?utf-8?B?$(echo "$1"|b64encode /dev/stdin|sed '1d;$d')?="; } render_template() { # h/t Devin Teske awk ' BEGIN { w = "[a-zA-Z_][a-zA-Z0-9_]*" var = sprintf("\\$(%s|{%s})", w, w) } { str = "" tail = $0 while (match(tail, var)) { head = substr(tail, 1, RSTART - 1) repl = substr(tail, RSTART, RLENGTH) tail = substr(tail, RSTART + RLENGTH) if ((match(head, /\\+/) ? RLENGTH + 1 : 1) % 2 == 1) { sub(/^\$/, "", repl) gsub(/(^{|}$)/, "", repl) repl = ENVIRON[repl] } str = str head repl } str = str tail print str }' } DEPS=' /bin/cat /bin/chmod /bin/date /bin/dd /bin/mkdir /bin/rm /bin/sh /usr/bin/awk /usr/bin/b64encode /usr/bin/grep /usr/bin/head /usr/bin/jot /usr/bin/printf /usr/bin/sed /usr/bin/stat /usr/bin/tail /usr/bin/tr /usr/lib/libc.so.92.5 /usr/lib/libm.so.10.1 /usr/lib/libz.so.5.0 /usr/libexec/ld.so ' test_bin() { echo "$DEPS" | grep 'bin' | while read -r file do test -x "$file" || http500 "$(no_bin "$file")" done echo "$DEPS" | grep 'lib' | while read -r file do test -f "$file" || http500 "$(no_bin "$file")" done } no_bin() { echo "$1: Not executable or not found" echo '
# copy binaries'
	echo "$DEPS" | grep . |
	while read -r file
	do echo "cp $file /var/rgz$file"
	done
}


main "$@"