/* Authentication tests Copyright (C) 2001-2006, Joe Orton This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include "config.h" #include #ifdef HAVE_STDLIB_H #include #endif #ifdef HAVE_UNISTD_H #include #endif #include "ne_request.h" #include "ne_auth.h" #include "ne_basic.h" #include "ne_md5.h" #include "tests.h" #include "child.h" #include "utils.h" static const char username[] = "Aladdin", password[] = "open sesame"; static int auth_failed; #define BASIC_WALLY "Basic realm=WallyWorld" #define CHAL_WALLY "WWW-Authenticate: " BASIC_WALLY #define EOL "\r\n" static int auth_cb(void *userdata, const char *realm, int tries, char *un, char *pw) { if (strcmp(realm, "WallyWorld")) { NE_DEBUG(NE_DBG_HTTP, "Got wrong realm '%s'!\n", realm); return -1; } strcpy(un, username); strcpy(pw, password); return tries; } static void auth_hdr(char *value) { #define B "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" auth_failed = strcmp(value, B); NE_DEBUG(NE_DBG_HTTP, "Got auth header: [%s]\nWanted header: [%s]\n" "Result: %d\n", value, B, auth_failed); #undef B } /* Sends a response with given response-code. If hdr is not NULL, * sends that header string too (appending an EOL). If eoc is * non-zero, request must be last sent down a connection; otherwise, * clength 0 is sent to maintain a persistent connection. */ static int send_response(ne_socket *sock, const char *hdr, int code, int eoc) { char buffer[BUFSIZ]; sprintf(buffer, "HTTP/1.1 %d Blah Blah" EOL, code); if (hdr) { strcat(buffer, hdr); strcat(buffer, EOL); } if (eoc) { strcat(buffer, "Connection: close" EOL EOL); } else { strcat(buffer, "Content-Length: 0" EOL EOL); } return SEND_STRING(sock, buffer); } /* Server function which sends two responses: first requires auth, * second doesn't. */ static int auth_serve(ne_socket *sock, void *userdata) { char *hdr = userdata; auth_failed = 1; /* Register globals for discard_request. */ got_header = auth_hdr; want_header = "Authorization"; discard_request(sock); send_response(sock, hdr, 401, 0); discard_request(sock); send_response(sock, NULL, auth_failed?500:200, 1); return 0; } /* Test that various Basic auth challenges are correctly handled. */ static int basic(void) { const char *hdrs[] = { /* simplest case */ CHAL_WALLY, /* several challenges, one header */ "WWW-Authenticate: BarFooScheme, " BASIC_WALLY, /* several challenges, one header */ CHAL_WALLY ", BarFooScheme realm=\"PenguinWorld\"", /* whitespace tests. */ "WWW-Authenticate: Basic realm=WallyWorld ", /* nego test. */ "WWW-Authenticate: Negotiate fish, Basic realm=WallyWorld", /* nego test. */ "WWW-Authenticate: Negotiate fish, bar=boo, Basic realm=WallyWorld", /* nego test. */ "WWW-Authenticate: Negotiate, Basic realm=WallyWorld", /* multi-header case 1 */ "WWW-Authenticate: BarFooScheme\r\n" CHAL_WALLY, /* multi-header cases 1 */ CHAL_WALLY "\r\n" "WWW-Authenticate: BarFooScheme bar=\"foo\"", /* multi-header case 3 */ "WWW-Authenticate: FooBarChall foo=\"bar\"\r\n" CHAL_WALLY "\r\n" "WWW-Authenticate: BarFooScheme bar=\"foo\"" }; size_t n; for (n = 0; n < sizeof(hdrs)/sizeof(hdrs[0]); n++) { ne_session *sess; CALL(make_session(&sess, auth_serve, (void *)hdrs[n])); ne_set_server_auth(sess, auth_cb, NULL); CALL(any_2xx_request(sess, "/norman")); ne_session_destroy(sess); CALL(await_server()); } return OK; } static int retry_serve(ne_socket *sock, void *ud) { discard_request(sock); send_response(sock, CHAL_WALLY, 401, 0); discard_request(sock); send_response(sock, CHAL_WALLY, 401, 0); discard_request(sock); send_response(sock, NULL, 200, 0); discard_request(sock); send_response(sock, CHAL_WALLY, 401, 0); discard_request(sock); send_response(sock, NULL, 200, 0); discard_request(sock); send_response(sock, NULL, 200, 0); discard_request(sock); send_response(sock, NULL, 200, 0); discard_request(sock); send_response(sock, CHAL_WALLY, 401, 0); discard_request(sock); send_response(sock, NULL, 200, 0); discard_request(sock); send_response(sock, CHAL_WALLY, 401, 0); discard_request(sock); send_response(sock, CHAL_WALLY, 401, 0); discard_request(sock); send_response(sock, CHAL_WALLY, 401, 0); discard_request(sock); send_response(sock, NULL, 200, 0); return OK; } static int retry_cb(void *userdata, const char *realm, int tries, char *un, char *pw) { int *count = userdata; /* dummy creds; server ignores them anyway. */ strcpy(un, "a"); strcpy(pw, "b"); switch (*count) { case 0: case 1: if (tries == *count) { *count += 1; return 0; } else { t_context("On request #%d, got attempt #%d", *count, tries); *count = -1; return 1; } break; case 2: case 3: /* server fails a subsequent request, check that tries has * reset to zero. */ if (tries == 0) { *count += 1; return 0; } else { t_context("On retry after failure #%d, tries was %d", *count, tries); *count = -1; return 1; } break; case 4: case 5: if (tries > 1) { t_context("Attempt counter reached #%d", tries); *count = -1; return 1; } return tries; default: t_context("Count reached %d!?", *count); *count = -1; } return 1; } /* Test that auth retries are working correctly. */ static int retries(void) { ne_session *sess; int count = 0; CALL(make_session(&sess, retry_serve, NULL)); ne_set_server_auth(sess, retry_cb, &count); /* This request will be 401'ed twice, then succeed. */ ONREQ(any_request(sess, "/foo")); /* auth_cb will have set up context. */ CALL(count != 2); /* this request will be 401'ed once, then succeed. */ ONREQ(any_request(sess, "/foo")); /* auth_cb will have set up context. */ CALL(count != 3); /* some 20x requests. */ ONREQ(any_request(sess, "/foo")); ONREQ(any_request(sess, "/foo")); /* this request will be 401'ed once, then succeed. */ ONREQ(any_request(sess, "/foo")); /* auth_cb will have set up context. */ CALL(count != 4); /* First request is 401'ed by the server at both attempts. */ ONV(any_request(sess, "/foo") != NE_AUTH, ("auth succeeded, should have failed: %s", ne_get_error(sess))); count++; /* Second request is 401'ed first time, then will succeed if * retried. 0.18.0 didn't reset the attempt counter though so * this didn't work. */ ONV(any_request(sess, "/foo") == NE_AUTH, ("auth failed on second try, should have succeeded: %s", ne_get_error(sess))); ne_session_destroy(sess); CALL(await_server()); return OK; } /* crashes with neon <0.22 */ static int forget_regress(void) { ne_session *sess = ne_session_create("http", "localhost", 7777); ne_forget_auth(sess); ne_session_destroy(sess); return OK; } static int fail_auth_cb(void *ud, const char *realm, int attempt, char *un, char *pw) { return 1; } /* this may trigger a segfault in neon 0.21.x and earlier. */ static int tunnel_regress(void) { ne_session *sess = ne_session_create("https", "localhost", 443); ne_session_proxy(sess, "localhost", 7777); ne_set_server_auth(sess, fail_auth_cb, NULL); CALL(spawn_server(7777, single_serve_string, "HTTP/1.1 401 Auth failed.\r\n" "WWW-Authenticate: Basic realm=asda\r\n" "Content-Length: 0\r\n\r\n")); any_request(sess, "/foo"); ne_session_destroy(sess); CALL(await_server()); return OK; } /* regression test for parsing a Negotiate challenge with on parameter * token. */ static int negotiate_regress(void) { ne_session *sess = ne_session_create("http", "localhost", 7777); ne_set_server_auth(sess, fail_auth_cb, NULL); CALL(spawn_server(7777, single_serve_string, "HTTP/1.1 401 Auth failed.\r\n" "WWW-Authenticate: Negotiate\r\n" "Content-Length: 0\r\n\r\n")); any_request(sess, "/foo"); ne_session_destroy(sess); CALL(await_server()); return OK; } static char *digest_hdr = NULL; static void dup_header(char *header) { if (digest_hdr) ne_free(digest_hdr); digest_hdr = ne_strdup(header); } struct digest_parms { const char *realm, *nonce, *opaque; int rfc2617; int send_ainfo; int md5_sess; int proxy; enum digest_failure { fail_not, fail_bogus_alg, fail_omit_qop, fail_omit_realm, fail_omit_nonce, fail_ai_bad_nc, fail_ai_bad_digest, fail_ai_bad_cnonce, fail_ai_omit_cnonce, fail_ai_omit_digest, fail_ai_omit_nc } failure; }; struct digest_state { const char *realm, *nonce, *uri, *username, *password, *algorithm, *qop, *method, *opaque; char *cnonce, *digest, *ncval; long nc; }; /* Write the request-digest into 'digest' (or response-digest if * auth_info is non-zero) for given digest auth state and * parameters. */ static void make_digest(struct digest_state *state, struct digest_parms *parms, int auth_info, char digest[33]) { struct ne_md5_ctx *ctx; char h_a1[33], h_a2[33]; /* H(A1) */ ctx = ne_md5_create_ctx(); ne_md5_process_bytes(state->username, strlen(state->username), ctx); ne_md5_process_bytes(":", 1, ctx); ne_md5_process_bytes(state->realm, strlen(state->realm), ctx); ne_md5_process_bytes(":", 1, ctx); ne_md5_process_bytes(state->password, strlen(state->password), ctx); ne_md5_finish_ascii(ctx, h_a1); if (parms->md5_sess) { ne_md5_reset_ctx(ctx); ne_md5_process_bytes(h_a1, 32, ctx); ne_md5_process_bytes(":", 1, ctx); ne_md5_process_bytes(state->nonce, strlen(state->nonce), ctx); ne_md5_process_bytes(":", 1, ctx); ne_md5_process_bytes(state->cnonce, strlen(state->cnonce), ctx); ne_md5_finish_ascii(ctx, h_a1); } /* H(A2) */ ne_md5_reset_ctx(ctx); if (!auth_info) ne_md5_process_bytes(state->method, strlen(state->method), ctx); ne_md5_process_bytes(":", 1, ctx); ne_md5_process_bytes(state->uri, strlen(state->uri), ctx); ne_md5_finish_ascii(ctx, h_a2); /* request-digest */ ne_md5_reset_ctx(ctx); ne_md5_process_bytes(h_a1, strlen(h_a1), ctx); ne_md5_process_bytes(":", 1, ctx); ne_md5_process_bytes(state->nonce, strlen(state->nonce), ctx); ne_md5_process_bytes(":", 1, ctx); if (parms->rfc2617) { ne_md5_process_bytes(state->ncval, strlen(state->ncval), ctx); ne_md5_process_bytes(":", 1, ctx); ne_md5_process_bytes(state->cnonce, strlen(state->cnonce), ctx); ne_md5_process_bytes(":", 1, ctx); ne_md5_process_bytes(state->qop, strlen(state->qop), ctx); ne_md5_process_bytes(":", 1, ctx); } ne_md5_process_bytes(h_a2, strlen(h_a2), ctx); ne_md5_finish_ascii(ctx, digest); ne_md5_destroy_ctx(ctx); } /* Verify that the response-digest matches expected state. */ static int check_digest(struct digest_state *state, struct digest_parms *parms) { char digest[33]; make_digest(state, parms, 0, digest); NE_DEBUG(NE_DBG_HTTP, "Got digest: %s, expected: %s\n", state->digest, digest); ONCMP(digest, state->digest, "Digest response", "request-digest"); return OK; } #define DIGCMP(field) \ do { \ ONCMP(state->field, newstate.field, \ "Digest response header", #field); \ } while (0) #define PARAM(field) \ do { \ if (ne_strcasecmp(name, #field) == 0) { \ ONV(newstate.field != NULL, \ ("received multiple %s params: %s, %s", #field, \ newstate.field, val)); \ newstate.field = val; \ } \ } while (0) /* Verify that Digest auth request header, 'header', meets expected * state and parameters. */ static int verify_digest_header(struct digest_state *state, struct digest_parms *parms, char *header) { char *ptr; struct digest_state newstate = {0}; ptr = ne_token(&header, ' '); ONCMP("Digest", ptr, "Digest response", "scheme name"); while (header) { char *name, *val; ptr = ne_qtoken(&header, ',', "\"\'"); ONN("quoting broken", ptr == NULL); name = ne_shave(ptr, " "); val = strchr(name, '='); ONV(val == NULL, ("bad name/value pair: %s", val)); *val++ = '\0'; val = ne_shave(val, "\"\' "); NE_DEBUG(NE_DBG_HTTP, "got field: [%s] = [%s]\n", name, val); PARAM(uri); PARAM(realm); PARAM(username); PARAM(nonce); PARAM(algorithm); PARAM(qop); PARAM(opaque); PARAM(cnonce); if (ne_strcasecmp(name, "nc") == 0) { long nc = strtol(val, NULL, 16); ONV(nc != state->nc, ("got bad nonce count: %ld (%s) not %ld", nc, val, state->nc)); state->ncval = ne_strdup(val); } else if (ne_strcasecmp(name, "response") == 0) { state->digest = ne_strdup(val); } } ONN("cnonce param missing for 2617-style auth", parms->rfc2617 && !newstate.cnonce); DIGCMP(realm); DIGCMP(username); DIGCMP(uri); DIGCMP(nonce); DIGCMP(opaque); DIGCMP(algorithm); if (parms->rfc2617) { DIGCMP(qop); } if (newstate.cnonce) { state->cnonce = ne_strdup(newstate.cnonce); } ONN("no digest param given", !state->digest); CALL(check_digest(state, parms)); return OK; } static char *make_authinfo_header(struct digest_state *state, struct digest_parms *parms) { ne_buffer *buf = ne_buffer_create(); char digest[33], *ncval, *cnonce; if (parms->failure == fail_ai_bad_digest) { strcpy(digest, "fish"); } else { make_digest(state, parms, 1, digest); } if (parms->failure == fail_ai_bad_nc) { ncval = "999"; } else { ncval = state->ncval; } if (parms->failure == fail_ai_bad_cnonce) { cnonce = "another-fish"; } else { cnonce = state->cnonce; } if (parms->proxy) { ne_buffer_czappend(buf, "Proxy-"); } ne_buffer_czappend(buf, "Authentication-Info: "); if (!parms->rfc2617) { ne_buffer_concat(buf, "rspauth=\"", digest, "\"", NULL); } else { if (parms->failure != fail_ai_omit_nc) { ne_buffer_concat(buf, "nc=", ncval, ", ", NULL); } if (parms->failure != fail_ai_omit_cnonce) { ne_buffer_concat(buf, "cnonce=\"", cnonce, "\", ", NULL); } if (parms->failure != fail_ai_omit_digest) { ne_buffer_concat(buf, "rspauth=\"", digest, "\", ", NULL); } ne_buffer_czappend(buf, "qop=\"auth\""); } return ne_buffer_finish(buf); } static char *make_digest_header(struct digest_state *state, struct digest_parms *parms) { ne_buffer *buf = ne_buffer_create(); const char *algorithm; algorithm = parms->failure == fail_bogus_alg ? "fish" : state->algorithm; ne_buffer_concat(buf, parms->proxy ? "Proxy-Authenticate" : "WWW-Authenticate", ": Digest " "realm=\"", parms->realm, "\", ", NULL); if (parms->rfc2617) { ne_buffer_concat(buf, "algorithm=\"", algorithm, "\", ", "qop=\"", state->qop, "\", ", NULL); } if (parms->opaque) { ne_buffer_concat(buf, "opaque=\"", parms->opaque, "\", ", NULL); } ne_buffer_concat(buf, "nonce=\"", state->nonce, "\"", NULL); return ne_buffer_finish(buf); } /* Server process for Digest auth handling. */ static int serve_digest(ne_socket *sock, void *userdata) { struct digest_parms *parms = userdata; struct digest_state state; char resp[NE_BUFSIZ]; state.uri = parms->proxy ? "http://www.example.com/fish" : "/fish"; state.method = "GET"; state.realm = parms->realm; state.nonce = parms->nonce; state.opaque = parms->opaque; state.username = username; state.password = password; state.nc = 1; state.algorithm = parms->md5_sess ? "MD5-sess" : "MD5"; state.qop = "auth"; state.cnonce = state.digest = state.ncval = NULL; want_header = parms->proxy ? "Proxy-Authorization" : "Authorization"; digest_hdr = NULL; got_header = dup_header; CALL(discard_request(sock)); ONV(digest_hdr != NULL, ("got unwarranted WWW-Auth header: %s", digest_hdr)); ne_snprintf(resp, sizeof resp, "HTTP/1.1 %d Auth Denied\r\n" "%s\r\n" "Content-Length: 0\r\n" "\r\n", parms->proxy ? 407 : 401, make_digest_header(&state, parms)); SEND_STRING(sock, resp); CALL(discard_request(sock)); ONN("no Authorization header sent", digest_hdr == NULL); CALL(verify_digest_header(&state, parms, digest_hdr)); if (parms->send_ainfo) { char *ai = make_authinfo_header(&state, parms); ne_snprintf(resp, sizeof resp, "HTTP/1.1 200 Well, if you insist\r\n" "Content-Length: 0\r\n" "%s\r\n" "\r\n", ai); ne_free(ai); } else { ne_snprintf(resp, sizeof resp, "HTTP/1.1 200 You did good\r\n" "Content-Length: 0\r\n" "\r\n"); } SEND_STRING(sock, resp); return OK; } static int test_digest(struct digest_parms *parms) { ne_session *sess; if (parms->proxy) { sess = ne_session_create("http", "www.example.com", 80); ne_session_proxy(sess, "localhost", 7777); ne_set_proxy_auth(sess, auth_cb, NULL); } else { sess = ne_session_create("http", "localhost", 7777); ne_set_server_auth(sess, auth_cb, NULL); } CALL(spawn_server(7777, serve_digest, parms)); CALL(any_2xx_request(sess, "/fish")); ne_session_destroy(sess); return await_server(); } /* Test for RFC2617-style Digest auth. */ static int digest(void) { struct digest_parms parms[] = { /* RFC 2617-style */ { "WallyWorld", "this-is-a-nonce", NULL, 1, 0, 0, 0, fail_not }, { "WallyWorld", "this-is-also-a-nonce", "opaque-string", 1, 0, 0, 0, fail_not }, /* ... with A-I */ { "WallyWorld", "nonce-nonce-nonce", "opaque-string", 1, 1, 0, 0, fail_not }, /* ... with md5-sess. */ { "WallyWorld", "nonce-nonce-nonce", "opaque-string", 1, 1, 1, 0, fail_not }, /* RFC 2069-style */ { "WallyWorld", "lah-di-da-di-dah", NULL, 0, 0, 0, 0, fail_not }, { "WallyWorld", "fee-fi-fo-fum", "opaque-string", 0, 0, 0, 0, fail_not }, { "WallyWorld", "fee-fi-fo-fum", "opaque-string", 0, 1, 0, 0, fail_not }, /* Proxy auth */ { "WallyWorld", "this-is-also-a-nonce", "opaque-string", 1, 1, 0, 0, fail_not }, /* Proxy + A-I */ { "WallyWorld", "this-is-also-a-nonce", "opaque-string", 1, 1, 0, 1, fail_not }, { NULL } }; size_t n; for (n = 0; parms[n].realm; n++) { CALL(test_digest(&parms[n])); } return OK; } static int digest_failures(void) { struct digest_parms parms; static const struct { enum digest_failure mode; const char *message; } fails[] = { { fail_ai_bad_nc, "nonce count mismatch" }, { fail_ai_bad_digest, "digest mismatch" }, { fail_ai_bad_cnonce, "client nonce mismatch" }, { fail_ai_omit_nc, "missing parameters" }, { fail_ai_omit_digest, "missing parameters" }, { fail_ai_omit_cnonce, "missing parameters" }, { fail_bogus_alg, "Unknown algorithm" }, { fail_not, NULL } }; size_t n; 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; for (n = 0; fails[n].message; n++) { ne_session *sess = ne_session_create("http", "localhost", 7777); int ret; parms.failure = fails[n].mode; ne_set_server_auth(sess, auth_cb, NULL); CALL(spawn_server(7777, serve_digest, &parms)); ret = any_2xx_request(sess, "/fish"); ONV(ret == NE_OK, ("request success; expecting error '%s'", fails[n].message)); ONV(strstr(ne_get_error(sess), fails[n].message) == NULL, ("request fails with error '%s'; expecting '%s'", ne_get_error(sess), fails[n].message)); ne_session_destroy(sess); if (fails[n].mode == fail_bogus_alg) { reap_server(); } else { CALL(await_server()); } } return OK; } /* test that digest has precedence over Basic for multi-scheme * challenges */ /* test logout */ /* proxy auth, proxy AND origin */ ne_test tests[] = { T(lookup_localhost), T(basic), T(retries), T(forget_regress), T(tunnel_regress), T(negotiate_regress), T(digest), T(digest_failures), T(NULL) };