mirror of
https://git.sakamoto.pl/laudom/http.sh.git
synced 2025-08-05 05:13:32 +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[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
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
|
||||
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
|
||||
|
|
|
@ -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
123
src/ws.sh
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue