+ advanced URL routing
parent
7d055ccab1
commit
27a6dfd5ed
69
README.md
69
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.
|
||||
|
|
37
http.sh
37
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 <<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>
|
||||
© 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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue