$NetBSD: patch-ae,v 1.1 2011/04/04 08:46:42 manu Exp $

Replace buildtin: diescovery URL by the discoProbe endpoint (pulled from
upstream)

Index: auth_mellon_handler.c
===================================================================
--- auth_mellon_handler.c	(revision 112)
+++ auth_mellon_handler.c	(working copy)
@@ -226,34 +226,7 @@
     return provider_id;
 }
 
-/* This returns built-in IdP discovery timeout
- *
- * Parameters:
- *  request_rec *r       The request we received.
- *
- * Returns:
- *  the timeout, -1 if not enabled.
- */
-static long am_builtin_discovery_timeout(request_rec *r)
-{
-    am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
-    const char *builtin = "builtin:get-metadata";
-    const char *timeout = "?timeout=";
-    const char *cp;
-    const long default_timeout = 1L;
 
-    if ((cfg->discovery_url == NULL) ||
-        (strncmp(cfg->discovery_url, builtin, strlen(builtin)) != 0))
-        return -1;
-    
-    cp = cfg->discovery_url + strlen(builtin);
-    if (strncmp(cp, timeout, strlen(timeout)) != 0)
-        return default_timeout;
-
-    cp += strlen(timeout);
-    return atoi(cp);
-}
-
 /* This function selects an IdP and returns its provider_id
  *
  * Parameters:
@@ -267,8 +240,6 @@
     am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
     const char *idp_provider_id;
     const char *idp_metadata_file;
-    apr_hash_index_t *index;
-    long timeout;
     
     /*
      * If we have a single IdP, return that one.
@@ -308,47 +279,6 @@
         return idp_provider_id;
     }
 
-    /*
-     * If built-in IdP discovery is not configured, return default.
-     */
-    timeout = am_builtin_discovery_timeout(r);
-    if (timeout == -1)
-        return am_first_idp(r);
-
-    /*
-     * Otherwise, proceed with built-in IdP discovery:
-     * send probes for all configures IdP to check availability.
-     * The first to answer is chosen. On error, use default.
-     */
-    for (index = apr_hash_first(r->pool, cfg->idp_metadata_files);
-         index;
-         index = apr_hash_next(index)) {
-        void *buffer;
-        apr_size_t len;
-        apr_ssize_t slen;
-        long status;
- 
-        apr_hash_this(index, 
-                      (const void **)&idp_provider_id, 
-                      &slen,
-                      (void *)&idp_metadata_file);
-
-        status = 0;
-        if (am_httpclient_get(r, idp_provider_id, &buffer, &len, 
-                              timeout, &status) != OK)
-            continue;
-
-        if (status != HTTP_OK) {
-	    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
-			  "Cannot probe %s: IdP returned HTTP %ld",
-			  idp_provider_id, status);
-            continue;
-        }
-
-        /* We got some succes */
-        return idp_provider_id;
-    }
-
     /* 
      * No IdP answered, use default 
      * Perhaps we should redirect to an error page instead.
@@ -2506,7 +2436,6 @@
 
     /* Check if IdP discovery is in use and no IdP was selected yet */
     if ((cfg->discovery_url != NULL) &&
-        (am_builtin_discovery_timeout(r) == -1) && /* no built-in discovery */
         (am_extract_query_parameter(r->pool, r->args, "IdP") == NULL)) {
         char *discovery_url;
         char *return_url;
@@ -2536,8 +2465,7 @@
     /* If IdP discovery is in use and we have an IdP selected,
      * set the relay_state
      */
-    if ((cfg->discovery_url != NULL) &&
-        (am_builtin_discovery_timeout(r) == -1)) { /* no built-in discovery */
+    if (cfg->discovery_url != NULL) {
         char *return_url;
 
         return_url = am_extract_query_parameter(r->pool, r->args, "ReturnTo");
@@ -2615,7 +2543,151 @@
     return am_send_authn_request(r, idp, return_to, is_passive);
 }
 
+/* This function handles requests to the probe discovery handler
+ *
+ * Parameters:
+ *  request_rec *r       The request.
+ *
+ * Returns:
+ *  OK on success, or an error if any of the steps fail.
+ */
+static int am_handle_probe_discovery(request_rec *r) {
+    am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
+    const char *idp = NULL;
+    int timeout;
+    apr_hash_index_t *index;
+    char *return_to;
+    char *idp_param;
+    char *redirect_url;
+    int ret;
 
+    /*
+     * If built-in IdP discovery is not configured, return error.
+     * For now we only have the get-metadata metadata method, so this
+     * information is not saved in configuration nor it is checked here.
+     */
+    timeout = cfg->probe_discovery_timeout;
+    if (timeout == -1) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
+                      "probe discovery handler invoked but not "
+                      "configured. Plase set MellonProbeDiscoveryTimeout.");
+        return HTTP_INTERNAL_SERVER_ERROR;
+    }
+
+    /*
+     * Check for mandatory arguments early to avoid sending 
+     * probles for nothing.
+     */
+    return_to = am_extract_query_parameter(r->pool, r->args, "return");
+    if(return_to == NULL) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
+                      "Missing required return parameter.");
+        return HTTP_BAD_REQUEST;
+    }
+
+    ret = am_urldecode(return_to);
+    if (ret != OK) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, ret, r,
+                      "Could not urldecode return value.");
+        return HTTP_BAD_REQUEST;
+    }
+
+    idp_param = am_extract_query_parameter(r->pool, r->args, "returnIDParam");
+    if(idp_param == NULL) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
+                      "Missing required returnIDParam parameter.");
+        return HTTP_BAD_REQUEST;
+    }
+
+    ret = am_urldecode(idp_param);
+    if (ret != OK) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, ret, r,
+                      "Could not urldecode returnIDParam value.");
+        return HTTP_BAD_REQUEST;
+    }
+
+    /*
+     * Proceed with built-in IdP discovery. 
+     *
+     * Send probes for all configured IdP to check availability.
+     * The first to answer is chosen, but the list of usable
+     * IdP can be restricted in configuration.
+     */
+    for (index = apr_hash_first(r->pool, cfg->idp_metadata_files);
+         index;
+         index = apr_hash_next(index)) {
+        void *dontcare;
+        const char *ping_url;
+        apr_size_t len;
+        apr_ssize_t slen;
+        long status;
+ 
+        apr_hash_this(index, (const void **)&idp, 
+                      &slen, (void *)&dontcare);
+        ping_url = idp;
+
+        /*
+         * If a list of IdP was given for probe discovery, 
+         * skip any IdP that does not match.
+         */
+        if (apr_hash_count(cfg->probe_discovery_idp) != 0) {
+            char *value = apr_hash_get(cfg->probe_discovery_idp,
+                                       idp, APR_HASH_KEY_STRING);
+
+            if (value == NULL) {
+                /* idp not in list, try the next one */
+                idp = NULL;
+                continue;
+            } else {
+                /* idp in list, use the value as the ping url */
+                ping_url = value;
+            }
+        }
+
+        status = 0;
+        if (am_httpclient_get(r, ping_url, &dontcare, &len, 
+                              timeout, &status) != OK)
+            continue;
+
+        if (status != HTTP_OK) {
+	    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
+			  "Cannot probe %s: \"%s\" returned HTTP %ld",
+			  idp, ping_url, status);
+            continue;
+        }
+
+        /* We got some succes */
+        ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r,
+                      "probeDiscovery using %s", idp);
+        break;
+    }
+
+    /* 
+     * On failure, try default
+     */
+    if (idp == NULL) {
+        idp = am_first_idp(r);
+        if (idp == NULL) {
+            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+                          "probeDiscovery found no usable IdP.");
+            return HTTP_INTERNAL_SERVER_ERROR;
+        } else {
+            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "probeDiscovery "
+                          "failed, trying default IdP %s", idp); 
+        }
+    }
+
+    redirect_url = apr_psprintf(r->pool, "%s%s%s=%s", return_to, 
+                                strchr(return_to, '?') ? "&" : "?",
+                                am_urlencode(r->pool, idp_param), 
+                                am_urlencode(r->pool, idp));
+
+    apr_table_setn(r->headers_out, "Location", redirect_url);
+
+    return HTTP_SEE_OTHER;
+}
+
+
 /* This function takes a request for an endpoint and passes it on to the
  * correct handler function.
  *
@@ -2656,6 +2728,8 @@
         return am_handle_logout(r);
     } else if(!strcmp(endpoint, "login")) {
         return am_handle_login(r);
+    } else if(!strcmp(endpoint, "probeDisco")) {
+        return am_handle_probe_discovery(r);
     } else {
 	ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
 		      "Endpoint \"%s\" not handled by mod_auth_mellon.",
Index: auth_mellon.h
===================================================================
--- auth_mellon.h	(revision 112)
+++ auth_mellon.h	(working copy)
@@ -174,6 +174,8 @@
 
     /* IdP discovery service */
     const char *discovery_url;
+    int probe_discovery_timeout;
+    apr_hash_t *probe_discovery_idp;
 
     /* Mutex to prevent us from creating several lasso server objects. */
     apr_thread_mutex_t *server_mutex;
Index: README
===================================================================
--- README	(revision 112)
+++ README	(working copy)
@@ -321,8 +321,17 @@
         # The IdP discovery must redirect the user to the return URL, 
         # with retueniDParam set to the selected IdP providerID.
         # 
-        # Alternatively, a simple built-in IdP discovery can be used,
-        # by specifying "builtin:get-metadata?timeout=1"
+        # The builtin:get-metadata discovery URL is not supported anymore
+        # starting with 0.3.1. See MellonProbeDiscoveryTimeout for
+        # a replacement.
+        #
+        # Default: None set.
+        MellonDiscoveryURL "http://www.example.net/idp-discovery"
+
+        # MellonProbeDiscoveryTimeout sets the timeout of the
+        # IdP probe discovery service, which is available on the
+        # probeDisco endoint.
+        #
         # This will cause the SP to send HTTP GET requests on the 
         # configured IdP PorviderID URL. Theses URL should be used to
         # publish metadata, though this is not mandatory. If the IdP
@@ -330,9 +339,17 @@
         # If the PorviderID URL requires SSL, MellonIdPCAFile is used
         # as a trusted CA bundle.
         #
-        # Default: None set.
-        MellonDiscoveryURL "http://www.example.net/idp-discovery"
+        # Default: unset, which means the feature is disabled
+        # MellonProbeDiscoveryTimeout 1
 
+        # MellonProbeDiscoveryIdP can be used to restrict the 
+        # list of IdP queried by the IdP probe discovery service.
+        #
+        # Default unset, which means that all configured IdP are 
+        # queried.
+        # MellonProbeDiscoveryIdP http://idp1.example.com/saml/metadata
+        # MellonProbeDiscoveryIdP http://idp2.example.net/saml/metadata
+
         # This option will make the SAML authentication assertion 
         # available in the MELLON_SAML_RESPONSE environement 
         # variable. This assertion holds a verifiable signature
@@ -476,7 +493,39 @@
 This will return the user to "https://www.example.org/logged_out.html"
 after the logout operation has completed.
 
+===========================================================================
+ Probe IdP discovery 
+===========================================================================
 
+mod_auth_mellon has an IdP probe discovery service that sends HTTP GET
+to IdP and picks the first that answers. This can be used as a poor
+man's failover setup that redirects to your organisation internal IdP. 
+Here is a sample configuration:
+
+  MellonEndpointPath "/saml"
+  (...)
+  MellonDiscoveryUrl "/saml/probeDisco"
+  MellonProbeDiscoveryTimeout 1
+
+The SP will sends HTTP GET to each configured IdP providerId URL until
+it gets an HTTP 200 response within the 1 second timeout. It will then 
+proceed with that IdP.
+
+If you are in a federation, then your IdP login page will need to provide 
+an IdP selection feature aimed at users from other institutions (after
+such a choice, the user would be redirected to the SP's /saml/login 
+endpoint, with ReturnTo and IdP set appropriately). In such a setup, 
+you will want to configure external IdP in mod_auth_mellon, but not 
+use them for IdP probe discovery. The MellonProbeDiscoveryIdP 
+directive can be used to limit the usable IdP for probe discovery:
+
+  MellonEndpointPath "/saml"
+  (...)
+  MellonDiscoveryUrl "/saml/probeDisco"
+  MellonProbeDiscoveryTimeout 1
+  MellonProbeDiscoveryIdP "https://idp1.example.net/saml/metadata"
+  MellonProbeDiscoveryIdP "https://idp2.example.net/saml/metadata"
+
 ===========================================================================
  Contributors
 ===========================================================================
Index: auth_mellon_config.c
===================================================================
--- auth_mellon_config.c	(revision 112)
+++ auth_mellon_config.c	(working copy)
@@ -76,6 +76,47 @@
  */
 static const int post_count = 100;
 
+/* This function handles configuration directives which set a 
+ * multivalued string slot in the module configuration (the destination
+ * strucure is a hash).
+ *
+ * Parameters:
+ *  cmd_parms *cmd       The command structure for this configuration
+ *                       directive.
+ *  void *struct_ptr     Pointer to the current directory configuration.
+ *                       NULL if we are not in a directory configuration.
+ *                       This value isn't used by this function.
+ *  const char *key      The string argument following this configuration
+ *                       directive in the configuraion file.
+ *  const char *value    Optional value to be stored in the hash.
+ *
+ * Returns:
+ *  NULL on success or an error string on failure.
+ */
+static const char *am_set_hash_string_slot(cmd_parms *cmd,
+                                          void *struct_ptr,
+                                          const char *key,
+                                          const char *value)
+{
+    server_rec *s = cmd->server;
+    apr_pool_t *pconf = s->process->pconf;
+    am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr;
+    int offset;
+    apr_hash_t **hash;
+
+    /*
+     * If no value is given, we just store the key in the hash.
+     */
+    if (value == NULL || *value == '\0')
+        value = key;
+
+    offset = (int)(long)cmd->info;
+    hash = (apr_hash_t **)((char *)cfg + offset);
+    apr_hash_set(*hash, apr_pstrdup(pconf, key), APR_HASH_KEY_STRING, value);
+
+    return NULL;
+}
+
 /* This function handles configuration directives which set a file
  * slot in the module configuration. If lasso is recent enough, it
  * attempts to read the file immediatly.
@@ -133,10 +174,10 @@
  *  NULL on success or an error string on failure.
  *  
  */
-static const char *am_get_proovider_id(apr_pool_t *p,
-                                       server_rec *s,
-                                       const char *file,
-                                       const char **provider)
+static const char *am_get_provider_id(apr_pool_t *p,
+                                      server_rec *s,
+                                      const char *file,
+                                      const char **provider)
 {
     char *data;
     apr_xml_parser *xp;
@@ -195,7 +236,7 @@
  * Returns:
  *  NULL on success or an error string on failure.
  */
-static const char *ap_set_idp_string_slot(cmd_parms *cmd,
+static const char *am_set_idp_string_slot(cmd_parms *cmd,
                                           void *struct_ptr,
                                           const char *arg)
 {
@@ -205,8 +246,8 @@
     const char *error;
     const char *provider_id;
 
-    if ((error = am_get_proovider_id(cmd->pool, s, 
-                                     arg, &provider_id)) != NULL)
+    if ((error = am_get_provider_id(cmd->pool, s, 
+                                    arg, &provider_id)) != NULL)
         return apr_psprintf(cmd->pool, "%s - %s", cmd->cmd->name, error);
 
     apr_hash_set(cfg->idp_metadata_files,
@@ -649,8 +690,8 @@
         ),
     AP_INIT_TAKE1(
         "MellonIdPMetadataFile",
-        ap_set_idp_string_slot,
-	NULL,
+        am_set_idp_string_slot,
+        NULL,
         OR_AUTHCFG,
         "Full path to xml metadata file for the IdP."
         ),
@@ -705,6 +746,21 @@
         "The URL of IdP discovery service. Default is unset."
         ),
     AP_INIT_TAKE1(
+        "MellonProbeDiscoveryTimeout",
+        ap_set_int_slot,
+        (void *)APR_OFFSETOF(am_dir_cfg_rec, probe_discovery_timeout),
+        OR_AUTHCFG,
+        "The timeout of IdP probe discovery service. "
+        "Default is 1s"
+        ),
+    AP_INIT_TAKE12(
+        "MellonProbeDiscoveryIdP",
+        am_set_hash_string_slot,
+        (void *)APR_OFFSETOF(am_dir_cfg_rec, probe_discovery_idp),
+        OR_AUTHCFG,
+        "An IdP that can be used for IdP probe discovery."
+        ),
+    AP_INIT_TAKE1(
         "MellonEndpointPath",
         am_set_endpoint_path,
         NULL,
@@ -760,6 +816,8 @@
     dir->idp_ca_file = NULL;
     dir->login_path = default_login_path;
     dir->discovery_url = NULL;
+    dir->probe_discovery_timeout = -1; /* -1 means no probe discovery */
+    dir->probe_discovery_idp = apr_hash_make(p);
 
     dir->sp_org_name = apr_hash_make(p);
     dir->sp_org_display_name = apr_hash_make(p);
@@ -903,6 +961,16 @@
                               add_cfg->discovery_url :
                               base_cfg->discovery_url);
 
+    new_cfg->probe_discovery_timeout = 
+                           (add_cfg->probe_discovery_timeout != -1 ?
+                            add_cfg->probe_discovery_timeout :
+                            base_cfg->probe_discovery_timeout);
+
+    new_cfg->probe_discovery_idp = apr_hash_copy(p,
+                      (apr_hash_count(add_cfg->probe_discovery_idp) > 0) ?
+                       add_cfg->probe_discovery_idp : 
+                       base_cfg->probe_discovery_idp);
+
     apr_thread_mutex_create(&new_cfg->server_mutex,
                             APR_THREAD_MUTEX_DEFAULT, p);
     new_cfg->server = NULL;
