From 8456138268c26b78936c0c2b22a7cc6ab9a7dd14 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Paul=20H=C3=A4nsch?= Date: Wed, 5 Aug 2020 16:17:37 +0200 Subject: [PATCH] Squashed 'cgilite/' changes from fa0f919..238f0f8 238f0f8 rudimentary debug function b8cec22 more escapes for use in html-sh and HTTP headers 4a73c71 limit escaping to necessary characters, more readable output, much faster escaping functions 5b013b6 bugfix: allow empty query string 7e6d863 improved handling of Connection header 87f88f1 quicker path sanitizing 06edc60 sanitizing and security 73550e0 updated copyright line 5a99761 speed improvements 7335b73 allow empty script headers f0383ee bugfix, STRING and UNSTRING input like "foo[bar]" 0d6196d faster STRING and UNSTRING functions git-subtree-dir: cgilite git-subtree-split: 238f0f8a2932b5ba66f7139c227eaaeb5dd7a013 --- cgilite.sh | 166 +++++++++++++++++++++++++++++++++-------------------- storage.sh | 40 ++++++++++++- 2 files changed, 143 insertions(+), 63 deletions(-) diff --git a/cgilite.sh b/cgilite.sh index 333334c..2e53832 100755 --- a/cgilite.sh +++ b/cgilite.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Copyright 2017 - 2018 Paul Hänsch +# Copyright 2017 - 2020 Paul Hänsch # # This is CGIlite. # A collection of posix shell functions for writing CGI scripts. @@ -27,15 +27,21 @@ BR=' ' cgilite_timeout=2 -HEADER(){ - # Read value of header line. Use this instead of - # referencing HTTP_* environment variables. - if [ -n "${cgilite_headers+x}" ]; then - printf %s "$cgilite_headers" \ - | sed -En 's;^'"${1}"': ([^\r]+)\r?$;\1;i; tX; d; :X;p;q;' - else - eval "printf %s \"\$HTTP_$(printf %s "${1}" |tr a-z A-Z |tr -c A-Z _)\"" - fi +debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; } + +PATH(){ + local str seg out + [ $# -eq 0 ] && str="$(cat)" || str="$*" + while [ "$str" ]; do + seg=${str%%/*}; str="${str#*/}" + case $seg in + ..) out="${out%/}"; out="${out%/*}/";; + .|'') out="${out%/}/";; + *) out="${out%/}/${seg}";; + esac; + [ "$seg" = "$str" ] && break + done + [ "${str}" -a "${out}" ] && printf %s "$out" || printf %s/ "${out%/}" } HEX_DECODE=' @@ -68,41 +74,59 @@ if [ -z "$REQUEST_METHOD" ]; then (sleep $cgilite_timeout && kill $$) & cgilite_watchdog=$! while read REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL; do + [ "${SERVER_PROTOCOL#HTTP/1.[01]${CR}}" ] && break kill $cgilite_watchdog - PATH_INFO="$(HEX_DECODE "${REQUEST_URI%\?*}")" - QUERY_STRING="${REQUEST_URI#*\?}" - cgilite_headers="$(while read -r hl; do [ "${hl%${CR}}" ] && printf '%s\n' "$hl" || break; done )" - HTTP_CONTENT_LENGTH="$(HEADER Content-Length |grep -xE '[0-9]+')" + SERVER_PROTOCOL="${SERVER_PROTOCOL%${CR}}" + PATH_INFO="$(HEX_DECODE "${REQUEST_URI%\?*}" |PATH)" + [ "${REQUEST_URI}" = "${REQUEST_URI#*\?}" ] \ + && QUERY_STRING='' \ + || QUERY_STRING="${REQUEST_URI#*\?}" + cgilite_headers=''; while read -r hl; do + hl="${hl%${CR}}"; [ "$hl" ] || break + case $hl in + 'Content-Length: '*) CONTENT_LENGTH="${hl#*: }";; + 'Content-Type: '*) CONTENT_TYPE="${hl#*: }";; + esac + cgilite_headers="${cgilite_headers}${hl}${BR}" + done export REMOTE_ADDR SERVER_NAME SERVER_PORT REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL \ - PATH_INFO QUERY_STRING HTTP_CONTENT_LENGTH + PATH_INFO QUERY_STRING CONTENT_TYPE CONTENT_LENGTH # Try to serve multiple requests, provided that script serves a # Content-Length header. # Without Content-Length header, connection will terminate after # script. - cgilite_status='200 OK'; cgilite_response=''; cgilite_cl="Connection: close${CR}"; + cgilite_status='200 OK'; cgilite_response=''; cgilite_cl="Connection: close${CR}${BR}"; . "$0" | while read -r l; do case $l in - Status:*) cgilite_status="${l#Status: }";; - Content-Length:*) cgilite_cl="${l}";; - $CR) printf '%s %s\r\n%s\n%s\n\r\n' \ + Status:*) + cgilite_status="${l#Status: }";; + Content-Length:*) + cgilite_cl="" + cgilite_response="${cgilite_response:+${cgilite_response}${BR}}${l}";; + Connection:*) + cgilite_cl="${l}${BR}";; + $CR) printf '%s %s\r\n%s%s\r\n' \ 'HTTP/1.1' "${cgilite_status%${CR}}" \ - "$cgilite_response" "${cgilite_cl}" + "${cgilite_response}${cgilite_response:+${BR}}" "${cgilite_cl}" cat || kill $$ [ "${cgilite_cl#Connection}" = "${cgilite_cl}" ]; exit;; *) cgilite_response="${cgilite_response:+${cgilite_response}${BR}}${l}";; esac; done || exit 0; (sleep $cgilite_timeout && kill $$) & cgilite_watchdog=$! done + kill $cgilite_watchdog + exit 0 fi -if [ "$REQUEST_METHOD" = POST -a "${HTTP_CONTENT_LENGTH:=${CONTENT_LENGTH:=0}}" -gt 0 ]; then - cgilite_post="$(head -c "$HTTP_CONTENT_LENGTH")" +if [ "${REQUEST_METHOD}" = POST -a "${CONTENT_LENGTH:-0}" -gt 0 -a \ + "${CONTENT_TYPE}" = "application/x-www-form-urlencoded" ]; then + cgilite_post="$(head -c "$CONTENT_LENGTH")" fi -[ -n "${DEBUG+x}" ] && env +[ "${DEBUG+x}" ] && env >&2 cgilite_count(){ printf %s "&$1" \ @@ -113,7 +137,8 @@ cgilite_count(){ cgilite_value(){ local str="&$1" name="$2" cnt="${3:-1}" while [ $cnt -gt 0 ]; do - str=${str#*&${name}=} + [ "${str}" = "${str#*&${name}=}" ] && return 1 + str="${str#*&${name}=}" cnt=$((cnt - 1)) done printf -- "$(printf %s "${str%%&*}" |sed -E 's;\+; ;g;'"$HEX_DECODE")" @@ -140,6 +165,21 @@ REF(){ cgilite_value "${HTTP_REFERER#*\?}" $@; } REF_COUNT(){ cgilite_count "${HTTP_REFERER#*\?}" $1; } REF_KEYS(){ cgilite_keys "${HTTP_REFERER#*\?}"; } +HEADER(){ + # Read value of header line. Use this instead of + # referencing HTTP_* environment variables. + if [ -n "${cgilite_headers+x}" ]; then + local str="${BR}${cgilite_headers}" + [ "${str}" = "${str#*${BR}${1}: }" ] && return 1 + str="${str#*${BR}${1}: }" + printf %s "${str%%${BR}*}" + else + local var="HTTP_$(printf %s "$1" |tr a-z- A-Z-)" + eval "[ \"\$$var\" ] && printf %s \"\$$var\" || return 1" + # eval "printf %s \"\$HTTP_$(printf %s "${1}" |tr a-z A-Z |tr -c A-Z _)\"" + fi +} + COOKIE(){ HEX_DECODE "$( HEADER Cookie \ @@ -149,50 +189,54 @@ COOKIE(){ } HTML(){ - # HTML Entity Coding - # Prints UTF-8 string as decimal Unicode Code Points - # Useful for escaping user input for use in HTML text and attributes - { [ $# -eq 0 ] && cat || printf %s "$*"; } \ - | hexdump -ve '/1 "%03o\n"' \ - | while read n; do - case $n in - # bitbanging octal UTF-8 chains into singular 7 digit octal numbers - [01]??) printf '0000%s' $n;; # 7 bit ASCII character, nothing to do - 2??) printf '%s' ${n#2};; # tail fragment, append 6 bit - 3[0123]?) printf '000%s' ${n#3};; # 2 octet (11 bit) chain start - 34?) printf '00%s' ${n#34};; # 3 octet (16 bit) chain start - 35?) printf '01%s' ${n#35};; # 3 octet chain start, high - 36?) printf '%s' ${n#36};; # 4 octet (21 bit) chain start + # Escape HTML cahracters + # Also escape [, ], and \n for use in html-sh + local str out + [ $# -eq 0 ] && str="$(cat)" || str="$*" + while [ "$str" ]; do + case $str in + \&*) out="${out}&";; + \<*) out="${out}<";; + \>*) out="${out}>";; + \"*) out="${out}"";; + \'*) out="${out}'";; + \[*) out="${out}[";; + \]*) out="${out}]";; + "${BR}"*) out="${out} ";; + *) out="${out}${str%"${str#?}"}";; esac - done \ - | sed -E 's;.{7};&\n;g;' \ - | while read n; do - printf '&#%d;' $((0$n)) + str="${str#?}" done + printf %s "$out" } URL(){ - # Code every character in URL escape hex format - # except alphanumeric ascii - - { [ $# -eq 0 ] && cat || printf %s "$*"; } \ - | hexdump -v -e '/1 ",%02X"' \ - | sed 's;,;%;g; s;%2F;/;g;' -} - -PATH(){ - { [ $# -eq 0 ] && cat || printf %s "$*"; } \ - | sed -E 's;^.*$;/&/;; s;/+;/;g; - :X; - s;^/\.\./;/;; s;/\./;/;g; - tX; - s;/[^/]+/\.\./;/;; - tX; - s;^(/.*)/$;\1;' + # Escape pathes, so they can be used in link tags and HTTP Headers + local str out + [ $# -eq 0 ] && str="$(cat)" || str="$*" + while [ "$str" ]; do + case $str in + \&*) out="${out}%26";; + \"*) out="${out}%22";; + \'*) out="${out}%27";; + \?*) out="${out}%3F";; + \#*) out="${out}%23";; + \[*) out="${out}%5B";; + \]*) out="${out}%5D";; + \ *) out="${out}%20";; + "${BR}"*) out="${out}%0A";; + %*) out="${out}%25";; + *) out="${out}${str%"${str#?}"}";; + esac + str="${str#?}" + done + printf %s "$out" } - SET_COOKIE(){ + # Param: session | +seconds | [date] + # Param: name=value + # Param: Path= | Domain= | Secure local expire cookie case "$1" in ''|0|session) expire='';; @@ -201,7 +245,7 @@ SET_COOKIE(){ esac cookie="$2" - printf 'Set-Cookie: %s' "$cookie" + printf 'Set-Cookie: %s; HttpOnly; SameSite=Lax' "$cookie" [ -n "$expire" ] && printf '; Expires=%s' "${expire%+????}${expire:+GMT}" [ $# -ge 3 ] && shift 2 && printf '; %s' "$@" printf '\r\n' diff --git a/storage.sh b/storage.sh index 10a8029..7f70e64 100755 --- a/storage.sh +++ b/storage.sh @@ -20,6 +20,10 @@ [ -n "$include_storage" ] && return 0 include_storage="$0" +CR=" " +BR=' +' + LOCK(){ local lock timeout block lock="${1}.lock" @@ -72,11 +76,27 @@ STRING=' s; ;+;g; ' -STRING(){ +STRING_OLD(){ { [ $# -eq 0 ] && cat || printf %s "$*"; } \ | sed -E ':X; $!{N;bX;}'"$STRING" } +STRING(){ + local in out='' + [ $# -gt 0 ] && in="$*" || in="$(cat)" + while [ "$in" ]; do case $in in + \\*) out="${out}\\\\"; in="${in#\\}" ;; + "$BR"*) out="${out}\\n"; in="${in#${BR}}" ;; + "$CR"*) out="${out}\\r"; in="${in#${CR}}" ;; + " "*) out="${out}\\t"; in="${in# }" ;; + +*) out="${out}\\+"; in="${in#+}" ;; + " "*) out="${out}+"; in="${in# }" ;; + *) out="${out}${in%%[\\${CR}${BR} + ]*}"; in="${in#"${in%%[\\${BR}${CR} + ]*}"}" ;; + esac; done + printf '%s' "$out" +} + + UNSTRING=' :UNSTRING_X s;((^|[^\\])(\\\\)*)\\n;\1\n;g; @@ -87,7 +107,23 @@ UNSTRING=' s;((^|[^\\])(\\\\)*)\\\+;\1+;g; s;\\\\;\\;g; ' -UNSTRING(){ +UNSTRING_OLD(){ { [ $# -eq 0 ] && cat || printf %s "$*"; } \ | sed -E "$UNSTRING" } +UNSTRING(){ + local in out='' + [ $# -gt 0 ] && in="$*" || in="$(cat)" + while [ "$in" ]; do case $in in + \\\\*) out="${out}\\"; in="${in#\\\\}" ;; + \\n*) out="${out}${BR}"; in="${in#\\n}" ;; + \\r*) out="${out}${CR}"; in="${in#\\r}" ;; + \\t*) out="${out} "; in="${in#\\t}" ;; + \\+) out="${out}+"; in="${in#\\+}" ;; + +*) out="${out} "; in="${in#+}" ;; + \\*) in="${in#\\}" ;; + *) out="${out}${in%%[\\+]*}"; in="${in#"${in%%[\\+]*}"}" ;; + esac; done + printf '%s' "$out" +} + -- 2.39.2