From 22cff303ffb275439156c95e454c95c1b0864dae Mon Sep 17 00:00:00 2001 From: =?utf8?q?Paul=20H=C3=A4nsch?= Date: Mon, 28 Mar 2022 12:46:31 +0200 Subject: [PATCH] Squashed 'cgilite/' changes from 1462517..b2b268b b2b268b corrected paragraph splitting and hr/h2 distinction 33cd660 faster hexdecode for mixed data (e.g. post-data) 6fe824f API CHANGE: do not set session cookie automatically a8f5776 enable pandoc fenced divs, and fenced code attributes fabbc00 make hr tag visible again 47295e6 bugfix: prevent HTML injection in reference style link titles 882f37d markdown support for external macro plugin 1d27862 bugfix URL escaping for ? and % 6147b0e faster HTML and URL functions b191eb8 export application globals dba2d39 idmap functions f477dc5 better data-layer / UI-layer abstraction in user functions 5a44f82 allow server site message page to confirm registration 24df501 perform _BASE striping outside of internal web server 343a22a cleaner display of activation link, include port number in activation link f6fa7fb strip _BASE path from PATH_INFO variable 6c1784b user invite function, handle invite/registration expire, always allow registration of first user af27357 bugfix tooltips 7459611 improved markup for styling 9451cdd min-height for textarea 84a16dd unambiguous cookie path when destroying user session 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 a76f6a5 allow suppression of default session cookie bcca3c0 STRING encodes empty values as backslash for easyer `read`ing of TAB-DBs, UNSTRING produces trailing newline for consistent output of encoded \n, obsoleted `sed` $UNSTRING code 9884092 typo in cli parsing 2218e82 Set _EXEC _DATA and _BASE variables dcab989 much faster hex decode function 07b4b96 simpler lock algorithm using files 38702db improved gonzo mac if openssl is unavailable 8be65ce bugfix: faulty check in update and append 904badc bugfix: parameter passing in cgilite_value calls 4a04dc4 portability GNU `date` / Busybox `date` 76395d4 Fix: prevent horizontal rule from masking 2nd order heading 52e7985 enable pipe/argument choice for more functions b65a5ae md: heading identifiers b089a33 md: handle DOS line breaks fa3afea md: task lists cd49a5c HTML escaping, switchable HTML processing 4f5d122 md: inline HTML fcdebd0 bugfix: stop condition in HTML block 987f4ef md: verbatim html block, md: allow emphasis before punctuation 1218334 md: image embedding, completing support for basic markdown c1eb795 md: horizontal rules b2cf4a3 md: allow hard line breaks; md extension: ignore embedded underscores 4f6c3fe todo items 3d2264c include markdown processor 80b3d8c try automatic switching to busybox for uuencode and sha256sum c207699 bugfix: fix error when reading literal "+" char from storage e7e354d basic print styles 4b913ff set foreground color where background color is set 49b4c44 remove obsolte escape functions 47a1cf6 introduce functions for cookie based cryptographically signed session variables e3e5c0d introduce simple DBM module 8070ac9 use debug function for error output 13c2995 change border of input elements 31fd9a7 experimental: basic set of css rules 6212086 simplified mac function and cookie format a836764 prefer hmac for session security a1caf91 include guard for main script, prevent double read of post data 147c722 mime types for streaming formats 1caf684 prevent line breaks in debug message 06a4763 try reading session key from post before trying cookie git-subtree-dir: cgilite git-subtree-split: b2b268b458208ba7746052e05f1f1f5ced081023 --- .gitignore | 3 + cgilite.sh | 192 +++++++++++++------ common.css | 145 +++++++++++++++ file.sh | 10 +- markdown.awk | 471 ++++++++++++++++++++++++++++++++++++++++++++++ session.sh | 103 ++++++++--- storage.sh | 184 ++++++++++++------ users.sh | 515 +++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1481 insertions(+), 142 deletions(-) create mode 100644 .gitignore create mode 100644 common.css create mode 100755 markdown.awk create mode 100755 users.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c9950a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +cgilite +serverkey +users.db diff --git a/cgilite.sh b/cgilite.sh index e145f2e..6cbd7ec 100755 --- a/cgilite.sh +++ b/cgilite.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Copyright 2017 - 2020 Paul Hänsch +# Copyright 2017 - 2021 Paul Hänsch # # This is CGIlite. # A collection of posix shell functions for writing CGI scripts. @@ -18,19 +18,48 @@ # You should have received a copy of the GNU Affero General Public License # along with CGIlite. If not, see . +[ -n "$include_cgilite" ] && return 0 +# guard set after webserver part + # ksh and zsh workaround # set -o posix # ksh, not portable setopt -o OCTAL_ZEROES 2>&- +# Integrated webserver request timeout +cgilite_timeout=2 + +# General environment variables +# $_EXEC - directory containing application itself +# $_DATA - direcotry where application data may be stored +# $_BASE - optional prefix for http path, e.g. "/myapp" +# +# Programmers should take care to use those variables throughout the +# application. +# Variables may be set via CLI argument, in environment, or left as default. + +for cgilite_arg in "$@"; do case $cgilite_arg in + --exec=*) _EXEC="${cgilite_arg#*=}";; + --data=*) _DATA="${cgilite_arg#*=}";; + --base=*) _BASE="${cgilite_arg#*=}";; +esac; done +unset cgilite_arg + +_EXEC="${_EXEC:-${0%/*}}" +_DATA="${_DATA:-.}" +_EXEC="${_EXEC%/}" _DATA="${_DATA%/}" _BASE="${_BASE%/}" + +export _EXEC _DATA _BASE + +# Carriage Return and Line Break characters for convenience CR=" " BR=' ' -cgilite_timeout=2 - -debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; } PATH(){ local str seg out + # normalize path + # read from stdin if no arguments are provided + [ $# -eq 0 ] && str="$(cat)" || str="$*" while [ "$str" ]; do seg=${str%%/*}; str="${str#*/}" @@ -44,18 +73,59 @@ PATH(){ [ "${str}" -a "${out}" ] && printf %s "$out" || printf %s/ "${out%/}" } -HEX_DECODE=' - s;\\;\\\\;g; :HEXDECODE_X; s;%([^0-9A-F]);\\045\1;g; tHEXDECODE_X; - # Hexadecimal { %00 - %FF } will be transformed to octal { \000 - \377 } for posix printf - s;%[0123].;&\\0;g; s;%[4567].;&\\1;g; s;%[89AB].;&\\2;g; s;%[CDEF].;&\\3;g; - s;%[048C][0-7]\\.;&0;g; s;%[048C][89A-F]\\.;&1;g; s;%[159D][0-7]\\.;&2;g; s;%[159D][89A-F]\\.;&3;g; - s;%[26AE][0-7]\\.;&4;g; s;%[26AE][89A-F]\\.;&5;g; s;%[37BF][0-7]\\.;&6;g; s;%[37BF][89A-F]\\.;&7;g; - s;%.[08](\\..);\10;g; s;%.[19](\\..);\11;g; s;%.[2A](\\..);\12;g; s;%.[3B](\\..);\13;g; - s;%.[4C](\\..);\14;g; s;%.[5D](\\..);\15;g; s;%.[6E](\\..);\16;g; s;%.[7F](\\..);\17;g; -' - HEX_DECODE(){ - printf -- "$(printf %s "$1" |sed -E "$HEX_DECODE")" + local pfx="$1" in="$2" out + # Print out Data encoded as Hex + # + # Arguments: + # pfx - required, prefix for a hex tupel, e.g. "\x", "%" "\", may be empty + # in - required, string to be decoded + # + # anything that does not constitute a tupel of valid Hex numerals + # will be copied to the output literally + + while [ "$in" ]; do + case $in in + "$pfx"[0-9a-fA-F][0-9a-fA-F]*) in="${in#${pfx}}";; + \\*) in="${in#?}"; out="${out}\\\\"; continue;; + %*) in="${in#?}"; out="${out}%%"; continue;; + *) att="${in%%"${pfx}"*}"; att="${att%%%*}"; att="${att%%\\*}" + out="${out}${att}"; in="${in#"${att}"}"; continue;; + esac; + + # Hex escapes for printf (e.g. \x41) are not portable + # The portable way for Hex output is transforming Hex to Octal + # (e.g. \x41 = \101) + case $in in + [0123]?*) out="${out}\\0";; + [4567]?*) out="${out}\\1";; + [89aAbB]?*) out="${out}\\2";; + [c-fC-F]?*) out="${out}\\3";; + esac + case $in in + [048cC][0-7]*) out="${out}0";; + [048cC][89a-fA-F]*) out="${out}1";; + [159dD][0-7]*) out="${out}2";; + [159dD][89a-fA-F]*) out="${out}3";; + [26aAeE][0-7]*) out="${out}4";; + [26aAeE][89a-fA-F]*) out="${out}5";; + [37bBfF][0-7]*) out="${out}6";; + [37bBfF][89a-fA-F]*) out="${out}7";; + esac + case $in in + ?[08]*) out="${out}0";; + ?[19]*) out="${out}1";; + ?[2aA]*) out="${out}2";; + ?[3bB]*) out="${out}3";; + ?[4cC]*) out="${out}4";; + ?[5dD]*) out="${out}5";; + ?[6eE]*) out="${out}6";; + ?[7fF]*) out="${out}7";; + esac + in="${in#?}" + in="${in#?}" + done + printf -- "$out" } if [ -z "$REQUEST_METHOD" ]; then @@ -78,7 +148,7 @@ if [ -z "$REQUEST_METHOD" ]; then kill $cgilite_watchdog SERVER_PROTOCOL="${SERVER_PROTOCOL%${CR}}" - PATH_INFO="$(HEX_DECODE "${REQUEST_URI%\?*}" |PATH)" + PATH_INFO="$(HEX_DECODE % "${REQUEST_URI%\?*}" |PATH)" [ "${REQUEST_URI}" = "${REQUEST_URI#*\?}" ] \ && QUERY_STRING='' \ || QUERY_STRING="${REQUEST_URI#*\?}" @@ -121,13 +191,20 @@ if [ -z "$REQUEST_METHOD" ]; then exit 0 fi +include_cgilite="$0" + if [ "${REQUEST_METHOD}" = POST -a "${CONTENT_LENGTH:-0}" -gt 0 -a \ "${CONTENT_TYPE}" = "application/x-www-form-urlencoded" ]; then cgilite_post="$(head -c "$CONTENT_LENGTH")" fi +PATH_INFO="$(PATH "/${PATH_INFO#${_BASE}}")" + +debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; } [ "${DEBUG+x}" ] && env >&2 +# general helper functions, see GET, POST, and REF below + cgilite_count(){ printf %s "&$1" \ | grep -oE '&'"$2"'=[^&]*' \ @@ -141,7 +218,7 @@ cgilite_value(){ str="${str#*&${name}=}" cnt=$((cnt - 1)) done - printf -- "$(printf %s "${str%%&*}" |sed -E 's;\+; ;g;'"$HEX_DECODE")" + HEX_DECODE % "$(printf %s "${str%%&*}" |tr + \ )" } cgilite_keys(){ @@ -153,15 +230,26 @@ cgilite_keys(){ | sort -u } -GET(){ cgilite_value "${QUERY_STRING}" $@; } +# Read arguments from GET, POST, or the query string of the referrer (REF). +# Example: +# GET varname n +# +# where n is number for the Nth occurence of a variable and defaults to 1 +# +# *_COUNT varname +# -> returns number of ocurences +# *_KEYS +# -> returns list of available varnames + +GET(){ cgilite_value "${QUERY_STRING}" "$@"; } GET_COUNT(){ cgilite_count "${QUERY_STRING}" $1; } GET_KEYS(){ cgilite_keys "${QUERY_STRING}"; } -POST(){ cgilite_value "${cgilite_post}" $@; } +POST(){ cgilite_value "${cgilite_post}" "$@"; } POST_COUNT(){ cgilite_count "${cgilite_post}" $1; } POST_KEYS(){ cgilite_keys "${cgilite_post}"; } -REF(){ cgilite_value "${HTTP_REFERER#*\?}" $@; } +REF(){ cgilite_value "${HTTP_REFERER#*\?}" "$@"; } REF_COUNT(){ cgilite_count "${HTTP_REFERER#*\?}" $1; } REF_KEYS(){ cgilite_keys "${HTTP_REFERER#*\?}"; } @@ -181,7 +269,8 @@ HEADER(){ } COOKIE(){ - HEX_DECODE "$( + # Read value of cookie + HEX_DECODE % "$( HEADER Cookie \ | grep -oE '(^|; ?)'"$1"'=[^;]*' \ | sed -En "${2:-1}"'{s;^[^=]+=;;; s;\+; ;g; p;}' @@ -193,21 +282,18 @@ HTML(){ # Also escape [, ], and \n for use in html-sh local str out [ $# -eq 0 ] && str="$(cat)" || str="$*" - while [ "$str" ]; do - case $str in - \&*) out="${out}&";; - \<*) out="${out}<";; - \>*) out="${out}>";; - \"*) out="${out}"";; - \'*) out="${out}'";; - \[*) out="${out}[";; - \]*) out="${out}]";; - "${CR}"*) out="${out} ";; - "${BR}"*) out="${out} ";; - *) out="${out}${str%"${str#?}"}";; - esac - str="${str#?}" - done + while [ "$str" ]; do case $str in + \&*) out="${out}&"; str="${str#?}";; + \<*) out="${out}<"; str="${str#?}";; + \>*) out="${out}>"; str="${str#?}";; + \"*) out="${out}""; str="${str#?}";; + \'*) out="${out}'"; str="${str#?}";; + \[*) out="${out}["; str="${str#?}";; + \]*) out="${out}]"; str="${str#?}";; + "${CR}"*) out="${out} "; str="${str#?}";; + "${BR}"*) out="${out} "; str="${str#?}";; + *) out="${out}${str%%[]&<>\"\'[]*}"; str="${str#"${str%%[]&<>\"\'[]*}"}";; + esac; done printf %s "$out" } @@ -215,24 +301,21 @@ URL(){ # Escape pathes, so they can be used in link tags and HTTP Headers local str out [ $# -eq 0 ] && str="$(cat)" || str="$*" - while [ "$str" ]; do - case $str in - \&*) out="${out}%26";; - \"*) out="${out}%22";; - \'*) out="${out}%27";; - \?*) out="${out}%3F";; - \#*) out="${out}%23";; - \[*) out="${out}%5B";; - \]*) out="${out}%5D";; - \ *) out="${out}%20";; - " "*) out="${out}%09";; - "${CR}"*) out="${out}%0D";; - "${BR}"*) out="${out}%0A";; - %*) out="${out}%25";; - *) out="${out}${str%"${str#?}"}";; - esac - str="${str#?}" - done + while [ "$str" ]; do case $str in + \&*) out="${out}%26"; str="${str#?}";; + \"*) out="${out}%22"; str="${str#?}";; + \'*) out="${out}%27"; str="${str#?}";; + \?*) out="${out}%3F"; str="${str#?}";; + \#*) out="${out}%23"; str="${str#?}";; + \[*) out="${out}%5B"; str="${str#?}";; + \]*) out="${out}%5D"; str="${str#?}";; + \ *) out="${out}%20"; str="${str#?}";; + " "*) out="${out}%09"; str="${str#?}";; + "${CR}"*) out="${out}%0D"; str="${str#?}";; + "${BR}"*) out="${out}%0A"; str="${str#?}";; + %*) out="${out}%25"; str="${str#?}";; + *) out="${out}${str%%[]&\"\'\?# ${CR}${BR}%[]*}"; str="${str#"${str%%[]&\"\'\?# ${CR}${BR}%[]*}"}";; + esac; done printf %s "$out" } @@ -255,6 +338,7 @@ SET_COOKIE(){ } REDIRECT(){ + # Trigger redirct and terminate script printf '%s: %s\r\n' \ Status "303 See Other" \ Content-Length 0 \ diff --git a/common.css b/common.css new file mode 100644 index 0000000..359f07d --- /dev/null +++ b/common.css @@ -0,0 +1,145 @@ +/* ======= GENERIC HTML STYLES ======= */ + +* { + box-sizing: border-box; + position: relative; + font: inherit; + text-decoration: inherit; + color: inherit; background: transparent; + max-width: 100%; + margin: 0; padding: 0; + border: none; +} + +body { + font: normal normal normal medium/1.5em sans-serif; + color: #000; background: #FFF; +} + +ul, ol, dl, table, p { margin-bottom: .5em; } + +a { + font-style: italic; + text-decoration: underline; + color: #068; +} +a.button { + font-style: inherit; + text-decoration: inherit; + color: inherit; +} + +sup { vertical-align: super; } +sub { vertical-align: sub; } +small { font-size: .75em; } +big { font-size: 1.25em; } +strike, del, s { text-decoration: line-through; } +u {text-decoration: underline; } +i, em { font-style: italic; } +b, strong { font-weight: bolder; } +tt, code, var, samp, kbd { font-family: monospace; } +kbd { font-style: italic; } + +ul, ol { margin-left: 1.125em; } +dl dt { font-weight: bolder; } +table th { font-weight: bold; } + +hr { border-bottom: 1pt solid; } + +h1, h2, h3 { + font-weight: bold; + margin-top: .75em; + margin-bottom: .5em; +} + +h4, h5, h6, form legend { + font-weight: bolder; + margin-bottom: .25em; +} + +h1 { font-size: 1.5em; } +h2 { font-size: 1.125em; } + +select, input, button, textarea, a.button { + display: inline-block; + color: #000; background-color: #FFF; + border: .5pt solid; + padding: .25em .75em; + vertical-align: text-bottom; + border: .5pt solid #000; + border-radius: 2pt; +} +select { padding: .375em 0; } +textarea { min-height: 7em; } + +input[type=radio], input[type=checkbox] { + vertical-align: baseline; +} +input[type=number] { text-align: right; padding-right: 0; } + +button, input[type=button], a.button { + box-shadow: .125em .125em .25em; + cursor: pointer; +} +input[type=radio], input[type=checkbox], label[for] { + cursor: pointer; +} + +label { margin-right: .75em; } +input + label { + margin-left: .375em; +} + +@media print { + @page { margin: 20mm; } + + h1 { text-align: center; } + + h1, h2, h3, h4, h5, h6, form legend { + page-break-inside: avoid; + page-break-after: avoid; + page-break-before: auto; + } + li { page-break-inside: avoid; } + th, dt { page-break-after: avoid; } +} + +/* ======= End Generic Styles ======= */ + +/* ======= Common Styles ======= */ + +*[tooltip]:hover:after { + display: block; + position: absolute; + min-width: 12em; + bottom: 100%; left: 50%; transform: translate(-50%, 0); + content: attr(tooltip); + padding: .5em; + color: #000; background-color: #FFC; + border: .5pt solid; + z-index: 1; +} + +input[type=radio].tab { display: none; } +input[type=radio].tab + label { + display: table-cell; + padding: .5em 1em; + color: #000; background-color: #EEE; + border: .5pt solid; +} +input[type=radio].tab:checked + label { + background-color: #FFF; + border-bottom: none; + box-shadow: .125em -.125em .125em #888; + z-index: 1; +} +input[type=radio].tab ~ *.tab { + display: none; + width: 100%; + margin-top: -.5pt; padding: .25em .75em; + border: .5pt solid; + border-radius: 0; + box-shadow: .125em .125em .125em #888; +} + +/* ======= End Common Styles ======= */ diff --git a/file.sh b/file.sh index 51ec245..6f956df 100755 --- a/file.sh +++ b/file.sh @@ -32,9 +32,14 @@ file_type(){ svg) printf 'image/svg+xml';; gif) printf 'image/gif';; webm) printf 'video/webm';; - mp4) printf 'video/mp4';; + mp4|m4v) printf 'video/mp4';; + m4a) printf 'audio/mp4';; ogg) printf 'audio/ogg';; xml) printf 'application/xml';; + m3u8) printf 'application/x-mpegURL';; + ts) printf 'video/MP2T';; + mpd) printf 'application/dash+xml';; + m4s) printf 'video/iso.segment';; *) printf 'application/octet-stream';; esac } @@ -53,8 +58,7 @@ FILE(){ file_size="$(stat -Lc %s "$file")" file_date="$(stat -Lc %Y "$file")" - http_date="$(date -uRd @$file_date)" - http_date="${http_date%+0000}GMT" + http_date="$(date -ud "@$file_date" +"%a, %d %b %Y %T GMT")" cachedate="$( # Parse the allowable date formats from Section 3.3.1 of # https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html diff --git a/markdown.awk b/markdown.awk new file mode 100755 index 0000000..d28c7cf --- /dev/null +++ b/markdown.awk @@ -0,0 +1,471 @@ +#!/bin/awk -f +#!/opt/busybox/awk -f + +# EXPERIMENTAL Markdown processor with minimal dependencies. +# Meant to support all features of John Grubers basic Markdown +# + a number of common extensions, mostly inspired by Pandoc Markdown +# +# ToDo: +# - HTML processing / escaping (according to environment flag) +# - em-dashes and arrows + +# Supported Features / TODO: +# ========================== +# [x] done [ ] todo [-] not planned ? unsure +# +# Basic Markdown - Block elements: +# ------------------------------- +# - [x] Paragraphs +# - [x] Double space line breaks +# - [x] Proper block element nesting +# - [x] Headings +# - [x] ATX-Style Headings +# - [x] Blockquotes +# - [x] Lists (ordered, unordered) +# - [x] Code blocks (using indention) +# - [x] Horizontal rules +# - [x] Verbatim HTML block (disabled by default) +# +# Basic Markdown - Inline elements: +# --------------------------------- +# - [x] Links +# - [x] Reference style links +# - [x] Emphasis *em*/**strong** (*Asterisk*, _Underscore_) +# - [x] `code`, also ``code containing `backticks` `` +# - [x] Images / reference style images +# - [x] +# - [x] backslash escapes +# - [x] Verbatim HTML inline (disabled by default) +# - [x] HTML escaping +# +# NOTE: Set the environment variable MD_HTML=true to enable verbatim HTML +# +# Extensions - Block elements: +# ---------------------------- +# - ? Heading identifiers (php md, pandoc) +# - [x] Automatic heading identifiers (custom) +# - [x] Fenced code blocks (php md, pandoc) +# - [x] Fenced code attributes +# - [ ] Tables +# - ? Simple table (pandoc) +# - ? Multiline table (pandoc) +# - ? Grid table (pandoc) +# - ? Pipe table (php md pandoc) +# - [x] Line blocks (pandoc) +# - [x] Task lists (pandoc) +# - [ ] Definition lists (php md, pandoc) +# - [-] Numbered example lists (pandoc) +# - [-] Metadata blocks (pandoc) +# - [x] Fenced Divs (pandoc) +# +# Extensions - Inline elements: +# ---------------------------- +# - [x] Ignore embedded_underscores (php md, pandoc) +# - [x] ~~strikeout~~ (pandoc) +# - [x] ^Superscript^ ~Subscript~ (pandoc) +# - [-] Bracketed spans (pandoc) +# - [-] Inline attributes (pandoc) +# - [-] TEX-Math (pandoc) +# - ? Footnotes (php md) +# - ? Abbreviations (php md) +# - ? "Curly quotes" (smartypants) +# - [ ] em-dashes (--) (smartypants old) +# - ? ... three-dot ellipsis (smartypants) +# - [-] en-dash (smartypants) +# - [ ] Automatic em-dash / en-dash +# - [ ] Automatic -> Arrows <- + +function debug(text) { printf "\n---\n%s\n---\n", text > "/dev/stderr"; } + +function HTML ( text ) { + gsub( /&/, "\\&", text ); + gsub( //, "\\>", text ); + gsub( /"/, "\\"", text ); + gsub( /'/, "\\'", text ); + gsub( /\\/, "\\\", text ); + return text; +} + +function inline( line, LOCAL, len, code, href, guard ) { + nu = "(\\\\\\\\|\\\\[^\\\\]|[^\\\\_]|_[[:alnum:]])*" # not underline (except when escaped) + na = "(\\\\\\\\|\\\\[^\\\\]|[^\\\\\\*])*" # not asterisk (except when escaped) + ieu = "_([^_[:space:]]|[^_[:space:]]" nu "[^_[:space:]])_" # inner (underline) + isu = "__([^_[:space:]]|[^_[:space:]]" nu "[^_[:space:]])__" # inner (underline) + iea = "\\*([^\\*[:space:]]|[^\\*[:space:]]" na "[^\\*[:space:]])\\*" # inner (asterisk) + isa = "\\*\\*([^\\*[:space:]]|[^\\*[:space:]]" na "[^\\*[:space:]])\\*\\*" # inner (asterisk) + + if ( line ~ /^$/ ) { # Recursion End + return ""; + + # omit processing of escaped characters + } else if ( line ~ /^\\[]\\`\*_\{\}\(\)#\+-\.![]/) { + return substr(line, 2, 1) inline( substr(line, 3) ); + + # hard brakes + } else if ( match(line, /^ \n/) ) { + return "
\n" inline( substr(line, RLENGTH + 1) ); + + # ``code spans`` + } else if ( match( line, /^`+/) ) { + len = RLENGTH + guard = substr( line, 1, len ) + if ( match(line, guard ".*" guard) ) { + code = substr( line, len + 1, match( substr(line, len + 1), guard ) - 1) + len = 2 * length(guard) + length(code) + # strip single surrounding white spaces + code = gensub( / (.*) /, "\\1", "1" , code) + # escape HTML within code span + gsub( /&/, "\\&", code ); gsub( //, "\\>", code ); + return "" code "" inline( substr( line, len + 1 ) ) + } + + # quick links ("automatic links" in md doc) + } else if ( match( line, /^<[a-zA-Z]+:\/\/([-\.[:alnum:]]+)(:[0-9]*)?(\/[^>]*)?>/ ) ) { + len = RLENGTH; + href = HTML( substr( line, 2, len - 2) ); + return "" href "" inline( substr( line, len + 1) ); + + # inline links + } else if ( match(line, /^\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/) ) { + len = RLENGTH; + text = gensub(/^\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\1", "g", line); + href = gensub(/^\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\2", "g", line); + title = gensub(/^\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\4", "g", line); + if ( title ) { + return "" inline( text ) "" inline( substr( line, len + 1) ); + } else { + return "" inline( text ) "" inline( substr( line, len + 1) ); + } + + # reference style links + } else if ( match(line, /^\[([^]]+)\] ?\[([^]]*)\]/ ) ) { + len = RLENGTH; + text = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\1", 1, line); + id = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\2", 1, line); + if ( ! id ) id = text; + if ( rl_href[id] && rl_title[id] ) { + return "" inline(text) "" inline( substr( line, len + 1) ); + } else if ( rl_href[id] ) { + return "" inline(text) "" inline( substr( line, len + 1) ); + } else { + return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) ); + } + + # inline images + } else if ( match(line, /^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/) ) { + len = RLENGTH; + text = gensub(/^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\1", "g", line); + href = gensub(/^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\2", "g", line); + title = gensub(/^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\4", "g", line); + if ( title ) { + return "\""" inline( substr( line, len + 1) ); + } else { + return "\""" inline( substr( line, len + 1) ); + } + + # reference style images + } else if ( match(line, /^!\[([^]]+)\] ?\[([^]]*)\]/ ) ) { + len = RLENGTH; + text = gensub(/^!\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\1", 1, line); + id = gensub(/^!\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\2", 1, line); + if ( ! id ) id = text; + if ( rl_href[id] && rl_title[id] ) { + return "\""" inline( substr( line, len + 1) ); + } else if ( rl_href[id] ) { + return "\""" inline( substr( line, len + 1) ); + } else { + return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) ); + } + + # ~~strikeout~~ (pandoc) + } else if ( match(line, /^~~([[:graph:]]|[[:graph:]]([^~]|~[^~])*[[:graph:]])~~/) ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 4 ) ) "" inline( substr( line, len + 1 ) ); + + # ^superscript^ (pandoc) + } else if ( match(line, /^\^([^[:space:]^]|\\[ ^])+\^/) ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # ~subscript~ (pandoc) + } else if ( match(line, /^~([^[:space:]~]|\\[ ~])+~/) ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # ignore embedded underscores (pandoc, php md) + } else if ( match(line, "^[[:alnum:]](__|_)") ) { + return HTML(substr( line, 1, RLENGTH)) inline( substr(line, RLENGTH + 1) ); + + # __strong__$ + } else if ( match(line, "^__(([^_[:space:]]|" ieu ")|([^_[:space:]]|" ieu ")(" nu "|" ieu ")*([^_[:space:]]|" ieu "))__$") ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 4 ) ) "" inline( substr( line, len + 1 ) ); + + # __strong__ + } else if ( match(line, "^__(([^_[:space:]]|" ieu ")|([^_[:space:]]|" ieu ")(" nu "|" ieu ")*([^_[:space:]]|" ieu "))__[[:space:][:punct:]]") ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 5 ) ) "" inline( substr( line, len) ); + + # **strong** + } else if ( match(line, "^\\*\\*(([^\\*[:space:]]|" iea ")|([^\\*[:space:]]|" iea ")(" na "|" iea ")*([^\\*[:space:]]|" iea "))\\*\\*") ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 4 ) ) "" inline( substr( line, len + 1 ) ); + + # _em_$ + } else if ( match(line, "^_(([^_[:space:]]|" isu ")|([^_[:space:]]|" isu ")(" nu "|" isu ")*([^_[:space:]]|" isu "))_$") ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # _em_ + } else if ( match(line, "^_(([^_[:space:]]|" isu ")|([^_[:space:]]|" isu ")(" nu "|" isu ")*([^_[:space:]]|" isu "))_[[:space:][:punct:]]") ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 3 ) ) "" inline( substr( line, len ) ); + + # *em* + } else if ( match(line, "^\\*(([^\\*[:space:]]|" isa ")|([^\\*[:space:]]|" isa ")(" na "|" isa ")*([^\\*[:space:]]|" isa "))\\*") ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # Macros + } else if ( AllowMacros && match( line, /^<<([^>]|>[^>])+>>/) ) { + len = RLENGTH; + return macro( substr( line, 3, len - 4 ) ) inline(substr(line, len + 1)); + + # Verbatim inline HTML + } else if ( AllowHTML && match( line, /^(|<\?([^\?]|\?[^>])*\?>|]*>|])*\]\]>|<\/[A-Za-z][A-Za-z0-9-]*[[:space:]]*>|<[A-Za-z][A-Za-z0-9-]*([[:space:]]+[A-Za-z_:][A-Za-z0-9_\.:-]*([[:space:]]*=[[:space:]]*([[:space:]"'=<>`]+|"[^"]*"|'[^']*'))?)*[[:space:]]*\/?>)/) ) { + len = RLENGTH; + return substr( line, 1, len) inline(substr(line, len + 1)); + + # Literal HTML entities + } else if ( match( line, /^&([a-zA-Z]{2,32}|#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6});/) ) { + len = RLENGTH; + return substr( line, 1, len ) inline(substr(line, len + 1)); + + # Escape lone HTML character + } else if ( match( line, /^[&<>"']/) ) { + return HTML(substr(line, 1, 1)) inline(substr(line, 2)); + + # continue walk over string + } else { + return substr(line, 1, 1) inline( substr(line, 2) ); + } +} + +function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent, attrib ) { + gsub( /^\n+|\n+$/, "", block ); + + if ( block == "" ) { + return ""; + + # HTML #2 #3 #4 $5 + } else if ( AllowHTML && match( block, /(^|\n) ? ? ?(|$)|<\?([^\?]|\?[^>])*(\?>|$)|]*(>|$)|])*(\]\]>|$))/) ) { + len = RLENGTH; st = RSTART; + return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len)); + + # HTML #6 + } else if ( AllowHTML && match( tolower(block), /(^|\n) ? ? ?<\/?(address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)([[:space:]\n>]|\/>)([^\n]|\n[ \t]*[^\n])*(\n[[:space:]]*\n|$)/) ) { + len = RLENGTH; st = RSTART; + return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len)); + + # HTML #1 + } else if ( AllowHTML && match( tolower(block), /(^|\n) ? ? ?<(script|pre|style)([[:space:]\n>]).*(<\/script>|<\/pre>|<\/style>|$)/) ) { + len = RLENGTH; st = RSTART; + match( tolower(substr(block, st, len)), /(<\/script>|<\/pre>|<\/style>)/); + len = RSTART + RLENGTH; + return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len)); + + # HTML #7 + } else if ( AllowHTML && match( block, /^ ? ? ?(<\/[A-Za-z][A-Za-z0-9-]*[[:space:]]*>|<[A-Za-z][A-Za-z0-9-]*([[:space:]]+[A-Za-z_:][A-Za-z0-9_\.:-]*([[:space:]]*=[[:space:]]*([[:space:]"'=<>`]+|"[^"]*"|'[^']*'))?)*[[:space:]]*\/?>)([[:space:]]*\n)([^\n]|\n[ \t]*[^\n])*(\n[[:space:]]*\n|$)/) ) { + len = RLENGTH; st = RSTART; + return substr(block, st, len) _block(substr(block, st + len)); + + # Blockquote (leading >) + } else if ( match( block, /^> /) ) { + match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match(block, /$/); + len = RLENGTH; st = RSTART; + return "
\n" _block( gensub( /(^|\n)> /, "\n", "g", substr(block, 1, st - 1) ) ) "
\n\n" \ + _block( substr(block, st + len) ); + + # Line Blocks (pandoc) + } else if ( match(block, /^\| [^\n]*(\n|$)(\| [^\n]*(\n|$)|[ \t]+[^\n[:space:]][^\n]*(\n|$))*/) ) { + len = RLENGTH; st = RSTART; + code = substr(block, 1, len); + gsub(/\n[[:space:]]+/, " ", code); + gsub(/\n\| /, "\n", code); + gsub(/^\| |\n$/, "", code); + return "
" gensub(/\n/, "
\n", "g", inline( code )) "
\n" \ + _block( substr( block, len + 1) ); + + # Indented Code Block + } else if ( match(block, /^( |\t)[^\n]+(\n|$)(( |\t)[^\n]+(\n|$)|[ \t]*(\n|$))*/) ) { + len = RLENGTH; st = RSTART; + code = substr(block, 1, len); + gsub(/(^|\n)( |\t)/, "\n", code); + gsub(/^\n|\n+$/, "", code); + return "
" HTML( code ) "
\n" \ + _block( substr( block, len + 1 ) ); + + # Fenced Divs (pandoc, custom) + } else if ( match( block, /^(:::+)/ ) ) { + guard = substr( block, 1, RLENGTH ); + code = gensub(/^[^\n]+\n/, "", 1, block); + attrib = gensub(/^:::+[ \t]*\{?[ \t]*([^\}\n]*)\}?[ \t]*\n.*$/, "\\1", 1, block); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); + gsub(/(^ | $)/, "", attrib); + if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) { + len = RLENGTH; st = RSTART; + return "
" _block( substr(code, 1, st - 1) ) "
\n" \ + _block( substr( code, st + len ) ); + } else { + match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ ); + len = RLENGTH; st = RSTART; + return "

" inline( substr(block, 1, st - 1) ) "

\n" \ + _block( substr(block, st + len) ); + } + + # Fenced Code Block (pandoc) + } else if ( match( block, /^(~~~+|```+)/ ) ) { + guard = substr( block, 1, RLENGTH ); + code = gensub(/^[^\n]+\n/, "", 1, block); + attrib = gensub(/^:::+[ \t]*\{?[ \t]*([^\}\n]*)\}?[ \t]*\n.*$/, "\\1", 1, block); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); + gsub(/(^ | $)/, "", attrib); + if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) { + len = RLENGTH; st = RSTART; + return "
" HTML( substr(code, 1, st - 1) ) "
\n" \ + _block( substr( code, st + len ) ); + } else { + match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ ); + len = RLENGTH; st = RSTART; + return "

" inline( substr(block, 1, st - 1) ) "

\n" \ + _block( substr(block, st + len) ); + } + + # Unordered list + } else if ( match( block, "^ ? ? ?[-+*][ \t]+[^\n]+(\n|$)" \ + "(([ \t]*\n)* ? ? ?[-+*][ \t]+[^\n]+(\n|$)" \ + "|([ \t]*\n)*( ? ? ?\t| +)[^\n]+(\n|$)" \ + "|[^\n]+(\n|$))*" ) ) { + list = substr( block, 1, RLENGTH); + block = substr( block, RLENGTH + 1); + indent = length( gensub(/[-+*][ \t]+[^\n]+.*$/, "", 1, list) ); + + gsub("(^|\n) {0," indent "}", "\n", list); + return "\n
    \n" _list( substr(list, 2) ) "
\n" _block( block ); + + # Ordered list + } else if ( match( block, "^ ? ? ?([0-9]+|#)\\.[ \t]+[^\n]+(\n|$)" \ + "(([ \t]*\n)* ? ? ?([0-9]+|#)\\.[ \t]+[^\n]+(\n|$)" \ + "|([ \t]*\n)*( ? ? ?\t| +)[^\n]+(\n|$)" \ + "|[^\n]+(\n|$))*" ) ) { + list = substr( block, 1, RLENGTH); + block = substr( block, RLENGTH + 1); + indent = length( gensub(/([0-9]+|#)\.[ \t]+[^\n]+.*$/, "", 1, list) ); + + gsub("(^|\n) {0," indent "}", "\n", list); + return "\n
    \n" _list( substr(list, 2) ) "
\n" _block( block ); + + # First Order Heading + } else if ( match( block, /^[^\n]+\n===+(\n|$)/ ) ) { + len = RLENGTH; + HL[1]++; HL[2] = 0; HL[3] = 0; HL[4] = 0; HL[5] = 0; HL[6] = 0; + return "

" inline( gensub( /\n.*$/, "", "g", block ) ) "

\n\n" \ + _block( substr( block, len + 1 ) ); + + # Second Order Heading + } else if ( match( block, /^[^\n]+\n---+(\n|$)/ ) ) { + len = RLENGTH; + HL[2]++; HL[3] = 0; HL[4] = 0; HL[5] = 0; HL[6] = 0; + return "

" inline( gensub( /\n.*$/, "", "g", block ) ) "

\n\n" \ + _block( substr( block, len + 1) ); + + # Nth Order Heading + } else if ( match( block, /^#{1,6}[ \t]*[^\n]+([ \t]*#*)(\n|$)/ ) ) { + len = RLENGTH; + hlvl = length( gensub( /^(#{1,6}).*$/, "\\1", "g", block ) ); + htxt = gensub(/^#{1,6}[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[^\n#])+)([ \t]*#*)(\n.*)?$/, "\\1", 1, block); + HL[hlvl]++; for ( n = hlvl + 1; n < 7; n++) { HL[n] = 0;} + hid = HL[1]; for ( n = 2; n <= hlvl; n++) { hid = hid "." HL[n] ; } + return "" inline( htxt ) "\n\n" \ + _block( substr( block, len + 1) ); + + # Split paragraphs + } else if ( match( block, /(^|\n)[[:space:]]*(\n|$)/) ) { + len = RLENGTH; st = RSTART; + return _block( substr(block, 1, st - 1) ) "\n" \ + _block( substr(block, st + len) ); + + # Horizontal rule + } else if ( match( block, /(^|\n) ? ? ?((\* *){3,}|(- *){3,}|(_ *){3,})($|\n)/) ) { + len = RLENGTH; st = RSTART; + return _block(substr(block, 1, st - 1)) "
\n" _block(substr(block, st + len)); + + # Plain paragraph + } else { + return "

" inline(block) "

\n"; + } +} + +function _list( block, last, LOCAL, p) { + if ( ! length(block) ) return ""; + gsub(/^([-+*]|[0-9]+\.|#\.)( ? ? ?|\t)/, "", block) + + # slice next list item from input + if ( match( block, /\n([-+*]|[0-9]+\.|#\.)[ \t]+[^\n]+/) ) { + p = substr( block, 1, RSTART); + block = substr( block, RSTART + 1); + } else { + p = block; block = ""; + } + sub( /\n +([-+*]|[0-9]+\.|#\.)/, "\n&", p ); + + # if this should be a paragraph item + # either previous item (last) or current item (p) contains blank lines + if (match(last, /\n[[:space:]]*\n/) || match(p, /\n[[:space:]]*\n/) ) { + last = p; p = _block(p); + } else { + last = p; p = _block(p); + sub( /^

/, "", p ); + sub( /<\/p>\n/, "", p ); + } + sub( /\n$/, "", p ); + + # Task List (pandoc) + if ( p ~ /^\[ \].*/ ) { p = "" substr(p, 4); } + else if ( p ~ /^\[[xX]\].*/ ) { p = "" substr(p, 4); } + else if ( p ~ /^

\[ \].*/ ) { p = "

" substr(p, 7); } + else if ( p ~ /^

\[[xX]\].*/ ) { p = "

" substr(p, 7); } + return "

  • " p "
  • \n" _list( block, last ); +} + +BEGIN { + # Global Vars + file = ""; rl_href[""] = ""; rl_title[""] = ""; + if (ENVIRON["MD_HTML"] == "true") { AllowHTML = "true"; } + HL[1] = 0; HL[2] = 0; HL[3] = 0; HL[4] = 0; HL[5] = 0; HL[6] = 0; + + # Buffering of full file ist necessary, e.g. to find reference links + while (getline) { file = file $0 "\n"; } + # Clean up MS-DOS line breaks + gsub(/\r\n/, "\n", file); + + # Fill array of reference links + f = file; rl_id; + re_reflink = "(^|\n) ? ? ?\\[([^]\n]+)\\]: ([^ \t\n]+)(\n?[ \t]+(\"([^\"]+)\"|'([^']+)'|\\(([^)]+)\\)))?(\n|$)"; + # /(^|\n) ? ? ?\[([^]\n]+)\]: ([^ \t\n]+)(\n?[ \t]+("([^"]+)"|'([^']+)'|\(([^)]+)\)))?(\n|$)/ + while ( match(f, re_reflink ) ) { + rl_id = gensub( re_reflink, "\\2", 1, substr(f, RSTART, RLENGTH) ); + rl_href[rl_id] = gensub( re_reflink, "\\3", 1, substr(f, RSTART, RLENGTH) ); + rl_title[rl_id] = gensub( re_reflink, "\\5", 1, substr(f, RSTART, RLENGTH) ); + f = substr(f, RSTART + RLENGTH); + rl_title[rl_id] = substr( rl_title[rl_id], 2, length(rl_title[rl_id]) - 2 ); + if ( rl_href[rl_id] ~ /<.*>/ ) rl_href[rl_id] = substr( rl_href[rl_id], 2, length(rl_href[rl_id]) - 2 ); + } + # Clear reflinks from File + while( gsub(re_reflink, "\n", file ) ); + # for (n in rl_href) { debug(n " | " rl_href[n] " | " rl_title[n] ); } + + # Run Block Processing -> The Actual Markdown! + printf "%s", _block( file ); +} diff --git a/session.sh b/session.sh index ee5c499..1f4699e 100755 --- a/session.sh +++ b/session.sh @@ -6,6 +6,28 @@ include_session="$0" _DATE="$(date +%s)" SESSION_TIMEOUT="${SESSION_TIMEOUT:-7200}" +if ! which uuencode >/dev/null; then + uuencode() { busybox uuencode "$@"; } +fi +if ! which sha256sum >/dev/null; then + sha256sum() { busybox sha256sum "$@"; } +fi + +if which openssl >/dev/null; then + session_mac(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | openssl dgst -sha1 -hmac "$(server_key)" -binary |slopecode; } +else + # Gonzo MAC if openssl is unavailable + session_mac(){ + { server_key | dd status=none bs=256 count=1 skip=1 + { server_key | dd status=none bs=256 count=1 + [ $# -gt 0 ] && printf %s "$*" || cat + } \ + | sha256sum -; + } \ + | sha256sum | cut -d\ -f1 + } +fi + server_key(){ IDFILE="${IDFILE:-${_DATA:-.}/serverkey}" if [ "$(stat -c %s "$IDFILE")" -ne 512 ] || ! cat "$IDFILE"; then @@ -18,7 +40,8 @@ slopecode(){ # 6-Bit Code that retains sort order of input data, while beeing safe to use # in ascii transmissions, unix file names, HTTP URLs, and HTML attributes - uuencode -m - | sed ' + { [ $# -gt 0 ] && printf %s "$*" || cat; } \ + | uuencode -m - | sed ' 1d;$d; y;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/;0123456789:=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz; ' @@ -42,42 +65,74 @@ timeid(){ } | slopecode } -checkid(){ grep -m 1 -xE '[0-9a-zA-Z:=]{16}'; } - transid(){ # transaction ID to modify a given file local file="$1" - { stat -c %F%i%n%N%s%Y "$file" 2>&- - printf %s "$SESSION_ID" - server_key - } | sha256sum | cut -d\ -f1 + session_mac "$(stat -c %F%i%n%N%s%Y "$file" 2>&-)" "$SESSION_ID" } +checkid(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | grep -m 1 -xE '[0-9a-zA-Z:=]{16}'; } + update_session(){ - local session sid time sig serverkey checksig + local session sid time sig checksig + unset SESSION_KEY SESSION_ID - IFS=- read -r sid time sig <<-END - $(COOKIE session) + read -r sid time sig <<-END + $(POST session_key || COOKIE session) END - serverkey="$(server_key)" - checksig="$(printf %s "$sid" "$time" "$serverkey" | sha256sum)" - checksig="${checksig%% *}" + 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="$(printf %s "$sid" "$time" "$serverkey" |sha256sum)" - sig="${sig%% *}" - printf %s\\n "${sid}-${time}-${sig}" + sig="$(session_mac "$sid" "$time")" + + 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")" Path="/${_BASE#/}" SameSite=Strict HttpOnly +} + +SESSION_VAR() { + # read authenticated cookie + # fail if value has been tampered with + local key="$1" value sig + value="$(COOKIE "$key")" + sig="${value##* }" value="${value% *}" + if [ "$sig" = "$(session_mac "$value" "$SESSION_ID")" ]; then + printf %s\\n "$value" + else + return 1 + fi +} + +SESSION_COOKIE() { + [ "$1" = new ] && new_session + SET_COOKIE 0 session="$SESSION_KEY" Path="/${_BASE#/}" SameSite=Strict HttpOnly } -SESSION_ID="$(update_session)" -SET_COOKIE 0 session="$SESSION_ID" Path=/ SameSite=Strict HttpOnly -SESSION_ID="${SESSION_ID%%-*}" +update_session || new_session diff --git a/storage.sh b/storage.sh index 7f70e64..22e6acc 100755 --- a/storage.sh +++ b/storage.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Copyright 2018, 2019 Paul Hänsch +# Copyright 2018, 2019, 2021 Paul Hänsch # # This is a file format helper, part of CGIlite. # @@ -25,62 +25,47 @@ BR=' ' LOCK(){ - local lock timeout block - lock="${1}.lock" - timeout="${2-20}" - if [ \! -w "${lock%/*}" ] || [ -e "$lock" -a \! -d "$lock" ]; then - printf 'Impossible to get lock: %s\n' "$lock" >&2 + local lock="${1}.lock" timeout="${2-20}" block + + if [ \! -w "${lock%/*}" ] || [ -e "$lock" -a \! -f "$lock" ]; then + debug "Impossible to get lock: $lock" return 1 fi - while ! mkdir "$lock" 2>&-; do - block="$(cat "$lock/pid" || printf 1)" - if ! { ps -eo pid |grep -qwF "$block"; }; then - printf 'Overriding stale lock: %s\n' "$lock" >&2 - break - fi - if [ $timeout -le 0 ]; then - printf 'Timeout while trying to get lock: %s\n' "$lock" >&2 - return 1 + while [ $timeout -gt 0 ]; do + printf '%i\n' $$ >>"${lock}" + read block <"$lock" + if [ "$block" = $$ ]; then + return 0 + elif ! { ps -eo pid |grep -qwF "$block"; }; then + debug "Trying to override stale lock: $lock" + if LOCK "$lock" 1; then + rm -- "$lock" + RELEASE "$lock" + fi + else + timeout=$((timeout - 1)) + [ $timeout -gt 0 ] && sleep 1 fi - timeout=$((timeout - 1)) - sleep 1 done - printf '%i\n' $$ >"${lock}/pid" - return 0 + + debug "Timeout while trying to get lock: $lock" + return 1 } RELEASE(){ - local lock - lock="${1}.lock" - if [ "$(cat "$lock/pid")" = "$$" ]; then - rm "$lock/pid" - if ! rmdir "$lock"; then - printf 'Cannot remove tainted lock: %s\n' "$lock" >&2 - printf '%i\n' $$ >"${lock}/pid" - return 1 - fi + local lock="${1}.lock" block + + read block <"$lock" + if [ "$block" = $$ ]; then + rm -- "$lock" return 0 else - printf 'Refusing to release foreign lock: %s\n' "$lock" >&2 + debug "Refusing to release foreign lock: $lock" return 1 fi } -STRING=' - s;\\;\\\\;g; - s;\n;\\n;g; - s;\t;\\t;g; - s;\r;\\r;g; - s;\+;\\+;g; - s; ;+;g; -' - -STRING_OLD(){ - { [ $# -eq 0 ] && cat || printf %s "$*"; } \ - | sed -E ':X; $!{N;bX;}'"$STRING" -} - STRING(){ local in out='' [ $# -gt 0 ] && in="$*" || in="$(cat)" @@ -93,24 +78,9 @@ STRING(){ " "*) out="${out}+"; in="${in# }" ;; *) out="${out}${in%%[\\${CR}${BR} + ]*}"; in="${in#"${in%%[\\${BR}${CR} + ]*}"}" ;; esac; done - printf '%s' "$out" + printf '%s' "${out:-\\}" } - -UNSTRING=' - :UNSTRING_X - s;((^|[^\\])(\\\\)*)\\n;\1\n;g; - s;((^|[^\\])(\\\\)*)\\t;\1\t;g; - s;((^|[^\\])(\\\\)*)\\r;\1\r;g; - s;((^|[^\\])(\\\\)*)\+;\1 ;g; - tUNSTRING_X; - s;((^|[^\\])(\\\\)*)\\\+;\1+;g; - s;\\\\;\\;g; -' -UNSTRING_OLD(){ - { [ $# -eq 0 ] && cat || printf %s "$*"; } \ - | sed -E "$UNSTRING" -} UNSTRING(){ local in out='' [ $# -gt 0 ] && in="$*" || in="$(cat)" @@ -119,11 +89,103 @@ UNSTRING(){ \\n*) out="${out}${BR}"; in="${in#\\n}" ;; \\r*) out="${out}${CR}"; in="${in#\\r}" ;; \\t*) out="${out} "; in="${in#\\t}" ;; - \\+) out="${out}+"; in="${in#\\+}" ;; + \\+*) out="${out}+"; in="${in#\\+}" ;; +*) out="${out} "; in="${in#+}" ;; \\*) in="${in#\\}" ;; *) out="${out}${in%%[\\+]*}"; in="${in#"${in%%[\\+]*}"}" ;; esac; done - printf '%s' "$out" + printf '%s\n' "$out" } +DBM() { + local file="$1" cmd="$2" + local k v key value + shift 2; + + case "$cmd" in + check|contains) + key="$(STRING "$1")" + while read -r k v; do if [ "$k" = "$key" ]; then + return 0 + fi; done <"$file" 2>&- + return 1 + ;; + get) + key="$(STRING "$1")" + while read -r k v; do if [ "$k" = "$key" ]; then + UNSTRING "$v" + return 0 + fi; done <"$file" 2>&- + return 1 + ;; + set|store) + key="$(STRING "$1")" value="$(STRING "$2")" + LOCK "$file" || return 1 + { while read -r k v; do + [ "$k" = "$key" ] || printf '%s\t%s\n' "$k" "$v" + done <"$file" 2>&- + printf '%s\t%s\n' "$key" "$value" + } >"${file}.$$.tmp" + mv "${file}.$$.tmp" "${file}" + RELEASE "$file" + return 0 + ;; + add|insert) + k="$1" key="$(STRING "$1")" value="$(STRING "$2")" + LOCK "$file" || return 1 + if DBM "$file" check "$k"; then + RELEASE "$file" + return 1 + else + printf '%s\t%s\n' "$key" "$value" >>"${file}" + RELEASE "$file" + return 0 + fi + ;; + update|replace) + k="$1" key="$(STRING "$1")" value="$(STRING "$2")" + LOCK "$file" || return 1 + if ! DBM "$file" check "$k"; then + RELEASE "$file" + return 1 + fi + { while read -r k v; do + [ "$k" = "$key" ] \ + && printf '%s\t%s\n' "$key" "$value" \ + || printf '%s\t%s\n' "$k" "$v" + done <"$file" 2>&- + } >"${file}.$$.tmp" + mv "${file}.$$.tmp" "${file}" + RELEASE "$file" + return 0 + ;; + append) + key="$(STRING "$1")" value="$(STRING "$2")" + LOCK "$file" || return 1 + if ! DBM "$file" check "$1"; then + RELEASE "$file" + return 1 + fi + { while read -r k v; do + [ "$k" = "$key" ] \ + && printf '%s\t%s\n' "$key" "$v$value" \ + || printf '%s\t%s\n' "$k" "$v" + done <"$file" 2>&- + } >"${file}.$$.tmp" + mv "${file}.$$.tmp" "${file}" + RELEASE "$file" + return 0 + ;; + delete|remove) + key="$(STRING "$1")" + LOCK "$file" || return 1 + { while read -r k v; do + [ "$k" = "$key" ] || printf '%s\t%s\n' "$k" "$v" + done <"$file" 2>&- + } >"${file}.$$.tmp" + mv "${file}.$$.tmp" "${file}" + RELEASE "$file" + return 0 + ;; + esac +} diff --git a/users.sh b/users.sh new file mode 100755 index 0000000..873edf0 --- /dev/null +++ b/users.sh @@ -0,0 +1,515 @@ +#!/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%:*}}" + +# == FILE FORMAT == +# UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE +# (pending|active|deleted) + +# == GLOBALS == +UNSET_USER='unset \ + USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE +' + +LOCAL_USER='local \ + USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE +' + +unset USER_IDMAP +eval "$UNSET_USER" + +user_db="${user_db:-${_DATA}/users.db}" + +read_user() { + local user="$1" + + # Global exports + USER_ID='' USER_NAME='' USER_STATUS='' USER_EMAIL='' USER_PWSALT='' + USER_PWHASH='' USER_EXPIRE='' USER_DEVICES='' USER_FUTUREUSE='' + + if [ $# -eq 0 ]; then + read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE + elif [ "$user" -a -f "$user_db" -a -r "$user_db" ]; then + read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE <<-EOF + $(grep "^${user} " "${user_db}") + EOF + fi + if [ "$USER_ID" -a "${USER_EXPIRE:-0}" -gt "$_DATE" ]; then + USER_NAME="$(UNSTRING "$USER_NAME")" + USER_EMAIL="$(UNSTRING "$USER_EMAIL")" + USER_DEVICES="$(UNSTRING "$USER_DEVICES")" + unset USER_PWSALT USER_PWHASH + else + eval "$UNSET_USER" + return 1 + fi +} + +update_user() { + # internal function for user update + local uid="$1" uname status email pwsalt pwhash expire devices futureuse + local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + local arg + + for arg in "$@"; do case $arg in + uname=*) uname="${arg#*=}";; + status=*) status="${arg#*=}";; + email=*) email="${arg#*=}";; + password=*) pwsalt="$(randomid)"; pwhash="$(user_pwhash "$pwsalt" "${arg#*=}")";; + expire=*) expire="${arg#*=}";; + devices=*) devices="${arg#*=}";; + esac; done + + if LOCK "$user_db"; then + while read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \ + FUTUREUSE; do + if [ "$UID" = "$uid" ]; then + printf '%s %s %s %s %s %s %i %s %s\n' \ + "$uid" "$(STRING "${uname-$(UNSTRING "$UNAME")}")" \ + "${status:-${status-${STATUS}}${status+\\}}" \ + "${email:-${email-${EMAIL}}${email+\\}}" \ + "${pwsalt:-${PWSALT}}" "${pwhash:-${PWHASH}}" \ + "${expire:-$((_DATE + 86400 * 730))}" \ + "$(STRING "${devices-$(UNSTRING "$DEVICES")}")" \ + "${FUTUREUSE:-\\}" + elif [ "$STATUS" = pending -a ! "$EXPIRE" -ge "$_DATE" ]; then + # omit expired invitations from output + : + else + printf '%s %s %s %s %s %s %i %s %s\n' \ + "$UID" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \ + "$EXPIRE" "$DEVICES" "$FUTUREUSE" + fi + done <"$user_db" >"${user_db}.$$" + mv -- "${user_db}.$$" "$user_db" + RELEASE "$user_db" + else + return 1 + fi +} + +new_user(){ + local user="${1:-$(timeid)}" + shift 1 + + if LOCK "$user_db"; then + if grep -q "^${user} " "$user_db"; then + RELEASE "$user_db" + return 1 + fi + printf '%s \\ %s \\ \\ \\ %i \\ \\\n' \ + "$user" "pending" "$(( $_DATE + 86400 ))" >>"$user_db" + else + return 1 + fi + + if [ $# -eq 0 ]; then + RELEASE "$user_db" + return 0 + elif update_user "$user" "$@"; then + return 0 + else + RELEASE "$user_db" + return 1 + fi +} + +user_idmap(){ + local uid="$1" ret + eval "$LOCAL_USER" + + if [ ! "$USER_IDMAP" ]; then + while read_user; do + USER_IDMAP="${USER_IDMAP}${USER_ID} ${USER_NAME}${BR}" + done <"$user_db" + fi + if [ "$uid" -a "$USER_IDMAP" != "${USER_IDMAP##*${uid} }" ]; then + ret="${USER_IDMAP##*${uid} }"; ret="${ret%%${BR}*}"; + printf '%s\n' "$ret" + return 0 + elif [ "$uid" ]; then + return 1 + else + printf '%s' "$USER_IDMAP" + return 0 + fi +} + +user_idof(){ + local name="$(STRING "$1")" ret + [ "$USER_IDMAP" ] || user_idmap >/dev/null + + if [ "${name%\\}" -a "$USER_IDMAP" != "${USER_IDMAP% ${name}${BR}*}" ]; then + ret="${USER_IDMAP% ${name}${BR}*}"; ret="${ret##*${BR}}" + printf '%s\n' "$ret" + return 0 + else + return 1 + 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 -a -s "$user_db" ]; 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 new_user "$uid" status=pending email="$email" expire="$((_DATE + 86400))"; then + debug "Sending Activation Link:" \ + "https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")" + 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 new_user "$uid" uname="$uname" status=active email="$email" password="$pw" expire="$((_DATE + 86400 * 730))"; then + 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_invite(){ + local uid="$(timeid)" + local email="$(POST email |user_checkemail)" + local message="$(POST message)" + + if [ ! "email" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID" + elif user_emailexist "$email"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS" + elif new_user "$uid" status=pending email="$email" expire="$((_DATE + 86400))"; then + debug "Sending Invitation Link:" \ + "https://${HTTP_HOST}${BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")" + sendmail -t -f "$MAILFROM" <<-EOF + From: ${MAILFROM} + To: ${email} + Subject: You have been invited to ${HTTP_HOST%:*} + + ${USER_NAME:-Someone} has offered an invitation to this email address. + + ${message} + + You can create 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 do not know what this is about, 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 +} + +user_confirm(){ + # enable account + eval "$LOCAL_USER" + local uid="$(POST uid |checkid || printf invalid)" + local signature="$(POST signature)" + local uname="$(POST uname |user_checkname)" + local pwsalt="$(randomid)" + local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)" + + read_user "${uid}" + + 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 [ "$USER_STATUS" != pending -o \! "$USER_EXPIRE" -gt "$_DATE" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID" + elif update_user "$USER_ID" uname="$uname" status=active password="$pw"; then + SESSION_COOKIE new + SESSION_BIND user_id "$USER_ID" + REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM" + 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 + SESSION_COOKIE new + SET_COOKIE 0 user_id="" Path="/${_BASE#/}" SameSite=Strict HttpOnly + REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT" +} + +user_update(){ + # passphrase, email + : +} +user_recover(){ + # send recover link + : +} +user_disable(){ + : +} + +read_user "$(SESSION_VAR user_id)" +[ "$USER_STATUS" -a "$USER_STATUS" != active ] && eval $UNSET_USER + +[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in + user_register) user_register ;; + user_confirm) user_confirm ;; + user_invite) user_invite ;; + 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 -a -s "$user_db" ]; 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_invite(){ + if [ "$(GET user_confirm)" ]; then + w_user_confirm + elif [ "$USER_ID" ]; then + cat <<-EOF + [form #user_invite method=POST + [input placeholder="Email Recipient" name=email autocomplete=off] + [textarea name="message" placeholder="Message to recipient" . ] + [submit "action" "user_invite" Send Invitation] + ] + EOF + else + cat <<-EOF + [div #user_invite .notallowed + Only registered users may send an invitation to another user. + ] + 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 Logged in as [span . $(HTML ${USER_NAME})]] + [submit "action" "user_logout" Logout] + ] + EOF + fi +} -- 2.39.2