From: Paul Hänsch Date: Wed, 3 Jun 2020 17:52:42 +0000 (+0200) Subject: Squashed 'cgilite/' changes from f0383ee..5b013b6 X-Git-Url: http://git.plutz.net/?p=serve0;a=commitdiff_plain;h=6a2f150eed622d61263b7137ffa0ef46764f5345 Squashed 'cgilite/' changes from f0383ee..5b013b6 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 git-subtree-dir: cgilite git-subtree-split: 5b013b64b7bbc9a62775ed861e9c8b34ffb3dfaa --- diff --git a/cgilite.sh b/cgilite.sh index 333334c..86ad280 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,19 @@ 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 +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 +72,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 +135,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 +163,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 \ @@ -180,19 +218,10 @@ URL(){ | 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;' -} - - SET_COOKIE(){ + # Param: session | +seconds | [date] + # Param: name=value + # Param: Path= | Domain= | Secure local expire cookie case "$1" in ''|0|session) expire='';; @@ -201,7 +230,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'