From eb5a36950265264798deef0e533de7a6a1237277 Mon Sep 17 00:00:00 2001 From: Arthur Khachaturov Date: Sat, 20 Jul 2024 01:32:20 +0300 Subject: [PATCH] chore: repo maintenance, minor code refinements --- {src/locust_files => benchmark}/locust.conf | 0 {src/locust_files => benchmark}/locustfile.py | 0 benchmark/requirements.txt | 1 + src/constants.sh | 82 ++++++++++++++++++ src/helpers.sh | 8 ++ src/html/index.html | 1 - src/{lib => }/http_server.sh | 83 ++++++++++--------- src/lib/constants.sh | 78 ----------------- src/lib/helpers.sh | 5 -- src/main.sh | 19 ----- src/tcp_server.sh | 50 +++++++++++ 11 files changed, 186 insertions(+), 141 deletions(-) rename {src/locust_files => benchmark}/locust.conf (100%) rename {src/locust_files => benchmark}/locustfile.py (100%) create mode 100644 benchmark/requirements.txt create mode 100644 src/constants.sh create mode 100644 src/helpers.sh delete mode 100644 src/html/index.html rename src/{lib => }/http_server.sh (53%) delete mode 100644 src/lib/constants.sh delete mode 100644 src/lib/helpers.sh delete mode 100755 src/main.sh create mode 100644 src/tcp_server.sh diff --git a/src/locust_files/locust.conf b/benchmark/locust.conf similarity index 100% rename from src/locust_files/locust.conf rename to benchmark/locust.conf diff --git a/src/locust_files/locustfile.py b/benchmark/locustfile.py similarity index 100% rename from src/locust_files/locustfile.py rename to benchmark/locustfile.py diff --git a/benchmark/requirements.txt b/benchmark/requirements.txt new file mode 100644 index 0000000..8eaebfd --- /dev/null +++ b/benchmark/requirements.txt @@ -0,0 +1 @@ +locust diff --git a/src/constants.sh b/src/constants.sh new file mode 100644 index 0000000..6d74044 --- /dev/null +++ b/src/constants.sh @@ -0,0 +1,82 @@ +# shellcheck disable=SC2034 # for now + +readonly HTTP_SERVER_VERSION='0.1.0' +readonly HTTP_PROTOCOL_VERSION='HTTP/1.1' +readonly HTTP_SERVER_STRING="httb/${HTTP_SERVER_VERSION}" + +readonly HTTP_DEFAULT_HOST='127.0.0.1' +readonly HTTP_DEFAULT_PORT='8081' + +declare -Ar HTTP_METHOD_NAMES=( + [100]='Continue' + [101]='Switching Protocols' + [200]='OK' + [201]='Created' + [202]='Accepted' + [203]='Non-Authoritative Information' + [204]='No Content' + [205]='Reset Content' + [206]='Partial Content' + [239]='Pratusevic' + [300]='Multiple Choices' + [301]='Moved Permanently' + [302]='Found' + [303]='See Other' + [304]='Not Modified' + [305]='Use Proxy' + [307]='Temporary Redirect' + [308]='Permanent Redirect' + [400]='Bad Request' + [401]='Unauthorized' + [402]='Payment Required' + [403]='Forbidden' + [404]='Not Found' + [405]='Method Not Allowed' + [406]='Not Acceptable' + [407]='Proxy Authentication Required' + [408]='Request Timeout' + [409]='Conflict' + [410]='Gone' + [411]='Length Required' + [412]='Precondition Failed' + [413]='Content Too Large' + [414]='URI Too Long' + [415]='Unsupported Media Type' + [416]='Range Not Satisfiable' + [417]='Expectation Failed' + [418]="I'm a teapot" + [421]='Misdirected Request' + [422]='Unprocessable Content' + [426]='Upgrade Required' + [500]='Internal Server Error' + [501]='Not Implemented' + [502]='Bad Gateway' + [503]='Service Unavailable' + [504]='Gateway Timeout' + [505]='HTTP Version Not Supported' +) + +declare -Ar HTTP_SUPPORTED_METHODS=( + [GET]=1 + [HEAD]=1 + [POST]='' + [PUT]='' + [DELETE]='' + [CONNECT]='' + [OPTIONS]='' + [TRACE]='' +) + +http::_status_code_page() { + cat <<-EOF + + + ${1} ${HTTP_METHOD_NAMES["$1"]} + +

${1} ${HTTP_METHOD_NAMES["$1"]}

+
+
Server: ${HTTP_SERVER_STRING}
+ + +EOF +} diff --git a/src/helpers.sh b/src/helpers.sh new file mode 100644 index 0000000..fe1d4f7 --- /dev/null +++ b/src/helpers.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# shellcheck disable=SC2015 + +http::_tempfile() { + [ ! -d "/dev/shm" ] && TMP_FOLDER="/tmp" || TMP_FOLDER="/dev/shm" + [ ! -d "${TMP_FOLDER}/httb-server" ] && mkdir "${TMP_FOLDER}/httb-server" || + mktemp -t 'httb-server.XXXXXXXXXXXX' -p "${TMP_FOLDER}/httb-server" +} diff --git a/src/html/index.html b/src/html/index.html deleted file mode 100644 index 8ab686e..0000000 --- a/src/html/index.html +++ /dev/null @@ -1 +0,0 @@ -Hello, World! diff --git a/src/lib/http_server.sh b/src/http_server.sh similarity index 53% rename from src/lib/http_server.sh rename to src/http_server.sh index 6067fab..e0b9f5a 100644 --- a/src/lib/http_server.sh +++ b/src/http_server.sh @@ -1,13 +1,15 @@ #!/bin/bash +# shellcheck shell=bash set -e shopt -s globstar extglob dotglob -# readonly HTTP_SCRIPT_PATH="$(dirname "$(readlink -e "${BASH_SOURCE[0]:-$0}")")" -# . "${SCRIPT_PATH}/mock_tcp_server.sh" -# . "${SCRIPT_PATH}/constants.sh" +# shellcheck source-path=../ +HTTP_SCRIPT_PATH="$(dirname "$(readlink -e "${BASH_SOURCE[0]:-$0}")")" +readonly HTTP_SCRIPT_PATH -. "./lib/constants.sh" && . "./lib/helpers.sh" +. "${HTTP_SCRIPT_PATH}/constants.sh" +. "${HTTP_SCRIPT_PATH}/helpers.sh" declare -A http__handlers=() @@ -16,7 +18,7 @@ declare -A http__all_routes=() # -- CONFIGURATION -- http::host() { export HTTP_HOST_NAME="$1"; } -http::bind() { export HTTP_HOST="${1:-127.0.0.1}"; export HTTP_PORT="${2:-8081}"; } +http::bind() { export HTTP_HOST="${1:-${HTTP_DEFAULT_HOST}}"; export HTTP_PORT="${2:-${HTTP_DEFAULT_PORT}}"; } http::markdown_base() { export HTTP_MARKDOWN_BASE="$1"; } http::static_folder() { @@ -31,7 +33,7 @@ http::route() { local _method while read -r _method; do # temp until all methods are suppoorted - [[ -z "${HTTP_SUPPORTED_METHODS[${_method}]}" ]] && echo "$1: Method ${_method} not implemented!" >&2 && exit 1 + [[ -z "${HTTP_SUPPORTED_METHODS["${_method}"]}" ]] && echo "$1: Method ${_method} not implemented!" >&2 && exit 1 http__handlers["$_method,$3"]="$1" http__all_routes["$3"]=1 done <<< "${2/ /$'\n'}" @@ -42,67 +44,66 @@ http::get() { http::route "$1" "GET" "$2"; } # -- REQUEST PROCESSING -- http::_parse_headers() { - local -n _h=$1 _m=$2 _p=$3 _v=$4 - - read -r header_first_ - read -r _m _p _v <<<"$header_first_" + read -r request_method request_path request_version while read -r header_; do [[ -z "${header_/$'\r'/}" ]] && break local h_name h_value IFS=: read -r h_name h_value <<< "${header_/': '/:}" - _h["${h_name}"]="${h_value}" + request_headers["${h_name}"]="${h_value}" done } http::_parse_body() { :; } # TODO http::_route_request() { - [[ -z "${HTTP_SUPPORTED_METHODS[${HTTP_REQUEST_METHOD}]}" ]] && http::response 501 && return 1 + [[ -z "${HTTP_SUPPORTED_METHODS[${request_method}]}" ]] && http::response 501 && return 1 local glob_endpoint handler path_found for glob_endpoint in "${!http__all_routes[@]}"; do # FIXME # shellcheck disable=SC2053 - if [[ "${HTTP_REQUEST_PATH}" == ${glob_endpoint} ]]; then + if [[ "${request_path}" == ${glob_endpoint} ]]; then declare -rx HTTP_REQUEST_GLOB="${glob_endpoint}" path_found=1 break fi done - handler=${http__handlers["$HTTP_REQUEST_METHOD,$glob_endpoint"]} + handler="${http__handlers["$request_method,$glob_endpoint"]}" if [[ -z "${path_found}" ]]; then http::response 404; return 1 elif [[ -z "${handler}" ]]; then http::response 405; return 1 else - ${handler} + "${handler}" fi - } # -- RESPONSE PROCESSING -- http::_response_base_headers() { - printf "%s %d %s\n" "${HTTP_PROTOCOL_VERSION}" "$1" "${HTTP_METHOD_NAMES["$1"]}" - cat <<-EOS - Server: ${HTTP_SERVER_STRING} - Connection: close - Date: $(date -Ru) - Cache-control: no-cache - Cache-control: max-age=0 - EOS + connection_string="${HTTP_PROTOCOL_VERSION} $1 ${HTTP_METHOD_NAMES["$1"]}" + response_headers["Server"]="${HTTP_SERVER_STRING}" + response_headers["Connection"]="close" + response_headers["Date"]="$(date -Ru)" + response_headers["Cache-control"]="no-cache" + response_headers["Cache-control"]="max-age=0" } +http::_set_response_content_type() { + response_headers["Content-Type"]="$(file -ib "$1")" +} + +# $1 = file with content $2 = filename http::_response_content_headers() { - echo "Content-Length: $(wc -c "$1" | cut -d ' ' -f 1)" - echo "Content-Type: ${HTTP_RESPONSE_CONTENT_TYPE:="$(file -ib "$1")"}" + response_headers["Content-Length"]="$(wc -c "$1" | cut -d ' ' -f 1)" + [[ -z "${response_headers["Content-Type"]}" ]] && response_headers["Content-Type"]="$(file -ib "$1")" echo } http::_response_content() { - local -r content_file="$(http::_ramfile)" # TODO mb adapt to mkfifo pipes + local -r content_file="$(http::_tempfile)" # TODO mb adapt to mkfifo pipes cat - > "${content_file}" http::_response_content_headers "${content_file}" @@ -112,9 +113,9 @@ http::_response_content() { } http::_static_file() { - local uri_path="${HTTP_REQUEST_PATH#"${HTTP_REQUEST_GLOB%'**'}"}" + local uri_path="${request_path#"${HTTP_REQUEST_GLOB%'**'}"}" local filename="${HTTP_STATIC_FOLDER}/${uri_path}" - [[ -z "${uri_path}" || ! -f "${filename}" ]] && http::response 404 && return 1 + [[ -z "${uri_path}" || ! -f "${filename}" || "${filename}" == *".."* ]] && http::response 404 && return 1 http::file "${filename}" } @@ -122,6 +123,7 @@ http::response() { local status_code page [[ -n "${HTTP_METHOD_NAMES["$1"]}" ]] && status_code="$1" || status_code="500" page="$(http::_status_code_page "${status_code}")" + response_headers["Content-Type"]="text/html" http::_response_base_headers "${status_code}" http::_response_content <<< "${page}" } @@ -129,32 +131,37 @@ http::response() { http::file() { [[ ! -f "$1" ]] && http::response 500 && return 1 http::_response_base_headers "200" + http::_set_response_content_type "$1" http::_response_content < "$1" } -http::html() { HTTP_RESPONSE_CONTENT_TYPE="text/html" http::file "$@"; } +http::html() { response_headers["Content-Type"]="text/html" http::file "$@"; } http::format_markdown() { :; } # TODO # -- COMMAND LINE LOGIC -- http::_process_request() { - declare -Ax HTTP_REQUEST_HEADERS - export HTTP_REQUEST_METHOD HTTP_REQUEST_PATH HTTP_REQUEST_VERSION HTTP_REQUEST_BODY + declare -Ax request_headers response_headers + export request_method request_path request_version connection_string - http::_parse_headers HTTP_REQUEST_HEADERS HTTP_REQUEST_METHOD HTTP_REQUEST_PATH HTTP_REQUEST_VERSION || return 1 - http::_parse_body HTTP_REQUEST_BODY || return 1 - if [[ -n "${HTTP_HOST_NAME}" && "${HTTP_HOST_NAME}" != "${HTTP_REQUEST_HEADERS["Host"]%:*}" ]]; then + http::_parse_headers || { http::response 500 && return 0; } + # { [[ "${request_method}" != "GET" ]] && http::_parse_body; || { http::response 500 && return 0; } + + if [[ -n "${HTTP_HOST_NAME}" && "${HTTP_HOST_NAME}" != "${request_headers["Host"]%:*}" ]]; then http::response 404 - return + return 0 fi + http::_route_request + [[ -z "${connection_string}" ]] && http::response 500 && return 0 + return 0 } http::_cmd_help() { echo "USAGE:" - echo " $0 [OPTIONS]" + echo " $0 " echo echo "ARGUMENTS:" echo " run Run the server" @@ -163,7 +170,7 @@ http::_cmd_help() { http::run() { if [[ "$1" = "run" ]]; then - echo "Listening on ${HTTP_HOST}:${HTTP_PORT}" + echo "Listening on ${HTTP_HOST:=${HTTP_DEFAULT_HOST}}:${HTTP_PORT:=${HTTP_DEFAULT_PORT}}" socat "TCP-LISTEN:${HTTP_PORT},bind=${HTTP_HOST},fork,reuseport" "EXEC:$(readlink -e "$0") parse" elif [[ "$1" = "parse" ]]; then http::_process_request diff --git a/src/lib/constants.sh b/src/lib/constants.sh deleted file mode 100644 index b234bc5..0000000 --- a/src/lib/constants.sh +++ /dev/null @@ -1,78 +0,0 @@ -# shellcheck disable=SC2034 # for now - -readonly HTTP_PROTOCOL_VERSION="HTTP/1.1" -readonly HTTP_SERVER_STRING="httb/0.1.0" - -declare -Ar HTTP_METHOD_NAMES=( - [100]="Continue" - [101]="Switching Protocols" - [200]="OK" - [201]="Created" - [202]="Accepted" - [203]="Non-Authoritative Information" - [204]="No Content" - [205]="Reset Content" - [206]="Partial Content" - [239]="Pratusevic" - [300]="Multiple Choices" - [301]="Moved Permanently" - [302]="Found" - [303]="See Other" - [304]="Not Modified" - [305]="Use Proxy" - [307]="Temporary Redirect" - [308]="Permanent Redirect" - [400]="Bad Request" - [401]="Unauthorized" - [402]="Payment Required" - [403]="Forbidden" - [404]="Not Found" - [405]="Method Not Allowed" - [406]="Not Acceptable" - [407]="Proxy Authentication Required" - [408]="Request Timeout" - [409]="Conflict" - [410]="Gone" - [411]="Length Required" - [412]="Precondition Failed" - [413]="Content Too Large" - [414]="URI Too Long" - [415]="Unsupported Media Type" - [416]="Range Not Satisfiable" - [417]="Expectation Failed" - [418]="I'm a teapot" - [421]="Misdirected Request" - [422]="Unprocessable Content" - [426]="Upgrade Required" - [500]="Internal Server Error" - [501]="Not Implemented" - [502]="Bad Gateway" - [503]="Service Unavailable" - [504]="Gateway Timeout" - [505]="HTTP Version Not Supported" -) - -declare -Ar HTTP_SUPPORTED_METHODS=( - [GET]=1 - [HEAD]=1 - [POST]='' - [PUT]='' - [DELETE]='' - [CONNECT]='' - [OPTIONS]='' - [TRACE]='' -) - -http::_status_code_page() { - cat <<-EOF - - - ${1} ${HTTP_METHOD_NAMES["$1"]} - -

${1} ${HTTP_METHOD_NAMES["$1"]}

-
-
Server: ${HTTP_SERVER_STRING}
- - -EOF -} diff --git a/src/lib/helpers.sh b/src/lib/helpers.sh deleted file mode 100644 index 299bb97..0000000 --- a/src/lib/helpers.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -http::_ramfile() { # TODO use tempdir instead of files - mktemp -t 'httb-server.XXXXXXXXXXXX' -p "/dev/shm" -} diff --git a/src/main.sh b/src/main.sh deleted file mode 100755 index 48166d1..0000000 --- a/src/main.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# shellcheck disable=SC1090 -. lib/http_server.sh -. routes/*.sh - -http::static_folder "/static" "./static" - -root() { - http::html "html/index.html" -} && http::get root "/" - - -main() { - http::file "./main.sh" -} && http::route main "GET" '/main' - - -http::run "$@" diff --git a/src/tcp_server.sh b/src/tcp_server.sh new file mode 100644 index 0000000..161c100 --- /dev/null +++ b/src/tcp_server.sh @@ -0,0 +1,50 @@ +#!/bin/bash +MAX_CONNECTIONS=1 + +_workers_=() + +tcp::worker_() { + local socket_input socket_output request src_address + local host="${1:-0.0.0.0}" + local port="${2:-8081}" + local delimiter="" + local delimiter_placeholder='DELIMTIER_PLACEHOLDER' + + exec {socket_input}<> <(:) + exec {socket_output}<> <(:) + + # { nc -lknv -s "${host}" -p "${port}" > >( stdbuf -o0 sed "s/$delimiter/$delimiter_placeholder/g" ) 2> >( stdbuf -o0 sed "/Listening on.*/d;s/Connection received on /$delimiter/" >&2; ) ; } <&$socket_input >&$socket_output 2>&1 & + { + socat "TCP-LISTEN:${host:-80},bind=${port:-127.0.0.1},fork,reuseport" \ + > >( stdbuf -o0 sed "s/$delimiter/$delimiter_placeholder/g" ) \ + 2> >( stdbuf -o0 sed "/Listening on.*/d;s/Connection received on /$delimiter/" >&2; ) ; + } <&$socket_input >&$socket_output 2>&1 & + + echo "listenning" + + while :; do + read -d "$delimiter" -r -u $socket_output request + read -r -u $socket_output src_address; src_address="${src_address/ /:}" + + printf -- '--- NEW REQUEST ---\n' + echo "$request" + printf -- '--- SOURCE ADDR ---\n' + echo "$src_address" + printf -- '--- END REQUEST ---\n\n\n' + + printf "%s\n\n%s" "$(cat response.txt)" "$(cat index.html)" >&$socket_input # main logic + done + + exec {socket_input}<&- + exec {socket_output}<&- +} + + +tcp::listen() { + for _ in $(seq $MAX_CONNECTIONS); do + tcp::worker_ "$1" "$2" & _workers_+=("$!") + done + wait +} + +tcp::listen "$@"