From: Paul Hänsch Date: Wed, 4 Nov 2020 12:21:28 +0000 (+0100) Subject: Merge commit '4a0c055c2051583f414362a85dafbf68e4392696' X-Git-Url: https://git.plutz.net/?p=httpchat;a=commitdiff_plain;h=a69240820e57ec23017a672d513a22802fb3edc6;hp=4a0c055c2051583f414362a85dafbf68e4392696 Merge commit '4a0c055c2051583f414362a85dafbf68e4392696' --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22b5907 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +&* +@* +serverkey diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24781a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: _subtrees + +_subtrees: _cgilite + +cgilite: + git subtree add --squash -P $@ https://git.plutz.net/git/$@ master + +_cgilite: cgilite + git subtree pull --squash -P $< https://git.plutz.net/git/$< master diff --git a/cgilite.sh b/cgilite/cgilite.sh similarity index 100% rename from cgilite.sh rename to cgilite/cgilite.sh diff --git a/file.sh b/cgilite/file.sh similarity index 100% rename from file.sh rename to cgilite/file.sh diff --git a/html-sh.sed b/cgilite/html-sh.sed similarity index 100% rename from html-sh.sed rename to cgilite/html-sh.sed diff --git a/logging.sh b/cgilite/logging.sh similarity index 100% rename from logging.sh rename to cgilite/logging.sh diff --git a/session.sh b/cgilite/session.sh similarity index 100% rename from session.sh rename to cgilite/session.sh diff --git a/storage.sh b/cgilite/storage.sh similarity index 100% rename from storage.sh rename to cgilite/storage.sh diff --git a/channel.sh b/channel.sh new file mode 100755 index 0000000..b930620 --- /dev/null +++ b/channel.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +if [ -f "$chatfile" ]; then + read -r channelkey x <"$chatfile" + channelkey="$( printf '%s-%s' "$channelkey" "$SESSION_ID" |sha256sum)" +fi + +case $(POST action) in + create) + if mkdir "${_DATA}/${LOCATION}"; then + { randomid; printf ' '; STRING "$nickname"; echo; } >"$chatfile" + fi + REDIRECT "$(URL "/$LOCATION")" + ;; + submit) + read lasttime x <<-EOFread + $(tail -n 50 "$chatfile" |grep -F " $(STRING "$nickname"): " |tail -n1) + EOFread + if [ "$lasttime" ]; then + lasttime="$(date -d "${lasttime%_*} ${lasttime#*_}" +%s)" + else + lasttime=0 + fi + if [ -f "$chatfile" -a "$channelkey" = "$(POST channelkey)" -a "$(POST timenonce)" -ge "$lasttime" ]; then + printf "%s %s: %s\n" "$(date +%F_%T)" "$(STRING "$nickname")" "$(POST message |STRING)" >>"$chatfile" + fi + REDIRECT "$(URL "/$LOCATION")" + ;; +esac + +if [ ! -f "$chatfile" ]; then + yield_page create <<-EOF + [form #nonexist method="POST" + There is no channel named $(HTML "$LOCATION") + [submit "action" "create" Create] + ] + EOF +else + printf '%s: %s\r\n' Refresh 1 + { printf ' + [form #channel method="POST" + [submit "action" "submit" style="display: none;"] + [hidden "session_key" "%s"][hidden "channelkey" "%s"][hidden "timenonce" "%s"] + [a .settings href="?settings#nick" Settings][input autocomplete="off" name="message" autofocus=true][submit "action" "submit" Send!] + ] + ' "$SESSION_KEY" "$channelkey" "$_DATE" + SHESCAPE='s;[]&<>#."[];\\&;g;' + + while sleep 10; do printf '\n'; done & + printf '[div #chat' + tail --pid $$ -n50 -f "$chatfile" \ + | sed -nuE ' + /^[^ ]+ [^ ]+ [^ ]+$/{ + h; s;^([^ ]+) ([^ ]+) ([^ ]+)$;\1;; s;.*_;;; s;.+;[p .message [span .date &];p; + g; s;^([^ ]+) ([^ ]+) ([^ ]+)$;a\2;; bESC; :A s;.;;; s;(.)(.+);[span .nick [span .indicator \1]\2];p; + g; s;^([^ ]+) ([^ ]+) ([^ ]+)$;b\3;; bESC; :B s;.;;; s;.+;[span .message &]];p; + } + b; :ESC + '"$UNSTRING"' '"$SHESCAPE"' + /^a/bA; /^b/bB; + ' + } |yield_page channel +fi diff --git a/index.cgi b/index.cgi new file mode 100755 index 0000000..f44d288 --- /dev/null +++ b/index.cgi @@ -0,0 +1,87 @@ +#!/bin/sh + +_EXEC=. +_DATA=. +SESSION_TIMEOUT=43200 +. "$_EXEC/cgilite/logging.sh" +. "$_EXEC/cgilite/cgilite.sh" +. "$_EXEC/cgilite/session.sh" +. "$_EXEC/cgilite/storage.sh" + +LOCATION="$(PATH "$PATH_INFO")" +LOCATION="${LOCATION#/}" +LOCATION="${LOCATION%%/*}" + +yield_page(){ + page="$1" + printf '%s\r\n' 'Content-Type: text/html; charset=utf-8' \ + "Content-Security-Policy: script-src 'none'" \ + '' + { printf '[html + [head + [meta name="viewport" content="width=device-width"] + [link rel="stylesheet" type="text/css" href="/webchat.css"] + [title Webchat] + ] [body class="%s" + ' "$page" + [ "$QUERY_STRING" = settings ] && settings_menu + cat + printf '] ]' + } |"$_EXEC/cgilite/html-sh.sed" -u +} + +settings_menu(){ + printf ' + [form #settings method="POST" action="?" + [hidden "session_key" "%s"] + [h1 Settings][a .settings href="?" Close]' + printf ' + [a .section href="#nick" Nickname] + [div #nick [input name="nickname" value="%s"][submit "action" "nick" Set Cookie]] + ' "$SESSION_KEY" "$(HTML "${nickname#\?}")" + printf ' + [a .section href="#register" Register Nickname] + [div #register + [p Registration will set a permanent Cookie in your Browser. + Registration requires neither a password, nor an email address.] + [input name="regnick" value="%s"][submit "action" "register" Register] + ]' "$(HTML "${nickname#\?}")" + printf ']' +} + +. "$_EXEC/usernick.sh" + +case ${LOCATION} in + webchat.css) + . "$_EXEC/cgilite/file.sh" + FILE "$_EXEC/webchat.css" + exit 0 + ;; + \&?*) + chatfile="$_DATA/${LOCATION}/channel" + . "$_EXEC/channel.sh" + exit 0 + ;; + @?*) + if [ -d "$_DATA/${LOCATION}" ]; then + chatfile="$_DATA/${LOCATION}/?${SESSION_ID}" + . "$_EXEC/channel.sh" + else + REDIRECT / + fi + exit 0 + ;; + ~?*) + if [ -d "$_DATA/@${LOCATION#~}" ]; then + pubinfo="$_DATA/@${LOCATION#~}/pubinfo" + else + REDIRECT / + fi + ;; + '') yield_page front <<-EOF + Front + EOF + ;; + *) REDIRECT / + ;; +esac diff --git a/usernick.sh b/usernick.sh new file mode 100755 index 0000000..8d62390 --- /dev/null +++ b/usernick.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +UNAME_VALID=' + # Remove trailing CR, which may have been added by browser + s;\r$;;; + # Collapse white spaces + s;[\r\t\n ]+; ;; + # Remove starting and trailing white spaces + s;^ ;;; s; $;;; + # Usernames starting with & # ? @ + will be invalid + /^[&#?@+]/d; + # Usernames containing a / will be invalid + /\//d; + # Usernames must be between 3 and 24 characters + /...+/!d; /.{25}/d; + # Usernames may not span multiple lines + q;' +username(){ + { [ $# -eq 0 ] && cat || printf %s "$*"; } \ + | sed -E ':X; $!{N;bX;}'"$UNAME_VALID" +} + +nickname="$(COOKIE nick |username)" +if [ ! "$nickname" ]; then + nickname='?Guest' +elif [ ! -d "$_DATA/@$nickname" ]; then + nickname="?$nickname" +else + userclient="$(COOKIE user_client)" + secuid="$(cat "$_DATA/@$nickname/secuid")" + clientid="${userclient%%-*}" + clientid="${clientid}-$(printf '%s%s' "${clientid}" "${secuid}" |sha256sum)" + clientid="${clientid%% *}" + if [ "$clientid" = "$userclient" ]; then + nickname=" $nickname" + SET_COOKIE +"$((86400 * 365))" "user_client=${clientid}" HttpOnly + SET_COOKIE +"$((86400 * 365))" "nick=$(URL "${nickname}")" + else + nickname='?Guest' + fi +fi + +case $(POST action) in + nick) + nick="$(POST nickname |username)" + if [ ! -d "$_DATA/@$nick" ]; then + SET_COOKIE +1209600 "nick=$(POST nickname |URL)" + REDIRECT "$(URL "/$LOCATION")" + else + # ToDo: Return Error Message + REDIRECT "$(URL "/$LOCATION")?settings#nick" + fi + ;; + register) + regnick="$(POST regnick |username)" + userdir="$_DATA/@${regnick}" + if [ "$regnick" ] && mkdir "$userdir"; then + secuid="$(randomid)"; clientid="$(randomid)" + printf %s\\n "$secuid" >"${userdir}/secuid" + clientid="${clientid}-$(printf '%s%s' "${clientid}" "${secuid}" |sha256sum |cut -d\ -f1)" + SET_COOKIE +"$((86400 * 365))" "user_client=${clientid}" HttpOnly + SET_COOKIE +"$((86400 * 365))" "nick=$(URL "${regnick}")" + REDIRECT "$(URL "/$LOCATION")" + else + # ToDo: Return Error Message + REDIRECT "$(URL "/$LOCATION")?settings#register" + fi + ;; +esac diff --git a/webchat.css b/webchat.css new file mode 100644 index 0000000..ff70761 --- /dev/null +++ b/webchat.css @@ -0,0 +1,125 @@ +* { + box-sizing: border-box; + font: normal normal normal medium/1.25 Sans-Serif; + font: normal normal normal normal medium/1.25 Sans-Serif; + text-decoration: none; + margin: 0; padding: 0; + border: none; + color: inherit; +} + +body { + background-color: #FFF; + color: #000; +} + +b, strong { font-weight: bold; } +i, em { font-style: italic; } + +input[type=text], input:not([type]) { + border: 1px solid #08b; + padding: .125ex .5ex; +} +button { + border: outset #DDD; + padding: .125ex 1ex; + background-color: #EEE; +} + +#settings { + display: block; + position: fixed; + min-width: 20%; max-width: 90%; + width: 30em; + top: 3em; + left: 50%; transform: translate(-50%); + background-color: #FFF; + border: 1px solid; + border-radius: 1ex 1ex .5ex .5ex; +} +#settings h1 { + background-color: #08b; + margin: 0; + padding: 0 1ex; + font-size: 1em; + font-weight: bold; + border-bottom: 1px solid; + border-radius: 1ex 1ex 0 0; +} +#settings a.settings { + position: absolute; + top: 0; right: 1px; + background-color: #F88; + border-left: 1px solid; + border-radius: 0 1ex 0 0; + width: 3ex; + overflow: hidden; +} +#settings a.settings:before { + content: "x"; + padding: 0 1ex; +} +#settings a.section { + display: block; + font-weight: bold; + text-decoration: underline; + margin: -1px 1px 0 0; padding: .5ex 1ex; + border-top: 1px solid; + background-color: #EEE; +} +#settings a.section + * { + display: block; + padding: .5ex 1ex 0 1ex; + max-height: .5ex; + overflow: hidden; + transition: max-height .5s; +} +#settings a.section + *:target { + max-height: 20ex; + padding: 1ex 1ex .5ex 1ex; +} +#settings input {margin-right: 1ex;} + +form#channel { + position: fixed; + bottom: 1ex; + left: .5ex; right: .5ex; +} +form#channel a.settings { + display: inline-block; +} +form#channel a.settings:before { + content: '\2699'; + padding: 0 .75ex; + margin-left: .5ex; + margin-right: 2em; +} +form#channel input[name=message] { + display: inline-block; + position: absolute; + right: 0; + width: calc(100% - 4.5ex); +} +form#channel button[value=submit] { display: none; } +#chat .message .date { + color: #888; + font-size: .75em; +} + +#chat { + position: fixed; + bottom: 2em; + left: 0; right: 0; + border: 1px solid #08b; + padding: 1ex; + margin: .5ex; + z-index: -1; +} + +#chat .message .nick { + font-weight: bold; +} + +#chat .message .nick .indicator { + color: #888; +}