#!/bin/bash # ====================================================================== # # REST API CLIENT USING CURL # # REQUIREMENTS # - Bash (Linux or MS Windows i.e with Cygwin) # - curl # - sha1sum (optional; for export functionality with AUTOFILE only) # ---------------------------------------------------------------------- # (1) source this script # (2) enter "http.help" to get a list of available commands # ---------------------------------------------------------------------- # 2020-02-07 v0.2 axel.hahn@iml.unibe.ch BETABETA # 2020-02-12 v0.4 axel.hahn@iml.unibe.ch Caching # 2020-03-02 v0.5 axel.hahn@iml.unibe.ch a few more response check functions # ====================================================================== # --- fetch incoming params RestApiCfg=$1 RestApiMethod=$2 ApiUrl=$3 Body="$4" http_cfg__about="Bash REST API client v0.5" typeset -i http_cfg__debug=0 typeset -i http_cfg__cacheTtl=0 http_cfg__cacheDir=/var/tmp/http-cache http_cfg__UA="${http_cfg__about}" http_cfg__prjurl="https://git-repo.iml.unibe.ch/iml-open-source/bash-rest-api-client" # --- curl meta infos to collect # see variables in man curl --write-out param curlMeta="\ http_code \ http_connect \ local_ip \ local_port \ num_connects \ num_redirects \ redirect_url \ remote_ip \ remote_port \ size_download \ size_header \ size_request \ size_upload \ speed_download \ speed_upload \ ssl_verify_result \ time_appconnect \ time_connect \ time_namelookup \ time_pretransfer \ time_redirect \ time_starttransfer \ time_total \ url_effective \ " # ---------------------------------------------------------------------- # # functions # # ---------------------------------------------------------------------- # ...................................................................... # # write a debug message to STDERR # Do no not change the prefix - is is read in inc_functions # # params strings output message function http._wd(){ if [ $http_cfg__debug -gt 0 ]; then echo -e "\e[33m# RESTAPI::DEBUG $*\e[0m" >&2 fi } # ...................................................................... # # write an error message to STDERR # Do no not change the prefix - is is read in inc_functions # # params strings output message function http._we(){ echo -e "\e[31m# RESTAPI::ERROR $*\e[0m" >&2 } function http(){ cat <<EOH $http_cfg__about A REST API Client with curl Enter http.help to show all commands. EOH # $0 is not the current file if we source a script # grep "function http.[a-z]" $0 | sort } function http.init(){ which curl >/dev/null || http.quit # request vars http_req__auth= http_req__auth= http_req__body= http_req__method=GET http_req__url= http_req__fullurl= http_req__docs= http_req__dataprefix="RESTAPICLIENTMETADATA_`date +%s`_$$" local writevar= for myvar in $curlMeta do writevar="${writevar}|${myvar}:%{${myvar}}" done http_curl__writeout="\\n${http_req__dataprefix}${writevar}\\n" # cache http_req__mode=undefined http_cfg__cacheTtl=0 http_cfg__cacheFile= # response http_resp__all= http_resp__neutral= mkdir ${http_cfg__cacheDir} 2>/dev/null chmod 777 ${http_cfg__cacheDir} 2>/dev/null } # execute the request # param string optional: full url function http.makeRequest(){ http._wd "${FUNCNAME[0]}($1)" # --- handle optional prams if [ $# -ne 0 ]; then echo $1 | grep "^[A-Z]*$" >/dev/null if [ $? -eq 0 ]; then http.setMethod "$1" shift 1 fi http.setUrl "$1" http.setBody "$2" fi # test -z "$1" || http.setFullUrl "$1" # --- detect caching http_req__mode=REQUEST useCache=0 makeRequest=1 if [ $http_cfg__cacheTtl -gt 0 -a "${http_req__method}" = "GET" ]; then useCache=1 test -z "${http_cfg__cacheFile}" && http_cfg__cacheFile=`http._genOutfilename "${http_cfg__cacheDir}/AUTOFILE"` if [ -f "${http_cfg__cacheFile}" ]; then http.responseImport "${http_cfg__cacheFile}" typeset -i local iAge=`http.getRequestAge` http._wd "INFO: Age of cache is $iAge sec - vs TTL $http_cfg__cacheTtl sec - file $http_cfg__cacheFile" if [ $iAge -gt 0 -a $iAge -lt $http_cfg__cacheTtl ]; then http._wd "INFO: Using cache" makeRequest=0 http_req__mode=CACHE else http._wd "INFO: Cache file will be updated after making the request" rm -f "${http_cfg__cacheFile}" 2>/dev/null fi fi fi # --- make the request if [ $makeRequest -eq 1 ]; then http_req__start=`date +%s` http._wd "${FUNCNAME[0]}($1) ${http_req__method} ${http_req__fullurl}" http_resp__all=$( if [ -z "${http_req__body}" ]; then curl -k -s \ -A "${http_cfg__UA}" \ -w "${http_curl__writeout}" \ -H 'Accept: application/json' \ ${http_req__auth} \ -i "${http_req__fullurl}" \ -X "${http_req__method}" else curl -k -s \ -A "${http_cfg__UA}" \ -w "${http_curl__writeout}" \ -H 'Accept: application/json' \ ${http_req__auth} \ -i "${http_req__fullurl}" \ -X "${http_req__method}" \ -d "${http_req__body}" fi ) || http.quit http._wd "OK - Curl finished the http request ... processing data" http_resp__neutral=`http._fetchAllAndReformat` if [ $useCache -eq 1 ]; then http._wd "INFO: writing cache ..." http.responseExport "${http_cfg__cacheFile}" fi fi http._wd "Request function finished; Code `http.getStatuscode`" } # ...................................................................... # # show error message with last return code and quit with this exitcode # no params function http.quit(){ http._wd "${FUNCNAME[0]}($1)" rc=$? echo >&2 echo -e "\e[31m# ERROR: command FAILED with rc $rc. \e[0m" >&2 if [ ! -z "${RestApiDocs}" ]; then echo "HINT: see ${RestApiDocs}" >&2 fi # dont make exit in a sourced file # exit $rc } # load a config file function http.loadcfg(){ http._wd "${FUNCNAME[0]}($1) !!! DEPRECATED !!!" # reset expected vars from config RestApiUser= RestApiPassword= RestApiBaseUrl= RestApiDocs= # source config file . "${1}" || http.quit # set "internal" vars if [-z "$RestApiPassword" ]; then http.setAuth "$RestApiUser:$RestApiPassword" else http.setAuth fi http.setBaseUrl "${RestApiBaseUrl}" http.setDocs "${RestApiDocs}" } # ====================================================================== # GETTER # ====================================================================== function http._fetchResponseHeaderOrBody(){ http._wd "${FUNCNAME[0]}($1)" local isheader=true # keep leading spaces IFS='' echo "${http_resp__all}" | grep -v "${http_req__dataprefix}" | while read -r line; do if $isheader; then if [[ $line = $'\r' ]]; then isheader=false else test "$1" = "header" && echo $line fi else # body="$body"$'\n'"$line" test "$1" = "body" && echo $line fi done } function http._fetchResponseData(){ http._wd "${FUNCNAME[0]}($1)" echo "${http_resp__all}" | sed "s#${http_req__dataprefix}#\n${http_req__dataprefix}#" | grep "${http_req__dataprefix}" | tail -1 | cut -f 2- -d "|" | sed "s#|#\n#g" | grep -v "${http_req__dataprefix}" | while read -r line; do echo $line done } function http._fetchAllAndReformat(){ http._wd "${FUNCNAME[0]}($1)" IFS='' line="#------------------------------------------------------------" echo "#_META_|about:$http_cfg__about" echo "#_META_|host:`hostname -f`" echo $line echo "#_REQUEST_|fullurl:$http_req__fullurl" echo "#_REQUEST_|method:$http_req__method" echo "#_REQUEST_|time:`date`" echo "#_REQUEST_|timestamp:`date +%s`" echo "#_REQUEST_|auth:`echo $http_req__auth | sed 's#:.*#:xxxxxxxx#'`" echo "#_REQUEST_|body:$http_req__body" echo "#_REQUEST_|baseurl:$http_req__baseurl" echo "#_REQUEST_|url:$http_req__url" echo "#_REQUEST_|docs:$http_req__docs" echo $line http._fetchResponseHeaderOrBody header | sed "s,^,#_HEADER_|,g" echo $line http._fetchResponseData | sed "s,^,#_DATA_|,g" echo $line http._fetchResponseHeaderOrBody body | sed "s,^,#_BODY_|,g" echo $line END } function http._getFilteredResponse(){ http._wd "${FUNCNAME[0]}($1)" echo "${http_resp__neutral}" | grep "^#_${1}_|" | cut -f 2- -d "|" } # ---------- PUBLIC REQUEST GETTER function http.getRequestTs(){ http._wd "${FUNCNAME[0]}($1)" http._getFilteredResponse REQUEST | grep "^timestamp" | cut -f 2 -d ":" } # get age of the response in sec. # It is especially useful after responseImport function http.getRequestAge(){ http._wd "${FUNCNAME[0]}($1)" typeset -i local iAge=`date +%s`-`http.getRequestTs` echo $iAge } # ---------- PUBLIC RESPONSE GETTER # get response body function http.getResponse(){ http._wd "${FUNCNAME[0]}($1)" http._getFilteredResponse BODY } # get curl data of this request with status, transferred bytes, speed, ... function http.getResponseData(){ http._wd "${FUNCNAME[0]}($1)" http._getFilteredResponse DATA } # get response header function http.getResponseHeader(){ http._wd "${FUNCNAME[0]}($1)" http._getFilteredResponse HEADER } # get raw response (not available after import) function http.getResponseRaw(){ http._wd "${FUNCNAME[0]}($1)" echo "${http_resp__all}" } # get Http status as string OK|Redirect|Error function http.getStatus(){ http._wd "${FUNCNAME[0]}($1)" http.isOk >/dev/null && echo OK http.isRedirect >/dev/null && echo Redirect http.isError >/dev/null && echo Error } # get Http status code of the request as 3 digit number function http.getStatuscode(){ http._wd "${FUNCNAME[0]}($1)" local _filter=$1 http.getResponseData | grep "^http_code:" | cut -f 2 -d ":" } # was response a 2xx status code? # output is a statuscode if it matches ... or empty # Additionally you can verify the return code # $? -eq 0 means YES # $? -ne 0 means NO function http.isOk(){ http._wd "${FUNCNAME[0]}($1)" http.getStatuscode | grep '2[0-9][0-9]' } # was the repsonse a redirect? function http.isRedirect(){ http._wd "${FUNCNAME[0]}($1)" http.getStatuscode | grep '3[0-9][0-9]' } # was the repsonse a client error (4xx or 5xx) function http.isError(){ http._wd "${FUNCNAME[0]}($1)" http.getStatuscode | grep '[45][0-9][0-9]' } # was the repsonse a client error (4xx) function http.isClientError(){ http._wd "${FUNCNAME[0]}($1)" http.getStatuscode | grep '4[0-9][0-9]' } # was the repsonse a client error (5xx) function http.isServerError(){ http._wd "${FUNCNAME[0]}($1)" http.getStatuscode | grep '5[0-9][0-9]' } # dump information about request and response function http.dump(){ http._wd "${FUNCNAME[0]}($1)" http.responseExport } # ====================================================================== # Import/ Export # ====================================================================== # helper to replace "AUTOFILE" with something uniq using full url # param string import or export filename function http._genOutfilename(){ http._wd "${FUNCNAME[0]}($1)" echo $1 | grep "AUTOFILE" >/dev/null if [ $? -ne 0 ]; then echo $1 else local sum=`echo ${http_req__fullurl} | sha1sum ` local autofile=`echo "${sum}__${http_req__fullurl}" | sed "s#[^a-z0-9]#_#g"` echo $1 | sed "s#AUTOFILE#${autofile}#" fi } # export to a file function http.responseExport(){ http._wd "${FUNCNAME[0]}($1)" if [ -z $1 ]; then echo "${http_resp__neutral}" else local outfile=`http._genOutfilename "$1"` http._wd "${FUNCNAME[0]}($1) writing to outfile $outfile" echo "${http_resp__neutral}" >$outfile fi } # import a former response from a file function http.responseImport(){ http._wd "${FUNCNAME[0]}($1)" local infile=`http._genOutfilename "$1"` if [ -r "${infile}" ]; then grep "^#_META_|about:$http_cfg__about" "${infile}" >/dev/null if [ $? -eq 0 ]; then http_resp__neutral=`cat "${infile}"` else echo "ERROR: Ooops [${infile}] does not seem to be an export dump." http.quit fi else echo "ERROR: Ooops the file [${infile}] is not readable." http.quit fi } # delete an exported file; this is especially useful if you use # AUTOFILE functionality function http.responseDelete(){ http._wd "${FUNCNAME[0]}($1)" local infile=`http._genOutfilename "$1"` if [ -r "${infile}" ]; then grep "^#_META_|about:$http_cfg__about" "${infile}" >/dev/null if [ $? -eq 0 ]; then rm -f "${infile}" if [ $? -eq 0 ]; then http._wd "OK, ${infile} was deleted." else http._wd "ERROR: unable to delete existing ${infile}. Check permissions." fi else http._wd "SKIP: ${infile} is not an export file." fi else http._wd "SKIP: ${infile} is not readable." fi } # ====================================================================== # SETTER # ====================================================================== # set authentication # param string USER:PASSWORD function http.setAuth(){ http._wd "${FUNCNAME[0]}($1)" if [ -z "$1" ]; then http_req__auth= else http_req__auth="-u $1" fi } # set body to send for PUTs and POSTs # param string body function http.setBody(){ http._wd "${FUNCNAME[0]}($1)" http_req__body=$1 } # set a base url of an API # Remark: Then use http.setUrl to complet the url to request # param string url function http.setBaseUrl(){ http._wd "${FUNCNAME[0]}($1)" http_req__baseurl=$1 http.setFullUrl } # Enable or disable debug mode # param integer 0|1 function http.setDebug(){ http._wd "${FUNCNAME[0]}($1)" http_cfg__debug=$1 } function http.setDocs(){ http._wd "${FUNCNAME[0]}($1)" http_req__docs=$1 } # set the method to use; GET|POST|PUT|DELETE # param string name of method function http.setMethod(){ http._wd "${FUNCNAME[0]}($1)" http_req__method=$1 } # set a full url to request # param string url function http.setFullUrl(){ http._wd "${FUNCNAME[0]}($1)" if [ -z "$1" ]; then http_req__fullurl=${http_req__baseurl}${http_req__url} else http_req__fullurl=$1 fi } # complete the base url # param string url part behind base url function http.setUrl(){ http._wd "${FUNCNAME[0]}($1)" http_req__url=$1 http.setFullUrl } # ----- caching function http.setCacheTtl(){ http._wd "${FUNCNAME[0]}($1)" http_cfg__cacheTtl=$1 } function http.setCacheFile(){ http._wd "${FUNCNAME[0]}($1)" http_cfg__cacheFile="$1" } function http.flushCache(){ http._wd "${FUNCNAME[0]}($1)" rm -f ${http_cfg__cacheDir}/* } # ...................................................................... # # show a help text # no params function http.help(){ cat <<EOH $http_cfg__about This is a bash solution to script REST API calls. Source:$http_cfg__prjurl License: GNU GPL 3 INSTRUCTION: - Source the file once - Then you can run functions starting with "http." http.init Start a new request. It resets internal vars of the last response (if there was one). http.setDebug 0|1 Enable or disable debugging infos during processing. It is written to STDERR. - initialize a request setAuth AUTH:PASSWORD set authentication http.setBody DATA set a body for POST/ PUT requests. http.setBaseUrl URL Set a base url to an api. renmark: Use http.setUrl to built a complete url. http.setDocs URL http.setMethod METHOD Set a http method. Use an uppercase string for GET|POST|PUT|DELETE|... http.setFullUrl URL Set a complete url for a request. http.setUrl REQUEST?QUERY Set a relative url for a request. This requires to use http.setBaseUrl before. - caching functions http.setCacheTtl SECONDS Enable caching with values > 0 Remark: only GET requests will be cached. Default: 0 (no caching) http.setCacheFile FILENAME Set a file where to read/ store a request Default: empty; autogenerated file below $http_cfg__cacheDir http.flushCache Delete all files in $http_cfg__cacheDir - make the request http.makeRequest [[METHOD] [URL] [BODY]] The parameters are optional. Without parameter the rquest will be started with given data in http.set* functions described above. If minimum one pram is given then they are handled: METHOD optional: set a method (must be uppercase) - see http.setMethod URL set a relative url - see http.setUrl BODY optional: set a body - see http.setBody The request will be skipped and uses a cached content if ... - METHOD is GET - http.setCacheTtl set a value > 0 - the cache file exists and is younger than the given TTL - handle response http.getResponse Get the Response Body http.getResponseData Get Meta infos from curl http.getResponseHeader Get The http reponse header - check http status code http.getStatus Get the http status as string Ok|Redirect|Error http.getStatuscode Get the http status code of a request as integer http.isOk Check if the http response code is a 2xx http.isRedirect Check if the http response code is a 3xx http.isError Check if the http response code is a 4xx or 5xx http.isClientError Check if the http response code is a 4xx http.isServerError Check if the http response code is a 5xx http.getRequestAge Get the age of the request in seconds. Remark: This function is useful after an import see http.responseImport. http.getRequestTs Get the Unix timestamp of the request - import/ export http.responseExport [FILE] dump the response data Without parameter it is written on STDOUT. You can set a filename to write it to a file. The filename can contain "AUTOFILE" this string will be replaced with a uniq string. (requires sha1sum and a set url) Example: http.makeRequest "https://example.com/api/" http.responseExport /tmp/something_AUTOFILE_.txt http.responseImport FILE Import an export file. To use the AUTOFILE mechanism from export set the url first. Example: http.setFullUrl "https://example.com/api/" http.responseImport /tmp/something_AUTOFILE_.txt http.responseDelete FILE Delete a file after http.responseExport. It is useful if you use the AUTOFILE mechanism. EOH } # ---------------------------------------------------------------------- # # main # # ---------------------------------------------------------------------- http.init # ----------------------------------------------------------------------