post2.cgi 9.4 KB

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