diff --git a/shellweb b/shellweb index 33b2b0f..3f7ced4 100755 --- a/shellweb +++ b/shellweb @@ -70,20 +70,24 @@ senderr() { $sent && return 0 local text + local v=1.0 case $1 in 400) text="Bad request";; 401) text="Unauthorized";; 403) text="Not allowed";; 404) text="Not found";; + 416) text="Range Not Satisfiable"; v=1.1;; *) text="Server error"; set -- 500;; esac sent=true - printf "HTTP/1.0 %s %s\r\n" "$1" "$text" + printf "HTTP/%s %s %s\r\n" "$v" "$1" "$text" printf "Date: %s\r\n" "$(env LC_ALL=en_US.UTF-8 date)" printf "Connection: close\r\n" if [[ $1 == 401 ]]; then printf "WWW-Authenticate: Basic realm=ShellWeb\r\n" + elif [[ $1 == 416 ]]; then + printf "Content-Range: bytes */%u\r\n" "$2" fi printf "\r\n" @@ -119,6 +123,8 @@ validate_and_send() { index "$1" elif [[ "$1" == *.cgi* ]]; then cgi "$1" + elif [[ $portion == true ]]; then + send_portion "$1" else sendfile "$1" fi @@ -186,6 +192,39 @@ sendfile() { exec 3<&- } +send_portion() { + $sent && return 0 + test -d "$1" && return + local ct=$(file -bi "$root/$1") || { senderr 500; return 0; } + local sz=$(ls -l "$root/$1" | awk '{print $5}') || { senderr 500; return 0; } + local range_from=${range%%-*} + local range_to=${range##*-} + if [ -n "$range_from" ]; then + if [ -z "$range_to" ]; then + range_to=$(($sz - 1)) + elif [ "$range_to" -ge "$sz" ]; then + range_to=$(($sz - 1)) + fi + else + if [ -z "$range_to" ]; then + senderr 400 + return 0 + else + range_from=$(($sz - $range_to)) + range_to=$(($sz - 1)) + fi + fi + test "$range_from" -gt "$range_to" && { senderr 416 "$sz"; return 0; } + sent=true + local length=$(( $range_to - $range_from + 1)) + printf "HTTP/1.1 206 Partial Content\r\n" + printf "Content-Range: bytes %s\r\n" "${range_from}-${range_to}/$sz" + printf "Content-Length: %u\r\n" "$length" + printf "Content-Type: %s\r\n" "$ct" + printf "\r\n" + dd skip="$range_from" count="$length" if="$root/$1" bs=1 status=none +} + html_escape() { echo "$*" | sed -E \ -e 's/&/\&/g;' \ @@ -332,6 +371,8 @@ while true; do $proxy_mode && proxy auth_passed=false keep_conn=false + portion=false + range= while ! $proxy_mode && ! $sent && read -p v1 v2 v3; do header=$(echo "$v1" | tr A-Z a-z) if [ -z "$file" ]; then @@ -365,9 +406,14 @@ while true; do fi file= auth_passed=false + portion=false if $keep_conn; then sent=false fi + elif [[ $header == "range:" ]]; then + $verbose && echo "Range $v2 from $file requested" >&2 + range=$(echo "${v2##bytes=}" | tr -d '\r') + portion=true # else ignore all headers fi done >&p diff --git a/tests/test-416 b/tests/test-416 new file mode 100644 index 0000000..ddfa232 --- /dev/null +++ b/tests/test-416 @@ -0,0 +1,22 @@ +#!/bin/ksh +#check for 416 response + +. ${0%/*}/common + +link="./tests/home.html" +bytes_request="25-14" +while getopts "l:r:" OPT; do case $OPT in + l) link=$OPTARG;; + r) byte_request=$OPTARG;; +esac; done +shift $((OPTIND - 1)) +run_shellweb +request_file=$(mktemp -p $www) +cp "$link" "$request_file" +response=$( + { printf "GET ${request_file#$www} HTTP/1.1\r\n" + printf "Range: bytes="$bytes_request"\r\n\r\n" + } | nc -w 1 127.0.0.1 $port | head -n 1 | tr -d '\r' +) +check="HTTP/1.1 416 Range Not Satisfiable" +test x"$response" = x"$check" || fail "$response" diff --git a/tests/test-part b/tests/test-part new file mode 100644 index 0000000..6d94f35 --- /dev/null +++ b/tests/test-part @@ -0,0 +1,59 @@ +#!/bin/ksh + +. ${0%/*}/common +check_portion() { + local first_byte="${2%%-*}" + local last_byte="${2##*-}" + local sz=$(ls -l "$1" | awk '{print $5}') + local length + + if [ -n "$first_byte" ]; then + if [ -z "$last_byte" ]; then + last_byte=$(($sz - 1)) + elif [ "$last_byte" -ge "$sz" ]; then + last_byte=$(($sz - 1)) + fi + else + if [ -z "$last_byte" ]; then + echo "Incorrect request" + return 0 + else + first_byte=$(($sz - $last_byte)) + last_byte=$(($sz - 1)) + fi + fi + length=$(($last_byte - $first_byte + 1)) + + local request_file=$(mktemp -p $www) + cp "$1" "$request_file" + local part_file=$(mktemp -p $tmpdir) || { fail "mktemp fail"; return 0; } + dd skip="$first_byte" count="$length" if="$1" bs=1 status=none > "$part_file" || { fail "incorrect range"; return 0; } + local response=$(mktemp -p $tmpdir) + { printf "GET ${request_file#$www} HTTP/1.1\r\n" + printf "Range: bytes=$2\r\n\r\n" + } | nc -w 1 127.0.0.1 $port | tr -d '\r' >> "$response" + local first_header=$(head -n 1 $response) + local check="HTTP/1.1 206 Partial Content" + test x"$first_header" = x"$check" || { fail "incorrect response header"; return 0; } + response_range=$(awk '/Content-Range:/{print $NF}' "$response") + test "${response_range%%/*}" = "${first_byte}-${last_byte}" || { fail "incorrect range"; return 0; } + sed -i '1,/^$/ d' "$response" + cmp -s "$part_file" "$response" || { fail "files don't match"; return 0; } + echo "Requested $2 bytes of $file: success" +} + + +file="./tests/home.html" +byte_request="25-30 26- -35" + +while getopts "f:r:" OPT; do case $OPT in + f) file=$OPTARG;; + r) byte_request="$OPTARG";; +esac; done +shift $((OPTIND - 1)) + +run_shellweb +for t in $byte_request; do + echo "Bytes from $file requested: $t" + check_portion "$file" $t || fail +done