post.cgi 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. #!/bin/bash
  2. ## SuperGlue project | http://superglue.it | 2014 | GPLv3
  3. ## http://git.superglue.it/superglue/serverside/edit/master/common/rootFS/www/lib/post.sh
  4. ## author: Danja Vasiliev <danja@k0a1a.net>
  5. ##
  6. ## post.sh - all POST requests are redirected to this script.
  7. ##
  8. ## examples:
  9. ## text: curl --data-urlencode '<html><title>' http://host/file.html
  10. ## image: curl --form "userimage=@file.png" -H "Expect:" http://host/file.png
  11. ## command: curl --data-urlencode 'ls' http://host/cmd
  12. ##
  13. ## returns: 200 (+ output of operation) on success
  14. ## 406 (+ error message in debug mode) on error
  15. ## no globbing, for safety
  16. set -o noglob
  17. ## some path variables
  18. _WWW='/www'
  19. _HTDOCS="${_WWW}/htdocs"
  20. _TMP="${_WWW}/tmp"
  21. _LOG="${_WWW}/log/post.log"
  22. ## multihost
  23. #if [[ $HTTP_HOST == 'demo.superglue.it' ]]; then
  24. # _HTDOCS="${_WWW}/htdocs-demo"
  25. #fi
  26. ## _DEBUG=0 no logging at all
  27. ## _DEBUG=1 writes to $_LOG file
  28. ## _DEBUG=2 adds [verbose].. to HTTP response. Can be triggered via 'Content-Type' header option
  29. ## eg: "Content-Type:application/octet-stream; verbose=1"
  30. _DEBUG=1
  31. #### FUNCTIONS
  32. ## logging
  33. logThis() {
  34. [[ $_DEBUG -gt 0 ]] || return 0
  35. [[ $_ERR -gt 0 ]] && _TYPE='E:' || _TYPE='I:' ## Info or Error indication
  36. local _TIME=$(printf '%(%d.%m.%Y %H:%M:%S)T' -1)
  37. printf '%b\n' "$_TIME $_TYPE ${1} " >> $_LOG
  38. [[ $_DEBUG -gt 1 ]] && printf '%b\n' "[verbose] $_TYPE ${1}"
  39. return 0
  40. }
  41. ## inject function execution trace to global _OUT
  42. wTf() {
  43. local _WTF="$(printf '%s -> ' '| trace: '${FUNCNAME[*]:1})"
  44. _OUT="$_OUT $_WTF"
  45. }
  46. ## urldecode
  47. urlDecode() {
  48. local encoded="${1//+/ }"
  49. printf '%b' "${encoded//%/\x}"
  50. }
  51. ## http response
  52. headerPrint() {
  53. case ${1} in
  54. 200) printf '%b' 'HTTP/1.1 200 OK\nAccess-Control-Allow-Origin: *\n\n';;
  55. 405) printf '%b' 'HTTP/1.1 405 Method Not Allowed\n\n';;
  56. 406) printf '%b' 'HTTP/1.1 406 Not Acceptable\n\n';;
  57. esac
  58. return 0
  59. }
  60. ## takes exit code variable $? and optional "message" string.
  61. ## exit code 0 simply falls through. when local message
  62. ## is not provided tries to assign global $_OUT.
  63. ##
  64. ## eg: errorCheck $? "bad zombie"
  65. ##
  66. ## produces HTTP 406 header, $_OUT message, triggers logThis()
  67. ## and exits the main loop with exit >= 1.
  68. errorCheck() {
  69. _ERR=${1} ## exit code
  70. [[ $_ERR -gt 0 ]] || return 0
  71. local _MSG=${2}
  72. ## if $_OUT is present cut it down to one line
  73. ## otherwise assign message from the invokation arguments
  74. [[ $_OUT ]] && _OUT="${_OUT%%$'\n'*}" || { _OUT=${_MSG:='unknown error occured'}; wTf; }
  75. [[ -e $_POST_TMP ]] && rm -f $_POST_TMP
  76. headerPrint '406'
  77. logThis "${_OUT}";
  78. exit $_ERR
  79. }
  80. ## get data from argv=data pair in POST request
  81. ## (any 4 character arg in the beginning of string is matched)
  82. postGetData() {
  83. _POST="${_POST##????'='}"
  84. }
  85. ## urlencoded POST dispatcher
  86. postUrlenc() {
  87. ## decode stream
  88. _POST=$(urlDecode "$(< $_POST_TMP)") ## decode global $_POST
  89. postGetData
  90. case "${_REQUEST_URI}" in
  91. \/cmd) postCmd ;; ## handle /cmd POST
  92. *) postHtml ;; ## handle html POST
  93. esac
  94. }
  95. ## handle /cmd POST
  96. postCmd() {
  97. local _CMD=( ${_POST} ) ## convert POST to array
  98. [[ ${#_CMD[@]} -lt 5 ]] || errorCheck '1' "'${_CMD[*]}': too many arguments"
  99. local _EXE="${_CMD[0]}" ## first member is command
  100. local _ARG="${_CMD[@]:1}" ## the rest is arguments
  101. ## note unquoted regex
  102. [[ ! "$_ARG" =~ (\.\.|^/| /) ]] || errorCheck '1' "'$_ARG': illegal path"
  103. ## 'ls' replacement function
  104. lss() {
  105. _D='\t' ## do we want a customizable delimiter?
  106. while getopts 'la' _OPT; do
  107. case $_OPT in
  108. l) local _LNG="$_D%F$_D%s$_D%y$_D%U$_D%G$_D%a" ;;
  109. a) shopt -s dotglob
  110. esac
  111. done
  112. shift $((OPTIND-1)) ## removing used args
  113. [[ -z "${@}" ]] && _PT="./*" ## list ./* if called with no args
  114. [[ -d "${@}" ]] && _PT="/*" ## add /* to directories
  115. ## if error occures return 0
  116. stat --printf "%n$_LNG\n" -- "${@%%/}"$_PT 2>/dev/null || _ERR=0
  117. return $_ERR
  118. }
  119. case "$_EXE" in
  120. ls|lss) _EXE="lss"; _ARG="${_ARG}" ;; ## no error is returned
  121. cp) _ARG="${_ARG}" ;;
  122. rm) _ARG="${_ARG}" ;; ## add recursive option if you need
  123. mv) _ARG="${_ARG}" ;;
  124. mkdir) _ARG="${_ARG}" ;;
  125. log) _EXE="tail"; _ARG="${_ARG} ${_LOG}" ;;
  126. wget) _ARG="-q ${_ARG/ */} -O ${_ARG/* /}" ;; ## quiet
  127. *) errorCheck '1' "'$_EXE': bad command" ;;
  128. esac
  129. ## toggle globbing
  130. set +o noglob
  131. _OUT=$($_EXE $_ARG 2>&1)
  132. _ERR=$?
  133. ## toggle globbing
  134. set -o noglob
  135. logThis "$_EXE $_ARG"
  136. errorCheck $_ERR
  137. }
  138. ## handle html POST
  139. postHtml() {
  140. ## save POST to file
  141. _OUT=$( (printf '%b' "${_POST}" > "${_HTDOCS}${_REQUEST_URI}") 2>&1)
  142. _ERR=$?
  143. errorCheck $_ERR
  144. }
  145. ## octet POST dispatcher
  146. postOctet() {
  147. ## get 'data:' header length
  148. local IFS=','; read -d',' -r _DH < $_POST_TMP
  149. case "${_ENC}" in
  150. base64) postBase64Enc;;
  151. binary) postBinary ;;
  152. *) postGuessEnc ;; ## handle data POST
  153. esac
  154. }
  155. ## to be converted into a proper data-type detection function
  156. postGuessEnc() {
  157. shopt -s nocasematch
  158. local _DTP="^.*\;([[:alnum:]].+)$" ## data-type header pattern
  159. ## look for encoding in the data header
  160. [[ "${_DH}" =~ ${_DTP} ]] && _ENC="${BASH_REMATCH[1]}"
  161. logThis "'$_ENC:' encoding is the best guess";
  162. shopt -u nocasematch
  163. case "$_ENC" in
  164. base64) postBase64Enc ;;
  165. ## binary) _ERR=1 ;;
  166. ## json) _ERR=1 ;;
  167. ## quoted-printable) _ERR=1 ;;
  168. *) _ERR=1; _OUT="'${_ENC:='unknown'}' encoding, unknown POST failed";;
  169. esac
  170. errorCheck $_ERR
  171. }
  172. ## handle base64 post
  173. postBase64Enc() {
  174. logThis "'${_ENC}:' decoding stream"
  175. _DL=${#_DH} ## get data-header length
  176. [[ $_DL -lt 10 ]] && { _DL=23; _SKP=0; } || { let _DL+=1; _SKP=1; } ## '23' - what?!
  177. ## the line below seems to be the best solution for the time being
  178. ## dd 'ibs' and 'iflags' seem not to work on OpenWRT - investigate as it might be very useful
  179. _OUT=$( dd if=${_POST_TMP} bs=${_DL} skip=${_SKP} | base64 -d > "${_HTDOCS}${_REQUEST_URI}" 2>&1)
  180. _ERR=$?
  181. errorCheck $_ERR
  182. }
  183. postBinary() {
  184. logThis "'binary': decoding stream"
  185. ## it is unclear what will be necessary to do here
  186. _OUT=$( dd if="${_POST_TMP}" of="${_HTDOCS}${_REQUEST_URI}" 2>&1 )
  187. _ERR=$?
  188. errorCheck $_ERR
  189. }
  190. postMpart() {
  191. logThis "'multipart': decoding stream"
  192. local _BND=$(findPostOpt 'boundary')
  193. ## bash is binary unsafe and eats away precious lines
  194. ## thus using gawk
  195. function cutFile() {
  196. gawk -v "want=$1" -v "bnd=$_BND" '
  197. BEGIN { RS="\r\n"; ORS="\r\n" }
  198. # reset based on boundaries
  199. $0 == "--"bnd"" { st=1; next; }
  200. $0 == "--"bnd"--" { st=0; next; }
  201. $0 == "--"bnd"--\r" { st=0; next; }
  202. # search for wanted file
  203. st == 1 && $0 ~ "^Content-Disposition:.* name=\""want"\"" { st=2; next; }
  204. st == 1 && $0 == "" { st=9; next; }
  205. # wait for newline, then start printing
  206. st == 2 && $0 == "" { st=3; next; }
  207. st == 3 { print $0 }
  208. ' 2>&1
  209. }
  210. cutFile 'userimage' < "${_POST_TMP}" > "${_HTDOCS}${_REQUEST_URI}"
  211. _ERR=$?
  212. errorCheck $_ERR
  213. }
  214. ## find arbitrary option supplied in Content-Type header
  215. ## eg: "Content-Type:application/octet-stream; verbose=1"
  216. findPostOpt() {
  217. for i in "${CONTENT_TYPE[@]:1}"; do
  218. case "${i/=*}" in
  219. "$1") printf '%b' "${i/*=}" ;;
  220. esac
  221. done
  222. return 0
  223. }
  224. ## sanitize by backslashing all expandable symbols
  225. escapeStr() {
  226. printf "%q" "${*}"
  227. }
  228. ## brutally replace unwanted characters
  229. cleanFname() {
  230. shopt -s extglob
  231. local _STR="${*}"
  232. echo -n "${_STR//[^[:alnum:]._\-\/\\]/_}"
  233. shopt -u extglob
  234. }
  235. #### MAIN LOOP
  236. ## timing
  237. ## TODO: remove it
  238. ## run once here and once at the end
  239. read t z < /proc/uptime
  240. ## check if we are in $_HTDOCS directory
  241. cd $_HTDOCS || errorCheck $? 'htdocs unavailable'
  242. [[ ${PWD} == ${_HTDOCS} ]] || errorCheck $? 'htdocs misconfigured'
  243. [[ $CONTENT_LENGTH -gt 0 ]] || errorCheck $? 'content length zero'
  244. ## URI is considered as a file dest to work with
  245. ## add 'index.html' to default and empty request uri
  246. _REQUEST_URI="${REQUEST_URI/%\///index.html}"
  247. _REQUEST_URI="$(urlDecode $_REQUEST_URI)"
  248. _PATH="${_REQUEST_URI%/*}"
  249. CONTENT_TYPE=( ${CONTENT_TYPE} )
  250. _CONTENT_TYPE="${CONTENT_TYPE[0]/;}"
  251. _ENC="${HTTP_CONTENT_ENCODING}"
  252. #logThis "Len: $CONTENT_LENGTH Ctype: $CONTENT_TYPE Enc: $_CONTENT_ENCODING"
  253. ## check for 'verbose' option in POST
  254. findPostOpt 'verbose' || { _DEBUG=2; logThis 'verbose mode is requested'; }
  255. _POST_TMP=$(mktemp -p $_TMP) ## make tmp POST file
  256. cat > $_POST_TMP ## cautiously storing entire POST in a file
  257. ## dispatching POST
  258. case "${_CONTENT_TYPE}" in
  259. application\/x-www-form-urlencoded) postUrlenc ;;
  260. application\/octet-stream) postOctet ;;
  261. multipart\/form-data) postMpart ;;
  262. *) _ERR=1; _OUT='this is not a post' ;;
  263. esac
  264. [[ -e $_POST_TMP ]] && rm -f $_POST_TMP
  265. ## make sure we are good
  266. errorCheck $_ERR
  267. [[ -z $_OUT ]] || _OUT="${_OUT}\n"
  268. headerPrint '200' ## on success
  269. printf '%b' "${_OUT}"
  270. logThis 'OK 200'
  271. read d z < /proc/uptime
  272. logThis $((${d/./}-${t/./}))"/100s"
  273. exit 0