diff --git a/doc/using.xml b/doc/using.xml index 4a3b733e..f38d63d7 100644 --- a/doc/using.xml +++ b/doc/using.xml @@ -104,7 +104,7 @@ chunked. - RFC 2617, HTTP Authentication: Basic and Digest Access Authentication + RFC 7616, HTTP Digest Access Authentication &neon; is not strictly compliant with the quoting rules given in the grammar for the Authorization @@ -114,7 +114,19 @@ (Microsoft® IIS 5) rejects the request if these parameters are not quoted. &neon; sends these parameters with quotes—this is not known to cause any problems with - other server implementations. + other server implementations. + + RFC 7616 predates RFC 9112 and uses conflicting language + around URIs. &neon; uses the RFC 9112 + request-target in both the + A2 grammar and the uri= + parameter of the Authorization + header. &neon; will accept (and resolve) any URI-reference in + the domain= parameter for + WWW-Authenticate response header + field. + + Namespaces in XML diff --git a/src/ne_auth.c b/src/ne_auth.c index a4422bfa..af287468 100644 --- a/src/ne_auth.c +++ b/src/ne_auth.c @@ -1,6 +1,6 @@ /* HTTP Authentication routines - Copyright (C) 1999-2021, Joe Orton + Copyright (C) 1999-2024, Joe Orton This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public @@ -226,8 +226,8 @@ struct auth_request { /*** Per-request details. ***/ ne_request *request; /* the request object. */ - /* The method and URI we are using for the current request */ - const char *uri; + /* The request-target and method for the current request, */ + const char *target; const char *method; int attempt; /* number of times this request has been retries due @@ -257,7 +257,7 @@ struct auth_protocol { * message to the error buffer 'errmsg'. */ int (*challenge)(auth_session *sess, int attempt, struct auth_challenge *chall, - const char *uri, ne_buffer **errmsg); + const char *target, ne_buffer **errmsg); /* Return the string to send in the -Authenticate request header: * (ne_malloc-allocated, NUL-terminated string) */ @@ -450,7 +450,7 @@ static char *get_scope_path(const char *uri) * Returns 0 if an valid challenge, else non-zero. */ static int basic_challenge(auth_session *sess, int attempt, struct auth_challenge *parms, - const char *uri, ne_buffer **errmsg) + const char *target, ne_buffer **errmsg) { char *tmp, password[ABUFSIZE]; @@ -481,14 +481,14 @@ static int basic_challenge(auth_session *sess, int attempt, ne__strzero(password, sizeof password); - if (strcmp(uri, "*") == 0) { - /* If the request-target is "*" the auth scope is explicitly - * the whole server. */ + if (strcmp(target, "*") == 0 || sess->context == AUTH_CONNECT) { + /* For CONNECT, or if the request-target is "*", the auth + * scope is implicitly the whole server. */ return 0; } sess->domains = ne_malloc(sizeof *sess->domains); - sess->domains[0] = get_scope_path(uri); + sess->domains[0] = get_scope_path(target); sess->ndomains = 1; NE_DEBUG(NE_DBG_HTTPAUTH, "auth: Basic auth scope is: %s\n", @@ -500,7 +500,7 @@ static int basic_challenge(auth_session *sess, int attempt, /* Add Basic authentication credentials to a request */ static char *request_basic(auth_session *sess, struct auth_request *req) { - if (sess->ndomains && !inside_domain(sess, req->uri)) { + if (sess->ndomains && !inside_domain(sess, req->target)) { return NULL; } @@ -634,7 +634,7 @@ static int continue_negotiate(auth_session *sess, const char *token, * if challenge is accepted. */ static int negotiate_challenge(auth_session *sess, int attempt, struct auth_challenge *chall, - const char *uri, ne_buffer **errmsg) + const char *target, ne_buffer **errmsg) { const char *token = chall->opaque; @@ -731,7 +731,7 @@ static int continue_sspi(auth_session *sess, int ntlm, const char *hdr) static int sspi_challenge(auth_session *sess, int attempt, struct auth_challenge *parms, - const char *uri, ne_buffer **errmsg) + const char *target, ne_buffer **errmsg) { int ntlm = ne_strcasecmp(parms->protocol->name, "NTLM") == 0; @@ -777,7 +777,7 @@ static int parse_domain(auth_session *sess, const char *domain) do { char *token = ne_token(&p, ' '); ne_uri rel, absolute; - + if (ne_uri_parse(token, &rel) == 0) { /* Resolve relative to the Request-URI. */ base.path = "/"; @@ -838,7 +838,7 @@ static char *request_ntlm(auth_session *sess, struct auth_request *request) static int ntlm_challenge(auth_session *sess, int attempt, struct auth_challenge *parms, - const char *uri, ne_buffer **errmsg) + const char *target, ne_buffer **errmsg) { int status; @@ -954,7 +954,7 @@ static char *get_digest_h_urp(auth_session *sess, ne_buffer **errmsg, * else non-zero. */ static int digest_challenge(auth_session *sess, int attempt, struct auth_challenge *parms, - const char *uri, ne_buffer **errmsg) + const char *target, ne_buffer **errmsg) { char *p, *h_urp = NULL; @@ -1064,9 +1064,9 @@ static int digest_challenge(auth_session *sess, int attempt, return 0; } -/* Returns non-zero if given Request-URI is inside the authentication - * domain defined for the session. */ -static int inside_domain(auth_session *sess, const char *req_uri) +/* Returns non-zero if given request-target is inside the + * authentication domain defined for the session. */ +static int inside_domain(auth_session *sess, const char *target) { int inside = 0; size_t n; @@ -1074,7 +1074,7 @@ static int inside_domain(auth_session *sess, const char *req_uri) /* Parse the Request-URI; it will be an absoluteURI if using a * proxy, and possibly '*'. */ - if (strcmp(req_uri, "*") == 0 || ne_uri_parse(req_uri, &uri) != 0) { + if (strcmp(target, "*") == 0 || ne_uri_parse(target, &uri) != 0) { /* Presume outside the authentication domain. */ return 0; } @@ -1104,12 +1104,15 @@ static char *request_digest(auth_session *sess, struct auth_request *req) /* 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)) { + if (sess->ndomains && !inside_domain(sess, req->target)) { return NULL; } - /* H(A2): https://tools.ietf.org/html/rfc7616#section-3.4.3 */ - h_a2 = ne_strhash(hash, req->method, ":", req->uri, NULL); + /* H(A2): https://tools.ietf.org/html/rfc7616#section-3.4.3 - Note + * that the RFC specifies that "request-uri" is used in the A2 + * grammar, which matches the RFC 9112 'request-target', which is + * what was passed through by ah_create. */ + h_a2 = ne_strhash(hash, req->method, ":", req->target, NULL); NE_DEBUG(NE_DBG_HTTPAUTH, "auth: H(A2): %s\n", h_a2); /* Calculate the 'response' to the Digest challenge to send the @@ -1140,7 +1143,7 @@ static char *request_digest(auth_session *sess, struct auth_request *req) ne_buffer_concat(ret, "Digest realm=\"", sess->realm, "\", " "nonce=\"", sess->nonce, "\", " - "uri=\"", req->uri, "\", " + "uri=\"", req->target, "\", " "response=\"", response, "\", " "algorithm=\"", sess->alg->name, "\"", NULL); @@ -1322,7 +1325,7 @@ static int verify_digest_response(struct auth_request *req, auth_session *sess, char *h_a2, *response; unsigned int hash = sess->alg->hash; - h_a2 = ne_strhash(hash, ":", req->uri, NULL); + h_a2 = ne_strhash(hash, ":", req->target, NULL); response = ne_strhash(hash, sess->h_a1, ":", sess->response_rhs, ":", h_a2, NULL); ne_free(h_a2); @@ -1589,7 +1592,7 @@ static int auth_challenge(auth_session *sess, int attempt, const char *uri, } static void ah_create(ne_request *req, void *session, const char *method, - const char *uri) + const char *target) { auth_session *sess = session; int is_connect = strcmp(method, "CONNECT") == 0; @@ -1603,7 +1606,7 @@ static void ah_create(ne_request *req, void *session, const char *method, NE_DEBUG(NE_DBG_HTTPAUTH, "auth: Create for %s\n", sess->spec->resp_hdr); areq->method = method; - areq->uri = uri; + areq->target = target; areq->request = req; ne_set_request_private(req, sess->spec->id, areq); @@ -1693,7 +1696,7 @@ static int ah_post_send(ne_request *req, void *cookie, const ne_status *status) /* note above: allow a 401 in response to a CONNECT request * from a proxy since some buggy proxies send that. */ NE_DEBUG(NE_DBG_HTTPAUTH, "auth: Got challenge (code %d).\n", status->code); - if (!auth_challenge(sess, areq->attempt++, areq->uri, auth_hdr)) { + if (!auth_challenge(sess, areq->attempt++, areq->target, auth_hdr)) { ret = NE_RETRY; } else { clean_session(sess); diff --git a/test/auth.c b/test/auth.c index 90a5f582..6f5a0dad 100644 --- a/test/auth.c +++ b/test/auth.c @@ -409,6 +409,7 @@ static void dup_header(char *header) #define PARM_LEGACY_ONLY (0x0100) #define PARM_QOP (0x0200) /* use qop= */ #define PARM_RFC2617 (0x0204) /* use algorithm= and qop= */ +#define PARM_OPTSTAR (0x0400) /* use OPTIONS * */ struct digest_parms { const char *realm, *nonce, *opaque, *domain; @@ -497,6 +498,8 @@ static char *make_digest(struct digest_state *state, struct digest_parms *parms, h_a1 = sess_h_a1; } + NE_DEBUG(NE_DBG_HTTP, "H(A2) from %s:%s\n", + !auth_info ? state->method : "", state->uri); h_a2 = hash(parms, !auth_info ? state->method : "", ":", state->uri, NULL); if (parms->flags & PARM_QOP) { @@ -767,14 +770,20 @@ static int serve_digest(ne_socket *sock, void *userdata) struct digest_parms *parms = userdata; struct digest_state state; char resp[NE_BUFSIZ], *rspdigest; + + state.method = "GET"; if ((parms->flags & PARM_PROXY)) state.uri = "http://www.example.com/fish"; else if (parms->domain) state.uri = "/fish/0"; + else if ((parms->flags & PARM_OPTSTAR)) { + state.method = "OPTIONS"; + state.uri = "*"; + } else state.uri = "/fish"; - state.method = "GET"; + state.realm = parms->realm; state.nonce = parms->nonce; state.opaque = parms->opaque; @@ -926,7 +935,10 @@ static int test_digest(struct digest_parms *parms) } do { - CALL(any_2xx_request(sess, "/fish")); + if (parms->flags & PARM_OPTSTAR) + CALL(any_2xx_request_method(sess, "OPTIONS", "*")); + else + CALL(any_2xx_request(sess, "/fish")); } while (--parms->num_requests); return destroy_and_wait(sess); @@ -969,6 +981,9 @@ static int digest(void) /* Proxy + nextnonce */ { "WallyWorld", "this-is-also-a-nonce", "opaque-string", NULL, ALG_MD5, PARM_RFC2617|PARM_AINFO|PARM_PROXY, 1, 0, fail_not }, + /* OPTIONS * test */ + { "WallyWorld", "options-nonce", "new-opaque", NULL, ALG_MD5, PARM_RFC2617|PARM_USERHASH|PARM_OPTSTAR, 1, 0, fail_not }, + { NULL } }; size_t n; diff --git a/test/common/child.c b/test/common/child.c index 14112225..17188f58 100644 --- a/test/common/child.c +++ b/test/common/child.c @@ -1,6 +1,6 @@ /* Framework for testing with a server process - Copyright (C) 2001-2010, Joe Orton + Copyright (C) 2001-2024, 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 @@ -386,9 +386,9 @@ int new_spawn_server2(int count, server_fn fn, void *userdata, *addr = ne_iaddr_make(ne_iaddr_ipv6, sa.in6.sin6_addr.s6_addr); } - NE_DEBUG(NE_DBG_SOCKET, "child using port %u\n", *port); + NE_DEBUG(NE_DBG_SOCKET, "child: using port %u\n", *port); - NE_DEBUG(NE_DBG_SOCKET, "child forking now...\n"); + NE_DEBUG(NE_DBG_SOCKET, "child: forking now...\n"); child = fork(); ONN("failed to fork server", child == -1); @@ -406,7 +406,7 @@ int new_spawn_server2(int count, server_fn fn, void *userdata, char errbuf[256]; int cret; - NE_DEBUG(NE_DBG_HTTP, "child iteration #%d (of %d), " + NE_DEBUG(NE_DBG_HTTP, "child: iteration #%d (of %d), " "awaiting connection...\n", iter, count); if (ne_sock_accept(sock, ls)) { @@ -415,19 +415,24 @@ int new_spawn_server2(int count, server_fn fn, void *userdata, exit(FAIL); } - NE_DEBUG(NE_DBG_HTTP, "child got connection, invoking server\n"); + NE_DEBUG(NE_DBG_HTTP, "child: got connection, invoking server\n"); + if (iter == count) { + close(ls); + NE_DEBUG(NE_DBG_HTTP, "child: closed listening socket.\n"); + } + ret = fn(sock, userdata); - NE_DEBUG(NE_DBG_HTTP, "child iteration #%d returns %d\n", + NE_DEBUG(NE_DBG_HTTP, "child: iteration #%d returns %d\n", iter, ret); cret = close_socket(sock); - NE_DEBUG(NE_DBG_HTTP, "child closed connection, %d: %s.\n", cret, + NE_DEBUG(NE_DBG_HTTP, "child: closed connection, %d: %s.\n", cret, cret ? ne_strerror(cret, errbuf, sizeof errbuf) : "no error"); } while (ret == 0 && ++iter <= count); - NE_DEBUG(NE_DBG_HTTP, "child terminating with %d\n", ret); + NE_DEBUG(NE_DBG_HTTP, "child: terminating with %d\n", ret); exit(ret); } diff --git a/test/ssl.c b/test/ssl.c index 2131d756..93f95b3e 100644 --- a/test/ssl.c +++ b/test/ssl.c @@ -469,28 +469,6 @@ static int simple(void) return accept_signed_cert(SERVER_CERT); } -#if 0 /* No longer works for modern SSL libraries, rightly so. */ -/* Test for SSL operation when server uses SSLv2 */ -static int simple_sslv2(void) -{ - ne_session *sess = ne_session_create("https", "localhost", 7777); - struct ssl_server_args args = {SERVER_CERT, 0}; - - args.use_ssl2 = 1; - ne_set_session_flag(sess, NE_SESSFLAG_SSLv2, 1); - - if (ne_get_session_flag(sess, NE_SESSFLAG_SSLv2) != 1) { - t_context("no SSLv2 support in SSL library"); - ne_session_destroy(sess); - return SKIP; - } - - CALL(any_ssl_request(sess, ssl_server, &args, CA_CERT, NULL, NULL)); - ne_session_destroy(sess); - return OK; -} -#endif - /* Test read-til-EOF behaviour with SSL. */ static int simple_eof(void) { @@ -1263,27 +1241,42 @@ static int proxy_tunnel(void) return OK; } -#define RESP_0LENGTH "HTTP/1.1 200 OK\r\n" "Content-Length: 0\r\n" "\r\n" +struct tunnel_args { + int iteration; + const char *first_response; /* first CONNECT response. */ + const char *second_response; /* second CONNECT response. */ + struct ssl_server_args *args; +}; -/* a tricky test which requires spawning a second server process in - * time for a new connection after a 407. */ -static int apt_post_send(ne_request *req, void *ud, const ne_status *st) +/* Server which acts as a proxy accepting a CONNECT request. */ +static int serve_auth_tunnel(ne_socket *sock, void *ud) { - int *code = ud; - if (st->code == *code) { - struct ssl_server_args args = {SERVER_CERT, NULL}; + struct tunnel_args *args = ud; + + /* check for a server auth function */ + want_header = "Authorization"; + got_header = tunnel_header; + got_server_auth = 0; + + CALL(discard_request(sock)); + + if (got_server_auth) { + SEND_STRING(sock, "HTTP/1.1 500 Leaked Server Auth Creds\r\n" + "Content-Length: 0\r\n" "Server: serve_tunnel\r\n\r\n"); + return 0; + } - if (*code == 407) args.numreqs = 2; - args.response = RESP_0LENGTH; - - NE_DEBUG(NE_DBG_HTTP, "Got challenge, awaiting server...\n"); - CALL(await_server()); - NE_DEBUG(NE_DBG_HTTP, "Spawning proper tunnel server...\n"); - /* serve *two* 200 OK responses. */ - CALL(spawn_server(7777, serve_tunnel, &args)); - NE_DEBUG(NE_DBG_HTTP, "Spawned.\n"); + if (args->iteration++ == 0) { + /* give the plaintext tunnel reply, acting as the proxy */ + + SEND_STRING(sock, args->first_response); + + return OK; } - return OK; + + SEND_STRING(sock, args->second_response); + + return ssl_server(sock, args->args); } static int apt_creds(void *userdata, const char *realm, int attempt, @@ -1299,23 +1292,27 @@ static int apt_creds(void *userdata, const char *realm, int attempt, * 0.24.0. */ static int auth_proxy_tunnel(void) { - ne_session *sess = ne_session_create("https", "localhost", 443); - int ret, code = 407; + struct ssl_server_args args = {SERVER_CERT, NULL}; + struct tunnel_args tunnel; + ne_session *sess; + + tunnel.first_response = + "HTTP/1.0 407 I WANT MORE BISCUITS\r\n" + "Server: auth_proxy_tunnel\r\n" + "Proxy-Authenticate: Basic realm=\"bigbluesea\"\r\n" + "Connection: close\r\n" "\r\n"; + tunnel.second_response = "HTTP/1.1 200 OK\r\n" + "Server: auth_proxy_tunnel r2\r\n\r\n"; + tunnel.iteration = 0; + tunnel.args = &args; + + CALL(proxied_multi_session_server(2, &sess, "https", "localhost", 443, + serve_auth_tunnel, &tunnel)); - ne_session_proxy(sess, "localhost", 7777); - ne_hook_post_send(sess, apt_post_send, &code); ne_set_proxy_auth(sess, apt_creds, NULL); ne_ssl_trust_cert(sess, def_ca_cert); - CALL(spawn_server(7777, single_serve_string, - "HTTP/1.0 407 I WANT MORE BISCUITS\r\n" - "Proxy-Authenticate: Basic realm=\"bigbluesea\"\r\n" - "Connection: close\r\n" "\r\n")); - - /* run two requests over the tunnel. */ - ret = any_2xx_request(sess, "/foobar"); - if (!ret) ret = any_2xx_request(sess, "/foobar2"); - CALL(ret); + CALL(any_2xx_request(sess, "/foobar")); return destroy_and_wait(sess); } @@ -1324,20 +1321,27 @@ static int auth_proxy_tunnel(void) * proxy in a CONNECT request. */ static int auth_tunnel_creds(void) { - ne_session *sess = ne_session_create("https", "localhost", 443); - int code = 401; - struct ssl_server_args args = {SERVER_CERT, 0}; + struct ssl_server_args args = {SERVER_CERT, NULL}; + struct tunnel_args tunnel; + ne_session *sess; + + args.response = "HTTP/1.1 401 I want a Shrubbery\r\n" + "WWW-Authenticate: Basic realm=\"bigredocean\"\r\n" + "Server: auth_tunnel_creds\r\n" "Content-Length: 0\r\n" "\r\n" + "" + "HTTP/1.1 200 OK\r\n\r\n"; + + tunnel.second_response = "HTTP/1.1 200 OK\r\n\r\n"; + tunnel.args = &args; + tunnel.iteration = 1; + + CALL(proxied_multi_session_server(2, &sess, "https", "localhost", 443, + serve_auth_tunnel, &tunnel)); - ne_session_proxy(sess, "localhost", 7777); - ne_hook_post_send(sess, apt_post_send, &code); ne_set_server_auth(sess, apt_creds, NULL); ne_ssl_trust_cert(sess, def_ca_cert); - args.response = "HTTP/1.1 401 I want a Shrubbery\r\n" - "WWW-Authenticate: Basic realm=\"bigredocean\"\r\n" - "Server: Python\r\n" "Content-Length: 0\r\n" "\r\n"; - - CALL(spawn_server(7777, serve_tunnel, &args)); + CALL(any_2xx_request(sess, "/foobar")); CALL(any_2xx_request(sess, "/foobar")); return destroy_and_wait(sess); @@ -1899,9 +1903,6 @@ ne_test tests[] = { T(clicert_import), T(simple), -#if 0 - T(simple_sslv2), -#endif T(simple_eof), T(empty_truncated_eof), T(fail_not_ssl), diff --git a/test/utils.c b/test/utils.c index 272747a0..8acfecfb 100644 --- a/test/utils.c +++ b/test/utils.c @@ -223,13 +223,14 @@ int session_server(ne_session **sess, server_fn fn, void *userdata) return multi_session_server(sess, "http", session_host, 1, fn, userdata); } -int proxied_session_server(ne_session **sess, const char *scheme, - const char *host, unsigned int fakeport, - server_fn fn, void *userdata) +int proxied_multi_session_server(int count, ne_session **sess, + const char *scheme, const char *host, + unsigned int fakeport, + server_fn fn, void *userdata) { unsigned int port; - CALL(new_spawn_server(1, fn, userdata, &port)); + CALL(new_spawn_server(count, fn, userdata, &port)); *sess = ne_session_create(scheme, host, fakeport); @@ -240,6 +241,15 @@ int proxied_session_server(ne_session **sess, const char *scheme, return OK; } + +int proxied_session_server(ne_session **sess, const char *scheme, + const char *host, unsigned int fakeport, + server_fn fn, void *userdata) +{ + return proxied_multi_session_server(1, sess, scheme, host, fakeport, + fn, userdata); +} + static void fakesess_destroy(void *userdata) { ne_inet_addr *addr = userdata; diff --git a/test/utils.h b/test/utils.h index fe8441c0..b7226e31 100644 --- a/test/utils.h +++ b/test/utils.h @@ -141,6 +141,11 @@ int proxied_session_server(ne_session **sess, const char *scheme, const char *host, unsigned int fakeport, server_fn fn, void *userdata); +int proxied_multi_session_server(int count, ne_session **sess, + const char *scheme, const char *host, + unsigned int fakeport, + server_fn fn, void *userdata); + /* As per proxied_session_server, but uses a "fake" (direct) TCP proxy * rather than an HTTP proxy. */ int fakeproxied_session_server(ne_session **sess, const char *scheme,