+ advanced URL routing

merge-requests/2/head
Dominika Liberda 2021-02-14 04:20:41 +01:00
parent 7d055ccab1
commit 27a6dfd5ed
5 changed files with 149 additions and 53 deletions

View File

@ -1,11 +1,9 @@
# HTTP.sh # HTTP.sh
Node.js, but `| sed s/Node/HTTP/;s/js/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/), [ptrcnull](https://ptrcnull.me/) and [selfisekai](https://selfisekai.rocks/).
Originally made for Junction Stupidhack 2020; Created by [sdomi](https://sakamoto.pl/), [selfisekai](https://selfisekai.rocks/) and [ptrcnull](https://ptrcnull.me/).
## Quick Start ## Quick Start
@ -25,8 +23,9 @@ git clone https://git.sakamoto.pl/laudom/ocw/ app # example repo :P
## Dependencies ## Dependencies
- Bash or 100% compatible shell - Bash (4.x should work, but we'll need 5.0 soon)
- [Ncat](https://nmap.org/ncat) - [Ncat](https://nmap.org/ncat), not openbsd-nc, not netcat, not nc
- socat (because the above is slightly broken)
- pkill - pkill
- mktemp - mktemp
- jq (probably not needed just yet, but it will be in 1.0) - 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. - 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 - `$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) ## Directory structure
- app (or any other namespace name! - check cfg[namespace]) - ${cfg[namespace]} (`app` by default)
- webroot/ - place your files **here** - ${cfg[root]} (`webroot` by default) - public application root
- workers/ - workers live here - workers/ - scripts that execute periodically live there (see examples)
- config.sh - views/ - for use with HTTP.sh router
- config.sh - application-level config file
- config - config
- master.sh - main config file, loaded with every request - master.sh - main server config file - loaded on boot and with every request
- localhost:1337 - example vhost file, loaded if `Host: ...` equals its name - host:port - if a file matching the Host header is found, HTTP.sh will load it request-wide
- src - src
- server source files and modules (e.g. `ws.sh`) - server source files and modules
- response - response
- files corresponding to specific HTTP status codes - 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 - 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) ![](https://f.sakamoto.pl/d6584c01-1c48-42b9-935b-d9a89af4e071file_101.jpg)
- $post_data - array, contains data from urlencoded POSTs - get_data - holds data from GET parameters
- $post_multipart - array, contains URIs to uploaded files from multipart/form-data POSTs - /?test=asdf -> `${get_data[test]}` == `"asdf"`
- $get_data - array, contains data from GETs - params - holds parsed data from URL router
- $cfg - array, contains config values (from master.sh and vhost configs) - /profile/test (assuming profile/:name) -> `${params[name]}` == `"test"`
- $r - array, contains data generated from the request - URI, URL and that kinda stuff. - 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.

37
http.sh
View File

@ -13,7 +13,6 @@ if [[ ! -f "$(pwd)/http.sh" ]]; then
exit 1 exit 1
fi fi
echo "-= HTTP.sh =-"
for i in $(cat src/dependencies.required); do for i in $(cat src/dependencies.required); do
which $i > /dev/null 2>&1 which $i > /dev/null 2>&1
@ -33,14 +32,13 @@ if [[ $error == true ]]; then
fi fi
if [[ $1 == "init" ]]; then # will get replaced with proper parameter parsing in 1.0 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" touch "${cfg[namespace]}/config.sh" "${cfg[namespace]}/workers/example/control"
cat <<LauraIsCute > "${cfg[namespace]}/config.sh" cat <<LauraIsCute > "${cfg[namespace]}/config.sh"
# app config - loaded on server bootup ## app config
# your application-specific config goes here! ## your application-specific config goes here!
# worker_add <worker> <interval>
# ---
# worker_add example 5 # worker_add example 5
LauraIsCute 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> <li>$(pwd)/src/ - HTTP.sh src, feel free to poke around :P</li></ul>
&copy; sdomi, selfisekai, ptrcnull - 2020" &copy; sdomi, selfisekai, ptrcnull - 2020"
LauraIsCute 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" chmod +x "${cfg[namespace]}/workers/example/worker.sh"
echo -e "Success..?\nTry running ./http.sh now" echo -e "Success..?\nTry running ./http.sh now"
exit 0 exit 0
fi fi
cat <<PtrcIsCute >&2
_ _ _______ _______ _____ ______ _ _
| | | |_______|_______| _ \/ ___/| | | |
| |__| | | | | | | |_| | |___ | |__| |
| |__| | | | | | | ___/\___ \ | |__| |
| | | | | | | | | | ___\ \| | | |
|_| |_| |_| |_| |_| □ /_____/|_| |_|
PtrcIsCute
if [[ $1 == "debug" ]]; then if [[ $1 == "debug" ]]; then
cfg[dbg]=true cfg[dbg]=true
echo "[DEBUG] Activated debug mode - stderr will be shown"
fi fi
source src/worker.sh source src/worker.sh

View File

@ -1,23 +1,33 @@
printf "HTTP/1.0 200 OK printf "HTTP/1.0 200 OK
${cfg[extra_headers]}\r\n" ${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) temp=$(mktemp)
php "${r[uri]}" "$(get_dump)" "$(post_dump)" > $temp 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 cat $temp
rm $temp rm $temp
elif [[ ${cfg[python_enabled]} == true && ${r[uri]} =~ ".py" ]]; then elif [[ "${cfg[python_enabled]}" == true && "${r[uri]}" =~ ".py" ]]; then
temp=$(mktemp) temp=$(mktemp)
python "${r[uri]}" "$(get_dump)" "$(post_dump)" > $temp 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 cat $temp
rm $temp rm $temp
elif [[ ${r[uri]} =~ \.${cfg[extension]}$ ]]; then elif [[ "${r[uri]}" =~ \.${cfg[extension]}$ ]]; then
temp=$(mktemp) temp=$(mktemp)
source "${r[uri]}" > $temp source "${r[uri]}" > $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"

8
src/route.sh Executable file
View File

@ -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")
}

View File

@ -4,12 +4,16 @@ source src/mime.sh
source src/misc.sh source src/misc.sh
source src/account.sh source src/account.sh
source src/mail.sh source src/mail.sh
source src/route.sh
[[ -f "${cfg[namespace]}/config.sh" ]] && source "${cfg[namespace]}/config.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 r # current request / response
declare -A meta # metadata for templates declare -A meta # metadata for templates
declare -A cookies # cookies! 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 r[status]=210 # Mommy always said that I was special
post_length=0 post_length=0
@ -73,7 +77,6 @@ while read param; do
data="$(echo ${r[url]} | sed -E 's/^(.*)\?//;s/\&/ /g')" data="$(echo ${r[url]} | sed -E 's/^(.*)\?//;s/\&/ /g')"
if [[ "$data" != "${r[url]}" ]]; then if [[ "$data" != "${r[url]}" ]]; then
data="$(echo ${r[url]} | sed -E 's/^(.*)\?//')" data="$(echo ${r[url]} | sed -E 's/^(.*)\?//')"
declare -A get_data
IFS='&' IFS='&'
for i in $data; do for i in $data; do
name="$(echo $i | sed -E 's/\=(.*)$//')" name="$(echo $i | sed -E 's/\=(.*)$//')"
@ -89,7 +92,6 @@ while read param; do
data="$(sed -E 's/^(.*)\?//;s/\&/ /g' <<< "${r[url]}")" data="$(sed -E 's/^(.*)\?//;s/\&/ /g' <<< "${r[url]}")"
if [[ "$data" != "${r[url]}" ]]; then if [[ "$data" != "${r[url]}" ]]; then
data="$(sed -E 's/^(.*)\?//' <<< "${r[url]}")" data="$(sed -E 's/^(.*)\?//' <<< "${r[url]}")"
declare -A get_data
IFS='&' IFS='&'
for i in $data; do for i in $data; do
name="$(echo $i | sed -E 's/\=(.*)$//')" 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]}" 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 [[ ${r[status]} != 101 ]]; then
if [[ -a ${r[uri]} && ! -r ${r[uri]} ]]; then for (( i=0; i<${#route[@]}; i=i+3 )); do
r[status]=403 if [[ "$(grep -Poh "${route[$((i+1))]}$" <<< "${r[url]}")" != "" ]]; then
elif [[ "$(echo -n ${r[uri]})" != "$(realpath "${cfg[namespace]}/${cfg[root]}")"* ]]; then r[status]=212
r[status]=403 r[view]="${route[$((i+2))]}"
elif [[ -f ${r[uri]} ]]; then IFS='/'
r[status]=200 url=(${route[$i]})
elif [[ -d ${r[uri]} ]]; then url_=(${r[url]})
for name in ${cfg[index]}; do unset IFS
if [[ -f "${r[uri]}/$name" ]]; then for (( j=0; j<${#url[@]}; j++ )); do
r[uri]="${r[uri]}/$name" if [[ ${url_[$j]} != '' ]]; then
r[status]=200 params[$(sed 's/://' <<< "${url[$j]}")]="${url_[$j]}"
fi fi
done done
else break
r[status]=404 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
fi fi
echo "${r[url]}" >&2
if [[ ${cfg[auth_required]} == true && ${r[authorized]} != true ]]; then if [[ ${cfg[auth_required]} == true && ${r[authorized]} != true ]]; then
echo "Auth failed." >> ${cfg[log_misc]} echo "Auth failed." >> ${cfg[log_misc]}
r[status]=401 r[status]=401
@ -173,8 +196,7 @@ if [[ ${r[post]} == true && ${r[status]} == 200 ]]; then
rm $tmpfile rm $tmpfile
else else
read -N "${r[content_length]}" data read -N "${r[content_length]}" data
declare -A post_data
IFS='&' IFS='&'
for i in $(tr -d '\n' <<< "$data"); do for i in $(tr -d '\n' <<< "$data"); do
name="$(sed -E 's/\=(.*)$//' <<< "$i")" name="$(sed -E 's/\=(.*)$//' <<< "$i")"
@ -189,7 +211,7 @@ if [[ ${r[status]} == 210 && ${cfg[autoindex]} == true ]]; then
source "src/response/listing.sh" source "src/response/listing.sh"
elif [[ ${r[status]} == 211 ]]; then elif [[ ${r[status]} == 211 ]]; then
source "src/response/proxy.sh" source "src/response/proxy.sh"
elif [[ ${r[status]} == 200 ]]; then elif [[ ${r[status]} == 200 || ${r[status]} == 212 ]]; then
source "src/response/200.sh" source "src/response/200.sh"
elif [[ ${r[status]} == 401 ]]; then elif [[ ${r[status]} == 401 ]]; then
source "src/response/401.sh" source "src/response/401.sh"