Commits

Jimmy Li [Atlassian]  committed 059f39b

ISLP-174 - Crowd SSO Interceptor

  • Participants
  • Parent commits 23c93ce

Comments (0)

Files changed (14)

 
 dist-hook:
 	find $(distdir)
-	rm -r $(distdir)/aclocal.m4 $(distdir)/config.guess $(distdir)/config.h.in $(distdir)/configure $(distdir)/config.sub $(distdir)/depcomp $(distdir)/install-sh $(distdir)/ltmain.sh $(distdir)/Makefile.in $(distdir)/missing $(distdir)/m4/* $(distdir)/src/Makefile.in $(distdir)/src/svn/Makefile.in   
+	rm -r $(distdir)/aclocal.m4 $(distdir)/config.guess $(distdir)/config.h.in $(distdir)/configure $(distdir)/config.sub $(distdir)/depcomp $(distdir)/install-sh $(distdir)/ltmain.sh $(distdir)/Makefile.in $(distdir)/missing $(distdir)/m4/* $(distdir)/src/Makefile.in $(distdir)/src/svn/Makefile.in $(distdir)/src/crowdsso/Makefile.in  

File configure.ac

     Makefile
     src/Makefile
     src/svn/Makefile
+    src/crowdsso/Makefile
 ])
 AC_CONFIG_MACRO_DIR([m4])
 

File src/Makefile.am

 AUTOMAKE_OPTIONS = -Wno-override
-SUBDIRS = svn
+SUBDIRS = svn crowdsso
 lib_LTLIBRARIES = mod_authnz_crowd.la
 mod_authnz_crowd_la_SOURCES = mod_authnz_crowd.c mod_authnz_crowd.h crowd_client.c crowd_client.h cache.c cache.h util.c util.h crowd_memcache.c crowd_memcache.h
 mod_authnz_crowd_la_LDFLAGS = -module -lcurl `xml2-config --libs` 
-mod_authnz_crowd_la_LIBADD = ../apr_memcache/lib/libapr_memcache.la
 AM_CFLAGS = `apr-1-config --cflags`
-AM_CPPFLAGS = -I@APACHE_INCLUDE_DIR@ `apr-1-config --cppflags --includes` `xml2-config --cflags` -I../apr_memcache/include/apr_memcache-0
+AM_CPPFLAGS = -I@APACHE_INCLUDE_DIR@ `apr-1-config --cppflags --includes` `xml2-config --cflags`
 CFLAGS=-g -O1   # -O2 causes mysterious crashes
 TESTS = test.py
 TESTS_ENVIRONMENT = APACHE_BIN_DIR=@APACHE_BIN_DIR@

File src/crowd_client.c

 cache_t *groups_cache;
 cache_t *cookie_config_cache;
 cache_t *session_cache;
+cache_t *user_details_cache;
 
 /*===========================
  * Initialisation & clean up
 xmlChar *cookie_config_xml_name = NULL;
 xmlChar *secure_xml_name = NULL;
 xmlChar *domain_xml_name = NULL;
+xmlChar *first_name_xml_name = NULL;
+xmlChar *last_name_xml_name = NULL;
+xmlChar *display_name_xml_name = NULL;
+xmlChar *email_xml_name = NULL;
+xmlChar *link_xml_name = NULL;
 
 /**
  * Must be called before the first use of the Crowd Client.
     cookie_config_xml_name = xml_string("cookie-config");
     secure_xml_name = xml_string("secure");
     domain_xml_name = xml_string("domain");
+    first_name_xml_name = xml_string("first-name");
+	last_name_xml_name = xml_string("last-name");
+	display_name_xml_name = xml_string("display-name");
+	email_xml_name = xml_string("email");
+	link_xml_name = xml_string("link");
     if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
         fprintf(stderr, PACKAGE_STRING " failed to initialise libcurl.");
         exit(1);
     xmlFree(cookie_config_xml_name);
     xmlFree(secure_xml_name);
     xmlFree(domain_xml_name);
+	xmlFree(first_name_xml_name);
+    xmlFree(last_name_xml_name);
+    xmlFree(display_name_xml_name);
+    xmlFree(email_xml_name);
+    xmlFree(link_xml_name);
 }
 
 /**
     free(cookie_config);
 }
 
+static void free_user_details(void *value) {
+    user_details_data_t *user_details = value;
+    free(user_details->alias);
+    free(user_details->first_name);
+    free(user_details->last_name);
+    free(user_details->display_name);
+    free(user_details->email);
+    free(user_details);
+}
+
+static void copy_user_details(void *data, apr_pool_t *p) {
+    user_details_data_t *original = data;
+    user_details_data_t *copy = log_palloc(p, apr_palloc(p, sizeof(user_details_data_t)));
+    if (copy == NULL) {
+        return NULL;
+    }
+    
+    copy->alias = log_ralloc(p, apr_pstrdup(p, original->alias));
+    if (copy->alias == NULL) {
+        return NULL;
+    }
+
+    copy->first_name = log_ralloc(p, apr_pstrdup(p, original->first_name));
+    copy->last_name = log_ralloc(p, apr_pstrdup(p, original->last_name));
+    copy->display_name = log_ralloc(p, apr_pstrdup(p, original->display_name));
+    copy->email = log_ralloc(p, apr_pstrdup(p, original->email));
+    return copy;
+}
+
 bool crowd_cache_create(apr_pool_t *pool, apr_time_t max_age, unsigned int max_entries) {
     auth_cache = cache_create("auth", pool, max_age, max_entries, copy_string, free);
     if (auth_cache == NULL) {
     if (session_cache == NULL) {
         return false;
     }
+    user_details_cache = cache_create("user details", pool, max_age, max_entries, copy_user_details, free_user_details);
+    if (user_details_cache == NULL) {
+        return false;
+    }
     return true;
 }
 
     return log_ralloc(r, apr_psprintf(r->pool, "a_\037%s\037%s", config->crowd_app_name, config->crowd_url));
 }
 
+static char *make_user_details_cache_key(const request_rec *r, const crowd_config *config) {
+	return log_ralloc(r, apr_psprintf(r->pool, "ud_\037%s\037%s\037%s", r->user, config->crowd_app_name, config->crowd_url));
+}
+
 static char *make_session_cache_key(const char *token, const char *forwarded_for, const request_rec *r, const crowd_config *config) {
     return log_ralloc(r, apr_psprintf(r->pool, "s_\037%s\037%s\037%s\037%s\037%s", token,
         forwarded_for == NULL ? "" : forwarded_for, r->connection->remote_ip, config->crowd_app_name,
 
         default:
             return CROWD_AUTHENTICATE_EXCEPTION;
+            
+    }
+}
+
+/*============================
+ * Crowd user details retrieval
+ *============================*/
+static const char *make_user_details_url(const request_rec *r, const crowd_config *config, CURL *curl_easy,
+                                   const void *extra) {
+    return make_url(r, config, curl_easy, r->user, "%srest/usermanagement/1/user?username=%s");
+}
+
+static bool handle_user_details_email_text(write_data_t *write_data, const xmlChar *text) {
+    user_details_data_t *data = write_data->extra;
+    data->email = log_ralloc(write_data->r, apr_pstrdup(write_data->r->pool, (char *)text));
+	return false;
+}
+
+static bool handle_user_details_email_element(write_data_t *write_data, const xmlChar *text) {
+    if (expect_xml_element(write_data, email_xml_name, text)) {
+        write_data->xml_node_handlers[XML_READER_TYPE_TEXT] = handle_user_details_email_text;
+        write_data->xml_node_handlers[XML_READER_TYPE_END_ELEMENT] = handle_end_of_data;
+        return false;
+    } else {
+        return true;
+    }
+}
+
+static bool handle_user_details_display_name_element_end(write_data_t *write_data, const xmlChar *text) {
+    write_data->xml_node_handlers[XML_READER_TYPE_ELEMENT] = handle_user_details_email_element;
+    return false;
+}
+
+static bool handle_user_details_display_name_element_text(write_data_t *write_data, const xmlChar *text) {
+    user_details_data_t *data = write_data->extra;
+    data->display_name = log_ralloc(write_data->r, apr_pstrdup(write_data->r->pool, (char *)text));
+	return false;
+}
+
+static bool handle_user_details_display_name_element(write_data_t *write_data, const xmlChar *text) {
+    if (expect_xml_element(write_data, display_name_xml_name, text)) {
+        write_data->xml_node_handlers[XML_READER_TYPE_TEXT] = handle_user_details_display_name_element_text;
+        write_data->xml_node_handlers[XML_READER_TYPE_END_ELEMENT] = handle_user_details_display_name_element_end;
+        return false;
+    } else {
+        return true;
+    }
+}
+
+static bool handle_user_details_last_name_element_end(write_data_t *write_data, const xmlChar *text) {
+    write_data->xml_node_handlers[XML_READER_TYPE_ELEMENT] = handle_user_details_display_name_element;
+    return false;
+}
+
+static bool handle_user_details_last_name_element_text(write_data_t *write_data, const xmlChar *text) {
+    user_details_data_t *data = write_data->extra;
+    data->last_name = log_ralloc(write_data->r, apr_pstrdup(write_data->r->pool, (char *)text));
+	return false;
+}
+
+static bool handle_user_details_last_name_element(write_data_t *write_data, const xmlChar *text) {
+    if (expect_xml_element(write_data, last_name_xml_name, text)) {
+        write_data->xml_node_handlers[XML_READER_TYPE_TEXT] = handle_user_details_last_name_element_text;
+        write_data->xml_node_handlers[XML_READER_TYPE_END_ELEMENT] = handle_user_details_last_name_element_end;
+        return false;
+    } else {
+        return true;
+    }
+}
+
+static bool handle_user_details_first_name_element_end(write_data_t *write_data, const xmlChar *text) {
+    write_data->xml_node_handlers[XML_READER_TYPE_ELEMENT] = handle_user_details_last_name_element;
+    return false;
+}
+
+static bool handle_user_details_first_name_text(write_data_t *write_data, const xmlChar *text) {
+    user_details_data_t *data = write_data->extra;
+    data->first_name = log_ralloc(write_data->r, apr_pstrdup(write_data->r->pool, (char *)text));
+	return false;
+}
+
+static bool handle_user_details_first_name_element(write_data_t *write_data, const xmlChar *text) {
+	if (expect_xml_element(write_data, first_name_xml_name, text)) {
+        write_data->xml_node_handlers[XML_READER_TYPE_TEXT] = handle_user_details_first_name_text;
+        write_data->xml_node_handlers[XML_READER_TYPE_END_ELEMENT] = handle_user_details_first_name_element_end;
+        return false;
+    } else {
+        return true;
+    }
+}
+
+static bool handle_user_details_link_element(write_data_t *write_data, const xmlChar *text) {
+	if (expect_xml_element(write_data, link_xml_name, text)) {
+        write_data->xml_node_handlers[XML_READER_TYPE_ELEMENT] = handle_user_details_first_name_element;
+        return false;
+    } else {
+        return true;
+    }
+}
+
+static bool handle_user_details_user_element(write_data_t *write_data, const xmlChar *text) {
+    user_details_data_t *data = write_data->extra;
+	if (!expect_xml_element(write_data, user_xml_name, text)) {
+        return true;
+    }
+    xmlChar *xmlUser = xmlTextReaderGetAttribute(write_data->xml_reader, name_xml_name);
+    if (xmlUser == NULL) {
+        return true;
+    }
+    data->alias = log_ralloc(write_data->r, apr_pstrdup(write_data->r->pool, (char *)xmlUser));
+    xmlFree(xmlUser);
+    if (data->alias == NULL) {
+        return handle_end_of_data(write_data, text);
+    } else {
+    	write_data->xml_node_handlers[XML_READER_TYPE_ELEMENT] = handle_user_details_link_element;
+    	return false;
+    }
+    return true;
+}
 
+user_details_data_t *crowd_user_details(const request_rec *r, const crowd_config *config, const memcached_config_t *memcached_config) {
+	/* Check cache */
+    char *cache_key = make_user_details_cache_key(r, config);
+    if (cache_key != NULL) {
+		if (memcached_config->host != NULL) {
+            char *user_details_str = NULL;
+            apr_size_t len;
+            apr_memcache_getp(memcached_config->memcache, r->pool, cache_key, &user_details_str, &len, NULL);
+            if (user_details_str != NULL) {
+                user_details_data_t *cached = log_ralloc(r, malloc(sizeof(user_details_data_t)));
+                if (cached != NULL) {
+                    char *user_details_ptr = NULL;
+                    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Cache HIT for user details: '%s'->'%s'", cache_key, user_details_str);
+                    cached->alias = apr_strtok(user_details_str, "\037", &user_details_ptr);
+                    cached->first_name = apr_strtok(NULL, "\037", &user_details_ptr);
+                    cached->last_name = apr_strtok(NULL, "\037", &user_details_ptr);
+                    cached->display_name = apr_strtok(NULL, "\037", &user_details_ptr);
+                    cached->email = apr_strtok(NULL, "\037", &user_details_ptr);
+                    
+                    if (apr_strtok(NULL, "\037", &user_details_ptr) != NULL) {
+                        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error parsing cache entry for user details: '%s'", user_details_str);
+                    } else {
+                        return cached;
+                    }
+                }
+            } else {
+                ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Cache MISS for user details: '%s'", cache_key);
+            }
+	    } else if (user_details_cache != NULL) {
+            user_details_data_t *cached = cache_get(user_details_cache, cache_key, r);
+            if (cached != NULL) {
+                ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Cache HIT for key='%s'->[alias='%s',first_name='%s',last_name='%s',display_name='%s',email='%s']", cache_key, cached->alias, cached->first_name, cached->last_name, cached->display_name, cached->email);
+                return cached;
+            }
+        }	
     }
+    
+    user_details_data_t *user_details = log_ralloc(r, apr_pcalloc(r->pool, sizeof(user_details_data_t)));
+    if (user_details == NULL) {
+        return NULL;
+    }
+
+    xml_node_handler_t *xml_node_handlers = make_xml_node_handlers(r);
+    if (xml_node_handlers == NULL) {
+        return NULL;
+    }
+    xml_node_handlers[XML_READER_TYPE_ELEMENT] = handle_user_details_user_element;
+    
+    if (crowd_request(r, config, false, make_user_details_url, NULL, xml_node_handlers, user_details) != HTTP_OK) {
+        return NULL;
+    }
+        
+    /* Cache result  */
+    if (cache_key != NULL) {
+		if (memcached_config->host != NULL) {
+			char *user_details_str = NULL;
+		    user_details_str = apr_psprintf(r->pool, "%s\037%s\037%s\037%s\037%s", user_details->alias, user_details->first_name, user_details->last_name, user_details->display_name, user_details->email);
+            if (user_details_str != NULL) {
+                ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Caching user details: '%s'->'%s'", cache_key, user_details_str);
+                apr_status_t rv = apr_memcache_set(memcached_config->memcache, cache_key, user_details_str, strlen(user_details_str), memcached_config->entry_timeout, 0);
+                if (rv != APR_SUCCESS) {
+                    ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "Failed to store on Memcached user details, code: %d", rv);
+                }
+            }		
+		} else if (user_details_cache != NULL) {
+            user_details_data_t *cached = log_ralloc(r, malloc(sizeof(user_details_data_t)));
+            if (cached != NULL) {
+                cached->alias = NULL;
+                cached->first_name = NULL;
+                cached->last_name = NULL;
+                cached->display_name = NULL;
+                cached->email = NULL;
+                
+                cached->alias = log_ralloc(r, strdup(user_details->alias));
+                if (cached->alias == NULL) {
+                    free(cached);
+                } else {
+                    cached->first_name = log_ralloc(r, strdup(user_details->first_name));
+                    cached->last_name = log_ralloc(r, strdup(user_details->last_name));
+                    cached->display_name = log_ralloc(r, strdup(user_details->display_name));
+                    cached->email = log_ralloc(r, strdup(user_details->email));
+                    
+                    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Caching user key='%s'->[alias='%s',first_name='%s',last_name='%s',display_name='%s',email='%s']", cache_key, cached->alias, cached->first_name, cached->last_name, cached->display_name, cached->email);
+                    cache_put(user_details_cache, cache_key, cached, r);
+                }
+            }
+        }
+    }
+    
+    return user_details;
 }
 
 /*============================

File src/crowd_client.h

 
 crowd_cookie_config_t *crowd_get_cookie_config(const request_rec *r, const crowd_config *config, const memcached_config_t *memcached_config);
 
+typedef struct {
+    char *alias;
+    char *first_name;
+    char *last_name;
+    char *display_name;
+    char *email;
+} user_details_data_t;
+
+user_details_data_t *crowd_user_details(const request_rec *r, const crowd_config *config, const memcached_config_t *memcached_config);  

File src/crowd_common.c

+/* Apache Portable Runtime includes */
+#include "apr_strings.h"
+#include "apr_tables.h"
+
+/* Apache httpd includes */
+#include "http_core.h"
+
+#include "util.h"
+
+#include "crowd_common.h"
+
+const char *set_once_error(const cmd_parms *parms) {
+    const char *error
+    = log_palloc(parms->temp_pool, apr_psprintf(parms->temp_pool, "%s specified multiple times", parms->cmd->name));
+    if (error == NULL) {
+        error = "Out of memory";
+    }
+    return error;
+}
+
+const char *set_once(const cmd_parms *parms, const char **location, const char *w) {
+    if (*location != NULL) {
+        return set_once_error(parms);
+    }
+    *location = log_palloc(parms->temp_pool, apr_pstrdup(parms->pool, w));
+    if (*location == NULL) {
+        return "Out of memory";
+    }
+    return NULL;
+}
+
+const char *set_flag_once(const cmd_parms *parms, bool *location, bool *set_location, int on) {
+    if (*set_location) {
+        return set_once_error(parms);
+    }
+    *location = on;
+    *set_location = true;
+    return NULL;
+}
+
+const char *set_url_once(const cmd_parms *parms, const char **location, const char *w, const bool trailing_slash) {
+	// Ignore empty URLs.  Will be reported as a missing parameter.
+    if (*w == '\0') {
+        return NULL;
+    }
+    // Add a trailing slash if one does not already exist.
+    if (trailing_slash && w[strlen(w) - 1] != '/') {
+        w = log_palloc(parms->temp_pool, apr_pstrcat(parms->temp_pool, w, "/", NULL));
+        if (w == NULL) {
+            return "Out of memory";
+        }
+    }
+    
+    return set_once(parms, location, w);
+}
+
+unsigned int parse_number(const char *string, const char *name, unsigned int min, unsigned int max,
+                                 unsigned int default_value, apr_pool_t *p, char **error_msg) {
+    *error_msg = NULL;
+    if (string == NULL) {
+        return default_value;
+    }
+    errno = 0;
+    apr_int64_t value = apr_atoi64(string);
+    if (errno != 0 || value < 0 ) {
+        *error_msg = apr_psprintf(p, "Could not parse %s: '%s' - (min: %u, max: %u, default: %u, errorno: %d)", name, string, min, max, default_value, errno);
+    } else if ((apr_uint64_t)value > max || (apr_uint64_t)value < min) {
+        *error_msg = apr_psprintf(p, "Invalid range for property %s: '%s' - (min: %u, max: %u, default: %u)", name, string, min, max, default_value, errno);
+    }
+    return (unsigned int)value;
+}
+
+const char *set_number(const cmd_parms *parms, const char *paramName, unsigned int min, unsigned int max,
+                              unsigned int default_value, apr_uint64_t *location, const char *w) {
+    char **error_msg = apr_pcalloc(parms->temp_pool, sizeof(char *));
+    *location=parse_number(w, paramName, min, max, default_value, parms->temp_pool, error_msg);
+    return *error_msg;
+}
+
+
+bool is_https(request_rec *r) {
+    const char *https = apr_table_get(r->subprocess_env, "HTTPS");
+    return https != NULL && strcmp(https, "on");
+}
+
+#define MAX_GROUPS 128
+void crowd_set_groups(request_rec *r, crowd_config *crowd_config, memcached_config_t *memcached_config, char *target_field, apr_table_t *target_table, char *delim) {
+    if (delim == NULL) {
+        delim = " ";
+    }
+    
+    int delim_len = sizeof(*delim);
+    
+    if (target_field == NULL) {
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Target field undefined; returning.");
+        return;
+    }
+    
+    apr_array_header_t *user_groups = crowd_user_groups(r->user, r, crowd_config, memcached_config);
+    if (user_groups == NULL) {
+        ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "While setting groups target field '%s' for remote user '%s': returned NULL.", target_field, r->user);
+        return;
+    }
+    
+    apr_size_t ngrps = user_groups->nelts;
+    if (ngrps <= 0) {
+        apr_table_set(target_table, target_field, "");
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Set groups target field '%s' for remote user '%s' to empty.", target_field, r->user);
+        return;
+    }
+    
+    if (ngrps > MAX_GROUPS) {
+        ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "While setting groups target field '%s' for remote user '%s': Value will be clipped as number of groups (%d) exceeds GRP_ENV_MAX_GROUPS (%d).", target_field, r->user, ngrps, MAX_GROUPS);
+        ngrps = MAX_GROUPS;
+    }
+    
+    apr_size_t nvec = ngrps + (ngrps - 1); /* Groups + conjunctive delimiters */
+    struct iovec *iov = apr_pcalloc(r->pool, sizeof(struct iovec) * nvec);
+    int i, k;
+    for (i = 0, k = 0; i < ngrps; i++) {
+        if (i >= 1) {
+            /* Join previous entry with a delimiter */
+            iov[k].iov_base = delim;
+            iov[k].iov_len = delim_len;
+            k++;
+        }
+        
+        void *grp = APR_ARRAY_IDX(user_groups, i, void *);
+        iov[k].iov_base = grp;
+        iov[k].iov_len = strlen(grp);
+        k++;
+    }
+    
+    const char *groups = apr_pstrcatv(r->pool, iov, nvec, NULL);
+    if (groups == NULL) {
+        ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "While setting groups target field '%s' for remote user '%s': apr_pstrcatv() returned NULL.", target_field, r->user);
+        return;
+    }
+    
+    apr_table_set(target_table, target_field, groups);
+    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Set groups target field '%s' for remote user '%s' to '%s'", target_field, r->user, groups);
+}

File src/crowd_common.h

+/* Standard includes */
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "http_config.h"
+
+#include "crowd_client.h"
+
+const char *set_once_error(const cmd_parms *parms);
+const char *set_once(const cmd_parms *parms, const char **location, const char *w);
+const char *set_flag_once(const cmd_parms *parms, bool *location, bool *set_location, int on);
+const char *set_url_once(const cmd_parms *parms, const char **location, const char *w, const bool trailing_slash);
+unsigned int parse_number(const char *string, const char *name, unsigned int min, unsigned int max, unsigned int default_value, apr_pool_t *p, char **error_msg);
+const char *set_number(const cmd_parms *parms, const char *paramName, unsigned int min, unsigned int max, unsigned int default_value, apr_uint64_t *location, const char *w);
+bool is_https(request_rec *r);
+void crowd_set_groups(request_rec *r, crowd_config *crowd_config, memcached_config_t *memcached_config, char *target_field, apr_table_t *target_table, char *delim);

File src/crowdsso/Makefile.am

+AUTOMAKE_OPTIONS = -Wno-override
+lib_LTLIBRARIES = mod_auth_crowdsso.la
+mod_auth_crowdsso_la_SOURCES = mod_auth_crowdsso.c mod_auth_crowdsso.h ../crowd_common.c ../crowd_common.h ../crowd_client.c ../crowd_client.h ../cache.c ../cache.h ../util.c ../util.h ../crowd_memcache.c ../crowd_memcache.h
+mod_auth_crowdsso_la_LDFLAGS = -module -lcurl `xml2-config --libs` 
+AM_CFLAGS = `apr-1-config --cflags`
+AM_CPPFLAGS = -I@APACHE_INCLUDE_DIR@ `apr-1-config --cppflags --includes` `xml2-config --cflags` -I..
+CFLAGS=-g -O1   # -O2 causes mysterious crashes
+TESTS = test.py
+TESTS_ENVIRONMENT = APACHE_BIN_DIR=@APACHE_BIN_DIR@
+
+test.py : httpd/conf/common.conf httpd/conf/httpd_svn.conf
+
+httpd/conf/common.conf : httpd/conf/common.conf.in
+	APACHE_MODULES_DIR=@APACHE_MODULES_DIR@ CURDIR=$(CURDIR) envsubst < httpd/conf/common.conf.in > httpd/conf/common.conf
+
+install:
+	cp @HTTPD_CONF@ /tmp/httpd.conf.bak
+	@APXS@ -i -a mod_auth_crowdsso.la
+	@APACHECTL@ configtest || mv /tmp/httpd.conf.bak @HTTPD_CONF@
+	@APACHECTL@ graceful

File src/crowdsso/httpd/conf/common.conf.in

+DocumentRoot htdocs
+Listen 8080
+CoreDumpDirectory logs
+<IfModule !authz_user_module>
+    LoadModule authz_user_module $APACHE_MODULES_DIR/mod_authz_user.so
+</IfModule>
+<IfModule !autoindex_module>
+    LoadModule autoindex_module $APACHE_MODULES_DIR/mod_autoindex.so
+</IfModule>
+<IfModule !dir_module>
+    LoadModule dir_module $APACHE_MODULES_DIR/mod_dir.so
+</IfModule>
+<IfModule !proxy_module>
+    LoadModule proxy_module $APACHE_MODULES_DIR/mod_proxy.so
+</IfModule>
+<IfModule !proxy_http_module>
+    LoadModule proxy_http_module $APACHE_MODULES_DIR/mod_proxy_http.so
+</IfModule>
+LoadModule auth_crowdsso_module modules/mod_auth_crowdsso.so
+LogLevel debug

File src/crowdsso/httpd/conf/httpd.conf

+Include conf/common.conf
+
+CrowdCacheMaxAge 10
+CrowdMemcachedHost 127.0.0.1
+
+User apache
+Group apache
+
+ProxyPass /disallow_anon http://localhost:8181
+ProxyPassReverse /disallow_anon http://localhost:8181
+
+ProxyPass /allow_anon http://localhost:8181
+ProxyPassReverse /allow_anon http://localhost:8181
+
+<Location /disallow_anon/>
+    AuthType CrowdSSO
+    CrowdSSOAppURL http://demo.atlassian.test
+    CrowdSSOLoginURL http://login.atlassian.test/AppLogin?app=demo&continue=
+	CrowdSSOAllowAnon Off
+		
+    CrowdAppName demo
+    CrowdAppPassword password
+    CrowdURL http://localhost:8095/
+    Require valid-user
+    CrowdSSLVerifyPeer Off
+</Location>
+
+<Location /allow_anon/>
+    AuthType CrowdSSO
+    CrowdSSOAppURL http://demo.atlassian.test
+    CrowdSSOLoginURL http://login.atlassian.test/AppLogin?app=demo&continue=
+	CrowdSSOAllowAnon On
+
+    CrowdAppName demo
+    CrowdAppPassword password
+    CrowdURL http://localhost:8095/
+    Require valid-user
+    CrowdSSLVerifyPeer Off
+</Location>

File src/crowdsso/httpd/conf/mime.types

+# This empty file is required to prevent httpd from raising errors during execution of the tests.

File src/crowdsso/mod_auth_crowdsso.c

+/* Standard headers */
+#include <stdbool.h>
+#include <string.h>
+
+/* Apache Portable Runtime includes */
+#include "apr_strings.h"
+#include "apr_xlate.h"
+
+/* Apache httpd includes */
+#include "ap_provider.h"
+#include "httpd.h"
+#include "http_core.h"
+#include "http_config.h"
+#include "http_log.h"
+#include "http_request.h"
+#include "mod_auth.h"
+
+#undef PACKAGE_BUGREPORT
+#undef PACKAGE_NAME
+#undef PACKAGE_STRING
+#undef PACKAGE_TARNAME
+#undef PACKAGE_VERSION
+
+#include "../../config.h"
+
+#include "crowd_common.h"
+#include "util.h"
+
+#include "mod_auth_crowdsso.h"
+
+static struct {
+    const char *cache_max_entries_string;
+    const char *cache_max_age_string;
+} crowdsso_process_config;
+
+typedef struct {
+    crowd_config *crowd_config;
+    const char *crowd_timeout_string;
+    const char *crowdsso_login_url;
+    const char *crowdsso_app_url;
+    const bool *crowdsso_allow_anon;
+    bool ssl_verify_peer_set;
+    bool crowdsso_allow_anon_set;
+} crowdsso_dir_config;
+
+typedef struct {
+    memcached_config_t *memcached_config;
+} crowdsso_server_config;
+
+module AP_MODULE_DECLARE_DATA auth_crowdsso_module;
+
+static apr_array_header_t *dir_configs = NULL;
+
+static crowdsso_server_config* crowd_get_server_config(server_rec *s);
+
+/** Function to allow all modules to create per directory configuration
+ *  structures.
+ *  @param p The pool to use for all allocations.
+ *  @param dir The directory currently being processed.
+ *  @return The per-directory structure created
+ */
+static void *create_dir_config(apr_pool_t *p, char *dir) {
+    ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, p, "Creating Crowd config for '%s'", dir);
+    crowdsso_dir_config *dir_config = log_palloc(p, apr_pcalloc(p, sizeof(crowdsso_dir_config)));
+    if (dir_config == NULL) {
+        exit(1);
+    }
+    dir_config->crowd_config = crowd_create_config(p);
+    if (dir_config->crowd_config == NULL) {
+        exit(1);
+    }
+    
+    dir_config->crowd_config->crowd_ssl_verify_peer = true;
+    
+    // Add new config to list of this module's per-directory configs, for checking during the post-config phase.
+    if (dir_configs == NULL)  {
+        dir_configs = log_palloc(p, apr_array_make(p, 0, sizeof(crowdsso_dir_config *)));
+        if (dir_configs == NULL) {
+            exit(1);
+        }
+    }
+    APR_ARRAY_PUSH(dir_configs, crowdsso_dir_config *) = dir_config;
+    
+    return dir_config;
+}
+
+static void *create_svr_config(apr_pool_t *p, server_rec *s) {
+    ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, p, "Creating Crowd server config");
+    
+    crowdsso_server_config *svr_config = apr_pcalloc(p, sizeof(crowdsso_server_config));
+    
+    svr_config->memcached_config = crowd_create_memcached_config(p);
+    if (svr_config->memcached_config == NULL) {
+        exit(1);
+    }
+    
+    return svr_config;
+}
+
+static const char *set_crowd_app_name(cmd_parms *parms, void *mconfig, const char *w)
+{
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_once(parms, &(config->crowd_config->crowd_app_name), w);
+}
+
+static const char *set_crowd_app_password(cmd_parms *parms, void *mconfig, const char *w)
+{
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_once(parms, &(config->crowd_config->crowd_app_password), w);
+}
+
+static const char *set_crowd_timeout(cmd_parms *parms, void *mconfig, const char *w)
+{
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_once(parms, &(config->crowd_timeout_string), w);
+}
+
+static const char *set_crowd_cert_path(cmd_parms *parms, void *mconfig, const char *w)
+{
+    // Ignore empty URLs.  Will be reported as a missing parameter.
+    if (*w == '\0') {
+        return;
+    }
+    
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_once(parms, &(config->crowd_config->crowd_cert_path), w);
+}
+
+static const char *set_crowdsso_login_url(cmd_parms *parms, void *mconfig, const char *w) {
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_url_once(parms, &(config->crowdsso_login_url), w, false);
+}
+
+static const char *set_crowdsso_app_url(cmd_parms *parms, void *mconfig, const char *w) {
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_url_once(parms, &(config->crowdsso_app_url), w, false);
+}
+
+static const char *set_crowdsso_allow_anon(cmd_parms *parms, void *mconfig, int on)
+{
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_flag_once(parms, &(config->crowdsso_allow_anon), &(config->crowdsso_allow_anon_set), on);
+}
+
+static const char *set_crowd_url(cmd_parms *parms, void *mconfig, const char *w)
+{
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_url_once(parms, &(config->crowd_config->crowd_url), w, true);
+}
+
+static const char *set_crowd_cache_max_age(cmd_parms *parms, void *mconfig __attribute__((unused)),
+                                           const char *w __attribute__((unused))) {
+    return set_once(parms, &(crowdsso_process_config.cache_max_age_string), w);
+}
+
+static const char *set_crowd_cache_max_entries(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    return set_once(parms, &(crowdsso_process_config.cache_max_entries_string), w);
+}
+
+static const char *set_crowd_ssl_verify_peer(cmd_parms *parms, void *mconfig, int on) {
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_flag_once(parms, &(config->crowd_config->crowd_ssl_verify_peer), &(config->ssl_verify_peer_set), on);
+}
+
+static const char *set_crowd_groups_env_name(cmd_parms *parms, void *mconfig, const char *w) {
+    crowdsso_dir_config *config = (crowdsso_dir_config *) mconfig;
+    return set_once(parms, &(config->crowd_config->groups_env_name), w);
+}
+
+static const char *set_crowd_memcached_host(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_once(parms, &(config->memcached_config->host), w);
+}
+
+static const char *set_crowd_memcached_port(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_number(parms, "CrowdMemcachedPort", 0, UINT_MAX, 11211, &(config->memcached_config->port), w);
+}
+
+static const char *set_crowd_memcached_entry_timeout(cmd_parms *parms, void *mconfig __attribute__((unused)),
+                                                     const char *w __attribute__((unused))) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_number(parms, "CrowdMemcachedEntryTimeout", 10, UINT_MAX, 300, &(config->memcached_config->entry_timeout), w);
+}
+
+static const char *set_crowd_memcached_failed_attempts_window(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_number(parms, "CrowdMemcachedFailedAttemptsWindow", 10, UINT_MAX, 120, &(config->memcached_config->failed_attempts_window), w);
+}
+
+static const char *set_crowd_memcached_blacklist_timeout(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_number(parms, "CrowdMemcachedBlacklistTimeout", 60, UINT_MAX, 600, &(config->memcached_config->blacklist_timeout), w);
+}
+
+static const char *set_crowd_memcached_max_failed_attempts(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_number(parms,"CrowdMemcachedMaxFailedAttempts", 0, UINT_MAX, 10, &(config->memcached_config->max_failed_attempts), w);
+}
+
+static const char *set_crowd_memcached_pool_min(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_number(parms, "CrowdMemcachedPoolMin", 0, UINT_MAX, 0, &(config->memcached_config->pool_min), w);
+}
+
+static const char *set_crowd_memcached_pool_smax(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_number(parms,"CrowdMemcachedPoolSoftMax", 0, UINT_MAX, 10, &(config->memcached_config->pool_smax), w);
+}
+
+static const char *set_crowd_memcached_pool_max(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_number(parms, "CrowdMemcachedPoolMax", 1, UINT_MAX, 30, &(config->memcached_config->pool_max), w);
+}
+
+static const char *set_crowd_memcached_pool_ttl(cmd_parms *parms, void *mconfig __attribute__((unused)), const char *w) {
+    crowdsso_server_config *config = crowd_get_server_config(parms->server);
+    return set_number(parms, "CrowdMemcachedPoolTTL", 1, UINT_MAX, 300, &(config->memcached_config->pool_ttl), w);
+}
+
+static const command_rec commands[] = {
+    
+    AP_INIT_TAKE1("CrowdAppName", set_crowd_app_name, NULL, OR_AUTHCFG,
+                  "The name of this application, as configured in Crowd"),
+    AP_INIT_TAKE1("CrowdAppPassword", set_crowd_app_password, NULL, OR_AUTHCFG,
+                  "The password of this application, as configured in Crowd"),
+    AP_INIT_TAKE1("CrowdTimeout", set_crowd_timeout, NULL, OR_AUTHCFG,
+                  "The maximum length of time, in seconds, to wait for a response from Crowd (default or 0 = no timeout)"),
+    AP_INIT_TAKE1("CrowdSSOLoginURL", set_crowdsso_login_url, NULL, OR_AUTHCFG, "The URL of the Single Sign On Login page"),
+    AP_INIT_TAKE1("CrowdSSOAppURL", set_crowdsso_app_url, NULL, OR_AUTHCFG, "The public URL of the target application"),
+    AP_INIT_FLAG("CrowdSSOAllowAnon", set_crowdsso_allow_anon, NULL, OR_AUTHCFG,
+                 "'On' if anonymous is allowed; 'Off' otherwise (default = Off)"),
+    AP_INIT_TAKE1("CrowdURL", set_crowd_url, NULL, OR_AUTHCFG, "The base URL of the Crowd server"),
+    AP_INIT_TAKE1("CrowdCertPath", set_crowd_cert_path, NULL, OR_AUTHCFG, "The path to the SSL certificate file to supply to curl for Crowd over SSL"),
+    AP_INIT_TAKE1("CrowdCacheMaxAge", set_crowd_cache_max_age, NULL, RSRC_CONF,
+                  "The maximum length of time that successful results from Crowd can be cached, in seconds"
+                  " (default = 60 seconds)"),
+    AP_INIT_TAKE1("CrowdCacheMaxEntries", set_crowd_cache_max_entries, NULL, RSRC_CONF,
+                  "The maximum number of successful results from Crowd that can be cached at any time"
+                  " (default = 500, 0 = disable cache)"),
+    AP_INIT_FLAG("CrowdSSLVerifyPeer", set_crowd_ssl_verify_peer, NULL, OR_AUTHCFG,
+                 "'On' if SSL certificate validation should occur when connecting to Crowd; 'Off' otherwise (default = On)"),
+    AP_INIT_TAKE1("CrowdMemcachedEntryTimeout", set_crowd_memcached_entry_timeout, NULL, RSRC_CONF,
+                  "The maximum length of time that successful results from Crowd can be cached, in seconds"
+                  " (minimum = 10 seconds, default = 300 seconds)"),
+    AP_INIT_TAKE1("CrowdMemcachedHost", set_crowd_memcached_host, NULL, RSRC_CONF,
+                  "The Memcached host to connect to."
+                  ),
+    AP_INIT_TAKE1("CrowdMemcachedPort", set_crowd_memcached_port, NULL, RSRC_CONF,
+                  "The Memcached port to connect to."
+                  " (default = 11211)"),
+    AP_INIT_TAKE1("CrowdMemcachedFailedAttemptsWindow", set_crowd_memcached_failed_attempts_window, NULL, RSRC_CONF,
+                  "The window, in seconds, where a number of failed authentication attempts must be made before blacklisting the user."
+                  " (default = 120 seconds)"),
+    AP_INIT_TAKE1("CrowdMemcachedBlacklistTimeout", set_crowd_memcached_blacklist_timeout, NULL, RSRC_CONF,
+                  "The length in time that the user will be blacklisted and denied authentication, in seconds"
+                  " (default = 600 seconds)"),
+    AP_INIT_TAKE1("CrowdMemcachedMaxFailedAttempts", set_crowd_memcached_max_failed_attempts, NULL, RSRC_CONF,
+                  "The number of failed attempts inside the failed attempts window that will make the user be blacklisted"
+                  " (default = 10)"),
+    AP_INIT_TAKE1("CrowdMemcachedPoolMin", set_crowd_memcached_pool_min, NULL, RSRC_CONF,
+                  "The minimum number of client sockets to open to memcached"
+                  " (default = 0)"),
+    AP_INIT_TAKE1("CrowdMemcachedPoolSoftMax", set_crowd_memcached_pool_smax, NULL, RSRC_CONF,
+                  "The soft maximum number of client connections to open to memcached"
+                  " (default = 10)"),
+    AP_INIT_TAKE1("CrowdMemcachedPoolMax", set_crowd_memcached_pool_max, NULL, RSRC_CONF,
+                  "The hard maximum number of client connections to open to memcached"
+                  " (default = 30)"),
+    AP_INIT_TAKE1("CrowdMemcachedPoolTTL", set_crowd_memcached_pool_ttl, NULL, RSRC_CONF,
+                  "The time to live in seconds of a client connection to memcached"
+                  " (default = 300)"),
+    { NULL }
+};
+
+static crowdsso_dir_config *get_config(request_rec *r) {
+    crowdsso_dir_config *config
+    = (crowdsso_dir_config *) ap_get_module_config(r->per_dir_config, &auth_crowdsso_module);
+    if (config == NULL) {
+        ap_log_rerror(APLOG_MARK, APLOG_CRIT, 0, r, "Configuration not found.");
+    }
+    return config;
+}
+
+static crowdsso_server_config* crowd_get_server_config(server_rec *s) {
+    crowdsso_server_config *config
+    = (crowdsso_server_config *) ap_get_module_config(s->module_config, &auth_crowdsso_module);
+    if (config == NULL) {
+        ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, "Server configuration not found.");
+    }
+    return config;
+}
+
+typedef struct {
+    request_rec *r;
+    crowdsso_dir_config *config;
+    char *cookie_name;
+    size_t cookie_name_len;
+    char *token;
+} check_for_cookie_data_t;
+
+static int check_for_cookie(void *rec, const char *key, const char *value) {
+    if (strcasecmp("Cookie", key) == 0) {
+        check_for_cookie_data_t *data = rec;
+        if (data->cookie_name == NULL) {
+            crowdsso_server_config *server_config = crowd_get_server_config(data->r->server);
+            crowd_cookie_config_t *cookie_config = crowd_get_cookie_config(data->r, data->config->crowd_config, server_config->memcached_config);
+            if (cookie_config == NULL || cookie_config->cookie_name == NULL || (cookie_config->secure && !is_https(data->r))) {
+                if (cookie_config != NULL && cookie_config->cookie_name != NULL) {
+                    ap_log_rerror(APLOG_MARK, APLOG_CRIT, 0, data->r, "Tried to authenticate with cookie_secure=1 but it's not HTTP/S, this is suspicious...");
+                }
+                return 0;
+            }
+            data->cookie_name = log_ralloc(data->r, apr_pstrcat(data->r->pool, cookie_config->cookie_name, "=", NULL));
+            
+            if (data->cookie_name == NULL) {
+                return 0;
+            }
+            data->cookie_name_len = strlen(data->cookie_name);
+        }
+        char *cookies = log_ralloc(data->r, apr_pstrdup(data->r->pool, value));
+        if (cookies == NULL) {
+            return 0;
+        }
+        apr_collapse_spaces(cookies, cookies);
+        char *last;
+        char *cookie = apr_strtok(cookies, ";,", &last);
+        while (cookie != NULL) {
+            if (strncasecmp(cookie, data->cookie_name, data->cookie_name_len) == 0) {
+                data->token = log_ralloc(data->r, apr_pstrdup(data->r->pool, cookie + data->cookie_name_len));
+                return 0;
+            }
+            cookie = apr_strtok(NULL, ";,", &last);
+        }
+    }
+    return 1;
+}
+
+static int check_user_id(request_rec *r) {
+    const char *current_auth = ap_auth_type(r);
+    crowdsso_dir_config *config = get_config(r);
+    
+    /* Decline if there is no configuration or the AuthType is not CrowdSSO */
+    if (!config || !current_auth || strcasecmp(current_auth, "CrowdSSO")) {
+        return DECLINED;
+    }
+    
+    /* Strip out any CrowdSSO headers to prevent spoofing */
+    if (apr_table_get(r->headers_in, "__ATL_USER") != NULL) {
+    	apr_table_unset(r->headers_in, "__ATL_USER");
+    }
+    
+    if (apr_table_get(r->headers_in, "__ATL_LOCAL_USER") != NULL) {
+    	apr_table_unset(r->headers_in, "__ATL_LOCAL_USER");
+    }
+    
+    if (apr_table_get(r->headers_in, "__ATL_GROUP_MEMBERSHIP") != NULL) {
+    	apr_table_unset(r->headers_in, "__ATL_GROUP_MEMBERSHIP");
+    }
+    
+	check_for_cookie_data_t data = { .r = r, .config = config };
+    apr_table_do(check_for_cookie, &data, r->headers_in, NULL);
+    crowdsso_server_config *server_config = crowd_get_server_config(r->server);
+    if (data.token != NULL && crowd_validate_session(r, config->crowd_config, server_config->memcached_config, data.token, &r->user) == CROWD_AUTHENTICATE_SUCCESS) {
+		apr_table_setn(r->headers_in, "__ATL_USER", r->user);
+		
+		user_details_data_t *user_details = crowd_user_details(r, config->crowd_config, server_config->memcached_config);
+		if (user_details != NULL && user_details->alias != NULL) {
+			apr_table_setn(r->headers_in, "__ATL_LOCAL_USER", user_details->alias);
+		}
+		
+		crowd_set_groups(r, config->crowd_config, server_config->memcached_config, "__ATL_GROUP_MEMBERSHIP", r->headers_in, ",");
+		
+		return OK;
+    } else if (config->crowdsso_allow_anon) {
+    	return OK;
+    }
+    
+    char *login_url = log_ralloc(r, apr_psprintf(r->pool, "%s%s%s", config->crowdsso_login_url, config->crowdsso_app_url, r->uri));
+    apr_table_setn(r->headers_out, "Location", login_url);
+    return HTTP_TEMPORARY_REDIRECT;
+}
+
+static void *crowd_merge_config(apr_pool_t *pool, void *base_conf, void *overrides_conf) {
+    crowdsso_server_config *base = (crowdsso_server_config *) base_conf;
+    crowdsso_server_config *overrides = (crowdsso_server_config *) overrides_conf;
+    crowdsso_server_config *config = apr_pcalloc(pool, sizeof(crowdsso_server_config));
+    
+    return (void *) base;
+}
+
+/* Called after configuration is set, to finalise it. */
+static int post_config(apr_pool_t *pconf, apr_pool_t *plog __attribute__((unused)), apr_pool_t *ptemp, server_rec *s)
+{
+    
+    char **error_msg = apr_pcalloc(ptemp, sizeof(char *));
+    
+    /* Create the caches, if required. */
+    unsigned cache_max_entries
+    = parse_number(crowdsso_process_config.cache_max_entries_string, "CrowdCacheMaxEntries", 0, UINT_MAX, 500,
+                   ptemp, error_msg);
+    if (*error_msg != NULL) {
+        ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, *error_msg); 
+        exit(1);
+    }
+    if (cache_max_entries > 0) {
+        apr_time_t cache_max_age
+        = apr_time_from_sec(parse_number(crowdsso_process_config.cache_max_age_string, "CrowdCacheMaxAge", 1,
+                                         UINT_MAX, 60, ptemp, error_msg));
+        if (*error_msg != NULL) {
+            ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, *error_msg); 
+            exit(1);
+        }
+        if (!crowd_cache_create(pconf, cache_max_age, cache_max_entries)) {
+            exit(1);
+        }
+    }
+    
+    if (dir_configs != NULL) {
+        
+        /* Iterate over each directory config */
+        crowdsso_dir_config **dir_config;
+        while ((dir_config = apr_array_pop(dir_configs)) != NULL) {
+            
+            /* If any of the configuration parameters are specified, ensure that all mandatory parameters are
+             specified. */
+            crowd_config *crowd_config = (*dir_config)->crowd_config;
+            if ((crowd_config->crowd_app_name != NULL || crowd_config->crowd_app_password != NULL
+                 || crowd_config->crowd_url != NULL)
+                && (crowd_config->crowd_app_name == NULL || crowd_config->crowd_app_password == NULL
+                    || crowd_config->crowd_url == NULL)) {
+                    ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s,
+                                 "Missing CrowdAppName, CrowdAppPassword or CrowdURL for a directory.");
+                    exit(1);
+                }
+            
+            if ((crowd_config->crowd_app_name != NULL || crowd_config->crowd_app_password != NULL
+                 || crowd_config->crowd_url != NULL)
+                && (crowd_config->crowd_ssl_verify_peer == true && crowd_config->crowd_cert_path == NULL)) {
+                ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s,
+                             "CrowdSSLVerifyPeer is On but CrowdCertPath is unspecified, will try to use default: /etc/pki/tls/certs.");
+                crowd_config->crowd_cert_path="/etc/pki/tls/certs";
+            }
+            
+            /* Parse the timeout parameter, if specified */
+    	    char **error_msg = apr_pcalloc(ptemp, sizeof(char *));
+            crowd_config->crowd_timeout
+            = parse_number((*dir_config)->crowd_timeout_string, "CrowdTimeout", 0, UINT_MAX, 0, ptemp, error_msg);
+            if (*error_msg != NULL) {
+                ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, *error_msg); 
+                exit(1);
+            }
+            
+        }
+        
+    	crowdsso_server_config *server_config = crowd_get_server_config(s);
+        if (server_config->memcached_config->host != NULL) {
+    	    apr_status_t rv = crowd_setup_memcached(s, s->process->pool, server_config->memcached_config);
+            if (rv != APR_SUCCESS) {
+                char *error_msg = apr_palloc(plog, 100);
+                error_msg = apr_strerror(rv, error_msg, 100);
+                ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, "Failed to setup Memcached, code: %d - %s", rv, error_msg);
+            } else {
+    	        rv = crowd_test_memcached_connection(s, plog, server_config->memcached_config);
+                if (rv != APR_SUCCESS) {
+                    char *error_msg = apr_palloc(plog, 100);
+                    error_msg = apr_strerror(rv, error_msg, 100);
+                    ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, "Failed to test connection to Memcached, code: %d - %s", rv, error_msg);
+                }
+            }
+        } else {
+            ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "Memcached host not configured, using in-process cache.");
+        }
+    }
+    
+    
+    return OK;
+}
+
+static void register_hooks(apr_pool_t *p)
+{
+    static const char * const pre_auth_checker[]={ "mod_authz_user.c", NULL };
+    ap_hook_post_config(post_config, NULL, NULL, APR_HOOK_MIDDLE);
+    ap_hook_check_user_id(check_user_id, NULL, NULL, APR_HOOK_FIRST);
+    ap_log_perror(APLOG_MARK, APLOG_NOTICE, 0, p, "mod_auth_crowdsso " PACKAGE_VERSION " installed.");
+}
+
+module AP_MODULE_DECLARE_DATA auth_crowdsso_module =
+{
+    STANDARD20_MODULE_STUFF,
+    create_dir_config,
+    NULL,
+    create_svr_config,
+    crowd_merge_config,
+    commands,
+    register_hooks
+};
+
+/* Library initialisation and termination functions */
+/* TODO: Another solution will likely be required for non-GCC platforms, e.g. Windows */
+
+void init() __attribute__ ((constructor));
+
+void init()
+{
+    crowd_init();
+}
+
+void term() __attribute__ ((destructor));
+
+void term()
+{
+    crowd_cleanup();
+}

File src/crowdsso/mod_auth_crowdsso.h

Empty file added.

File src/crowdsso/test.py

+#! /usr/bin/env python
+
+import BaseHTTPServer
+import os
+import urllib2
+import subprocess
+import threading
+import time
+import base64
+import unittest2
+
+demo_app_public_url = 'http://demo.atlassian.test'
+httpd_url = 'http://localhost:8080'
+demo_app_url = 'http://localhost:8181'
+crowd_url = 'http://localhost:8095/ROOT'
+login_url = 'http://login.atlassian.test/AppLogin?app=demo&continue='
+
+def start_httpd(config_file = 'conf/httpd.conf'):
+	apache_bin_dir = os.getenv('APACHE_BIN_DIR')
+	assert not apache_bin_dir is None, 'You must have a environment variable named APACHE_BIN_DIR with the httpd path'
+	print apache_bin_dir
+
+	# Create base httpd dirs as Git won't let us have an empty dir on the repository
+	subprocess.call(['mkdir', '-p', os.getcwd() + '/httpd/run'])
+	subprocess.call(['mkdir', '-p', os.getcwd() + '/httpd/logs'])
+	subprocess.call(['mkdir', '-p', os.getcwd() + '/httpd/modules'])
+	subprocess.call(['mkdir', '-p', os.getcwd() + '/httpd/htdocs'])
+
+	# Install mod_auth_crowdsso module.
+	subprocess.check_call([apache_bin_dir + '/apxs', '-S', 'LIBEXECDIR=' + os.getcwd() + '/httpd/modules', '-i', 'mod_auth_crowdsso.la'])
+	
+	return subprocess.Popen([apache_bin_dir + '/httpd', '-X', '-d', 'httpd', '-e', 'debug', '-f', config_file], stdout = subprocess.PIPE)
+
+def start_demo_app():
+	# Demo App is an example target application, it returns the SSO request headers in the response headers so they can be inspected but tests
+	class Demo_App_BaseHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+		def do_GET(self):
+			self.send_response(200)
+			self.send_header('Content-type', 'text/html')
+			self.send_header('__ATL_USER', self.headers.get('__ATL_USER'))
+			self.send_header('__ATL_LOCAL_USER', self.headers.get('__ATL_LOCAL_USER'))
+			self.send_header('__ATL_GROUP_MEMBERSHIP', self.headers.get('__ATL_GROUP_MEMBERSHIP'))
+			self.send_header('__TEST', self.headers.get('__TEST'))
+			self.end_headers()
+	demo_app = BaseHTTPServer.HTTPServer(('', 8181), Demo_App_BaseHTTPRequestHandler)
+	threading.Thread(target = demo_app.serve_forever).start()
+	return demo_app
+
+def get_sso_token():
+	# Create a valid SSO token to use in the tests
+	url = crowd_url + '/rest/usermanagement/1/session';
+	request = urllib2.Request(url)
+	request.add_header('Content-Type', 'application/xml')
+	request.add_header('Authorization', 'Basic %s' % base64.encodestring('%s:%s' % ('demo', 'password'))[:-1])
+	request.add_data('<authentication-context><username>admin</username><password>admin</password><validation-factors><validation-factor><name>remote_address</name><value>::1</value></validation-factor></validation-factors></authentication-context>')
+	response = urllib2.urlopen(request)
+	# Crowd redirects to the URL http://crowd_url/rest/usermanagement/1/session/{SOO_token} after successfully creating a session
+	return response.headers['Location'].replace(url + '/', '');
+	
+def validate_sso_token():
+    # Create a valid SSO token to use in the tests
+    url = crowd_url + '/rest/usermanagement/1/session/vWvW0bwcs5Wedr5dxKuEVA00';
+    request = urllib2.Request(url)
+    request.add_header('Content-Type', 'application/xml')
+    request.add_header('Authorization', 'Basic %s' % base64.encodestring('%s:%s' % ('demo', 'password'))[:-1])
+    request.add_data('<validation-factors><validation-factor><name>remote_address</name><value>::1</value></validation-factor></validation-factors>')
+    response = urllib2.urlopen(request)
+    print response.read()
+
+def wait_for_startup(url = None):
+	assert url is not None, 'No URL specified for wait_for_startup'
+	# Repeat until the server starts.
+	timeout = 5
+	while True:
+		try:
+			urllib2.urlopen(url + '/')
+			break
+		except urllib2.URLError as exception:
+			assert timeout > 0, 'Could not open url: ' + url
+			timeout -= 1
+			time.sleep(1)
+			continue
+	
+def http_get(relative_url = None, custom_headers = None, sso_token = None, forwarded_for = None):
+    class Disable_HTTPRedirectHandler(urllib2.HTTPRedirectHandler):
+        def http_error_307(self, req, fp, code, msg, headers):
+            return urllib2.HTTPError(req, code, msg, headers, fp)
+    opener = urllib2.build_opener(Disable_HTTPRedirectHandler)
+
+    if custom_headers is not None:
+        opener.addheaders = custom_headers	
+	
+    if sso_token is not None:
+        opener.addheaders.append(('Cookie', 'crowd.token_key=' + sso_token))
+		
+    if forwarded_for is not None:
+        opener.addheaders.append(('X-Forwarded-For', forwarded_for))
+	
+    return opener.open(httpd_url + relative_url)
+    
+#validate_sso_token()
+
+class TestCrowdSSO(unittest2.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        print 'Start httpd.'
+        cls.httpd = start_httpd()
+
+        print 'Start demo app'
+        cls.demo_app = start_demo_app()
+		
+        wait_for_startup(url = httpd_url)
+        wait_for_startup(url = demo_app_url)
+        cls.valid_sso_token = get_sso_token()
+
+    @classmethod
+    def tearDownClass(cls):
+        print 'Stop httpd'
+        cls.httpd.kill()
+        cls.httpd.wait()
+    	
+        print 'Stop demo app'
+        cls.demo_app.shutdown()
+        cls.demo_app.socket.close()
+	
+    def test_redirect_to_login_for_no_sso_token(self):
+        response = http_get(relative_url = '/disallow_anon/')
+        self.assertEqual(response.code, 307, 'Incorrect HTTP response code')
+        self.assertEqual(response.headers['Location'], login_url + demo_app_public_url + '/disallow_anon/', 'Incorrect redirect login url')
+	
+    def test_redirect_to_login_for_invalid_sso_token(self):
+        response = http_get(relative_url = '/disallow_anon/', sso_token = 'INVALID-TOKEN')
+        self.assertEqual(response.code, 307, 'Incorrect HTTP response code')
+        self.assertEqual(response.headers['Location'], login_url + demo_app_public_url + '/disallow_anon/', 'Incorrect redirect login url')
+	
+    def test_access_for_valid_sso_token(self):
+        response = http_get(relative_url = '/disallow_anon/', sso_token = self.__class__.valid_sso_token)
+        self.assertEqual(response.code, 200, 'Incorrect HTTP response code')
+        self.assertEqual(response.headers['__ATL_USER'], 'admin', 'SSO headers should not be empty')
+        self.assertEqual(response.headers['__ATL_LOCAL_USER'], 'admin_alias', 'SSO headers should not be empty')
+        self.assertEqual(response.headers['__ATL_GROUP_MEMBERSHIP'], 'crowd-administrators', 'SSO headers should not be empty')
+	
+    def test_access_for_anon(self):
+        response = http_get(relative_url = '/allow_anon/')
+        self.assertEqual(response.code, 200, 'Incorrect HTTP response code')
+        self.assertEqual(response.headers['__ATL_USER'], 'None', 'SSO headers should be empty')
+        self.assertEqual(response.headers['__ATL_LOCAL_USER'], 'None', 'SSO headers should be empty')
+        self.assertEqual(response.headers['__ATL_GROUP_MEMBERSHIP'], 'None', 'SSO headers should be empty')
+        
+    def test_access_for_anon_and_invalid_sso_token(self):
+        response = http_get(relative_url = '/allow_anon/', sso_token = 'INVALID-TOKEN')
+        self.assertEqual(response.code, 200, 'Incorrect HTTP response code')
+        self.assertEqual(response.headers['__ATL_USER'], 'None', 'SSO headers should be empty')
+        self.assertEqual(response.headers['__ATL_LOCAL_USER'], 'None', 'SSO headers should be empty')
+        self.assertEqual(response.headers['__ATL_GROUP_MEMBERSHIP'], 'None', 'SSO headers should be empty')
+        
+    def test_access_for_anon_and_valid_sso_token(self):
+        response = http_get(relative_url = '/allow_anon/', sso_token = self.__class__.valid_sso_token)
+        self.assertEqual(response.code, 200, 'Incorrect HTTP response code')
+        self.assertEqual(response.headers['__ATL_USER'], 'admin', 'SSO headers should not be empty')
+        self.assertEqual(response.headers['__ATL_LOCAL_USER'], 'admin_alias', 'SSO headers should not be empty')
+        self.assertEqual(response.headers['__ATL_GROUP_MEMBERSHIP'], 'crowd-administrators', 'SSO headers should not be empty')
+	
+    def test_header_scrub_for_anon(self):
+        custom_headers = [('__TEST', 'test_control'), ('__ATL_USER', 'fake_user'), ('__ATL_LOCAL_USER', 'fake_user'), ('__ATL_GROUP_MEMBERSHIP', 'fake_group')]
+        response = http_get(relative_url = '/allow_anon/', custom_headers = custom_headers)
+        self.assertEqual(response.code, 200, 'Incorrect HTTP response code')
+        self.assertEqual(response.headers['__TEST'], 'test_control', '__TEST header should pass through')
+        self.assertEqual(response.headers['__ATL_USER'], 'None', 'SSO headers should be scrubbed')
+        self.assertEqual(response.headers['__ATL_LOCAL_USER'], 'None', 'SSO headers should be scrubbed')
+        self.assertEqual(response.headers['__ATL_GROUP_MEMBERSHIP'], 'None', 'SSO headers should be scrubbed')
+
+    def test_header_scrub_for_valid_sso_token(self):
+        custom_headers = [('__TEST', 'test_control'), ('__ATL_USER', 'fake_user'), ('__ATL_LOCAL_USER', 'fake_user'), ('__ATL_GROUP_MEMBERSHIP', 'fake_group')]
+        response = http_get(relative_url = '/disallow_anon/', custom_headers = custom_headers,  sso_token = self.__class__.valid_sso_token)
+        self.assertEqual(response.code, 200, 'Incorrect HTTP response code')
+        self.assertEqual(response.headers['__TEST'], 'test_control', '__TEST header should pass through')
+        self.assertEqual(response.headers['__ATL_USER'], 'admin', 'SSO headers should be scrubbed')
+        self.assertEqual(response.headers['__ATL_LOCAL_USER'], 'admin_alias', 'SSO headers should be scrubbed')
+        self.assertEqual(response.headers['__ATL_GROUP_MEMBERSHIP'], 'crowd-administrators', 'SSO headers should be scrubbed')
+
+if __name__ == "__main__":
+    unittest2.main()