diff --git a/bin/probe_saml_idp.sh b/bin/probe_saml_idp.sh index d0d365d..7ab2cf6 100755 --- a/bin/probe_saml_idp.sh +++ b/bin/probe_saml_idp.sh @@ -28,28 +28,26 @@ display_help () { ${user_agent_string} Given a single identifier, assumed to be an IdP entityID, probe - all browser-facing SAML2 SSO endpoints in IdP metadata. + all browser-facing SSO endpoints in IdP metadata. - Usage: ${0##*/} [-hvq] [-t CONNECT_TIME [-m MAX_TIME]] [-r MAX_REDIRS] (-u MDQ_BASE_URL | -f MD_PATH) ID + Usage: ${0##*/} [-hvq] [-t CONNECT_TIME [-m MAX_TIME]] [-r MAX_REDIRS] ID Options: -h Display this message -v Write verbose messages to stdout -q Run quietly (i.e., write no messages to stdout) - -t Time (in secs) to connect to the host + -t Allowed time (in secs) to connect to the host -m Maximum time (in secs) of a complete probe -r Maximum number of HTTP redirects followed - -u Base URL of a Metadata Query Server - -f Path to a local metadata file Option -h is mutually exclusive of all other options. Options -q and -v are mutually exclusive of each other. Options -u and -f are mutually exclusive of each other as well. The argument of the -t option is the TCP connect time, that is, - the maximum time (in secs) allotted to the TCP connection. Note - that the TCP connect time includes the time it takes to do a - DNS name lookup. Since the latter is unconstrained, it may + the maximum time (in secs) allotted to obtain a TCP connection. + Note that the TCP connect time includes the time it takes to do + a DNS name lookup. Since the latter is unconstrained, it may consume all available TCP connect time. Thus the TCP connect time should be kept small (on the order of a few seconds) since larger values will slow this script considerably. @@ -59,17 +57,12 @@ display_help () { beyond the TCP connect time. Any value less than the TCP connect time causes the script to immediately fail. - Entity metadata is required to process each identifier. Metadata is - obtained in one of two ways, by consulting a Metadata Query Server - just-in-time or by using a pre-provisioned metadata aggregate. These - correspond to options -u and -f, respectively. Exactly one of these - options is required. + CONFIGURATION - Option -f takes an optional file argument (MD_PATH), the absolute - path to a local SAML metadata file. The script searches this file for - a corresponding entity descriptor as it processes each identifier. + Entity metadata is required to process each identifier. Metadata is + obtained by consulting a Metadata Query Server just-in-time. - Option -u takes an optional URI argument (MDQ_BASE_URL), the base + ...(MDQ_BASE_URL), the base URL of a Metadata Query Server (i.e., a server that conforms to the Metadata Query Protocol). The base URL is used to construct an MDQ request URL, which the script uses to request entity metadata @@ -99,7 +92,8 @@ LIB_FILENAMES="command_paths.sh compatible_mktemp.sh http_tools.sh md_tools.sh -saml_tools.sh" +saml_tools.sh +config_tools.sh" # source lib files for lib_filename in $LIB_FILENAMES; do @@ -116,37 +110,21 @@ for lib_filename in $LIB_FILENAMES; do fi done -# default parameters -connect_timeout_default=2 -max_redirs_default=7 - -# default env vars -saml2_sp_entity_id_default=https://fm.incommon.org/sp -saml2_sp_acs_url_default=https://service1.internet2.edu/Shibboleth.sso/SAML2/POST -saml2_sp_acs_binding_default=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST +# basic curl defaults +connect_timeout_default=2; max_redirs_default=7 -# check env vars, set defaults if neccessary -if [ -z "$SAML2_SP_ENTITY_ID" ]; then - echo "WARNING: $script_name: env var SAML2_SP_ENTITY_ID does not exist, using default: $saml2_sp_entity_id_default" >&2 - SAML2_SP_ENTITY_ID=$saml2_sp_entity_id_default -fi -if [ -z "$SAML2_SP_ACS_URL" ]; then - echo "WARNING: $script_name: env var SAML2_SP_ACS_URL does not exist, using default: $saml2_sp_acs_url_default" >&2 - SAML2_SP_ACS_URL=$saml2_sp_acs_url_default -fi -if [ -z "$SAML2_SP_ACS_BINDING" ]; then - echo "WARNING: $script_name: env var SAML2_SP_ACS_BINDING does not exist, using default: $saml2_sp_acs_binding_default" >&2 - SAML2_SP_ACS_BINDING=$saml2_sp_acs_binding_default -fi +# default config file +config_file_default="${script_bin}/.config" ####################################################################### # Process command-line options and arguments ####################################################################### help_mode=false; quiet_mode=false; verbose_mode=false -md_query_mode=false; md_file_mode=false +probe_saml1=false # TODO: implement -1 option local_opts=; connect_timeout=; max_time=; max_redirs= -while getopts ":hqvt:m:r:u:f:" opt; do +curl_opts= +while getopts ":hqvt:m:r:" opt; do case $opt in h) help_mode=true @@ -163,25 +141,15 @@ while getopts ":hqvt:m:r:u:f:" opt; do ;; t) connect_timeout="$OPTARG" - local_opts="$local_opts -t $OPTARG" + curl_opts="$curl_opts -t $OPTARG" ;; m) max_time="$OPTARG" - local_opts="$local_opts -m $OPTARG" + curl_opts="$curl_opts -m $OPTARG" ;; r) max_redirs="$OPTARG" - local_opts="$local_opts -r $OPTARG" - ;; - u) - md_query_mode=true - md_file_mode=false - mdq_base_url="$OPTARG" - ;; - f) - md_query_mode=false - md_file_mode=true - md_path="$OPTARG" + curl_opts="$curl_opts -r $OPTARG" ;; \?) echo "ERROR: $script_name: Unrecognized option: -$OPTARG" >&2 @@ -199,41 +167,6 @@ if $help_mode; then exit 0 fi -# report bootstrap operations -if $verbose_mode; then - printf "$script_name using source lib directory: %s\n" "$LIB_DIR" - for lib_filename in $LIB_FILENAMES; do - lib_file="$LIB_DIR/$lib_filename" - printf "$script_name sourced lib file: %s\n" "$lib_file" - done -fi - -# determine the metadata source -if $md_query_mode; then - if [ -z "$mdq_base_url" ]; then - echo "ERROR: $script_name: option -u requires an argument" >&2 - exit 2 - fi - $verbose_mode && printf "$script_name using base URL: %s\n" "$mdq_base_url" -elif $md_file_mode; then - # temporary - echo "ERROR: $script_name: option -f not yet implemented" >&2 - exit 2 - - if [ -z "$md_path" ]; then - echo "ERROR: $script_name: option -f requires an argument" >&2 - exit 2 - fi - if [ ! -f "$md_path" ]; then - echo "ERROR: $script_name: file does not exist: $md_path" >&2 - exit 2 - fi - $verbose_mode && printf "$script_name using metadata file: %s\n" "$md_path" -else - echo "ERROR: $script_name: one of options -u or -f required" >&2 - exit 2 -fi - # redirect stdout and stderr to the bit bucket $quiet_mode && exec 1>/dev/null $quiet_mode && exec 2>/dev/null @@ -247,7 +180,7 @@ fi # set default connect timeout if necessary if [ -z "$connect_timeout" ]; then connect_timeout=$connect_timeout_default - local_opts="$local_opts -t $connect_timeout" + curl_opts="$curl_opts -t $connect_timeout" else if [ "$connect_timeout" -le 0 ] ; then echo "ERROR: $script_name: connect timeout ($connect_timeout) must be a positive integer" >&2 @@ -258,7 +191,7 @@ fi # compute max time if necessary if [ -z "$max_time" ]; then max_time=$(( connect_timeout + 2 )) - local_opts="$local_opts -m $max_time" + curl_opts="$curl_opts -m $max_time" else if [ "$max_time" -le "$connect_timeout" ]; then echo "ERROR: $script_name: max time ($max_time) must be greater than the connect timeout ($connect_timeout)" >&2 @@ -269,7 +202,7 @@ fi # check maximum number of redirects if [ -z "$max_redirs" ]; then max_redirs=$max_redirs_default - local_opts="$local_opts -r $max_redirs" + curl_opts="$curl_opts -r $max_redirs" fi if $verbose_mode; then @@ -278,6 +211,10 @@ if $verbose_mode; then printf "$script_name using max redirects: %d\n" $max_redirs fi +# TODO: implement new option +config_file="$config_file_default" +$verbose_mode && echo "$script_name using config file $config_file" + # determine the entityID shift $(( OPTIND - 1 )) if [ $# -ne 1 ]; then @@ -295,15 +232,23 @@ $verbose_mode && echo "$script_name using entityID $entityID" # Initialization ##################################################################### +if $verbose_mode; then + printf "$script_name using source lib directory: %s\n" "$LIB_DIR" + for lib_filename in $LIB_FILENAMES; do + lib_file="$LIB_DIR/$lib_filename" + printf "$script_name sourcing lib file: %s\n" "$lib_file" + done +fi + # determine temporary directory if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then - # use user-provided temporary directory (remove trailing slash) + $verbose_mode && printf "$script_name using existing temporary dir: %s\n" "$TMP_DIR" + # use existing temporary directory (remove trailing slash) tmp_dir="${TMP_DIR%%/}/probe_saml_idp_$$" - $verbose_mode && printf "$script_name using temp dir: %s\n" "$tmp_dir" elif [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + $verbose_mode && printf "$script_name using system temporary dir: %s\n" "$TMPDIR" # use system temporary directory (remove trailing slash) tmp_dir="${TMPDIR%%/}/probe_saml_idp_$$" - $verbose_mode && printf "$script_name using temp dir: %s\n" "$tmp_dir" else # create temporary directory new_dir="$( make_temp_file -d )" @@ -311,40 +256,74 @@ else printf "ERROR: $script_name unable to create temporary dir\n" >&2 exit 2 fi - # use temporary directory (remove trailing slash) + $verbose_mode && printf "$script_name using new temporary dir: %s\n" "$new_dir" + # use new temporary directory (remove trailing slash) tmp_dir="${new_dir%%/}/probe_saml_idp_$$" - $verbose_mode && printf "$script_name creating temp dir: %s\n" "$tmp_dir" +fi + +# every run of this script gets its own subdir +if [ -d "$tmp_dir" ]; then + echo "ERROR: $script_name: directory already exists: $tmp_dir" >&2 + exit 2 fi # create temporary directory if necessary -if [ ! -d "$tmp_dir" ]; then - /bin/mkdir "$tmp_dir" - exit_status=$? - if [ $exit_status -ne 0 ]; then - echo "ERROR: $script_name failed to create tmp dir ($exit_status) $tmp_dir" >&2 - exit 2 - fi +$verbose_mode && printf "$script_name creating temporary subdir: %s\n" "$tmp_dir" +/bin/mkdir "$tmp_dir" +status_code=$? +if [ $status_code -ne 0 ]; then + echo "ERROR: $script_name failed to create tmp dir ($status_code) $tmp_dir" >&2 + exit 2 fi -# create temporary subdirectories if necessary -http_bindings="Redirect POST POST-SimpleSign" -for http_binding in $http_bindings; do - if [ ! -d "$tmp_dir/$http_binding" ]; then - /bin/mkdir "$tmp_dir/$http_binding" - exit_status=$? - if [ $exit_status -ne 0 ]; then - echo "ERROR: $script_name failed to create tmp dir ($exit_status) $tmp_dir/$http_binding" >&2 - exit 2 - fi +# load config file +$verbose_mode && echo "$script_name loading config file $config_file" +load_config $local_opts "$config_file" +status_code=$? +if [ $status_code -ne 0 ]; then + echo "ERROR: $script_name failed to load $config_file" >&2 + exit 2 +fi + +# validate config parameters +if [ -z "$MDQ_BASE_URL" ]; then + echo "ERROR: $script_name requires config param MDQ_BASE_URL" >&2 + exit 2 +fi +if [ -z "$SAML2_SP_ENTITY_ID" ]; then + echo "ERROR: $script_name requires config param SAML2_SP_ENTITY_ID" >&2 + exit 2 +fi +if [ -z "$SAML2_SP_ACS_URL" ]; then + echo "ERROR: $script_name requires config param SAML2_SP_ACS_URL" >&2 + exit 2 +fi +if [ -z "$SAML2_SP_ACS_BINDING" ]; then + echo "ERROR: $script_name requires config param SAML2_SP_ACS_BINDING" >&2 + exit 2 +fi + +if $probe_saml1; then + if [ -z "$SAML1_SP_ENTITY_ID" ]; then + echo "ERROR: $script_name requires config param SAML1_SP_ENTITY_ID" >&2 + exit 2 fi -done + if [ -z "$SAML1_SP_ACS_URL" ]; then + echo "ERROR: $script_name requires config param SAML1_SP_ACS_URL" >&2 + exit 2 + fi + if [ -z "$SAML1_SP_ACS_BINDING" ]; then + echo "ERROR: $script_name requires config param SAML1_SP_ACS_BINDING" >&2 + exit 2 + fi +fi ##################################################################### # Main processing ##################################################################### # get entity metadata -entityDescriptor=$( getEntityFromServer -d "$tmp_dir" -u "$mdq_base_url" $entityID ) +entityDescriptor=$( getEntityFromServer -d "$tmp_dir" -u "$MDQ_BASE_URL" $entityID ) exit_status=$? if [ "$exit_status" -ne 0 ]; then echo "ERROR: $script_name: unable to obtain metadata for entityID: $entityID" >&2 @@ -363,6 +342,7 @@ endpoints=$( echo "$entityDescriptor" \ ) # iterate over the SAML2 browser-facing SSO endpoints +http_bindings="Redirect POST POST-SimpleSign" for http_binding in $http_bindings; do # compute the endpoint @@ -388,10 +368,21 @@ for http_binding in $http_bindings; do exit 3 fi + # create temporary subdirectory if necessary + tmp_subdir="$tmp_dir/$http_binding" + if [ ! -d "$tmp_subdir" ]; then + /bin/mkdir "$tmp_subdir" + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $script_name failed to create tmp dir ($exit_status) $tmp_subdir" >&2 + exit 2 + fi + fi + # probe the endpoint output=$( probe_saml2_idp_endpoint -v \ -t $connect_timeout -m $max_time -r $max_redirs \ - -T "$tmp_dir/$http_binding" \ + -T "$tmp_subdir" \ $location $binding "$saml_message" ) exit_status=$? diff --git a/install.sh b/install.sh index f6ef27c..4b1d6a4 100755 --- a/install.sh +++ b/install.sh @@ -109,6 +109,7 @@ done <&2 + return 2 + fi + config_file="$1" + + /bin/cat <<- DEFAULT_CONFIG_FILE > $config_file + #!/bin/bash + + # MDQ base URL + MDQ_BASE_URL=http://mdq-beta.incommon.org/global + + # default SAML2 endpoint for testing + SAML2_SP_ENTITY_ID=https://service1.internet2.edu/shibboleth + SAML2_SP_ACS_URL=https://service1.internet2.edu/Shibboleth.sso/SAML2/POST + SAML2_SP_ACS_BINDING=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST + + # default SAML1 endpoint for testing + SAML1_SP_ENTITY_ID=https://service1.internet2.edu/shibboleth + SAML1_SP_ACS_URL=https://service1.internet2.edu/Shibboleth.sso/SAML/POST + SAML1_SP_ACS_BINDING=urn:oasis:names:tc:SAML:1.0:profiles:browser-post +DEFAULT_CONFIG_FILE + + return 0 +} + +####################################################################### +# Load a config file. +# +# Usage: load_config [-v] CONFIG_PATH +# +# The CONFIG_PATH argument is the absolute path to the config file. +# The -v option produces verbose output, which is most useful for +# testing and debugging. +# +# If a required config parameter is missing, this function halts +# and returns a non-zero return code. +####################################################################### + +load_config () { + + local config_file + local status_code + local param_name + local param_names + local param_value + + # process command-line options (if any) + local OPTARG + local OPTIND + local opt + local verbose_mode=false + + while getopts ":v" opt; do + case $opt in + v) + verbose_mode=true + ;; + \?) + echo "ERROR: $FUNCNAME: Unrecognized option: -$OPTARG" >&2 + return 2 + ;; + :) + echo "ERROR: $FUNCNAME: Option -$OPTARG requires an argument" >&2 + return 2 + ;; + esac + done + + # make sure there's at least one command-line argument + shift $(( OPTIND-1 )) + if [ $# -eq 0 ]; then + echo "ERROR: $FUNCNAME: no config file to load" >&2 + return 2 + fi + config_file="$1" + + # create config file if necessary + if [ ! -f "$config_file" ]; then + create_config $config_file + status_code=$? + if [ $status_code -ne 0 ]; then + echo "ERROR: $FUNCNAME: failed to create config file $config_file" >&2 + return $status_code + fi + $verbose_mode && echo "$FUNCNAME creating default config file $config_file" + fi + + # load config file + source "$config_file" + status_code=$? + if [ $status_code -ne 0 ]; then + echo "ERROR: $FUNCNAME failed to source config file $config_file" >&2 + return $status_code + fi + $verbose_mode && echo "$FUNCNAME sourcing config file $config_file" + + # check required config parameters + param_names="MDQ_BASE_URL SAML2_SP_ENTITY_ID SAML2_SP_ACS_URL SAML2_SP_ACS_BINDING SAML1_SP_ENTITY_ID SAML1_SP_ACS_URL SAML1_SP_ACS_BINDING" + for param_name in $param_names; do + eval "param_value=\${$param_name}" + if [ ! "$param_value" ]; then + echo "ERROR: $FUNCNAME failed to find $param_name config parameter" >&2 + return 3 + fi + $verbose_mode && printf "$FUNCNAME using $param_name=%s\n" "$param_value" + done + + return 0 +}