From 27a6dfd5ed2c8ed27fde9b37e27035ffe0661a6c Mon Sep 17 00:00:00 2001 From: Dominika Liberda Date: Sun, 14 Feb 2021 04:20:41 +0100 Subject: [PATCH] + advanced URL routing --- README.md | 69 ++++++++++++++++++++++++++++++++------------- http.sh | 37 ++++++++++++++++++++---- src/response/200.sh | 24 +++++++++++----- src/route.sh | 8 ++++++ src/server.sh | 64 +++++++++++++++++++++++++++-------------- 5 files changed, 149 insertions(+), 53 deletions(-) create mode 100755 src/route.sh diff --git a/README.md b/README.md index 62cabb5..8f76c3a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ # HTTP.sh Node.js, but `| sed s/Node/HTTP/;s/js/sh/`. -Launch with `./http.sh`. Does not need root priviliges ~~- in fact, DO NOT run it as superuser~~ unless you're running it on ports lower than 1024. If you're running on 80 and 443, superuser is more or less mandatory, but THIS MAY BE UNSAFE. +HTTP.sh is (by far) the most extensible attempt at creating a web framework in Bash, and (AFAIK) the only one that's actively maintained. Although I strive for code quality, this is still rather experimental and may contain bugs. -To prevent running malicious scripts, by default only scripts with extension `.shs` can be run by the server, but this can be changed in the config. Also, cfg[index] ignores this. SHS stands for Shell Server. - -Originally made for Junction Stupidhack 2020; Created by [sdomi](https://sakamoto.pl/), [selfisekai](https://selfisekai.rocks/) and [ptrcnull](https://ptrcnull.me/). +Originally made for Junction Stupidhack 2020; Created by [sdomi](https://sakamoto.pl/), [ptrcnull](https://ptrcnull.me/) and [selfisekai](https://selfisekai.rocks/). ## Quick Start @@ -25,8 +23,9 @@ git clone https://git.sakamoto.pl/laudom/ocw/ app # example repo :P ## Dependencies -- Bash or 100% compatible shell -- [Ncat](https://nmap.org/ncat) +- Bash (4.x should work, but we'll need 5.0 soon) +- [Ncat](https://nmap.org/ncat), not openbsd-nc, not netcat, not nc +- socat (because the above is slightly broken) - pkill - mktemp - jq (probably not needed just yet, but it will be in 1.0) @@ -38,17 +37,19 @@ git clone https://git.sakamoto.pl/laudom/ocw/ app # example repo :P - can't change the HTTP status code from Shell Server scripts. This could theoretically be done with custom vhost configs and some `if` statements, but this would be a rather nasty solution to that problem. - `$post_multipart` doesn't keep original names - could be fixed by parsing individual headers from the multipart request instead of skipping them all +- it won't ever throw a 500, thus it fails silently -## Directory structure (incomplete) -- app (or any other namespace name! - check cfg[namespace]) - - webroot/ - place your files **here** - - workers/ - workers live here - - config.sh +## Directory structure +- ${cfg[namespace]} (`app` by default) + - ${cfg[root]} (`webroot` by default) - public application root + - workers/ - scripts that execute periodically live there (see examples) + - views/ - for use with HTTP.sh router + - config.sh - application-level config file - config - - master.sh - main config file, loaded with every request - - localhost:1337 - example vhost file, loaded if `Host: ...` equals its name + - master.sh - main server config file - loaded on boot and with every request + - host:port - if a file matching the Host header is found, HTTP.sh will load it request-wide - src - - server source files and modules (e.g. `ws.sh`) + - server source files and modules - response - files corresponding to specific HTTP status codes - listing.sh (code 210) is actually HTTP 200, but triggered in a directory with autoindex turned on and without a valid `index.shs` file @@ -60,8 +61,38 @@ git clone https://git.sakamoto.pl/laudom/ocw/ app # example repo :P ![](https://f.sakamoto.pl/d6584c01-1c48-42b9-935b-d9a89af4e071file_101.jpg) -- $post_data - array, contains data from urlencoded POSTs -- $post_multipart - array, contains URIs to uploaded files from multipart/form-data POSTs -- $get_data - array, contains data from GETs -- $cfg - array, contains config values (from master.sh and vhost configs) -- $r - array, contains data generated from the request - URI, URL and that kinda stuff. +- get_data - holds data from GET parameters + - /?test=asdf -> `${get_data[test]}` == `"asdf"` +- params - holds parsed data from URL router + - /profile/test (assuming profile/:name) -> `${params[name]}` == `"test"` +- post_data - same as above, but for urlencoded POST params + - test=asdf -> `${post_data[test]}` == `"asdf"` +- post_multipart - contains paths to uploaded files from multipart/form-data POST requests. **WARNING**: it doesn't hold field names yet, it relies on upload order for identification + - first file (in upload order) -> `cat ${post_multipart[0]}` + - second file -> `cat ${post_multipart[1]}` +- r - misc request data + - authorization + - content_boundary + - content_boundary + - content_length + - content_type + - headers + - host + - host_portless + - ip + - post + - proto + - status + - uri + - url + - user_agent + - view + - websocket_key +- cfg - server and app config - see `config/master.sh` for more details + +## Fun stuff + +- To prevent running malicious scripts, by default only scripts with extension `.shs` can be run by the server, but this can be changed in the config. +- ${cfg[index]} ignores the above - see config/master.sh +- Trans rights! +- SHS stands for Shell Server. diff --git a/http.sh b/http.sh index 16e36e0..680b0cc 100755 --- a/http.sh +++ b/http.sh @@ -13,7 +13,6 @@ if [[ ! -f "$(pwd)/http.sh" ]]; then exit 1 fi -echo "-= HTTP.sh =-" for i in $(cat src/dependencies.required); do which $i > /dev/null 2>&1 @@ -33,14 +32,13 @@ if [[ $error == true ]]; then fi if [[ $1 == "init" ]]; then # will get replaced with proper parameter parsing in 1.0 - mkdir -p "${cfg[namespace]}/${cfg[root]}" "${cfg[namespace]}/workers/example" + mkdir -p "${cfg[namespace]}/${cfg[root]}" "${cfg[namespace]}/workers/example" "${cfg[namespace]}/views" touch "${cfg[namespace]}/config.sh" "${cfg[namespace]}/workers/example/control" cat < "${cfg[namespace]}/config.sh" -# app config - loaded on server bootup -# your application-specific config goes here! +## app config +## your application-specific config goes here! + -# worker_add -# --- # worker_add example 5 LauraIsCute @@ -60,14 +58,41 @@ echo "

Hello from HTTP.sh!


To get started with your app, check out $(
  • $(pwd)/src/ - HTTP.sh src, feel free to poke around :P
  • © sdomi, selfisekai, ptrcnull - 2020" LauraIsCute + cat < "${cfg[namespace]}/routes.sh" +## routes - application-specific routes +## +## HTTP.sh supports both serving files using a directory structure (webroot), +## and using routes. The latter may come in handy if you want to create nicer +## paths, e.g. +## +## (webroot) https://example.com/profile.shs?name=ptrcnull +## ... may become ... +## (routes) https://example.com/profile/ptrcnull +## +## To set up routes, define rules in this file (see below for examples) + +# router "/test" "app/views/test.shs" +# router "/profile/:user" "app/views/user.shs" +PtrcIsCute + chmod +x "${cfg[namespace]}/workers/example/worker.sh" echo -e "Success..?\nTry running ./http.sh now" exit 0 fi +cat <&2 + _ _ _______ _______ _____ ______ _ _ +| | | |_______|_______| _ \/ ___/| | | | +| |__| | | | | | | |_| | |___ | |__| | +| |__| | | | | | | ___/\___ \ | |__| | +| | | | | | | | | | ___\ \| | | | +|_| |_| |_| |_| |_| □ /_____/|_| |_| +PtrcIsCute + if [[ $1 == "debug" ]]; then cfg[dbg]=true + echo "[DEBUG] Activated debug mode - stderr will be shown" fi source src/worker.sh diff --git a/src/response/200.sh b/src/response/200.sh index 825092e..06beddd 100755 --- a/src/response/200.sh +++ b/src/response/200.sh @@ -1,23 +1,33 @@ printf "HTTP/1.0 200 OK ${cfg[extra_headers]}\r\n" -get_mime "${r[uri]}" -[[ "$mimetype" != '' ]] && printf "content-type: $mimetype\r\n" -if [[ ${cfg[php_enabled]} == true && ${r[uri]} =~ ".php" ]]; then +if [[ ${r[status]} == 200 ]]; then + get_mime "${r[uri]}" + [[ "$mimetype" != '' ]] && printf "content-type: $mimetype\r\n" +fi + +if [[ ${r[status]} == 212 ]]; then + temp=$(mktemp) + source "${r[view]}" > $temp + [[ "${r[headers]}" != '' ]] && printf "${r[headers]}\r\n\r\n" || printf "\r\n" + cat $temp + rm $temp + +elif [[ "${cfg[php_enabled]}" == true && "${r[uri]}" =~ ".php" ]]; then temp=$(mktemp) php "${r[uri]}" "$(get_dump)" "$(post_dump)" > $temp - [[ ${r[headers]} != '' ]] && printf "${r[headers]}\r\n\r\n" || printf "\r\n" + [[ "${r[headers]}" != '' ]] && printf "${r[headers]}\r\n\r\n" || printf "\r\n" cat $temp rm $temp -elif [[ ${cfg[python_enabled]} == true && ${r[uri]} =~ ".py" ]]; then +elif [[ "${cfg[python_enabled]}" == true && "${r[uri]}" =~ ".py" ]]; then temp=$(mktemp) python "${r[uri]}" "$(get_dump)" "$(post_dump)" > $temp - [[ ${r[headers]} != '' ]] && printf "${r[headers]}\r\n\r\n" || printf "\r\n" + [[ "${r[headers]}" != '' ]] && printf "${r[headers]}\r\n\r\n" || printf "\r\n" cat $temp rm $temp -elif [[ ${r[uri]} =~ \.${cfg[extension]}$ ]]; then +elif [[ "${r[uri]}" =~ \.${cfg[extension]}$ ]]; then temp=$(mktemp) source "${r[uri]}" > $temp [[ "${r[headers]}" != '' ]] && printf "${r[headers]}\r\n\r\n" || printf "\r\n" diff --git a/src/route.sh b/src/route.sh new file mode 100755 index 0000000..6a651c0 --- /dev/null +++ b/src/route.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# router(uri, path) +function router() { + route+=("$1") + route+=("$(sed -E 's/:[A-Za-z0-9]+/[A-Za-z0-9.,%:-_]+/g' <<< "$1")") + route+=("$2") +} diff --git a/src/server.sh b/src/server.sh index baa3124..e489555 100755 --- a/src/server.sh +++ b/src/server.sh @@ -4,12 +4,16 @@ source src/mime.sh source src/misc.sh source src/account.sh source src/mail.sh +source src/route.sh [[ -f "${cfg[namespace]}/config.sh" ]] && source "${cfg[namespace]}/config.sh" - +[[ -f "${cfg[namespace]}/routes.sh" ]] && source "${cfg[namespace]}/routes.sh" declare -A r # current request / response declare -A meta # metadata for templates declare -A cookies # cookies! +declare -A get_data # all GET params +declare -A post_data # all POST params +declare -A params # parsed router data r[status]=210 # Mommy always said that I was special post_length=0 @@ -73,7 +77,6 @@ while read param; do data="$(echo ${r[url]} | sed -E 's/^(.*)\?//;s/\&/ /g')" if [[ "$data" != "${r[url]}" ]]; then data="$(echo ${r[url]} | sed -E 's/^(.*)\?//')" - declare -A get_data IFS='&' for i in $data; do name="$(echo $i | sed -E 's/\=(.*)$//')" @@ -89,7 +92,6 @@ while read param; do data="$(sed -E 's/^(.*)\?//;s/\&/ /g' <<< "${r[url]}")" if [[ "$data" != "${r[url]}" ]]; then data="$(sed -E 's/^(.*)\?//' <<< "${r[url]}")" - declare -A get_data IFS='&' for i in $data; do name="$(echo $i | sed -E 's/\=(.*)$//')" @@ -114,24 +116,45 @@ fi echo "$(date) - IP: ${r[ip]}, PROTO: ${r[proto]}, URL: ${r[url]}, GET_data: ${get_data[@]}, POST_data: ${post_data[@]}, POST_multipart: ${post_multipart[@]}" >> "${cfg[namespace]}/${cfg[log]}" if [[ ${r[status]} != 101 ]]; then - if [[ -a ${r[uri]} && ! -r ${r[uri]} ]]; then - r[status]=403 - elif [[ "$(echo -n ${r[uri]})" != "$(realpath "${cfg[namespace]}/${cfg[root]}")"* ]]; then - r[status]=403 - elif [[ -f ${r[uri]} ]]; then - r[status]=200 - elif [[ -d ${r[uri]} ]]; then - for name in ${cfg[index]}; do - if [[ -f "${r[uri]}/$name" ]]; then - r[uri]="${r[uri]}/$name" - r[status]=200 - fi - done - else - r[status]=404 + for (( i=0; i<${#route[@]}; i=i+3 )); do + if [[ "$(grep -Poh "${route[$((i+1))]}$" <<< "${r[url]}")" != "" ]]; then + r[status]=212 + r[view]="${route[$((i+2))]}" + IFS='/' + url=(${route[$i]}) + url_=(${r[url]}) + unset IFS + for (( j=0; j<${#url[@]}; j++ )); do + if [[ ${url_[$j]} != '' ]]; then + params[$(sed 's/://' <<< "${url[$j]}")]="${url_[$j]}" + fi + done + break + fi + done + unset IFS + if [[ ${r[status]} != 212 ]]; then + if [[ -a "${r[uri]}" && ! -r "${r[uri]}" ]]; then + r[status]=403 + elif [[ "$(echo -n "${r[uri]}")" != "$(realpath "${cfg[namespace]}/${cfg[root]}")"* ]]; then + r[status]=403 + elif [[ -f "${r[uri]}" ]]; then + r[status]=200 + elif [[ -d "${r[uri]}" ]]; then + for name in ${cfg[index]}; do + if [[ -f "${r[uri]}/$name" ]]; then + r[uri]="${r[uri]}/$name" + r[status]=200 + fi + done + else + r[status]=404 + fi fi fi +echo "${r[url]}" >&2 + if [[ ${cfg[auth_required]} == true && ${r[authorized]} != true ]]; then echo "Auth failed." >> ${cfg[log_misc]} r[status]=401 @@ -173,8 +196,7 @@ if [[ ${r[post]} == true && ${r[status]} == 200 ]]; then rm $tmpfile else read -N "${r[content_length]}" data - declare -A post_data - + IFS='&' for i in $(tr -d '\n' <<< "$data"); do name="$(sed -E 's/\=(.*)$//' <<< "$i")" @@ -189,7 +211,7 @@ if [[ ${r[status]} == 210 && ${cfg[autoindex]} == true ]]; then source "src/response/listing.sh" elif [[ ${r[status]} == 211 ]]; then source "src/response/proxy.sh" -elif [[ ${r[status]} == 200 ]]; then +elif [[ ${r[status]} == 200 || ${r[status]} == 212 ]]; then source "src/response/200.sh" elif [[ ${r[status]} == 401 ]]; then source "src/response/401.sh"