]> git.plutz.net Git - serve0/commitdiff
Squashed 'cgilite/' changes from a76f6a5..6bfa64b
authorPaul Hänsch <paul@plutz.net>
Fri, 24 Sep 2021 10:19:22 +0000 (12:19 +0200)
committerPaul Hänsch <paul@plutz.net>
Fri, 24 Sep 2021 10:19:22 +0000 (12:19 +0200)
6bfa64b automatically swap in confirmation dialog for registration
5d5fc0f fix in email syntax and confirm path
d468e35 ignore automatic files from modules
5a714a2 syntax fixes, minor sanity checks
142f5b0 user account functions
d6e0c1a function new_session to force session update, limit session cookies to _BASE path

git-subtree-dir: cgilite
git-subtree-split: 6bfa64b084ea028f9078f679a4a77ffc57e02361

.gitignore [new file with mode: 0644]
session.sh
users.sh [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..5c9950a
--- /dev/null
@@ -0,0 +1,3 @@
+cgilite
+serverkey
+users.db
index 5b36ae032af4e818af91d4b73190a9f83614da15..8fb623639c14b372c7c88b98695b8edca24b1943 100755 (executable)
@@ -75,6 +75,7 @@ checkid(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | grep -m 1 -xE '[0-9a-zA-
 
 update_session(){
   local session sid time sig checksig
+  unset SESSION_KEY SESSION_ID
 
   read -r sid time sig <<-END
        $(POST session_key || COOKIE session)
@@ -82,23 +83,38 @@ update_session(){
   
   checksig="$(session_mac "$sid" "$time")"
   
-  if [ "$checksig" = "$sig" \
-    -a "$time" -ge "$_DATE" \
-    -a "$(printf %s "$sid" |checkid)" ] 2>&-
+  if [ "$checksig" = "$sig" \
+       -a "$time" -ge "$_DATE" \
+       -a "$(checkid "$sid")" ] 2>&-
   then
-    debug "Setting up new session"
-    sid="$(randomid)"
+    time=$(( $_DATE + $SESSION_TIMEOUT ))
+    sig="$(session_mac "$sid" "$time")"
+
+    SESSION_KEY="${sid} ${time} ${sig}"
+    SESSION_ID="${sid}"
+    return 0
+  else
+    return 1
   fi
 
+}
+
+new_session(){
+  local sid time sig
+
+  debug "Setting up new session"
+  sid="$(randomid)"
   time=$(( $_DATE + $SESSION_TIMEOUT ))
   sig="$(session_mac "$sid" "$time")"
-  printf %s\\n "${sid} ${time} ${sig}"
+
+  SESSION_KEY="${sid} ${time} ${sig}"
+  SESSION_ID="${sid}"
 }
 
 SESSION_BIND() {
   # Set tamper-proof authenticated cookie
   local key="$1" value="$2"
-  SET_COOKIE session "$key"="${value} $(session_mac "$value" "$SESSION_ID")"
+  SET_COOKIE session "$key"="${value} $(session_mac "$value" "$SESSION_ID")" Path="/${_BASE#/}" SameSite=Strict HttpOnly
 }
 
 SESSION_VAR() {
@@ -115,10 +131,10 @@ SESSION_VAR() {
 }
 
 SESSION_COOKIE() {
-  SET_COOKIE 0 session="$SESSION_KEY" Path=/ SameSite=Strict HttpOnly
+  [ "$1" = new ] && new_session
+  SET_COOKIE 0 session="$SESSION_KEY" Path="/${_BASE#/}" SameSite=Strict HttpOnly
 }
 
-SESSION_KEY="$(update_session)"
-SESSION_ID="${SESSION_KEY%% *}"
+update_session || new_session
 
 [ "$1" = nocookie ] || SESSION_COOKIE
diff --git a/users.sh b/users.sh
new file mode 100755 (executable)
index 0000000..1959e9d
--- /dev/null
+++ b/users.sh
@@ -0,0 +1,346 @@
+#!/bin/sh
+
+[ -n "$include_users" ] && return 0
+include_users="$0"
+
+. "${_EXEC}/cgilite/session.sh"
+. "${_EXEC}/cgilite/storage.sh"
+
+USER_REGISTRATION="${USER_REGISTRATION:-true}"
+USER_REQUIREEMAIL="${USER_REQUIREEMAIL:-true}"
+
+HTTP_HOST="$(HEADER Host)"
+MAILFROM="${MAILDOMAIN:-noreply@${HTTP_HOST%:*}}"
+
+user_db="${_DATA}/users.db"
+unset USER_ID USER_NAME USER_EMAIL
+
+# USER DB
+# UID  UNAME   STATUS (pending|active|deleted) EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+
+user_init(){
+   local user_id="$(SESSION_VAR user_id)"
+   local UID   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+   [ "$user_id" ] \
+   && read -r UID      UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
+       $(grep "^${user_id}     " "$user_db")
+       EOF
+   [ "$STATUS" -a "$EXPIRE" ] \
+   && if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" ]; then
+     USER_ID="$UID"
+     USER_NAME="$(UNSTRING "$UNAME")"
+     USER_EMAIL="$(UNSTRING "$EMAIL")"
+   fi
+}
+
+user_checkname(){
+  { [ $# -gt 0 ] && printf %s "$*" || cat; } \
+  | sed -nE '
+    :X; $!{N;bX;}
+    s;[ \t\r\n]+; ;g;
+    s;^ ;;; s; $;;;
+    /@/d;
+    /^[a-zA-Z][a-zA-Z0-9 -~]{2,127}$/!d;
+    p;
+    '
+}
+
+user_checkemail(){
+  { [ $# -gt 0 ] && printf %s "$*" || cat; } \
+  | sed -nE '
+    # W3C recommended email regex
+    # https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email)
+    /^[a-zA-Z0-9.!#$%&'\''*+\/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/p;
+    '
+}
+
+user_nameexist(){
+  local uname="$(STRING "$1")"
+  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  [ -f "$user_db" -a -r "$user_db" ] \
+  && while read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+    [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0
+  done <"$user_db"
+  return 1
+}
+
+user_emailexist(){
+  local email="$(STRING "$1")"
+  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  [ -f "$user_db" -a -r "$user_db" ] \
+  && while read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+    [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0
+  done <"$user_db"
+  return 1
+}
+
+user_pwhash(){
+  local salt="$1" secret="$2" hash
+  hash="$(printf '%s\n%s\n' "$secret" "$salt" |sha256sum)"
+  printf '%s\n' "${hash%% *}"
+}
+
+user_register(){
+  # reserve account, send registration mail
+  # preliminary uid, expiration, signature
+  local uid="$(timeid)"
+  local uname="$(POST uname |user_checkname)"
+  local email="$(POST email |user_checkemail)"
+  local pwsalt="$(randomid)"
+  local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
+
+  if [ "$USER_REGISTRATION" != true ]; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED"
+  fi
+
+  if   [ "$USER_REQUIREEMAIL" = true ]; then
+    if [ ! "email" ]; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
+    elif user_emailexist "$email"; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
+    elif LOCK "$user_db"; then
+      printf '%s       \\      pending %s      \\      \\      %i      \\      \\\n' \
+             "$uid" "$(STRING "$email")" "$(( $_DATE + 86400 ))" \
+             >>"$user_db"
+      RELEASE "$user_db"
+      sendmail -t -f "$MAILFROM" <<-EOF
+       From: ${MAILFROM}
+       To: ${email}
+       Subject: Your account registration at ${HTTP_HOST%:*}
+
+       Someone tried to sign up for a user account using this email address.
+
+       You can activate your account using this link:
+
+           https://${HTTP_HOST%:*}/${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+
+       This registration link will expire after 24 hours.
+
+       If you did not request an account at ${HTTP_HOST%:*}, then someone else
+       probably entered your email address by accident. In this case you shoud
+       simply ignore this message and we will remove your email address from
+       our database within the next day.
+
+       This is an automatic email. Any direct reply will not be received.
+       Your Account Registration Robot.
+       EOF
+      REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+    else
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
+    fi
+
+  elif [ "$USER_REQUIREEMAIL" != true ]; then
+    if [ ! "$uname" ]; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_INVALID"
+    elif user_nameexist "$uname"; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_EXISTS"
+    elif [ ! "$pw" ]; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT"
+    elif [ "$pw" != "$pwconfirm" ]; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
+    elif LOCK "$user_db"; then
+      printf '%s       %s      active  %s      %s      %s      %i      \\      \\\n' \
+             "$uid" "$(STRING "$uname")" "$(STRING "$email")" \
+             "$pwsalt" "$(user_pwhash "$pwsalt" "$pw")" \
+             "$(( $_DATE + 86400 * 730 ))" \
+             >>"$user_db"
+      RELEASE "$user_db"
+
+      SESSION_COOKIE new
+      SESSION_BIND user_id "$uid"
+
+      REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+    else
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
+    fi
+  fi
+}
+
+user_confirm(){
+  # enable account
+  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local uid="$(POST uid |checkid)"
+  local signature="$(POST signature)"
+  local uname="$(POST uname |user_checkname)"
+  local pwsalt="$(randomid)"
+  local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
+
+  if [ "$signature" != "$(session_mac "$uid")" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
+  elif [ ! "$uname" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_INVALID"
+  elif user_nameexist "$uname"; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_EXISTS"
+  elif [ ! "$pw" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT"
+  elif [ "$pw" != "$pwconfirm" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH"
+  elif LOCK "$user_db"; then
+    read -r UID        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
+       $(grep "^${uid} " "$user_db")
+       EOF
+
+    if [ "$STATUS" != pending -o "$EXPIRE" -le "$_DATE" ]; then
+      RELEASE "$user_db"
+      REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
+    else
+      printf '%s       %s      active  %s      %s      %s      %i      %s      %s\n' \
+             "$UID" "$(STRING "$uname")" "$EMAIL" \
+             "$pwsalt" "$(user_pwhash "$pwsalt" "$pw")" \
+             "$(( $_DATE + 86400 * 730 ))" "$DEVICES" "$FUTUREUSE" \
+             >"${user_db}.$$"
+      grep -v "^${uid} " "$user_db" >>"${user_db}.$$"
+      mv "${user_db}.$$" "${user_db}"
+      RELEASE "$user_db"
+
+      SESSION_COOKIE new
+      SESSION_BIND user_id "$UID"
+      REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+    fi
+  else
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
+  fi
+}
+
+user_login(){
+  # set cookie
+  # keep logged in - device cookie?
+  # initialize new session!
+  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local uname="$(POST uname |STRING)" pw="$(POST pw)"
+
+  [ -f "$user_db" -a -r "$user_db" ] \
+  && while read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+    if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then
+      if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then
+        SESSION_COOKIE new
+        SESSION_BIND user_id "$UID"
+        REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN"
+      fi
+    fi
+  done <"$user_db"
+  REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_LOGIN"
+}
+
+user_logout(){
+  # destroy cookie, destroy session
+  # keep device cookie
+  new_session
+  SET_COOKIE 0 session=""
+  SET_COOKIE 0 user_id=""
+  REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT"
+}
+
+user_update(){
+  # passphrase, email
+  :
+}
+user_recover(){
+  # send recover link
+  :
+}
+user_disable(){
+  :
+}
+
+user_init
+
+[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in
+  user_register) user_register ;;
+  user_confirm)  user_confirm ;;
+  user_login)    user_login ;;
+  user_logout)   user_logout ;;
+  user_update)
+    :;;
+  user_recover)
+    :;;
+  user_disable)
+    :;;
+esac
+
+w_user_register(){
+  if [ "$(GET user_confirm)" ]; then
+    w_user_confirm
+  elif [ "$USER_REGISTRATION" != true ]; then
+    cat <<-EOF
+       [div #user_register .disabled
+       User Registration is disabled.
+       ]
+       EOF
+  elif [ "$USER_REQUIREEMAIL" = true ]; then
+    cat <<-EOF
+       [form #user_register .registeremail method=POST
+         [p We will send an activation mail to your email address.
+           You can continue the signup process when you click on the
+           activation link in this email.]
+         [input type=email name=email placeholder="Email"]
+         [submit "action" "user_register" Sign Up]
+       ]
+       EOF
+  elif [ "$USER_REQUIREEMAIL" != true ]; then
+    cat <<-EOF
+       [form #user_register .registername method=POST
+          [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[a-zA-Z\]\[a-zA-Z0-9 -~\]{2,127}$" autocomplete=off]
+         [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
+         [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
+         [submit "action" "user_register" Sign Up]
+       ]
+       EOF
+  fi
+}
+
+w_user_confirm(){
+  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local user_confirm="$(GET user_confirm)"
+  local uid="${user_confirm% *}" signature="${user_confirm#* }"
+
+  if [ "$signature" = "$(session_mac "$uid")" ]; then
+    read -r UID        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
+       $(grep "^${uid} " "$user_db")
+       EOF
+    if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then
+      cat <<-EOF
+       [form #user_confirm method=POST
+         [input type=hidden name=uid value="${uid}"]
+         [input type=hidden name=signature value="${signature}"]
+         [input disabled=disabled value="$(HTML "$EMAIL")"]
+          [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[a-zA-Z\]\[a-zA-Z0-9 -~\]{2,127}$" autocomplete=off]
+         [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
+         [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
+         [submit "action" "user_confirm" Finish Registration]
+       ]
+       EOF
+    else
+      cat <<-EOF
+       [div #user_confirm .expired
+         [p This activation link is not valid anymore.]
+       ]
+       EOF
+    fi
+  else
+    cat <<-EOF
+       [div #user_confirm .invalid
+         [p This activation link is invalid. Make sure you copied the whole activation link from your email and be careful not to include any line breaks.]
+       ]
+       EOF
+  fi
+}
+
+w_user_login(){
+  if [ ! "$USER_ID" ]; then
+    cat <<-EOF
+       [form #user_login .login method=POST
+         [input name=uname placeholder="Username or Email" autocomplete=off]
+         [input type=password name=pw placeholder="Passphrase"]
+         [submit "action" "user_login" Login]
+       ]
+       EOF
+  elif [ "$USER_ID" ]; then
+    cat <<-EOF
+       [form #user_login .logout method=POST
+         [p You are currently logged in as "${USER_NAME}"]
+         [submit "action" "user_logout" Logout]
+       ]
+       EOF
+  fi
+}