Compare commits

..

No commits in common. "ec0c81f9f5aa6df2662515ceaed807dfe83e614c" and "75e6b66973d984e2d577f371c0432aa334b92812" have entirely different histories.

5 changed files with 119 additions and 205 deletions

View file

@ -21,7 +21,6 @@ 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)"

View file

@ -1,76 +0,0 @@
# 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,29 +1,31 @@
#!/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 -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"
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
fi
echo -ne "\r\n\r\n"
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 "
source ./src/ws.sh
unset IFS
if [[ "${r[status]}" == 101 ]]; then
source "${r[uri]}"
elif [[ "${r[status]}" == 102 ]]; then
source "${r[view]}"
fi
#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

View file

@ -167,44 +167,42 @@ echo "$(date) - IP: ${r[ip]}, PROTO: ${r[proto]}, URL: ${r[url]}, GET_data: ${ge
[[ -f "${cfg[namespace]}/routes.sh" ]] && source "${cfg[namespace]}/routes.sh"
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
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
else
r[status]=404
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
@ -268,20 +266,12 @@ fi
if [[ ${r[status]} == 210 && ${cfg[autoindex]} == true ]]; then
source "src/response/listing.sh"
elif [[ ${r[status]} == 200 || ${r[status]} == 212 ]]; then
# 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
source "src/response/200.sh"
elif [[ ${r[status]} == 401 ]]; then
source "src/response/401.sh"
elif [[ ${r[status]} == 404 ]]; then
source "src/response/404.sh"
elif [[ ${r[status]} == 101 || ${r[status]} == 102 ]] &&
[[ ${cfg[websocket_enable]} == true ]]; then
elif [[ ${r[status]} == 101 && ${cfg[websocket_enable]} == true ]]; then
source "src/response/101.sh"
else
source "src/response/403.sh"

123
src/ws.sh
View file

@ -1,70 +1,69 @@
#!/bin/bash
#!/bin/zsh
read bytes
# _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
}
echo "bytes:" > /dev/stderr
echo $bytes | hexdump -C > /dev/stderr
# _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))"
}
readarray -t data <<< $(echo -n $bytes | hexdump -e '/1 "%u\n"')
# ws_send(payload)
ws_send() {
local num out len
len="${#1}"
[[ $len == 0 ]] && return
_ws_to_varlen $len
echo "data:" > /dev/stderr
echo ${data[@]} | hexdump -C > /dev/stderr
( echo -n "81$num" | xxd -p -r; echo -n "$1" )
}
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 "FIN bit:" $(( ${data[0]} >> 7 )) > /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 "opcode:" $(( ${data[0]} & 0x0f )) > /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
echo ${data[0]} > /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
}
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