diff --git a/LICENSE b/LICENSE index 6e6b088..713327d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,19 @@ -Copyright (c) 2013 Alexander - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +Copyright (c) 2013 Alexander + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 8304bf6..2c589d9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ Features: - Response body checks - Response code checks - Response header checks -- GET/POST on endpoints +- GET/POST/OPTIONS on endpoints +- ORIGIN support for testing CORS responses - CSRF tokens - Reporting and sane exit codes @@ -33,7 +34,7 @@ Running: ```bash $ ./smoke-google -> http://google.com/ +> GET http://google.com/ [ OK ] 2xx Response code [ OK ] Body contains "search" OK (2/2) @@ -78,6 +79,25 @@ The minimal smoke test will check if a URL returns with a 200 response code: smoke_url_ok "http://google.com" ``` +It is also possible to check for other response codes explicitly: + +```bash +smoke_url "http://google.com/doesnotexist" +smoke_assert_code 404 +``` + +### GET a URL and check for redirects + +In order to check for redirects, you must call `smoke_no_follow` before calling `smoke_url`: + +```bash +smoke_no_follow +smoke_url "http://google.com" +smoke_assert_code 302 +``` + +You can follow redirects again by calling `smoke_follow` + ### POST a URL and check the response code A more advanced smoke test will POST data to a URL. Such a test can be used to @@ -110,6 +130,15 @@ By checking response headers, you can make sure to get the correct content type: smoke_assert_headers "Content-Type: text/html; charset=utf-8" ``` +### Checking the server is not responding + +In order to check a server is not responding, you can use `smoke_assert_no_response` after calling `smoke_url`: + +```bash +smoke_url "http://myserver.com:5000/" +smoke_assert_no_response +``` + ### Configuring a base URL It is possible to setup a base URL that is prepended for each URL that is @@ -125,13 +154,13 @@ smoke_url_ok "/login" If the server requires a certain host header to be set, override the host from the URL with -``` +```bash smoke_host "example.org" ``` To un-override, set it empty: -``` +```bash smoke_host "" ``` @@ -146,6 +175,22 @@ smoke_header "X-Forwarded-Proto: https" Existing custom headers can be unset with `remove_smoke_headers`. +### Checking CORS is enabled for a certain Origin + +First of all, set the origin header with: + +``` +smoke_origin "https://acme.corp" +``` + +Then test for CORS headers using: + +``` +smoke_url_cors "https://api.com/endpoint" + smoke_assert_headers "Access-Control-Allow-Credentials: true" + smoke_assert_headers "Access-Control-Allow-Origin: https://acme.corp" +``` + ### CSRF tokens Web applications that are protected with CSRF tokens will need to extract a @@ -185,6 +230,33 @@ smoke_response_body # raw body (html/json/...) smoke_response_headers # list of headers ``` +### Authentication + +If the server requires an authentication (for example : HTTP Basic authentication), you must call `smoke_credentials` before calling `smoke_url`. +If you simply specify the user name, you will be prompted for a password. + +```bash +smoke_credentials "username" "password" +smoke_url "http://secured-website.com" +``` + +To un-set credentials, call `smoke_no_credentials` : + +```bash +smoke_no_credentials +``` + +### Debugging + +In order to debug your requests, call `smoke_debug` before calling `smoke_url`: + +```bash +smoke_debug +smoke_url_ok "http://google.com" +``` + +You can turn off debugging by calling `smoke_no_debug` + Advanced example ---------------- @@ -229,22 +301,28 @@ smoke_report API --- -| function | description | -|---------------------------------|------------------------------------------------------| -|`smoke_assert_body ` | assert that the body contains `` | -|`smoke_assert_code ` | assert that there was a `` response code | -|`smoke_assert_code_ok` | assert that there was a `2xx` response code | -|`smoke_assert_headers ` | assert that the headers contain `` | -|`smoke_csrf ` | set the csrf token to use in POST requests | -|`smoke_form ` | POST data on url | -|`smoke_form_ok ` | POST data on url and check for a `2xx` response code | -|`smoke_report` | prints the report and exits | -|`smoke_response_body` | body of the last response | -|`smoke_response_code` | code of the last response | -|`smoke_response_headers` | headers of the last response | -|`smoke_url ` | GET a url | -|`smoke_url_ok ` | GET a url and check for a `2xx` response code | -|`smoke_url_prefix ` | set the prefix to use for every url (e.g. domain) | -|`smoke_host ` | set the host header to use | -|`smoke_header
` | set additional request header | -|`smoke_tcp_ok ` | open a tcp connection and check for a `Connected` response | +| function | description | +|-----------------------------------------|------------------------------------------------------------------------------------------------| +|`smoke_assert_body ` | assert that the body contains `` | +|`smoke_assert_code ` | assert that there was a `` response code | +|`smoke_assert_code_ok` | assert that there was a `2xx` response code | +|`smoke_assert_headers ` | assert that the headers contain `` | +|`smoke_assert_no_response` | assert that the server is not responding | +|`smoke_credentials []` | set the credentials to use : login (and password). If password is not set, it will be prompted | +|`smoke_csrf ` | set the csrf token to use in POST requests | +|`smoke_form ` | POST data on url | +|`smoke_form_ok ` | POST data on url and check for a `2xx` response code | +|`smoke_origin ` | set the `Origin` header | +|`smoke_proxy ` | set the HTTP proxy to use [protocol://][user:password@]proxyhost[:port] | +|`smoke_no_proxy []` | Comma-separated list of hosts which do not use a proxy | +|`smoke_report` | prints the report and exits | +|`smoke_response_body` | body of the last response | +|`smoke_response_code` | code of the last response | +|`smoke_response_headers` | headers of the last response | +|`smoke_url ` | GET a url | +|`smoke_url_ok ` | GET a url and check for a `2xx` response code | +|`smoke_url_prefix ` | set the prefix to use for every url (e.g. domain) | +|`smoke_host ` | set the host header to use | +|`smoke_header
` | set additional request header (in the form `Key: value`) | +|`remove_smoke_headers` | remove all headers | +|`smoke_tcp_ok ` | open a tcp connection and check for a `Connected` response | diff --git a/smoke.sh b/smoke.sh index 4932637..1d1ddb4 100644 --- a/smoke.sh +++ b/smoke.sh @@ -1,27 +1,93 @@ #!/bin/bash -SMOKE_TMP_DIR=$(mktemp -d) - +# Create a temporary directory that works on both Linux and Darwin +SMOKE_TMP_DIR=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'` SMOKE_AFTER_RESPONSE="" SMOKE_CURL_CODE="$SMOKE_TMP_DIR/smoke_curl_code" SMOKE_CURL_HEADERS="$SMOKE_TMP_DIR/smoke_curl_headers" SMOKE_CURL_BODY="$SMOKE_TMP_DIR/smoke_curl_body" SMOKE_CURL_COOKIE_JAR="$SMOKE_TMP_DIR/smoke_curl_cookie_jar" +SMOKE_CURL_FOLLOW="--location" +SMOKE_CURL_VERBOSE="--silent" +SMOKE_CURL_CREDENTIALS="" SMOKE_CSRF_TOKEN="" SMOKE_CSRF_FORM_DATA="$SMOKE_TMP_DIR/smoke_csrf_form_data" +SMOKE_HEADERS=() + +SMOKE_ORIGIN="" + +SMOKE_NO_PROXY=() +SMOKE_PROXY=() + SMOKE_TESTS_FAILED=0 SMOKE_TESTS_RUN=0 SMOKE_URL_PREFIX="" -SMOKE_HEADERS=() ## "Public API" +# Configuration +smoke_url_prefix() { + SMOKE_URL_PREFIX="$1" +} + smoke_csrf() { SMOKE_CSRF_TOKEN="$1" } +smoke_debug() { + SMOKE_CURL_VERBOSE="--verbose" +} + +smoke_no_debug() { + SMOKE_CURL_VERBOSE="--silent" +} + +smoke_follow() { + SMOKE_CURL_FOLLOW="--location" +} + +smoke_no_follow() { + SMOKE_CURL_FOLLOW="" +} + +smoke_credentials() { + USERNAME="$1" + PASSWORD="$2" + + if [[ -z "${USERNAME// /}" ]]; then + _smoke_print_failure "Username is unset or empty" + _smoke_cleanup + exit 1 + fi + SMOKE_CURL_CREDENTIALS="-u $USERNAME" + if [[ -n "$PASSWORD" ]]; then + SMOKE_CURL_CREDENTIALS="$SMOKE_CURL_CREDENTIALS:$PASSWORD" + fi +} + +smoke_no_credentials() { + USERNAME="" + PASSWORD="" + SMOKE_CURL_CREDENTIALS="" +} + +smoke_origin() { + SMOKE_ORIGIN="$1" +} + +smoke_proxy() { + SMOKE_PROXY=('--proxy' "$1") +} + +smoke_no_proxy() { + # Default is "*" for all (need to use an array to prevent escape/expansion issues) + local noproxy="${1-*}" + SMOKE_NO_PROXY=('--noproxy' "$noproxy") +} + +# Request smoke_form() { URL="$1" FORMDATA="$2" @@ -35,14 +101,26 @@ smoke_form() { _curl_post $URL $FORMDATA } -smoke_form_ok() { +smoke_url() { URL="$1" - FORMDATA="$2" + _curl_get $URL +} - smoke_form "$URL" "$FORMDATA" - smoke_assert_code_ok + +# Response +smoke_response_code() { + cat $SMOKE_CURL_CODE +} + +smoke_response_body() { + cat $SMOKE_CURL_BODY } +smoke_response_headers() { + cat $SMOKE_CURL_HEADERS +} + +# Report smoke_report() { _smoke_cleanup if [[ $SMOKE_TESTS_FAILED -ne 0 ]]; then @@ -52,79 +130,81 @@ smoke_report() { _smoke_print_report_success "OK ($SMOKE_TESTS_RUN/$SMOKE_TESTS_RUN)" } -smoke_response_code() { - cat $SMOKE_CURL_CODE -} - -smoke_response_body() { - cat $SMOKE_CURL_BODY +smoke_header() { + SMOKE_HEADERS+=("$1") } -smoke_response_headers() { - cat $SMOKE_CURL_HEADERS +smoke_host() { + smoke_header "Host: $1" } -smoke_tcp_ok() { - URL="$1 $2" - _smoke_print_url "$URL" - echo EOF | telnet $URL > $SMOKE_CURL_BODY - smoke_assert_body "Connected" +remove_smoke_headers() { + unset SMOKE_HEADERS } -smoke_url() { +smoke_url_cors() { URL="$1" - _curl_get $URL + _curl_options $URL } + +## Assertions + smoke_url_ok() { URL="$1" smoke_url "$URL" smoke_assert_code_ok } -smoke_url_prefix() { - SMOKE_URL_PREFIX="$1" -} - -smoke_header() { - SMOKE_HEADERS+=("$1") +smoke_tcp_ok() { + URL="$1 $2" + _smoke_print_url "TCP" "$URL" + echo EOF | telnet $URL > $SMOKE_CURL_BODY + smoke_assert_body "Connected" } -smoke_host() { - smoke_header "Host: $1" -} +smoke_form_ok() { + URL="$1" + FORMDATA="$2" -remove_smoke_headers() { - unset SMOKE_HEADERS + smoke_form "$URL" "$FORMDATA" + smoke_assert_code_ok } -## Assertions - smoke_assert_code() { EXPECTED="$1" - CODE=$(cat $SMOKE_CURL_CODE) + CODE=$(smoke_response_code) - if [[ $CODE == $1 ]]; then - _smoke_success "$1 Response code" + if [[ $CODE == $EXPECTED ]]; then + _smoke_success "$EXPECTED Response code" else - _smoke_fail "$1 Response code" + _smoke_fail "$EXPECTED Response code (${CODE:-No response})" fi } smoke_assert_code_ok() { - CODE=$(cat $SMOKE_CURL_CODE) + CODE=$(smoke_response_code) if [[ $CODE == 2* ]]; then _smoke_success "2xx Response code" else - _smoke_fail "2xx Response code" + _smoke_fail "2xx Response code (${CODE:-No response})" + fi +} + +smoke_assert_no_response() { + CODE=$(smoke_response_code) + if [[ -z "${CODE// }" ]]; then + _smoke_success "No response from server" + else + _smoke_fail "Got a response from server" fi } smoke_assert_body() { STRING="$1" - smoke_response_body | grep --quiet "$STRING" + smoke_response_body | grep -q "$STRING" if [[ $? -eq 0 ]]; then _smoke_success "Body contains \"$STRING\"" @@ -136,7 +216,7 @@ smoke_assert_body() { smoke_assert_headers() { STRING="$1" - smoke_response_headers | grep --quiet "$STRING" + smoke_response_headers | grep -q "$STRING" if [[ $? -eq 0 ]]; then _smoke_success "Headers contain \"$STRING\"" @@ -162,6 +242,12 @@ _smoke_fail() { _smoke_print_failure "$REASON" } +_smoke_success() { + REASON="$1" + _smoke_print_success "$REASON" + (( ++SMOKE_TESTS_RUN )) +} + _smoke_prepare_formdata() { FORMDATA="$1" @@ -173,16 +259,13 @@ _smoke_prepare_formdata() { fi } -_smoke_success() { - REASON="$1" - _smoke_print_success "$REASON" - (( ++SMOKE_TESTS_RUN )) -} ## Curl helpers _curl() { - local opt=(--cookie $SMOKE_CURL_COOKIE_JAR --cookie-jar $SMOKE_CURL_COOKIE_JAR --location --dump-header $SMOKE_CURL_HEADERS --silent) - + # Prepare request + local opt=(--cookie $SMOKE_CURL_COOKIE_JAR --cookie-jar $SMOKE_CURL_COOKIE_JAR $SMOKE_CURL_FOLLOW --dump-header $SMOKE_CURL_HEADERS $SMOKE_CURL_VERBOSE $SMOKE_CURL_CREDENTIALS "${SMOKE_PROXY[@]}" "${SMOKE_NO_PROXY[@]}") + + # Add headers if (( ${#SMOKE_HEADERS[@]} )); then for header in "${SMOKE_HEADERS[@]}" do @@ -190,6 +273,13 @@ _curl() { done fi + # Add origin + if [[ -n "$SMOKE_ORIGIN" ]] + then + opt+=(-H "Origin: $SMOKE_ORIGIN") + fi + + # Do CURL curl "${opt[@]}" "$@" > $SMOKE_CURL_BODY } @@ -197,7 +287,7 @@ _curl_get() { URL="$1" SMOKE_URL="$SMOKE_URL_PREFIX$URL" - _smoke_print_url "$SMOKE_URL" + _smoke_print_url "GET" "$SMOKE_URL" _curl $SMOKE_URL @@ -206,13 +296,26 @@ _curl_get() { $SMOKE_AFTER_RESPONSE } +_curl_options() { + URL="$1" + + SMOKE_URL="$SMOKE_URL_PREFIX$URL" + _smoke_print_url "OPTIONS" "$SMOKE_URL" + + _curl -X OPTIONS $SMOKE_URL + + grep -oE 'HTTP[^ ]+ [0-9]{3}' $SMOKE_CURL_HEADERS | tail -n1 | grep -oE '[0-9]{3}' > $SMOKE_CURL_CODE + + $SMOKE_AFTER_RESPONSE +} + _curl_post() { URL="$1" FORMDATA="$2" FORMDATA_FILE="@"$(_smoke_prepare_formdata $FORMDATA) SMOKE_URL="$SMOKE_URL_PREFIX$URL" - _smoke_print_url "$SMOKE_URL" + _smoke_print_url "POST" "$SMOKE_URL" _curl --data "$FORMDATA_FILE" $SMOKE_URL @@ -257,6 +360,11 @@ _smoke_print_success() { } _smoke_print_url() { - TEXT="$1" - echo "> $TEXT" + VERB="$1" + URL="$2" + local url_to_print="> ${VERB} ${bold}${URL}${normal}" + if [[ -n "${SMOKE_CURL_CREDENTIALS}" ]]; then + url_to_print="$url_to_print (authenticate as ${USERNAME})" + fi + echo "$url_to_print" }