$line
\n" 463 | done 464 | 465 | echo -e "$HTML" 466 | } 467 | 468 | # Initializes various project-wide things. 469 | function project.Load { 470 | PROJECT=$(realpath $1) 471 | DOMAIN=${PROJECT##*/} 472 | [[ -f $1/.etc/.env ]] && loggg "Loading $(realpath $1/.etc/.env)" && source $1/.etc/.env 473 | 474 | local URL=$(project.URL) 475 | 476 | loggg "Project directory is ${lcWhite}$PROJECT" 477 | loggg "Project URL is ${lcU}${lcCyan}$URL${lcX}" 478 | } 479 | 480 | # Yields a fully qualified project URL, with domain name and port number. 481 | # All arguments are joined with / and used as path. 482 | function project.URL { 483 | [[ $PORT != 80 ]] && uPORT=":$PORT" 484 | 485 | local path=$(array.join '/' $@) 486 | echo "http://$DOMAIN$uPORT/$path" 487 | } 488 | 489 | # A regular HTTP redirect response. 490 | # Arguments: 491 | # $1: A URL to relocate useragent to. 492 | # $2: An optional 30* HTTP status code. 493 | function resp.Redirect { 494 | resp.Status ${2:-302} 495 | resp.Header "Location" "$1" 496 | resp.Body "" 497 | } 498 | -------------------------------------------------------------------------------- /libbashttpd/content/application/json.sh: -------------------------------------------------------------------------------- 1 | _IFS=$IFS 2 | IFS=$'\r' 3 | CL=0 4 | 5 | # Reading body, 1 char at a time 6 | # Regular read can't get the last line because of missing newline on this Content-Type 7 | if [ -z ${CONTENT_LENGTH+x} ]; then 8 | : 9 | else 10 | while [ $CL -lt $CONTENT_LENGTH ]; do 11 | read -n1 -r CHAR 12 | BODY="$BODY$CHAR" 13 | let CL=CL+1 14 | done; 15 | 16 | # Debug dump 17 | [[ ! -z $DEBUG_DUMP_BODY ]] && echo -n "$BODY" > $DEBUG_DUMP_BODY 18 | fi 19 | 20 | # An implementation of req.Data. 21 | function req.DataImpl { 22 | if ! sys.Installed jq; then 23 | error "jq is not installed." 24 | return 255 25 | fi 26 | 27 | Q=$1 28 | [[ ${Q:0:1} != '.' ]] && Q=".$Q" 29 | local r 30 | r=$(echo -nE "$BODY" | jq -r $Q 2>&1) 31 | local __xc=$? 32 | yield "$r" 33 | return $__xc 34 | } 35 | 36 | IFS=$_IFS -------------------------------------------------------------------------------- /libbashttpd/content/application/x-www-form-urlencoded.sh: -------------------------------------------------------------------------------- 1 | CL=0 2 | 3 | # Reading body, 1 char at a time 4 | # Regular read can't get the last line because of missing newline on this Content-Type 5 | if [ -z ${CONTENT_LENGTH+x} ]; then 6 | : 7 | else 8 | while [ $CL -lt $CONTENT_LENGTH ]; do 9 | read -n1 CHAR 10 | BODY="$BODY$CHAR" 11 | let CL=CL+1 12 | done; 13 | 14 | # Debug dump 15 | [[ ! -z $DEBUG_DUMP_BODY ]] && echo -nE $BODY > $DEBUG_DUMP_BODY 16 | fi 17 | 18 | IFS_backup="$IFS" 19 | IFS='&' 20 | 21 | read -r -a FIELDS <<< "$BODY" 22 | for FIELD in "${FIELDS[@]}"; do 23 | fieldName=$(echo -En "$FIELD" | cut -d "=" -f 1) 24 | fieldValue=$(echo -En "$FIELD" | cut -d "=" -f 2) 25 | 26 | fieldValue=$(HTTP.urldecode $fieldValue) 27 | var "DATA_$fieldName" "$fieldValue" 28 | done 29 | 30 | IFS="$IFS_backup" 31 | 32 | # An implementation of req.Data. 33 | function req.DataImpl { 34 | vn="DATA_$1" 35 | yield ${!vn} 36 | } 37 | -------------------------------------------------------------------------------- /libbashttpd/content/multipart/form-data.sh: -------------------------------------------------------------------------------- 1 | _IFS=$IFS 2 | IFS=$'' 3 | LANG=C 4 | LC_ALL=C 5 | 6 | CL=0 7 | 8 | CHUNK_SIZE=2000 9 | CLT=$CHUNK_SIZE 10 | 11 | function renderProgress { 12 | echo -en "\r Read ${lcCyan}$CL/$CONTENT_LENGTH${lcX} bytes " >&2 13 | local n=$(($CLT/$CHUNK_SIZE)) 14 | local mark="=" 15 | local numMarks=40 16 | local markCLT=$(($CONTENT_LENGTH/$numMarks)) 17 | 18 | echo -en "${lcLCyan}[${lcLMagenta}" >&2 19 | 20 | for ((i=0; i<$CL; i+=$markCLT)); do 21 | echo -En "$mark" >&2 22 | done 23 | 24 | for ((i=$CL; i<$CONTENT_LENGTH; i+=$markCLT)); do 25 | echo -En " " >&2 26 | done 27 | 28 | echo -en "${lcLCyan}] " >&2 29 | } 30 | 31 | # Reads from input until the supplied predicate function returns 0, 32 | # and dumps the contents to a specified file. 33 | # Usage: 34 | # dumpUntil CRLFFound $tmp - reads until found a \r\n sequence and dumps the data to the $tmp file 35 | function dumpUntil { 36 | loggggg " Dumping fast to ${lcWhite}$2${lcX} until ${lcBlue}$1" 37 | LINE="" 38 | # loggggg "" 39 | 40 | if [[ ! -z $X_BWF_UPLOAD_ID ]]; then 41 | # Going to report the progress 42 | loggggg " Upload ID is \"$X_BWF_UPLOAD_ID\", going to report the upload progress to the client." 43 | echo "HTTP/1.1 200" 44 | echo "Content-Type: application/javascript" 45 | echo "" 46 | fi 47 | 48 | while [ $CL -lt $CONTENT_LENGTH ]; do 49 | read -r -d '' -n1 CHAR 50 | let CL=CL+1 51 | 52 | # Slashes interfere with \x00 53 | [[ $CHAR == "\\" ]] && CHAR="\x5c" 54 | [[ -z $CHAR ]] && CHAR="\x00" 55 | 56 | LINE="$LINE$CHAR" 57 | 58 | # Making sure the chunking won't chunk the last content boundary line, 59 | # and CB parsers are able to detect it, so leaving a padding. 60 | CLREM=$(($CONTENT_LENGTH-$CL)) 61 | CB_PADDING=$((${#CONTENT_BOUNDARY}+10)) 62 | if [[ $CL -ge $CLT ]] && [[ $CLREM -gt $CB_PADDING ]]; then 63 | echo -en $LINE >> $2 64 | LINE="" 65 | 66 | CLT=$((CLT+CHUNK_SIZE)) 67 | renderProgress; 68 | 69 | if [[ ! -z $X_BWF_UPLOAD_ID ]]; then 70 | # Reporting the progress as JS to the client. 71 | echo "bwf.renderUploadProgress($CL, $CONTENT_LENGTH);" 72 | fi 73 | fi 74 | 75 | # Testing & breaking 76 | if $1; then 77 | if [[ ${#LINE} > 0 ]]; then 78 | echo -en $LINE >> $2 79 | LINE="" 80 | fi 81 | 82 | renderProgress; 83 | 84 | return 0 85 | fi 86 | done; 87 | 88 | return 255 89 | } 90 | 91 | # Reads from input until the supplied predicate function returns 0 92 | # Usage: 93 | # readUntil CRLFFound - reads until found a \r\n sequence 94 | function readUntil { 95 | LINE="" 96 | HEXLINE="" 97 | while [ $CL -lt $CONTENT_LENGTH ]; do 98 | read -r -d '' -n1 CHAR 99 | let CL=CL+1 100 | LINE="$LINE$CHAR" 101 | 102 | let CLR=$CL%500 103 | if [[ $CLR == 0 ]]; then 104 | local safechar=$(echo -n "$CHAR" | tr '\n' '\\') 105 | loggggg " READ $CL/$CONTENT_LENGTH $hexchar ($safechar)" 106 | fi 107 | 108 | # Testing & breaking 109 | if $1; then 110 | return 0 111 | fi 112 | done; 113 | 114 | return 255 115 | } 116 | 117 | # A predicate function for readUntil. 118 | # Stops when a content boundary is encountered. 119 | function BoundaryFound { 120 | if [[ $LINE =~ "$CONTENT_BOUNDARY"$ ]]; then 121 | return 0 122 | fi 123 | 124 | return 255 125 | } 126 | 127 | # A predicate function for readUntil. 128 | # Stops when a CRLF is encountered. 129 | function CRLFFound { 130 | if [[ ${LINE:${#LINE}-2:2} == $'\r\n' ]]; then 131 | LINE=${LINE::-2} 132 | return 0 133 | fi 134 | 135 | return 255 136 | } 137 | 138 | # A predicate function for readUntil. 139 | # Stops when a CRLF followed by a content boundary is encountered. 140 | function CRLFBoundaryFound { 141 | # The '--' are required by RFC https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html 142 | CB="--$CONTENT_BOUNDARY" 143 | let LEN=${#CB}+2 144 | SEP=$'\r\n'"$CB" 145 | if [[ ${LINE:${#LINE}-$LEN:$LEN} == $SEP ]]; then 146 | LINE=${LINE::-$LEN} 147 | return 0 148 | fi 149 | 150 | return 255 151 | } 152 | 153 | # Looks for a Content-Disposition line and extract param eter and file names from it. 154 | function parseContentDisposition { 155 | if [[ $LINE =~ Content-Disposition: ]]; then 156 | logggg " Found a Content-Disposition" 157 | # Found a content disposition, extracting a parameter name from it 158 | CURRENT_PARAMETER=$(echo -e $LINE | sed -rn 's/.* name\=\"([^"]*)\";{0,1}.*/\1/p') 159 | logggg " Found a parameter ${lcGreen}\"$CURRENT_PARAMETER\"" 160 | 161 | if [[ $LINE =~ ' 'filename= ]]; then 162 | # Found a 'filename=' substring, extracting a file name from it 163 | CURRENT_FILENAME=$(echo -e $LINE | sed -rn 's/.* filename\=\"([^"]*)\";{0,1}.*/\1/p') 164 | logggg " Found a filename ${lcBlue}\"$CURRENT_FILENAME\"" 165 | fi 166 | 167 | NEXT_PARSER=parseCRLF_or_ContentType 168 | 169 | return 0 170 | fi 171 | 172 | return 255 173 | } 174 | 175 | function parseCRLF { 176 | if [[ -z $LINE ]]; then 177 | logggg " Found a CRLF, proceeding to the content body" 178 | # Not setting NEXT_PARSER because parseContent will read the input itself. 179 | parseContent 180 | return 0 181 | fi 182 | 183 | return 255 # evaluates as false in parseCRLF_or_ContentType 184 | } 185 | 186 | # Reads a part of the request body until encounters a content boundary value. 187 | function parseContent { 188 | logggg " Reading the request body" 189 | T=$(sys.TimeElapsed) 190 | 191 | if [[ -z $CURRENT_FILENAME ]]; then 192 | readUntil CRLFBoundaryFound 193 | T=$(sys.TimeElapsed) 194 | loggggg " Took $T seconds to read the request body." 195 | 196 | # Regular values are stored as variables. 197 | var "DATA_$CURRENT_PARAMETER" "$LINE" 198 | loggg " Set ${lcR}${lcGreen}$CURRENT_PARAMETER${lcX} to ${lcWhite}\"$LINE\"${lcX}" 199 | else 200 | # Uploaded files are stored in /tmp... 201 | tmp=$(mktemp) 202 | dumpUntil CRLFBoundaryFound $tmp 203 | T=$(sys.TimeElapsed) 204 | loggg " " 205 | loggggg " Took $T seconds to dump the request body." 206 | 207 | # ...and their filenames are stored as variables. 208 | var "FILE_$CURRENT_PARAMETER" $tmp 209 | var "FILENAME_$CURRENT_PARAMETER" $CURRENT_FILENAME 210 | var "FILECT_$CURRENT_PARAMETER" $CURRENT_CONTENT_TYPE 211 | loggg " Saved ${lcR}${lcGreen}\"$CURRENT_PARAMETER\"${lcX} as ${lcWhite}$tmp${lcX}" 212 | fi 213 | 214 | NEXT_PARSER=parseContentDisposition_or_Fin 215 | 216 | CURRENT_FILENAME="" 217 | CURRENT_PARAMETER="" 218 | CURRENT_CONTENT_TYPE="" 219 | } 220 | 221 | # Multipart data sometimes has it's own Content-Type 222 | function parseContentType { 223 | if [[ $LINE =~ Content-Type: ]]; then 224 | CURRENT_CONTENT_TYPE=$(echo -nE "$LINE" | sed -r 's/\s+//g' | sed -\n 's/.*:\s*\(.*\)/\1/p') 225 | logggg " Found a Content-Type of '$CURRENT_CONTENT_TYPE', proceeding to a CRLF" 226 | NEXT_PARSER=parseCRLF 227 | return 0 228 | fi 229 | 230 | return 255 231 | } 232 | 233 | function parseNothing { 234 | return 0 235 | } 236 | 237 | function parseCRLF_or_ContentType { 238 | if ! parseCRLF; then 239 | parseContentType 240 | fi 241 | } 242 | 243 | function parseContentDisposition_or_Fin { 244 | if ! parseContentDisposition; then 245 | parseFin 246 | fi 247 | } 248 | 249 | function parseFin { 250 | if [[ $LINE = "--" ]]; then 251 | logggg " Found the request end."x 252 | renderProgress; 253 | log "" 254 | NEXT_PARSER=parseNothing 255 | return 0 256 | fi 257 | } 258 | 259 | NEXT_PARSER=parseContentDisposition 260 | 261 | if ! [ -z ${CONTENT_LENGTH+x} ]; then 262 | readUntil BoundaryFound 263 | let LI=0 264 | 265 | T1=$(sys.Time) 266 | while readUntil CRLFFound; do 267 | let LI=$LI+1 268 | logggg "" 269 | loggggg "${lcLGray}Line #$LI is ($LINE) (${#LINE} chars)" 270 | logggg "The parser is ${lcBlue}$NEXT_PARSER" 271 | 272 | [[ ! -z $NEXT_PARSER ]] && $NEXT_PARSER 273 | 274 | done 275 | 276 | T2=$(sys.Time) 277 | loggggg "Done in $(($T2-$T1)) seconds." 278 | 279 | # Debug dump 280 | [[ ! -z $DEBUG_DUMP_BODY ]] && echo -n "$BODY" > $DEBUG_DUMP_BODY 281 | fi 282 | 283 | # An implementation of req.Data. 284 | function req.DataImpl { 285 | vn="DATA_$1" 286 | yield ${!vn} 287 | } 288 | 289 | IFS=$_IFS -------------------------------------------------------------------------------- /libbashttpd/handler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source libbashttpd/utility.sh 4 | source libbashttpd/request.sh 5 | source libbashttpd/router.sh 6 | source libbashttpd/bwf.sh 7 | 8 | [[ -f .env ]] && loggg "Loading .env" && source .env 9 | 10 | project.Load $1 11 | 12 | HTTP.readHeaders 13 | HTTP.normalizeHeaders 14 | HTTP.readBody 15 | 16 | router 17 | 18 | loggg "" 19 | loggg "Fin." -------------------------------------------------------------------------------- /libbashttpd/request.sh: -------------------------------------------------------------------------------- 1 | IFS='' 2 | 3 | rxHeader='^([a-zA-Z-]+)\s*:\s*(.*)' 4 | rxMethod='^(GET|POST|PUT|DELETE|OPTIONS) +(.*) +HTTP' 5 | 6 | # Reads HTTP request headers. 7 | function HTTP.readHeaders { 8 | loggg "${lcLGray}Reading request headers" 9 | 10 | # Debug dump (clear) 11 | [[ ! -z $DEBUG_DUMP_HEADERS ]] && echo -nE "" > $DEBUG_DUMP_HEADERS 12 | while read INPUT; do 13 | # Debug dump 14 | [[ ! -z $DEBUG_DUMP_HEADERS ]] && echo -nE $INPUT >> $DEBUG_DUMP_HEADERS 15 | 16 | if [[ $INPUT =~ $rxHeader ]]; then 17 | headerName=${BASH_REMATCH[1]} 18 | headerValue=${BASH_REMATCH[2]} 19 | 20 | # Trimming off whitespace 21 | headerValue="$(echo -e "${headerValue}" | sed -r 's/\s+//g')" 22 | 23 | loggg " ${lcYellow}$headerName${lcX}: ${lcLGray}$headerValue${lcX}" 24 | 25 | # Replacing - with _ in header names and uppercasing them 26 | headerName="$(echo -e "${headerName}" | sed -r 's/-/_/g' | sed -e 's/\(.*\)/\U\1/g')" 27 | 28 | # This creates variables named after header names with header values 29 | var $headerName "$headerValue" 30 | 31 | # Figuring out the request method used 32 | elif [[ $INPUT =~ $rxMethod ]]; then 33 | reqMethod=${BASH_REMATCH[1]} 34 | reqURL=${BASH_REMATCH[2]} 35 | reqPath=${reqURL%%\?*} 36 | reqQuery=${reqURL#*\?} 37 | 38 | [[ $reqQuery == $reqPath ]] && reqQuery="" 39 | 40 | log "Request is ${lcLYellow}$reqMethod${lcX} @ ${lcU}${lcLCyan}$reqPath${lcX}" 41 | 42 | if [[ ! -z $reqQuery ]]; then 43 | logg "Query string is ${lcCyan}$reqQuery" 44 | 45 | # Parsing the query string. 46 | readarray -t -d '&' QSA <<< "$reqQuery" 47 | for QSP in ${QSA[@]}; do 48 | # Somehow this fixes that weird trailing \n 49 | QSP=$(echo "${QSP}") 50 | readarray -t -d '=' QSKV <<< "$QSP" 51 | QSK=$(echo "${QSKV[0]}") 52 | QSV=$(echo "${QSKV[1]}") 53 | QSK=$(HTTP.urldecode $QSK) 54 | QSV=$(HTTP.urldecode $QSV) 55 | var "QS_$QSK" "$QSV" 56 | 57 | loggg " ${lcCyan}$QSK${lcX} = ${lcLGray}$QSV${lxC}" 58 | done 59 | fi 60 | 61 | # Done with headers 62 | else 63 | loggg "" 64 | break 65 | fi 66 | done 67 | } 68 | 69 | # Pulls extra info from some headers' values, like content boundaries, strips unused stuff. 70 | function HTTP.normalizeHeaders { 71 | # Figuring out the content boundary in case we have a multipart/form-data Content-Type 72 | if [[ $CONTENT_TYPE =~ ^multipart\/form\-data ]]; then 73 | CONTENT_BOUNDARY="$(echo $CONTENT_TYPE | sed -n 's/.*data\;boundary=\(.*\)/\1/p')" 74 | fi 75 | 76 | # Cleaning Content-Type if it has stuff after ; 77 | if [[ $CONTENT_TYPE =~ \; ]]; then 78 | CONTENT_TYPE="$(echo $CONTENT_TYPE | sed -n 's/\(.*\);.*/\1/p')" 79 | fi 80 | } 81 | 82 | # Reads an HTTP request body contents. Different Content-Types must be read & parsed differently, 83 | # so it relies on specific implementations of body parsers. 84 | function HTTP.readBody { 85 | if ! [[ -z $CONTENT_TYPE ]] && [[ $CONTENT_LENGTH -gt 0 ]]; then 86 | loggg "${lcLGray}Reading request body" 87 | 88 | # Choosing a parser for the rest of request data based on Content-Type 89 | parserFile="libbashttpd/content/$CONTENT_TYPE.sh" 90 | 91 | if [[ -f $parserFile ]]; then 92 | source $parserFile 93 | else 94 | log "${lcbgLRed}${lcWhite}The Content-Type \"$CONTENT_TYPE\" is not supported yet. Please implement and submit a pull request @ github.com/x1n13y84issmd42/bashttpd${lcX}" 95 | fi 96 | 97 | loggg "" 98 | fi 99 | } -------------------------------------------------------------------------------- /libbashttpd/router.sh: -------------------------------------------------------------------------------- 1 | # Routing of requests happens here. 2 | # It works in 3 ways: 3 | # If request path exactly matches a file within the $PROJECT directory - serve the file as it is; 4 | # If request path exactly matches a directory within the $PROJECT directory - serve "index.html" from there; 5 | # Otherwise it concatenates the request path and method, adds a trailing ".sh", 6 | # then tries to execute the result as a controller script. 7 | function router() { 8 | ctrler="$PROJECT$reqPath/$reqMethod.sh" 9 | staticFile="$PROJECT$reqPath" 10 | 11 | if [ -f "$ctrler" ]; then 12 | log "${lcLGray}Executing the controller ${lcBlue}$PROJECT${lcLCyan}$reqPath/${lcX}${lcLYellow}$reqMethod${lcBlue}.sh" 13 | # This must be here in order for POST variables with spaces 14 | # to expand in templates correctly 15 | IFS=$'' 16 | source $ctrler 17 | 18 | elif [ -f "$staticFile" ] && safeToServeStatically "$staticFile"; then 19 | log "${lcLGray}Serving the static file ${lcBlue}$PROJECT${lcLCyan}$reqPath" 20 | serveStatic $staticFile 21 | 22 | elif [ -d "$staticFile" ] && [ -f "$staticFile/index.html" ]; then 23 | log "${lcLGray}Serving the static file ${lcBlue}$PROJECT${lcLCyan}$reqPath${lcBlue}/index.html" 24 | serveStatic "$staticFile/index.html" 25 | 26 | else 27 | log "404 Not Found" 28 | resp.Status 404 29 | resp.Header "Content-Type" "text/html" 30 | resp.Body "$reqPath Was Not Found" 31 | 32 | fi 33 | } 34 | 35 | # Checks the request path and a path BWF has chosen to serve 36 | # statically for various criterias that may make serving impossible. 37 | # Examples are dotfiles & handler scripts. 38 | # Arguments: 39 | # $1: the path BWF decided to serve. 40 | function safeToServeStatically { 41 | # Checking the path for dotfiles 42 | if [[ $1 =~ \/\. && $SERVE_DOTFILES == 0 ]]; then 43 | logg "${lcLRed}Requests to dotfiles are forbidden for security reasons." 44 | return 255 45 | fi 46 | 47 | # Checking if the path ends up with a handler script 48 | if [[ $1 =~ (GET|POST|PUT|DELETE|OPTIONS).sh$ && $SERVE_HANDLER_SCRIPTS == 0 ]]; then 49 | logg "${lcLRed}Requests to handler scripts are forbidden for security reasons." 50 | return 255 51 | fi 52 | 53 | return 0 54 | } 55 | 56 | # Serves static files from file system. 57 | # Tries to guess Content-Type from their extensions. 58 | function serveStatic() { 59 | filePath=$1 60 | fileName=$(basename "$filePath") 61 | fileExt="${fileName##*.}" 62 | fileMIMEType=$(file -b --mime-type "$filePath") 63 | fileSize=$(stat --printf="%s" "$filePath") 64 | 65 | resp.Status "200" 66 | 67 | resp.Header "Content-Length" $fileSize 68 | 69 | case $fileExt in 70 | # Somehow `file --mime-type` recognizes css files as text/x-asm 71 | "css") 72 | resp.Header "Content-Type" "text/css" 73 | ;; 74 | 75 | *) 76 | resp.Header "Content-Type" "$fileMIMEType" 77 | ;; 78 | esac 79 | 80 | resp.File $filePath 81 | } -------------------------------------------------------------------------------- /libbashttpd/utility.sh: -------------------------------------------------------------------------------- 1 | # Default value, override it in the .env file 2 | LOG_VERBOSITY=1 3 | 4 | # Colors & styles 5 | lc0="\e[0m" 6 | 7 | lc1="\e[1m" 8 | lc2="\e[2m" 9 | lc4="\e[4m" 10 | lc5="\e[5m" 11 | lc7="\e[7m" 12 | lc8="\e[8m" 13 | 14 | lcB=$lc1 15 | lcD=$lc2 16 | lcU=$lc4 17 | lcL=$lc5 18 | lcR=$lc7 19 | lcH=$lc8 20 | 21 | lcBlack="\e[30m" 22 | lcRed="\e[31m" 23 | lcGreen="\e[32m" 24 | lcYellow="\e[33m" 25 | lcBlue="\e[34m" 26 | lcMagenta="\e[35m" 27 | lcCyan="\e[36m" 28 | lcLGray="\e[37m" 29 | 30 | lcDGray="\e[90m" 31 | lcLRed="\e[91m" 32 | lcLGreen="\e[92m" 33 | lcLYellow="\e[93m" 34 | lcLBlue="\e[94m" 35 | lcLMagenta="\e[95m" 36 | lcLCyan="\e[96m" 37 | lcWhite="\e[97m" 38 | 39 | lcbgBlack="\e[40m" 40 | lcbgRed="\e[41m" 41 | lcbgGreen="\e[42m" 42 | lcbgYellow="\e[43m" 43 | lcbgBlue="\e[44m" 44 | lcbgMagenta="\e[45m" 45 | lcbgCyan="\e[46m" 46 | lcbgLGray="\e[47m" 47 | 48 | lcbgDGray="\e[100m" 49 | lcbgLRed="\e[101m" 50 | lcbgLGreen="\e[102m" 51 | lcbgLYellow="\e[103m" 52 | lcbgLBlue="\e[104m" 53 | lcbgLMagenta="\e[105m" 54 | lcbgLCyan="\e[106m" 55 | lcbgWhite="\e[107m" 56 | 57 | lcX="$lc0$lcDGray" 58 | 59 | lcEm="$lcWhite" 60 | 61 | # A pinch of syntatic sugar for declaring and initializing variables 62 | function var { 63 | printf -v $1 "%s" "$2" 64 | } 65 | 66 | # Outputs to the host's stderr 67 | function log { 68 | _IFS=$IFS 69 | IFS='' 70 | # [[ $LOG_VERBOSITY -ge 1 ]] && printf "%s " $@ >&2 && echo "" >&2 71 | [[ $LOG_VERBOSITY -ge 1 ]] && echo -e "${lcX}" $@ "\e[0m" >&2 72 | IFS=$_IFS 73 | return 0 74 | } 75 | 76 | # Verbose logging 77 | function logg { 78 | [[ $LOG_VERBOSITY -ge 2 ]] && log $@ 79 | return 0 80 | } 81 | 82 | # Even more verbose logging 83 | function loggg { 84 | [[ $LOG_VERBOSITY -ge 3 ]] && log $@ 85 | return 0 86 | } 87 | 88 | # Slightly annoying logging 89 | function logggg { 90 | [[ $LOG_VERBOSITY -ge 4 ]] && log $@ 91 | return 0 92 | } 93 | 94 | # Absolutely annoying chatter 95 | function loggggg { 96 | [[ $LOG_VERBOSITY -ge 5 ]] && log $@ 97 | return 0 98 | } 99 | 100 | function error { 101 | echo -En "$1" 102 | } 103 | 104 | # Like `return` in other languages, capture it with $() 105 | function yield { 106 | if [[ -z $2 ]]; then 107 | echo -En "$1" 108 | else 109 | var $2 "$1" 110 | eval "${2}=\"$1\"" 111 | fi 112 | } 113 | 114 | # Taken from https://gist.github.com/cdown/1163649#file-gistfile1-sh 115 | function HTTP.urldecode { 116 | local plussless="${1//+/ }" 117 | printf '%b' "${plussless//%/\\x}" 118 | } 119 | 120 | # Taken from https://gist.github.com/cdown/1163649#gistcomment-1256298 121 | function HTTP.urlencode() { 122 | local length="${#1}" 123 | for (( i = 0; i < length; i++ )); do 124 | local c="${1:i:1}" 125 | case $c in 126 | [a-zA-Z0-9.~_-]) printf "$c" ;; 127 | *) printf "$c" | xxd -p -c1 | while read x;do printf "%%%s" "$x";done 128 | esac 129 | done 130 | } 131 | 132 | # Joins it's arguments into a string. 133 | # Delimiter goes as a first argument. 134 | function array.join { 135 | local d=$1; 136 | shift 137 | 138 | res="" 139 | 140 | for a in ${@}; do 141 | res="$res$d$a" 142 | done 143 | 144 | echo ${res:${#d}} 145 | } 146 | 147 | # Declares a copy of an associative array by its provided name. 148 | # Context: 149 | # $1 must be a name of an associative array variable. 150 | # Creates a local variable $E which is a copy of $the array referenced by $1. 151 | alias array.getbyref='e="$( declare -p ${1} )"; eval "declare -A E=${e#*=}"' 152 | 153 | # Declares a copy of an associative array by its provided name, gets the name from $2. 154 | alias array.getbyref2='e="$( declare -p ${2} )"; eval "declare -A E=${e#*=}"' 155 | 156 | # Iterates over the array created by array.getbyref. 157 | # Context: 158 | # array.getbyref must be called prior to this. 159 | # the do...done block must be supplied by the caller. 160 | alias array.foreach='for key in "${!E[@]}"' 161 | 162 | # Tries to figure out the type of given variable. 163 | # Takes a name of a variable, not the variable itself. 164 | # Usage: 165 | # userName="John" 166 | # listOfThings=(1 2 33 444) 167 | # declare -A mapOfThings=([first]=1 [other]=2 [nextAfterOther]=33 [plenty]=444) 168 | # boolFlagValue=true 169 | # userAge=234 170 | # reflection.Type userName # outputs "STRING" 171 | # reflection.Type listOfThings # outputs "ARRAY" 172 | # reflection.Type mapOfThings # outputs "MAP" 173 | # reflection.Type boolFlagValue # outputs "BOOLEAN" 174 | # reflection.Type userAge # outputs "NUMBER" 175 | function reflection.Type { 176 | decl=$(declare -p $1) 177 | mode=${decl:8:2} 178 | 179 | val=$(eval echo \$${1}) 180 | 181 | case $mode in 182 | "-a") 183 | echo "ARRAY" 184 | ;; 185 | 186 | "-A") 187 | echo "MAP" 188 | ;; 189 | 190 | *) 191 | if [[ $val == "true" || $val == "false" ]]; then 192 | echo "BOOLEAN" 193 | elif [[ $val =~ ^[[:digit:]]+$ ]]; then 194 | echo "NUMBER" 195 | else 196 | echo "STRING" 197 | fi 198 | ;; 199 | esac 200 | } 201 | 202 | declare -a IFS_backup_stack 203 | 204 | # Changes the IFS variable while backing it up and automatically restoring. 205 | # To set a new IFS: sys.IFS $'\r' 206 | # To reset IFS to it's original value: sys.IFS 207 | function sys.IFS { 208 | if [[ -z ${1+x} ]]; then 209 | # Resetting 210 | IFS_backup=${IFS_backup_stack[${#IFS_backup_stack[@]}-1]} 211 | if ! [[ -z $IFS_backup ]]; then 212 | IFS=$IFS_backup 213 | unset IFS_backup_stack[${#IFS_backup_stack[@]}-1] 214 | fi 215 | else 216 | # Setting a new value 217 | IFS_backup_stack+=("$IFS") 218 | IFS=$1 219 | fi 220 | } 221 | 222 | # Initilizes function arguments in reversed order, so $#-th argument becomes $_0, $#-1 becomes $_1 and so on. 223 | # Context: 224 | # Used within a function. 225 | # Creates local variables $_0, $_1 ... 226 | alias fn.arguments='local _0; local _1; local _2; eval "_0=\$$(($#-0)); _1=\$$(($#-1)); _2=\$$(($#-2))"' 227 | -------------------------------------------------------------------------------- /localhost/.etc/.env: -------------------------------------------------------------------------------- 1 | # A path to the storage folder where image files for localhost:8080/gallery are hosted. 2 | GALLERY_STORAGE=storage/images -------------------------------------------------------------------------------- /localhost/.etc/db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE image_comments ( 2 | id INT(11) NOT NULL AUTO_INCREMENT, 3 | imageID VARCHAR(50) NOT NULL, 4 | message VARCHAR(5000) NOT NULL, 5 | date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | PRIMARY KEY (id) 7 | ) 8 | COLLATE='utf8_general_ci' 9 | ENGINE=InnoDB 10 | AUTO_INCREMENT=38 11 | ; 12 | 13 | 14 | INSERT INTO `image_comments` (`id`, `imageID`, `message`, `date`) VALUES (38, 'UXRf0oub.jpg', 'REST APIs are totally possible with BWF and it\'s support for MySQL and JSON data.', '2019-08-25 11:19:44'); 15 | INSERT INTO `image_comments` (`id`, `imageID`, `message`, `date`) VALUES (39, 'UXRf0oub.jpg', 'You can leave comments in the field below and they will be saved to the DB.', '2019-08-25 10:24:57'); 16 | -------------------------------------------------------------------------------- /localhost/.etc/tpl/age.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |~# This is how we template.
13 |~# Now we know that $name is $age years old.
14 |~# The cookie is still here, showing $visits visits.
15 |16 | 17 |
~#
18 |~# ${__HTTP_STATUS_SENT:-500}.
13 |~# That's a lot of errors.
14 |15 | 16 |
~#
17 |~# $COMMAND
13 |14 |
18 | 19 |
20 |
~#
21 |Gallery
13 |~# An image gallery page in Bash. With ls on backend
14 |~# You can upload images there too. Click the button below to select a file
15 |16 |
23 | 24 |25 |
~# As you may have alreay noticed, this is not the most performant HTTP server on the scene; so here is a progress bar of it reading your bytes from it's stdin
27 |~# You can observe the same process in the server terminal output
28 |30 |
31 | 32 |
~#
33 |Bashttpd
14 |~# An HTTP server in pure Bash script
15 | 16 |BWF
17 |~# A web application framework in Bash script for the refined souls
18 |~# It exists as a manifestation of minimalism in the world of virtualization of personal disciplne & responsibility, continous delivery of hype and ever growing stacks of disappointment
19 |~# And other binary data formats are supported, proudly served by cat
24 |FORMS
25 |&
26 |JSON
27 |~# Form data & JSON APIs are at your service, easier than ever. Head to the Gallery page to experience that and something more
28 |MySQL
32 |~# BWF makes working with MySQL databases a breeze
33 |TEMPLATES
34 |~# You can have dynamic page content by expanding Bash variables inside your HTML markup. Submit the form below and witness
35 | 44 |COOKIES
48 |~# This is how we know that you are visiting this page for the wait for it time
49 |[.]
50 |~# A nicer and bigger number to make this column taller and balance the page layout
51 |