diff --git a/rest-api-client.sh b/rest-api-client.sh index 2a55f363374498332234b5d9f4779e558b06240d..8f0f7c389249c1104006a87738813a9cfbdf2448 100644 --- a/rest-api-client.sh +++ b/rest-api-client.sh @@ -8,6 +8,10 @@ # - curl # - sha1sum (optional; for export functionality with AUTOFILE only) # ---------------------------------------------------------------------- +# License: GPL 3.0 +# Source: <https://git-repo.iml.unibe.ch/iml-open-source/bash-rest-api-client> +# Docs: <https://os-docs.iml.unibe.ch/bash-rest-api-client/> +# ---------------------------------------------------------------------- # (1) source this script # (2) enter "http.help" to get a list of available commands # ---------------------------------------------------------------------- @@ -16,9 +20,10 @@ # 2020-03-02 v0.5 axel.hahn@iml.unibe.ch a few more response check functions # 2021-01-21 v0.6 axel.hahn@iml.unibe.ch add Content-type in request header # 2022-01-11 v0.7 axel.hahn@iml.unibe.ch fixes using shellcheck +# 2024-10-09 v0.8 axel.hahn@unibe.ch add setAuthorization; customize accept header, add custom request headers # ====================================================================== - http_cfg__about="Bash REST API client v0.7" + http_cfg__about="Bash REST API client v0.8" typeset -i http_cfg__debug=0 typeset -i http_cfg__cacheTtl=0 http_cfg__cacheDir=/var/tmp/http-cache @@ -97,6 +102,9 @@ EOH # grep "function http.[a-z]" $0 | sort } + # ...................................................................... + # + # Initialize the client function http.init(){ http._wd "${FUNCNAME[0]}()" which curl >/dev/null || http.quit @@ -104,7 +112,10 @@ EOH # request vars http_req__auth= - http_req__auth= + http_req__authorization= + http_req__accept="application/json" + http_req__headers=() + http_req__body= http_req__method=GET http_req__url= @@ -120,7 +131,6 @@ EOH http_curl__writeout="\\n${http_req__dataprefix}${writevar}\\n" # cache - http_req__mode=undefined http_cfg__cacheTtl=0 http_cfg__cacheFile= @@ -131,7 +141,10 @@ EOH chmod 777 ${http_cfg__cacheDir} 2>/dev/null } - # execute the request + # ...................................................................... + # + # Execute the request + # # param string optional: method; GET|POST|PUT|DELETE|... # param string optional: full url # param string optional: request body @@ -140,8 +153,7 @@ EOH # --- handle optional prams if [ $# -ne 0 ]; then - echo "$1" | grep "^[A-Z]*$" >/dev/null - if [ $? -eq 0 ]; then + if echo "$1" | grep -q "^[A-Z]*$"; then http.setMethod "$1" shift 1 fi @@ -151,21 +163,19 @@ EOH # test -z "$1" || http.setFullUrl "$1" # --- detect caching - http_req__mode=REQUEST - useCache=0 - makeRequest=1 + local useCache=0 + local makeRequest=1 if [ $http_cfg__cacheTtl -gt 0 ] && [ "${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 + local iAge; typeset -i iAge 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 ] && [ $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 @@ -176,29 +186,25 @@ EOH # --- make the request if [ $makeRequest -eq 1 ]; then - 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' \ - -H 'Content-type: 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' \ - -H 'Content-type: application/json' \ - ${http_req__auth} \ - -i "${http_req__fullurl}" \ - -X "${http_req__method}" \ - -d "${http_req__body}" - fi - ) || http.quit + + # build curl parameters + local curl_params=( + -k + -s + -i "${http_req__fullurl}" + -X "${http_req__method}" + -w "${http_curl__writeout}" + -A "${http_cfg__UA}" + ) + test -n "$http_req__auth" && curl_params+=( -u "$http_req__auth" ) + test -n "$http_req__authorization" && curl_params+=( -H "Authorization: $http_req__authorization" ) + test -n "$http_req__accept" && curl_params+=( -H "Accept: $http_req__accept" ) + test -n "$http_req__body" && curl_params+=( -d "${http_req__body}" ) + curl_params+=( "${http_req__headers[@]}" ) + + http._wd "${FUNCNAME[0]}($1) ${http_req__method} ${http_req__fullurl}" + http_resp__all=$( curl "${curl_params[@]}") || http.quit + http._wd "OK - Curl finished the http request ... processing data" http_resp__neutral=$(http._fetchAllAndReformat) if [ $useCache -eq 1 ]; then @@ -211,7 +217,8 @@ EOH # ...................................................................... # - # show error message with last return code and quit with this exitcode + # Show error message with last return code and quit with this exitcode + # # no params function http.quit(){ rc=$? @@ -252,7 +259,10 @@ EOH # GETTER # ====================================================================== - # get the response header or response body + # ...................................................................... + # + # Get the response header or response body + # # param string what to return; one of header|body function http._fetchResponseHeaderOrBody(){ http._wd "${FUNCNAME[0]}($1)" @@ -274,6 +284,10 @@ EOH fi done } + + # ...................................................................... + # + # Get the response data function http._fetchResponseData(){ http._wd "${FUNCNAME[0]}()" 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 @@ -281,6 +295,9 @@ EOH done } + # ...................................................................... + # + # Generate the dump with request and response function http._fetchAllAndReformat(){ http._wd "${FUNCNAME[0]}()" IFS='' @@ -307,6 +324,11 @@ EOH echo $line END } + # ...................................................................... + # + # Get a section from dump data + # + # param string what to return; one of HEADER|DATA|BODY function http._getFilteredResponse(){ http._wd "${FUNCNAME[0]}($1)" echo "${http_resp__neutral}" | grep "^#_${1}_|" | cut -f 2- -d "|" @@ -314,17 +336,23 @@ EOH # ---------- PUBLIC REQUEST GETTER + # ...................................................................... + # + # Get timestamp of the response function http.getRequestTs(){ http._wd "${FUNCNAME[0]}()" http._getFilteredResponse REQUEST | grep "^timestamp" | cut -f 2 -d ":" } - # get age of the response in sec. + # ...................................................................... + # + # Get age of the response in sec. # It is especially useful after responseImport + # function http.getRequestAge(){ http._wd "${FUNCNAME[0]}()" - typeset -i local iAge - typeset -i local iTs + local iAge; typeset -i iAge + local iTs; typeset -i iTs iTs=$( http.getRequestTs ) iAge=$( date +%s )-${iTs} echo "$iAge" @@ -332,29 +360,41 @@ EOH # ---------- PUBLIC RESPONSE GETTER - # get response body + # ...................................................................... + # + # Get response body function http.getResponse(){ http._wd "${FUNCNAME[0]}()" http._getFilteredResponse BODY } - # get curl data of this request with status, transferred bytes, speed, ... + + # ...................................................................... + # + # Get curl data of this request with status, transferred bytes, speed, ... function http.getResponseData(){ http._wd "${FUNCNAME[0]}()" http._getFilteredResponse DATA } - # get response header + + # ...................................................................... + # + # Get response header function http.getResponseHeader(){ http._wd "${FUNCNAME[0]}()" http._getFilteredResponse HEADER } - # get raw response (not available after import) + # ...................................................................... + # + # Get raw response (not available after import) function http.getResponseRaw(){ http._wd "${FUNCNAME[0]}()" echo "${http_resp__all}" } - # get Http status as string OK|Redirect|Error + # ...................................................................... + # + # Get Http status as string OK|Redirect|Error function http.getStatus(){ http._wd "${FUNCNAME[0]}($1)" http.isOk >/dev/null && echo OK @@ -362,44 +402,61 @@ EOH http.isError >/dev/null && echo Error } - # get Http status code of the request as 3 digit number + # ...................................................................... + # + # Get Http status code of the request as 3 digit number function http.getStatuscode(){ http._wd "${FUNCNAME[0]}()" http.getResponseData | grep "^http_code:" | cut -f 2 -d ":" } - # was response a 2xx status code? + # ...................................................................... + # + # Check: 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]}()" http.getStatuscode | grep '2[0-9][0-9]' } - # was the repsonse a redirect? + + # ...................................................................... + # + # Was the repsonse a redirect? function http.isRedirect(){ http._wd "${FUNCNAME[0]}()" http.getStatuscode | grep '3[0-9][0-9]' } - # was the repsonse a client error (4xx or 5xx) + # ...................................................................... + # + # Was the repsonse a client error (4xx or 5xx) function http.isError(){ http._wd "${FUNCNAME[0]}()" http.getStatuscode | grep '[45][0-9][0-9]' } - # was the repsonse a client error (4xx) + + # ...................................................................... + # + # Was the repsonse a client error (4xx) function http.isClientError(){ http._wd "${FUNCNAME[0]}()" http.getStatuscode | grep '4[0-9][0-9]' } - # was the repsonse a client error (5xx) + + # ...................................................................... + # + # Was the repsonse a client error (5xx) function http.isServerError(){ http._wd "${FUNCNAME[0]}()" http.getStatuscode | grep '5[0-9][0-9]' } - + # ...................................................................... + # # dump information about request and response function http.dump(){ http._wd "${FUNCNAME[0]}()" @@ -410,13 +467,15 @@ EOH # 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 + if echo "$1" | grep -q "AUTOFILE"; then + echo "$1" else local sum sum=$(echo ${http_req__fullurl} | sha1sum ) @@ -426,7 +485,8 @@ EOH fi } - + # ...................................................................... + # # export response to a file # param string optional: custom filename function http.responseExport(){ @@ -441,14 +501,17 @@ EOH fi } - # import a former response from a file + # ...................................................................... + # + # Import a former response from a file + # + # param string filename with stored response function http.responseImport(){ http._wd "${FUNCNAME[0]}($1)" local infile infile=$(http._genOutfilename "$1") if [ -r "${infile}" ]; then - grep "^#_META_|about:$http_cfg__about" "${infile}" >/dev/null - if [ $? -eq 0 ]; then + if grep -q "^#_META_|about:$http_cfg__about" "${infile}"; then http_resp__neutral=$(cat "${infile}") else echo "ERROR: Ooops [${infile}] does not seem to be an export dump." @@ -459,17 +522,20 @@ EOH http.quit fi } - # delete an exported file; this is especially useful if you use + + # ...................................................................... + # + # Delete an exported file; this is especially useful if you use # AUTOFILE functionality + # + # param string filename with stored response function http.responseDelete(){ http._wd "${FUNCNAME[0]}($1)" local infile 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 + if grep -q "^#_META_|about:$http_cfg__about" "${infile}"; then + if rm -f "${infile}"; then http._wd "OK, ${infile} was deleted." else http._wd "ERROR: unable to delete existing ${infile}. Check permissions." @@ -486,31 +552,85 @@ EOH # SETTER # ====================================================================== - # set authentication + # ...................................................................... + # + # Add a line to the request header + # + # param string line to add, eg "Connection: keep-alive" + function http.addHeader(){ + http._wd "${FUNCNAME[0]}($1)" + http_req__headers+=( -H "$1") + } + + # ...................................................................... + # + # set Accept request header and override default + # + # param string accept header value, eg text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + function http.setAccept(){ + http._wd "${FUNCNAME[0]}($1)" + if [ -z "$1" ]; then + http_req__accept= + else + http_req__accept="$1" + fi + } + + # ...................................................................... + # + # Set basic 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" + http_req__auth="$1" + fi + } + + # ...................................................................... + # + # Set authentication via Athorization header + # + # param string type, eg. Basic|Bearer|Negotiate + # param string token or encoded user + password + function http.setAuthorization(){ + http._wd "${FUNCNAME[0]}($1 $2)" + if [ -z "$1" ]; then + http_req__authorization= + else + http_req__authorization="${1} ${2}" fi } - # set body to send for PUTs and POSTs + + # ...................................................................... + # + # 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 + + # ...................................................................... + # + # 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)" @@ -521,14 +641,20 @@ EOH http_req__docs=$1 } - # set the method to use; GET|POST|PUT|DELETE + # ...................................................................... + # + # 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 + # ...................................................................... + # + # Set a full url to request + # # param string optional: url function http.setFullUrl(){ http._wd "${FUNCNAME[0]}($1)" @@ -538,7 +664,11 @@ EOH http_req__fullurl=$1 fi } - # complete the base url + + # ...................................................................... + # + # Complete the base url + # # param string url part behind base url function http.setUrl(){ http._wd "${FUNCNAME[0]}($1)" @@ -548,16 +678,29 @@ EOH # ----- caching + # ...................................................................... + # + # Set cache ttl in seconds + # + # param integer ttl in seconds function http.setCacheTtl(){ http._wd "${FUNCNAME[0]}($1)" http_cfg__cacheTtl=$1 } + # ...................................................................... + # + # Set cache file + # + # param string filename function http.setCacheFile(){ http._wd "${FUNCNAME[0]}($1)" http_cfg__cacheFile="$1" } + # ...................................................................... + # + # Flush the cache function http.flushCache(){ http._wd "${FUNCNAME[0]}($1)" rm -f ${http_cfg__cacheDir}/* @@ -593,8 +736,17 @@ INSTRUCTION: - initialize a request + setAccept ACCEPT + Set authentication with user and password for basic auth + Default: $http_req__accept + setAuth AUTH:PASSWORD - set authentication + Set authentication with user and password for basic auth + + setAuthorization TYPE TOKEN|HASH + Set authentication with Authorization header. + As TYPE you can use Basic|Bearer|Negotiate|... + 2nd param is the token or hased user+password http.setBody DATA set a body for POST/ PUT requests. @@ -616,6 +768,10 @@ INSTRUCTION: Set a relative url for a request. This requires to use http.setBaseUrl before. + http.addHeader HEADER_LINE + Add a header line to the request. + This command can be repeated multiple times. + - caching functions http.setCacheTtl SECONDS