From: Paul Hänsch Date: Thu, 19 Nov 2020 01:32:51 +0000 (+0100) Subject: Merge commit 'b4afadf46825c28910365f4e6dce5ec20f6e1341' X-Git-Url: https://git.plutz.net/?p=httpchat;a=commitdiff_plain;h=7dbb8789442cb924b7c3ed4d7126ef9b3173ffc5;hp=b4afadf46825c28910365f4e6dce5ec20f6e1341 Merge commit 'b4afadf46825c28910365f4e6dce5ec20f6e1341' --- 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..0f1bda7 --- /dev/null +++ b/channel.sh @@ -0,0 +1,99 @@ +#!/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 + +nicklist(){ + local nickfile="${chatfile%/channel}/nicks" + case $1 in + enter) + trap 'sed -i -E "/^${SESSION_KEY%%-*} $$ /d" "$nickfile"' INT QUIT + sed -i -E "/^${SESSION_KEY%%-*} /d" "$nickfile" + printf '%s %i %s\n' "${SESSION_KEY%%-*}" "$$" "$nickname" >>"$nickfile" + ;; + leave) + sed -i -E "/^${SESSION_KEY%%-*} $$ /d" "$nickfile" + ;; + esac + + nicklist='NICKNAMES: ' + while read -r s p nick; do + nicklist="${nicklist}/$nick/" + done <"$nickfile" + if ! tail -n20 "$chatfile" |grep -qxF "$nicklist"; then + printf '%s\n' "$nicklist" >>"$chatfile" + fi +} + +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 + nicklist enter + + 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 ' + /^[0-9-]{10}_[0-9:]{8}+ [^ ]+ [^ ]+$/{ + 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; + } + /^NICKNAMES: .*/{ + s;^NICKNAMES: ;;; h; s;^.*$;[div .nicklist [h2 Nicknames];p; g; + :NICKLIST + h; s;^/([^/]+)/.*$;c\1;; bESC :C + s;^.([^?])(.*)$;[a .nick href="/~\1\2" \1\2];p; + s;^.(\?)(.*)$;[span .nick [span .indcator \1]\2];p; + g; s;/[^/]+/;;; /.+/bNICKLIST s;^.*$;];p; + b; + } + b; :ESC + '"$UNSTRING"' '"$SHESCAPE"' + /^a/bA; /^b/bB; /^c/bC; + ' + } |yield_page channel + + nicklist leave +fi diff --git a/index.cgi b/index.cgi new file mode 100755 index 0000000..ac9a110 --- /dev/null +++ b/index.cgi @@ -0,0 +1,100 @@ +#!/bin/sh + +_EXEC="${_EXEC:-.}" +_DATA="${_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%%/*}" + +NICK_REGISTRATION="${NICK_REGISTRATION:-on}" + +# ToDo: +# COOKIE_NICK_EXPIRE=$((86400 * 14)) +# REGEISTERED_NICK_EXPIRE=$((86400 * 365)) + +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="?settings" + [hidden "session_key" "%s"] + [h1 Settings][a .settings href="?" Close] + ' "$SESSION_KEY" + if [ "$ERROR" ]; then + printf '[p .error %s %s]' "${ERROR%% *}" "$(HTML "${ERROR#.* }")" + unset ERROR + fi + printf ' + [a .section href="#nick" Nickname] + [div #nick [input name="nickname" placeholder="%s"][submit "action" "nick" Set Cookie]] + ' "$(HTML "${nickname#\?}")" + [ "$NICK_REGISTRATION" = on -a "$nickname" != '?Guest' ] && 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/${LOCATION}" + return 0 + ;; + \&?*) + [ "$(COOKIE nick)" -o "$QUERY_STRING" = settings ] || REDIRECT "/$LOCATION?settings#nick" + 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 + # ToDo Edit / Display of public user information + REDIRECT / + fi + ;; + '') yield_page front <<-EOF + Front + EOF + ;; + *) REDIRECT / + ;; +esac diff --git a/usernick.sh b/usernick.sh new file mode 100755 index 0000000..13759ee --- /dev/null +++ b/usernick.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +UNAME_VALID=' + # Remove trailing CR, which may have been added by browser + s;\r$;;; + # Collapse white spaces + s;[\r\t\n ]+; ;g; + # 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 + export ERROR=".nick Your current nickname has been registered elsewhere." + QUERY_STRING=settings + nickname='?Guest' + fi +fi + +case $(POST action) in + nick) + nick="$(POST nickname |username)" + if [ -d "$_DATA/@$nick" ]; then + export ERROR=".nick This nickname has already been registered elsewhere." + elif [ -z "$(POST nickname)" ]; then + SET_COOKIE +1209600 "nick=$(URL "${nickname#\?}")" + REDIRECT "$(URL "/$LOCATION")" + elif [ -z "$nick" ]; then + export ERROR='.nick Nicknames must be between 3 and 24 characters long. They may not start with "&", "#", "?", "@", or "+" and they must not contain a "/".' + else + SET_COOKIE +1209600 "nick=$(URL $nick)" + REDIRECT "$(URL "/$LOCATION")" + fi + ;; + register) + regnick="$(POST regnick |username)" + userdir="$_DATA/@${regnick}" + if [ "$NICK_REGISTRATION" != on ]; then + export ERROR='.register Nickname registration is disabled on this server.' + elif [ "$regnick" = Guest ]; then + export ERROR='.register The name "Guest" may not be registered as a permanent nickname.' + elif [ -z "$regnick" ]; then + export ERROR='.register Nicknames must be between 3 and 24 characters long. They may not start with "&", "#", "?", "@", or "+" and they must not contain a "/".' + elif [ -d "$userdir" ]; then + export ERROR=".register This nickname has already been registered elsewhere." + elif 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 + export ERROR=".register Registration failed, possibly due to faulty server configuration." + fi + ;; +esac diff --git a/webchat.css b/webchat.css new file mode 100644 index 0000000..1335fe6 --- /dev/null +++ b/webchat.css @@ -0,0 +1,174 @@ +* { + 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; +} + +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; + bottom: 12em; + left: 50%; transform: translate(-50%); + color: #000; + background-color: #FFF; + border: 1px solid; + border-radius: 1ex 1ex .5ex .5ex; + z-index: 2; +} +#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 .error { + padding: 1ex 1ex .5ex 1ex; + background-color: #FCC; + font-weight: bold; +} +#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 .error.nick ~ a.section + #nick, +#settings .error.register ~ a.section + #register, +#settings a.section + #nick, +#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 { + position: fixed; + bottom: 2em; + left: 0; right: 0; + padding: 1ex; + margin: .5ex; + -z-index: -1; +} + +#chat p.message { + margin-top: .25em; +} +#chat p.message > * { + display: table-cell; + color: #000; + background-color: rgba(255, 255, 255, .5); + line-height: 1.5em; + padding: .125em 0 .125em .5em; +} + +#chat .message .date { + color: #888; + font-size: .75em; +} +#chat .message .nick .indicator { + color: #888; +} +#chat .message .nick { + font-weight: bold; + white-space: nowrap; +} +#chat .message .message { + border-radius: 0 .5em .5em 0; + padding-right: .5em; +} + +#chat div.nicklist { + position: fixed; + top: 1em; bottom: 3em; + right: 0; width: 1em; + background-color: rgba(255, 255, 255, .875); + padding: .5em; + z-index: 1; + border: 1px solid black; + border-width: 1px 0 1px 1px; + overflow-y: auto; + white-space: nowrap; +} +#chat div.nicklist:hover { + width: 20%; + min-width: 10em; + white-space: normal; +} +#chat div.nicklist h2 { + font-weight: bold; + border-bottom: 1px solid black; +} +#chat div.nicklist .nick { + margin-top: .5em; + display: block; + line-height: 1.5em; +} +#chat div.nicklist a.nick { + color: #00F; +}