Commit 27a6dfd5 authored by Dominika Liberda's avatar Dominika Liberda 💽
Browse files

+ advanced URL routing

parent 7d055cca
# 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
## Directory structure (incomplete)
- app (or any other namespace name! - check cfg[namespace])
- webroot/ - place your files **here**
- workers/ - workers live here
- config.sh
- it won't ever throw a 500, thus it fails silently
## 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.
......@@ -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 <<LauraIsCute > "${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> <interval>
# ---
# worker_add example 5
LauraIsCute
......@@ -60,14 +58,41 @@ echo "<h1>Hello from HTTP.sh!</h1><br>To get started with your app, check out $(
<li>$(pwd)/src/ - HTTP.sh src, feel free to poke around :P</li></ul>
&copy; sdomi, selfisekai, ptrcnull - 2020"
LauraIsCute
cat <<PtrcIsCute > "${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 <<PtrcIsCute >&2
_ _ _______ _______ _____ ______ _ _
| | | |_______|_______| _ \/ ___/| | | |
| |__| | | | | | | |_| | |___ | |__| |
| |__| | | | | | | ___/\___ \ | |__| |
| | | | | | | | | | ___\ \| | | |
|_| |_| |_| |_| |_| □ /_____/|_| |_|
PtrcIsCute
if [[ $1 == "debug" ]]; then
cfg[dbg]=true
echo "[DEBUG] Activated debug mode - stderr will be shown"
fi
source src/worker.sh
......
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"
......
#!/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")
}
......@@ -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"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment