diff options
-rw-r--r-- | TODO | 4 | ||||
-rw-r--r-- | src/ne_auth.c | 113 | ||||
-rw-r--r-- | test/auth.c | 102 |
3 files changed, 191 insertions, 28 deletions
@@ -10,10 +10,6 @@ For one-point-oh Longer term ----------- -2. Add proper domain support to authentication code. (requires full - URI parsing support). Need to tell the auth layer the server - details. - 6. PUT with ranges... ne_put_range 9. DeltaV support (http://www.webdav.org/deltav/). See also the diff --git a/src/ne_auth.c b/src/ne_auth.c index 178d705..8bee30e 100644 --- a/src/ne_auth.c +++ b/src/ne_auth.c @@ -107,9 +107,7 @@ struct auth_handler { struct auth_challenge { const struct auth_protocol *protocol; struct auth_handler *handler; - const char *realm; - const char *nonce; - const char *opaque; + const char *realm, *nonce, *opaque, *domain; unsigned int stale; /* if stale=true */ unsigned int got_qop; /* we were given a qop directive */ unsigned int qop_auth; /* "auth" token in qop attrib */ @@ -178,6 +176,8 @@ typedef struct { char *nonce; char *cnonce; char *opaque; + char **domains; /* list of paths given as domain. */ + size_t ndomains; /* size of domains array */ auth_qop qop; auth_algorithm alg; unsigned int nonce_count; @@ -244,6 +244,16 @@ struct auth_protocol { static void challenge_error(ne_buffer **errmsg, const char *fmt, ...) ne_attribute((format(printf, 2, 3))); +/* Free the domains array, precondition sess->ndomains > 0. */ +static void free_domains(auth_session *sess) +{ + do { + ne_free(sess->domains[sess->ndomains - 1]); + } while (--sess->ndomains); + ne_free(sess->domains); + sess->domains = NULL; +} + static void clean_session(auth_session *sess) { if (sess->basic) ne_free(sess->basic); @@ -257,6 +267,7 @@ static void clean_session(auth_session *sess) ne_md5_destroy_ctx(sess->stored_rdig); sess->stored_rdig = NULL; } + if (sess->ndomains) free_domains(sess); #ifdef HAVE_GSSAPI { unsigned int major; @@ -612,6 +623,61 @@ static int sspi_challenge(auth_session *sess, int attempt, } #endif +/* Parse the "domain" challenge parameter and set the domains array up + * in the session appropriately. */ +static int parse_domain(auth_session *sess, const char *domain) +{ + char *cp = ne_strdup(domain), *p = cp; + ne_uri base = {0}; + int invalid = 0; + + ne_fill_server_uri(sess->sess, &base); + + do { + char *token = ne_token(&p, ','); + ne_uri rel, absolute; + + if (ne_uri_parse(token, &rel) == 0) { + /* Resolve relative to the Request-URI. */ + ne_uri_resolve(&base, &rel, &absolute); + + base.path = absolute.path; + + /* Ignore URIs not on this server. */ + if (ne_uri_cmp(&absolute, &base) == 0) { + sess->domains = ne_realloc(sess->domains, + ++sess->ndomains * + sizeof(*sess->domains)); + sess->domains[sess->ndomains - 1] = ne_strdup(absolute.path); + NE_DEBUG(NE_DBG_HTTPAUTH, "auth: Using domain %s from %s\n", + absolute.path, token); + absolute.path = NULL; + } + else { + NE_DEBUG(NE_DBG_HTTPAUTH, "auth: Ignoring domain %s\n", + token); + } + + ne_uri_free(&absolute); + } + else { + invalid = 1; + } + + ne_uri_free(&rel); + + } while (p && !invalid); + + if (invalid && sess->ndomains) { + free_domains(sess); + } + + ne_free(cp); + ne_uri_free(&base); + + return invalid; +} + /* Examine a digest challenge: return 0 if it is a valid Digest challenge, * else non-zero. */ static int digest_challenge(auth_session *sess, int attempt, @@ -648,6 +714,14 @@ static int digest_challenge(auth_session *sess, int attempt, /* Non-stale challenge: clear session and request credentials. */ clean_session(sess); + /* The domain paramater must be parsed after the session is + * cleaned; ignore domain for proxy auth. */ + if (parms->domain && sess->spec == &ah_server_class + && parse_domain(sess, parms->domain)) { + challenge_error(errmsg, _("could not parse domain in Digest challenge")); + return -1; + } + sess->realm = ne_strdup(parms->realm); sess->alg = parms->alg; sess->cnonce = get_cnonce(); @@ -721,6 +795,30 @@ static int digest_challenge(auth_session *sess, int attempt, return 0; } +/* Returns non-zero if given URI is inside the authentication domain + * defined for the session. */ +static int inside_domain(auth_session *sess, const char *uri) +{ + size_t n; + int inside = 0; + + /* For non-abs_path URIs, ignore. */ + if (uri[0] != '/') { + return 1; + } + + for (n = 0; n < sess->ndomains && !inside; n++) { + const char *d = sess->domains[n]; + + inside = (d[strlen(d)-1] == '/' && strncmp(uri, d, strlen(d)) == 0) + || strcmp(d, uri) == 0; + } + + NE_DEBUG(NE_DBG_HTTPAUTH, "auth: Inside auth domain: %d.\n", inside); + + return inside; +} + /* Return Digest authentication credentials header value for the given * session. */ static char *request_digest(auth_session *sess, struct auth_request *req) @@ -731,6 +829,12 @@ static char *request_digest(auth_session *sess, struct auth_request *req) const char *qop_value = "auth"; /* qop-value */ ne_buffer *ret; + /* Do not submit credentials if an auth domain is defined and this + * request-uri fails outside it. */ + if (sess->ndomains && !inside_domain(sess, req->uri)) { + return NULL; + } + /* Increase the nonce-count */ if (sess->qop != auth_qop_none) { sess->nonce_count++; @@ -1163,6 +1267,9 @@ static int auth_challenge(auth_session *sess, int attempt, chall->got_qop = chall->qop_auth; } + else if (ne_strcasecmp(key, "domain") == 0) { + chall->domain = val; + } } sess->protocol = NULL; diff --git a/test/auth.c b/test/auth.c index 7473346..19a632d 100644 --- a/test/auth.c +++ b/test/auth.c @@ -368,7 +368,7 @@ static void dup_header(char *header) } struct digest_parms { - const char *realm, *nonce, *opaque; + const char *realm, *nonce, *opaque, *domain; int rfc2617; int send_ainfo; int md5_sess; @@ -389,7 +389,8 @@ struct digest_parms { fail_ai_bad_cnonce, fail_ai_omit_cnonce, fail_ai_omit_digest, - fail_ai_omit_nc + fail_ai_omit_nc, + fail_outside_domain } failure; }; @@ -647,6 +648,10 @@ static char *make_digest_header(struct digest_state *state, ne_buffer_concat(buf, "opaque=\"", parms->opaque, "\", ", NULL); } + if (parms->domain) { + ne_buffer_concat(buf, "domain=\"", parms->domain, "\", ", NULL); + } + if (parms->failure == fail_req0_stale || parms->stale == parms->num_requests) { ne_buffer_concat(buf, "stale='true', ", NULL); @@ -664,7 +669,12 @@ static int serve_digest(ne_socket *sock, void *userdata) struct digest_state state; char resp[NE_BUFSIZ]; - state.uri = parms->proxy ? "http://www.example.com/fish" : "/fish"; + if (parms->proxy) + state.uri = "http://www.example.com/fish"; + else if (parms->domain) + state.uri = "/fish/0"; + else + state.uri = "/fish"; state.method = "GET"; state.realm = parms->realm; state.nonce = parms->nonce; @@ -708,11 +718,31 @@ static int serve_digest(ne_socket *sock, void *userdata) } do { + digest_hdr = NULL; CALL(discard_request(sock)); - ONN("no Authorization header sent", digest_hdr == NULL); - - CALL(verify_digest_header(&state, parms, digest_hdr)); + if (digest_hdr && parms->domain && (parms->num_requests & 1) == 0) { + SEND_STRING(sock, "HTTP/1.1 400 Used Auth Outside Domain\r\n\r\n"); + return OK; + } + else if (parms->domain && (parms->num_requests & 1) == 0) { + /* Do nothing. */ + } + else if (parms->domain && parms->num_requests == 5) { + /* next URI */ + state.uri = "/fish/2"; + } + else if (parms->domain && parms->num_requests == 3) { + state.uri = "/other"; + } + else if (parms->domain && parms->num_requests == 1) { + state.uri = "*"; + } + else { + ONN("no Authorization header sent", digest_hdr == NULL); + + CALL(verify_digest_header(&state, parms, digest_hdr)); + } if (parms->num_requests == parms->stale) { state.nonce = ne_concat("stale-", state.nonce, NULL); @@ -781,27 +811,27 @@ static int digest(void) { struct digest_parms parms[] = { /* RFC 2617-style */ - { "WallyWorld", "this-is-a-nonce", NULL, 1, 0, 0, 0, 0, 1, 0, fail_not }, - { "WallyWorld", "this-is-also-a-nonce", "opaque-string", 1, 0, 0, 0, 0, 1, 0, fail_not }, + { "WallyWorld", "this-is-a-nonce", NULL, NULL, 1, 0, 0, 0, 0, 1, 0, fail_not }, + { "WallyWorld", "this-is-also-a-nonce", "opaque-string", NULL, 1, 0, 0, 0, 0, 1, 0, fail_not }, /* ... with A-I */ - { "WallyWorld", "nonce-nonce-nonce", "opaque-string", 1, 1, 0, 0, 0, 1, 0, fail_not }, + { "WallyWorld", "nonce-nonce-nonce", "opaque-string", NULL, 1, 1, 0, 0, 0, 1, 0, fail_not }, /* ... with md5-sess. */ - { "WallyWorld", "nonce-nonce-nonce", "opaque-string", 1, 1, 1, 0, 0, 1, 0, fail_not }, + { "WallyWorld", "nonce-nonce-nonce", "opaque-string", NULL, 1, 1, 1, 0, 0, 1, 0, fail_not }, /* many requests, with changing nonces; tests for next-nonce handling bug. */ - { "WallyWorld", "this-is-a-nonce", "opaque-thingy", 1, 1, 0, 0, 1, 20, 0, fail_not }, + { "WallyWorld", "this-is-a-nonce", "opaque-thingy", NULL, 1, 1, 0, 0, 1, 20, 0, fail_not }, /* staleness. */ - { "WallyWorld", "this-is-a-nonce", "opaque-thingy", 1, 1, 0, 0, 0, 3, 2, fail_not }, + { "WallyWorld", "this-is-a-nonce", "opaque-thingy", NULL, 1, 1, 0, 0, 0, 3, 2, fail_not }, /* RFC 2069-style */ - { "WallyWorld", "lah-di-da-di-dah", NULL, 0, 0, 0, 0, 0, 1, 0, fail_not }, - { "WallyWorld", "fee-fi-fo-fum", "opaque-string", 0, 0, 0, 0, 0, 1, 0, fail_not }, - { "WallyWorld", "fee-fi-fo-fum", "opaque-string", 0, 1, 0, 0, 0, 1, 0, fail_not }, + { "WallyWorld", "lah-di-da-di-dah", NULL, NULL, 0, 0, 0, 0, 0, 1, 0, fail_not }, + { "WallyWorld", "fee-fi-fo-fum", "opaque-string", NULL, 0, 0, 0, 0, 0, 1, 0, fail_not }, + { "WallyWorld", "fee-fi-fo-fum", "opaque-string", NULL, 0, 1, 0, 0, 0, 1, 0, fail_not }, /* Proxy auth */ - { "WallyWorld", "this-is-also-a-nonce", "opaque-string", 1, 1, 0, 0, 0, 1, 0, fail_not }, + { "WallyWorld", "this-is-also-a-nonce", "opaque-string", NULL, 1, 1, 0, 0, 0, 1, 0, fail_not }, /* Proxy + A-I */ - { "WallyWorld", "this-is-also-a-nonce", "opaque-string", 1, 1, 0, 1, 0, 1, 0, fail_not }, + { "WallyWorld", "this-is-also-a-nonce", "opaque-string", NULL, 1, 1, 0, 1, 0, 1, 0, fail_not }, { NULL } }; @@ -835,16 +865,14 @@ static int digest_failures(void) }; size_t n; + memset(&parms, 0, sizeof parms); + parms.realm = "WallyWorld"; parms.nonce = "random-invented-string"; parms.opaque = NULL; parms.rfc2617 = 1; parms.send_ainfo = 1; - parms.md5_sess = 0; - parms.proxy = 0; - parms.send_nextnonce = 0; parms.num_requests = 1; - parms.stale = 0; for (n = 0; fails[n].message; n++) { ne_session *sess = ne_session_create("http", "localhost", 7777); @@ -906,6 +934,8 @@ static int fail_challenge(void) "unknown algorithm in Digest challenge" }, { "Digest algorithm=MD5-sess, realm=\"foo\"", "incompatible algorithm in Digest challenge" }, + { "Digest algorithm=MD5, qop=auth, nonce=\"foo\", realm=\"foo\", " + "domain=\"http://[::1/\"", "could not parse domain" }, /* Multiple challenge failure cases: */ { "Basic, Digest", @@ -1016,6 +1046,35 @@ static int multi_handler(void) return await_server(); } +static int domains(void) +{ + ne_session *sess; + struct digest_parms parms; + + memset(&parms, 0, sizeof parms); + parms.realm = "WallyWorld"; + parms.rfc2617 = 1; + parms.nonce = "agoog"; + parms.domain = "http://localhost:7777/fish/,https://example.com/agaor,/other"; + parms.num_requests = 7; + + CALL(make_session(&sess, serve_digest, &parms)); + + ne_set_server_auth(sess, auth_cb, NULL); + + CALL(any_2xx_request(sess, "/fish/0")); + CALL(any_2xx_request(sess, "/outside")); + CALL(any_2xx_request(sess, "/other")); + CALL(any_2xx_request(sess, "/fish")); + CALL(any_2xx_request(sess, "/fish/2")); + CALL(any_2xx_request(sess, "/goop")); + CALL(any_2xx_request(sess, "*")); + + ne_session_destroy(sess); + + return await_server(); +} + /* test logout */ /* proxy auth, proxy AND origin */ @@ -1031,5 +1090,6 @@ ne_test tests[] = { T(digest_failures), T(fail_challenge), T(multi_handler), + T(domains), T(NULL) }; |