Compare commits

...

2 commits

Author SHA1 Message Date
sdomi
ec0c81f9f5 docs: websockets api documentation 2025-02-26 01:22:42 +01:00
sdomi
519fdbe6c8 ws: full hecking rewrite 2025-02-26 01:22:23 +01:00
5 changed files with 205 additions and 119 deletions

View file

@ -21,6 +21,7 @@ cfg[ssl_cert]=''
cfg[ssl_key]=''
cfg[extension]='shs'
cfg[extension_websocket]='shx'
#cfg[encoding]='UTF-8' # UTF-8 by default, used by iconv
cfg[extra_headers]="server: HTTP.sh/$HTTPSH_VERSION (devel)"

76
docs/websockets.md Normal file
View 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.

View file

@ -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
Connection: Upgrade
Upgrade: WebSocket
${cfg[extra_headers]}"
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)
echo "Sec-WebSocket-Accept: "$accept
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 -n "Sec-WebSocket-Accept: $accept"
fi
echo -e "\r\n\r\n"
#echo "Laura is cute <3"
#WebSocket-Location: ws://localhost:1337/
#WebSocket-Origin: http://localhost:1337/\r\n\r\n "
echo -ne "\r\n\r\n"
source ./src/ws.sh
unset IFS
#input=''
#while read -N 1 chr; do
# input=$input$chr
# if [[ $chr == "\r" ]]; then
# break
# fi
#done
exit 0
#while true; do
# read test
# echo $test
# sleep 1
#done
if [[ "${r[status]}" == 101 ]]; then
source "${r[uri]}"
elif [[ "${r[status]}" == 102 ]]; then
source "${r[view]}"
fi

View file

@ -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"
if [[ ${r[status]} != 101 ]]; then
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
r[status]=212
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
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 [[ "${r[status]}" == 101 ]]; then
r[status]=102 # internal, temp
else
r[status]=404
r[status]=212
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
@ -266,12 +268,20 @@ fi
if [[ ${r[status]} == 210 && ${cfg[autoindex]} == true ]]; then
source "src/response/listing.sh"
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
source "src/response/401.sh"
elif [[ ${r[status]} == 404 ]]; then
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"
else
source "src/response/403.sh"

123
src/ws.sh
View file

@ -1,69 +1,70 @@
#!/bin/zsh
read bytes
#!/bin/bash
echo "bytes:" > /dev/stderr
echo $bytes | hexdump -C > /dev/stderr
# _ws_to_varlen(num) -> $num
_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
echo ${data[@]} | hexdump -C > /dev/stderr
# ws_send(payload)
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 ))
echo "Mask (bit):" $mask_bit > /dev/stderr
len=$(( ${data[1]} & 0x7f ))
offset=2 # 2 for header
if [[ $len == 126 ]]; then
len=$(( ${data[2]} << 8 + ${data[3]} ))
offset=4 # 2 for header, 2 for extended length
elif [[ $len == 127 ]]; then
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
bread $len
if [[ $mask_ == 1 ]]; then
val=()
for (( i=0; i<(len*2); i=i+2 )); do
val+=($(( 0x${bres:i:2} ^ 0x${mask[i%8]} )))
done
ws_res="$(printf '%x' ${val[@]} | unhex)" # TODO: variant that doesn't unhex
else
ws_res="$(unhex <<< "$bres")"
fi
}