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 "$@"