]> git.plutz.net Git - serve0/commitdiff
Merge commit '00aa1675c49f23eb075d1fc7ceb69bac5e1ba83c'
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)
cgilite/.gitignore [new file with mode: 0644]
cgilite/session.sh
cgilite/users.sh [new file with mode: 0755]

diff --git a/cgilite/.gitignore b/cgilite/.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/cgilite/users.sh b/cgilite/users.sh
new file mode 100755 (executable)
index 0000000..1959e9d
--- /dev/null
@@ -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
+}