mirror of
https://git.sakamoto.pl/laudom/http.sh.git
synced 2025-08-10 15:33:34 +02:00
Compare commits
2 commits
75e6b66973
...
ec0c81f9f5
Author | SHA1 | Date | |
---|---|---|---|
|
ec0c81f9f5 | ||
|
519fdbe6c8 |
5 changed files with 205 additions and 119 deletions
|
@ -21,6 +21,7 @@ cfg[ssl_cert]=''
|
||||||
cfg[ssl_key]=''
|
cfg[ssl_key]=''
|
||||||
|
|
||||||
cfg[extension]='shs'
|
cfg[extension]='shs'
|
||||||
|
cfg[extension_websocket]='shx'
|
||||||
#cfg[encoding]='UTF-8' # UTF-8 by default, used by iconv
|
#cfg[encoding]='UTF-8' # UTF-8 by default, used by iconv
|
||||||
cfg[extra_headers]="server: HTTP.sh/$HTTPSH_VERSION (devel)"
|
cfg[extra_headers]="server: HTTP.sh/$HTTPSH_VERSION (devel)"
|
||||||
|
|
||||||
|
|
76
docs/websockets.md
Normal file
76
docs/websockets.md
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
# HTTP.sh and WebSockets
|
||||||
|
|
||||||
|
WebSocket support is currently experimental; It has been rewritten from scratch and verified, but
|
||||||
|
it's a bit rough around the edges. Additionally, you have to manually obtain a copy of libsh
|
||||||
|
before using websockets:
|
||||||
|
|
||||||
|
git clone https://git.sakamoto.pl/domi/libsh src/libsh
|
||||||
|
|
||||||
|
Afterwards, you need to set `cfg[websocket_enable]` to `true` and optionally customize
|
||||||
|
`cfg[extension_websocket]`. Both of those options can be found in `config/master.sh`.
|
||||||
|
|
||||||
|
It is **required** that all of your websocket handling scripts have a different extension than
|
||||||
|
your normal scripts. By default that's set to `shx` (opposed to default `shs` for normal scripts).
|
||||||
|
The way of interfacing with WebSockets is fundamentally different enough that it warrants wrapper
|
||||||
|
functions. While one could create scripts compatible with both, in most cases executing a script
|
||||||
|
with the wrong interface is NOT what we want to happen.
|
||||||
|
|
||||||
|
## The API
|
||||||
|
|
||||||
|
`ws.sh` exposes two main functions: `ws_recv` and `ws_send`. An example use is presented below:
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
while ws_recv; do
|
||||||
|
ws_send "hiii! here's your message: $ws_res"
|
||||||
|
done
|
||||||
|
|
||||||
|
### ws_recv
|
||||||
|
|
||||||
|
`ws_recv` waits for a new message, parses and unmasks it, then puts it into `$ws_res`.
|
||||||
|
This variable name can be overriden by doing `ws_recv varname`, which will then use `$varname` for
|
||||||
|
the payload.
|
||||||
|
|
||||||
|
The return codes are:
|
||||||
|
- 0 for successful read
|
||||||
|
- 1 if EOF is encountered (analogous to builtin `read`)
|
||||||
|
|
||||||
|
### ws_send
|
||||||
|
|
||||||
|
`ws_send <payload>` generates a header and sends the message out.
|
||||||
|
|
||||||
|
## Note about async I/O
|
||||||
|
|
||||||
|
Waiting for a message to arrive before sending out our data is not always useful. Thus, some use of
|
||||||
|
asynchronous I/O is in order. Bash doesn't make this very easy, but it's not impossible:
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
{
|
||||||
|
while sleep 1; do
|
||||||
|
ws_send "a"
|
||||||
|
done
|
||||||
|
} &
|
||||||
|
|
||||||
|
while ws_recv; do
|
||||||
|
ws_send "$ws_res"
|
||||||
|
done
|
||||||
|
|
||||||
|
pkill -P $$
|
||||||
|
|
||||||
|
This spawns a subshell which sends the letter "a". Then, we create a receive loop where we echo
|
||||||
|
everything back. Finally, and most importantly, when that receive loop quits, we kill all children
|
||||||
|
of the current shell process, cleaning up the previous subshell. IF THIS ISN'T DONE, BAD THINGS
|
||||||
|
HAPPEN TO YOUR SERVER - especially if your loop *doesn't* include a sleep like the example.
|
||||||
|
|
||||||
|
With this style of async I/O, you have to take care of the IPC yourself. That is, no variables are
|
||||||
|
shared after the subshell spawns. The best way to handle this is to fully decouple the input and
|
||||||
|
output; If that's impossible, you may use a file approach (with or without notORM).
|
||||||
|
|
||||||
|
## Quirks
|
||||||
|
|
||||||
|
- Currently, anything manually sent to stdout will mangle the bitstream,
|
||||||
|
causing all subsequent I/O to fail.
|
||||||
|
- Endless loops are dangerous if not cleaned up correctly at the end of the file!
|
||||||
|
Additionally, you **need** a receive loop (it can be a dummy, with `:`) if you want to continously
|
||||||
|
send and cleanup after the connection closes; Otherwise there's nothing indicating whether the
|
||||||
|
connection is still alive or not.
|
||||||
|
- There's no way to transmit binary data w/o mangling it. TODO.
|
|
@ -1,31 +1,29 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
[[ ! -f src/libsh/bin/bin.sh ]] && return # no websocket for you! clone the repo
|
||||||
|
source src/libsh/bin/bin.sh
|
||||||
|
|
||||||
|
if [[ "${r[status]}" == 101 && "${r[url_clean]}" != *\.${cfg[extension_websocket]} ]] ||
|
||||||
|
[[ "${r[status]}" == 102 && "${r[view]}" != *\.${cfg[extension_websocket]} ]]; then
|
||||||
|
echo "Rejecting an invalid WebSocket connection." >&2
|
||||||
|
declare -p r >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
echo "HTTP/1.1 101 Web Socket Protocol Handshake
|
echo "HTTP/1.1 101 Web Socket Protocol Handshake
|
||||||
Connection: Upgrade
|
Connection: Upgrade
|
||||||
Upgrade: WebSocket
|
Upgrade: WebSocket
|
||||||
${cfg[extra_headers]}"
|
${cfg[extra_headers]}"
|
||||||
if [[ ${r[websocket_key]} != '' ]]; then
|
if [[ ${r[websocket_key]} != '' ]]; then
|
||||||
accept=$(echo -ne $(echo "${r[websocket_key]}""258EAFA5-E914-47DA-95CA-C5AB0DC85B11" | sha1sum | sed 's/ //g;s/-//g;s/.\{2\}/\\x&/g') | base64)
|
accept=$(echo -ne $(echo -n "${r[websocket_key]}""258EAFA5-E914-47DA-95CA-C5AB0DC85B11" | sha1sum | sed 's/ //g;s/-//g;s/.\{2\}/\\x&/g') | base64)
|
||||||
echo "Sec-WebSocket-Accept: "$accept
|
echo -n "Sec-WebSocket-Accept: $accept"
|
||||||
fi
|
fi
|
||||||
echo -e "\r\n\r\n"
|
echo -ne "\r\n\r\n"
|
||||||
|
|
||||||
#echo "Laura is cute <3"
|
|
||||||
#WebSocket-Location: ws://localhost:1337/
|
|
||||||
#WebSocket-Origin: http://localhost:1337/\r\n\r\n "
|
|
||||||
|
|
||||||
source ./src/ws.sh
|
source ./src/ws.sh
|
||||||
|
unset IFS
|
||||||
|
|
||||||
#input=''
|
if [[ "${r[status]}" == 101 ]]; then
|
||||||
#while read -N 1 chr; do
|
source "${r[uri]}"
|
||||||
# input=$input$chr
|
elif [[ "${r[status]}" == 102 ]]; then
|
||||||
# if [[ $chr == "\r" ]]; then
|
source "${r[view]}"
|
||||||
# break
|
fi
|
||||||
# fi
|
|
||||||
#done
|
|
||||||
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
#while true; do
|
|
||||||
# read test
|
|
||||||
# echo $test
|
|
||||||
# sleep 1
|
|
||||||
#done
|
|
||||||
|
|
|
@ -167,42 +167,44 @@ echo "$(date) - IP: ${r[ip]}, PROTO: ${r[proto]}, URL: ${r[url]}, GET_data: ${ge
|
||||||
|
|
||||||
[[ -f "${cfg[namespace]}/routes.sh" ]] && source "${cfg[namespace]}/routes.sh"
|
[[ -f "${cfg[namespace]}/routes.sh" ]] && source "${cfg[namespace]}/routes.sh"
|
||||||
|
|
||||||
if [[ ${r[status]} != 101 ]]; then
|
for (( i=0; i<${#route[@]}; i=i+3 )); do
|
||||||
for (( i=0; i<${#route[@]}; i=i+3 )); do
|
if [[ "$(grep -Poh "^${route[$((i+1))]}$" <<< "${r[url_clean]}")" != "" ]] || [[ "$(grep -Poh "^${route[$((i+1))]}$" <<< "${r[url_clean]}/")" != "" ]]; then
|
||||||
if [[ "$(grep -Poh "^${route[$((i+1))]}$" <<< "${r[url_clean]}")" != "" ]] || [[ "$(grep -Poh "^${route[$((i+1))]}$" <<< "${r[url_clean]}/")" != "" ]]; then
|
if [[ "${r[status]}" == 101 ]]; then
|
||||||
r[status]=212
|
r[status]=102 # internal, temp
|
||||||
r[view]="${route[$((i+2))]}"
|
|
||||||
IFS='/'
|
|
||||||
url=(${route[$i]})
|
|
||||||
url_=(${r[url_clean]})
|
|
||||||
unset IFS
|
|
||||||
for (( j=0; j<${#url[@]}; j++ )); do
|
|
||||||
# TODO: think about the significance of this if really hard when i'm less tired
|
|
||||||
if [[ ${url_[$j]} != '' && ${url[$j]} == ":"* ]]; then
|
|
||||||
params[${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 [[ "${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
|
else
|
||||||
r[status]=404
|
r[status]=212
|
||||||
fi
|
fi
|
||||||
|
r[view]="${route[$((i+2))]}"
|
||||||
|
IFS='/'
|
||||||
|
url=(${route[$i]})
|
||||||
|
url_=(${r[url_clean]})
|
||||||
|
unset IFS
|
||||||
|
for (( j=0; j<${#url[@]}; j++ )); do
|
||||||
|
# TODO: think about the significance of this if really hard when i'm less tired
|
||||||
|
if [[ ${url_[$j]} != '' && ${url[$j]} == ":"* ]]; then
|
||||||
|
params[${url[$j]/:/}]="${url_[$j]}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
unset IFS
|
||||||
|
if [[ ${r[status]} != 212 && ${r[status]} != 102 ]]; then
|
||||||
|
if [[ -a "${r[uri]}" && ! -r "${r[uri]}" ]]; then
|
||||||
|
r[status]=403
|
||||||
|
elif [[ "${r[uri]}" != "$(realpath "${cfg[namespace]}/${cfg[root]}")"* ]]; then
|
||||||
|
r[status]=403
|
||||||
|
elif [[ -f "${r[uri]}" ]]; then
|
||||||
|
[[ "${r[status]}" != 101 ]] && 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
|
||||||
|
|
||||||
|
@ -266,12 +268,20 @@ fi
|
||||||
if [[ ${r[status]} == 210 && ${cfg[autoindex]} == true ]]; then
|
if [[ ${r[status]} == 210 && ${cfg[autoindex]} == true ]]; then
|
||||||
source "src/response/listing.sh"
|
source "src/response/listing.sh"
|
||||||
elif [[ ${r[status]} == 200 || ${r[status]} == 212 ]]; then
|
elif [[ ${r[status]} == 200 || ${r[status]} == 212 ]]; then
|
||||||
source "src/response/200.sh"
|
# ban the WS executable extensions
|
||||||
|
# default defined here for legacy compat only
|
||||||
|
if [[ "${r[view]}" == *".${cfg[extension_websocket]-shx}" ||
|
||||||
|
"${r[url_clean]}" == *".${cfg[extension_websocket]-shx}" ]]; then
|
||||||
|
source "src/response/403.sh" # TODO: should be 400/500, ig
|
||||||
|
else
|
||||||
|
source "src/response/200.sh"
|
||||||
|
fi
|
||||||
elif [[ ${r[status]} == 401 ]]; then
|
elif [[ ${r[status]} == 401 ]]; then
|
||||||
source "src/response/401.sh"
|
source "src/response/401.sh"
|
||||||
elif [[ ${r[status]} == 404 ]]; then
|
elif [[ ${r[status]} == 404 ]]; then
|
||||||
source "src/response/404.sh"
|
source "src/response/404.sh"
|
||||||
elif [[ ${r[status]} == 101 && ${cfg[websocket_enable]} == true ]]; then
|
elif [[ ${r[status]} == 101 || ${r[status]} == 102 ]] &&
|
||||||
|
[[ ${cfg[websocket_enable]} == true ]]; then
|
||||||
source "src/response/101.sh"
|
source "src/response/101.sh"
|
||||||
else
|
else
|
||||||
source "src/response/403.sh"
|
source "src/response/403.sh"
|
||||||
|
|
123
src/ws.sh
123
src/ws.sh
|
@ -1,69 +1,70 @@
|
||||||
#!/bin/zsh
|
#!/bin/bash
|
||||||
read bytes
|
|
||||||
|
|
||||||
echo "bytes:" > /dev/stderr
|
# _ws_to_varlen(num) -> $num
|
||||||
echo $bytes | hexdump -C > /dev/stderr
|
_ws_to_varlen() {
|
||||||
|
if [[ $1 -lt 126 ]]; then
|
||||||
|
num=$(u8 "$1")
|
||||||
|
elif [[ $1 -ge 126 ]]; then # 2 bytes extended
|
||||||
|
num=7e$(u16 "$1")
|
||||||
|
elif [[ $1 -gt 65535 ]]; then # 8 bytes extended
|
||||||
|
num=7f$(u64 "$1")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
readarray -t data <<< $(echo -n $bytes | hexdump -e '/1 "%u\n"')
|
# _ws_from_varlen(len) -> $len
|
||||||
|
_ws_from_varlen() {
|
||||||
|
if [[ $1 -lt 126 ]]; then
|
||||||
|
len="$1"
|
||||||
|
return
|
||||||
|
elif [[ $1 == 126 ]]; then
|
||||||
|
bread 2
|
||||||
|
elif [[ $1 == 127 ]]; then
|
||||||
|
bread 8
|
||||||
|
else # what
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
len="$((0x$bres))"
|
||||||
|
}
|
||||||
|
|
||||||
echo "data:" > /dev/stderr
|
# ws_send(payload)
|
||||||
echo ${data[@]} | hexdump -C > /dev/stderr
|
ws_send() {
|
||||||
|
local num out len
|
||||||
|
len="${#1}"
|
||||||
|
[[ $len == 0 ]] && return
|
||||||
|
_ws_to_varlen $len
|
||||||
|
|
||||||
|
( echo -n "81$num" | xxd -p -r; echo -n "$1" )
|
||||||
|
}
|
||||||
|
|
||||||
echo "FIN bit:" $(( ${data[0]} >> 7 )) > /dev/stderr
|
ws_recv() {
|
||||||
|
local flags fin opcode mask mask_ len bres
|
||||||
|
if [[ "$1" ]]; then
|
||||||
|
local -n ws_res=$1
|
||||||
|
fi
|
||||||
|
bread 2 || return 1
|
||||||
|
flags=$bres
|
||||||
|
|
||||||
echo "opcode:" $(( ${data[0]} & 0x0f )) > /dev/stderr
|
fin=$(bit_slice $flags 15..16)
|
||||||
|
opcode=$(bit_slice $flags 8..12)
|
||||||
|
mask_=$(bit_slice $flags 7)
|
||||||
|
len=$(bit_slice $flags 0..7)
|
||||||
|
_ws_from_varlen "$len" # check for extended length
|
||||||
|
|
||||||
echo ${data[0]} > /dev/stderr
|
if [[ $mask_ == 1 ]]; then
|
||||||
|
bread 4
|
||||||
|
# split into 4 separate bytes, separate them with dummy items at odd positions
|
||||||
|
# this is saving us a div/mul in the for loop below! :3
|
||||||
|
mask=( ${bres:0:2} . ${bres:2:2} . ${bres:4:2} . ${bres:6:2} )
|
||||||
|
fi
|
||||||
|
|
||||||
mask_bit=$(( ${data[1]} >> 7 ))
|
bread $len
|
||||||
echo "Mask (bit):" $mask_bit > /dev/stderr
|
if [[ $mask_ == 1 ]]; then
|
||||||
|
val=()
|
||||||
len=$(( ${data[1]} & 0x7f ))
|
for (( i=0; i<(len*2); i=i+2 )); do
|
||||||
offset=2 # 2 for header
|
val+=($(( 0x${bres:i:2} ^ 0x${mask[i%8]} )))
|
||||||
|
done
|
||||||
if [[ $len == 126 ]]; then
|
ws_res="$(printf '%x' ${val[@]} | unhex)" # TODO: variant that doesn't unhex
|
||||||
len=$(( ${data[2]} << 8 + ${data[3]} ))
|
else
|
||||||
offset=4 # 2 for header, 2 for extended length
|
ws_res="$(unhex <<< "$bres")"
|
||||||
elif [[ $len == 127 ]]; then
|
fi
|
||||||
len=0
|
}
|
||||||
for i in {0..8}; do
|
|
||||||
len=$(( $len << 8 + ${data[2+i]} ))
|
|
||||||
done
|
|
||||||
offset=10 # 2 for header, 8 for extended length
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Data length:" $len > /dev/stderr
|
|
||||||
|
|
||||||
echo "Offset:" $offset > /dev/stderr
|
|
||||||
|
|
||||||
if [[ $mask_bit == 1 ]]; then
|
|
||||||
read -ra mask <<< ${data[@]:$offset:4}
|
|
||||||
|
|
||||||
echo "Mask:" $mask > /dev/stderr
|
|
||||||
|
|
||||||
offset=$(( $offset + 4 ))
|
|
||||||
fi
|
|
||||||
|
|
||||||
read -ra payload <<< ${data[@]:$offset:$len}
|
|
||||||
|
|
||||||
echo "Payload:" ${payload[@]} > /dev/stderr
|
|
||||||
|
|
||||||
if [[ $mask_bit == 1 ]]; then
|
|
||||||
for ((i = 0; i < $len; i++)); do
|
|
||||||
byte=$(( ${payload[$i]} ^ ${mask[$i % 4]} ))
|
|
||||||
echo "Byte $i: $byte" > /dev/stderr
|
|
||||||
payload[$i]=$byte
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Payload unmasked:" ${payload[@]} > /dev/stderr
|
|
||||||
fi
|
|
||||||
|
|
||||||
payload_hex=""
|
|
||||||
for ((i = 0; i < $len; i++)); do
|
|
||||||
hex=$(printf '\\x%x\n' $(( ${payload[$i]} )))
|
|
||||||
payload_hex+=$hex
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Payload hex:" $payload_hex > /dev/stderr
|
|
||||||
echo -e "Payload: $payload_hex" > /dev/stderr
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue