commit fd57b9ac7573b07909aad4c25ebf4feddabde89d Author: Arthur Khachaturov Date: Thu Jul 11 00:20:48 2024 +0300 initial: implement get method for static files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..670a936 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +.venv/ diff --git a/src/html/index.html b/src/html/index.html new file mode 100644 index 0000000..8ab686e --- /dev/null +++ b/src/html/index.html @@ -0,0 +1 @@ +Hello, World! diff --git a/src/lib/constants.sh b/src/lib/constants.sh new file mode 100644 index 0000000..b234bc5 --- /dev/null +++ b/src/lib/constants.sh @@ -0,0 +1,78 @@ +# 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 new file mode 100644 index 0000000..299bb97 --- /dev/null +++ b/src/lib/helpers.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +http::_ramfile() { # TODO use tempdir instead of files + mktemp -t 'httb-server.XXXXXXXXXXXX' -p "/dev/shm" +} diff --git a/src/lib/http_server.sh b/src/lib/http_server.sh new file mode 100644 index 0000000..6067fab --- /dev/null +++ b/src/lib/http_server.sh @@ -0,0 +1,174 @@ +#!/bin/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" + +. "./lib/constants.sh" && . "./lib/helpers.sh" + + +declare -A http__handlers=() +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::markdown_base() { export HTTP_MARKDOWN_BASE="$1"; } + +http::static_folder() { + http__handlers["GET,$1/**"]="http::_static_file" + http__all_routes["$1/**"]=1 + export HTTP_STATIC_FOLDER="$2"; +} + +# -- PUBLIC METHODS -- + +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 + http__handlers["$_method,$3"]="$1" + http__all_routes["$3"]=1 + done <<< "${2/ /$'\n'}" +} + +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_" + + 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}" + done +} + +http::_parse_body() { :; } # TODO + +http::_route_request() { + [[ -z "${HTTP_SUPPORTED_METHODS[${HTTP_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 + declare -rx HTTP_REQUEST_GLOB="${glob_endpoint}" + path_found=1 + break + fi + done + + handler=${http__handlers["$HTTP_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} + 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 +} + +http::_response_content_headers() { + echo "Content-Length: $(wc -c "$1" | cut -d ' ' -f 1)" + echo "Content-Type: ${HTTP_RESPONSE_CONTENT_TYPE:="$(file -ib "$1")"}" + echo +} + +http::_response_content() { + local -r content_file="$(http::_ramfile)" # TODO mb adapt to mkfifo pipes + cat - > "${content_file}" + + http::_response_content_headers "${content_file}" + + cat "${content_file}" + rm -f "${content_file}" +} + +http::_static_file() { + local uri_path="${HTTP_REQUEST_PATH#"${HTTP_REQUEST_GLOB%'**'}"}" + local filename="${HTTP_STATIC_FOLDER}/${uri_path}" + [[ -z "${uri_path}" || ! -f "${filename}" ]] && http::response 404 && return 1 + http::file "${filename}" +} + +http::response() { + local status_code page + [[ -n "${HTTP_METHOD_NAMES["$1"]}" ]] && status_code="$1" || status_code="500" + page="$(http::_status_code_page "${status_code}")" + http::_response_base_headers "${status_code}" + http::_response_content <<< "${page}" +} + +http::file() { + [[ ! -f "$1" ]] && http::response 500 && return 1 + http::_response_base_headers "200" + http::_response_content < "$1" +} + +http::html() { HTTP_RESPONSE_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 + + 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::response 404 + return + fi + http::_route_request + return 0 +} + +http::_cmd_help() { + echo "USAGE:" + echo " $0 [OPTIONS]" + echo + echo "ARGUMENTS:" + echo " run Run the server" + echo " parse Parse http request from stdin and output to stdout" +} + +http::run() { + if [[ "$1" = "run" ]]; then + echo "Listening on ${HTTP_HOST}:${HTTP_PORT}" + socat "TCP-LISTEN:${HTTP_PORT},bind=${HTTP_HOST},fork,reuseport" "EXEC:$(readlink -e "$0") parse" + elif [[ "$1" = "parse" ]]; then + http::_process_request + else + http::_cmd_help >&2 + exit 1 + fi +} diff --git a/src/locust_files/locust.conf b/src/locust_files/locust.conf new file mode 100644 index 0000000..7ac7954 --- /dev/null +++ b/src/locust_files/locust.conf @@ -0,0 +1,5 @@ +autostart=1 +headless=1 +host=http://localhost:8081 +users=1024 +spawn-rate=128 diff --git a/src/locust_files/locustfile.py b/src/locust_files/locustfile.py new file mode 100644 index 0000000..11c12b9 --- /dev/null +++ b/src/locust_files/locustfile.py @@ -0,0 +1,5 @@ +from locust import HttpUser, task +class RootUser(HttpUser): + @task + def root(self): + self.client.get("/") diff --git a/src/main.sh b/src/main.sh new file mode 100755 index 0000000..48166d1 --- /dev/null +++ b/src/main.sh @@ -0,0 +1,19 @@ +#!/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 "$@"