]> git.plutz.net Git - httpchat/commitdiff
Merge commit 'b4afadf46825c28910365f4e6dce5ec20f6e1341'
authorPaul Hänsch <paul@plutz.net>
Thu, 19 Nov 2020 01:32:51 +0000 (02:32 +0100)
committerPaul Hänsch <paul@plutz.net>
Thu, 19 Nov 2020 01:32:51 +0000 (02:32 +0100)
12 files changed:
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
cgilite/cgilite.sh [moved from cgilite.sh with 100% similarity]
cgilite/file.sh [moved from file.sh with 100% similarity]
cgilite/html-sh.sed [moved from html-sh.sed with 100% similarity]
cgilite/logging.sh [moved from logging.sh with 100% similarity]
cgilite/session.sh [moved from session.sh with 100% similarity]
cgilite/storage.sh [moved from storage.sh with 100% similarity]
channel.sh [new file with mode: 0755]
index.cgi [new file with mode: 0755]
usernick.sh [new file with mode: 0755]
webchat.css [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..22b5907
--- /dev/null
@@ -0,0 +1,3 @@
+&*
+@*
+serverkey
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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
similarity index 100%
rename from cgilite.sh
rename to cgilite/cgilite.sh
similarity index 100%
rename from file.sh
rename to cgilite/file.sh
similarity index 100%
rename from html-sh.sed
rename to cgilite/html-sh.sed
similarity index 100%
rename from logging.sh
rename to cgilite/logging.sh
similarity index 100%
rename from session.sh
rename to cgilite/session.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 (executable)
index 0000000..0f1bda7
--- /dev/null
@@ -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 (executable)
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 (executable)
index 0000000..13759ee
--- /dev/null
@@ -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 (file)
index 0000000..1335fe6
--- /dev/null
@@ -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;
+}