diff --git a/CMakeLists-implied-options.txt b/CMakeLists-implied-options.txt index dc82bba96..e3e800419 100644 --- a/CMakeLists-implied-options.txt +++ b/CMakeLists-implied-options.txt @@ -179,6 +179,10 @@ if (LWS_WITH_SECURE_STREAMS_AUTH_SIGV4) set(LWS_WITH_GENCRYPTO 1) endif() +if (LWS_WITH_HTTP_DIGEST_AUTH) + set(LWS_WITH_GENCRYPTO 1) +endif() + if (APPLE) set(LWS_ROLE_DBUS 0) endif() diff --git a/CMakeLists.txt b/CMakeLists.txt index 5156f67ff..bce26a794 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,6 +159,7 @@ option(LWS_WITH_SYS_ASYNC_DNS "Nonblocking internal IPv4 + IPv6 DNS resolver" OF option(LWS_WITH_SYS_NTPCLIENT "Build in tiny ntpclient good for tls date validation and run via lws_system" OFF) option(LWS_WITH_SYS_DHCP_CLIENT "Build in tiny DHCP client" OFF) option(LWS_WITH_HTTP_BASIC_AUTH "Support Basic Auth" ON) +option(LWS_WITH_HTTP_DIGEST_AUTH "Support Digest Auth (caution deprecated crypto)" ON) option(LWS_WITH_HTTP_UNCOMMON_HEADERS "Include less common http header support" ON) option(LWS_WITH_SYS_STATE "lws_system state support" ON) option(LWS_WITH_SYS_SMD "Lws System Message Distribution" ON) diff --git a/cmake/lws_config.h.in b/cmake/lws_config.h.in index dfd25d52e..3a1ec0d77 100644 --- a/cmake/lws_config.h.in +++ b/cmake/lws_config.h.in @@ -179,6 +179,7 @@ #cmakedefine LWS_WITH_GZINFLATE #cmakedefine LWS_WITH_HTTP2 #cmakedefine LWS_WITH_HTTP_BASIC_AUTH +#cmakedefine LWS_WITH_HTTP_DIGEST_AUTH #cmakedefine LWS_WITH_HTTP_BROTLI #cmakedefine LWS_HTTP_HEADERS_ALL #cmakedefine LWS_WITH_HTTP_PROXY diff --git a/include/libwebsockets/lws-client.h b/include/libwebsockets/lws-client.h index dd474a9cf..9108484de 100644 --- a/include/libwebsockets/lws-client.h +++ b/include/libwebsockets/lws-client.h @@ -244,6 +244,8 @@ struct lws_client_connect_info { * context template to take a copy of for this wsi. Used to isolate * wsi-specific logs into their own stream or file. */ + const char *auth_username; + const char *auth_password; /* Add new things just above here ---^ * This is part of the ABI, don't needlessly break compatibility diff --git a/lib/core-net/client/connect.c b/lib/core-net/client/connect.c index 9dd3e6c24..db625171b 100644 --- a/lib/core-net/client/connect.c +++ b/lib/core-net/client/connect.c @@ -361,6 +361,8 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) lws_snprintf(buf_localport, sizeof(buf_localport), "%u", i->local_port); cisin[CIS_LOCALPORT] = buf_localport; cisin[CIS_ALPN] = i->alpn; + cisin[CIS_USERNAME] = i->auth_username; + cisin[CIS_PASSWORD] = i->auth_password; if (lws_client_stash_create(wsi, cisin)) goto bail; diff --git a/lib/core-net/client/connect2.c b/lib/core-net/client/connect2.c index 3712b4244..352502362 100644 --- a/lib/core-net/client/connect2.c +++ b/lib/core-net/client/connect2.c @@ -187,6 +187,12 @@ lws_client_connect_2_dnsreq(struct lws *wsi) goto solo; } + if (wsi->keepalive_rejected) { + lwsl_notice("defeating pipelining due to no " + "keepalive on server\n"); + goto solo; + } + /* only pipeline things we associate with being a stream */ if (meth && strcmp(meth, "RAW") && strcmp(meth, "GET") && diff --git a/lib/core-net/close.c b/lib/core-net/close.c index a8dc95274..354767b06 100644 --- a/lib/core-net/close.c +++ b/lib/core-net/close.c @@ -137,6 +137,13 @@ __lws_reset_wsi(struct lws *wsi) lws_buflist_destroy_all_segments(&wsi->http.buflist_post_body); #endif +#if defined(LWS_WITH_HTTP_DIGEST_AUTH) + if (wsi->http.digest_auth_hdr) { + lws_free(wsi->http.digest_auth_hdr); + wsi->http.digest_auth_hdr = NULL; + } +#endif + #if defined(LWS_WITH_SERVER) lws_dll2_remove(&wsi->listen_list); #endif diff --git a/lib/core-net/private-lib-core-net.h b/lib/core-net/private-lib-core-net.h index 344081107..a3c5b3c98 100644 --- a/lib/core-net/private-lib-core-net.h +++ b/lib/core-net/private-lib-core-net.h @@ -222,7 +222,8 @@ enum { CIS_IFACE, CIS_ALPN, CIS_LOCALPORT, - + CIS_USERNAME, + CIS_PASSWORD, CIS_COUNT }; diff --git a/lib/roles/http/client/client-http.c b/lib/roles/http/client/client-http.c index 70373bbcd..42cd7e14d 100644 --- a/lib/roles/http/client/client-http.c +++ b/lib/roles/http/client/client-http.c @@ -579,6 +579,368 @@ lws_http_client_http_response(struct lws *wsi) } #endif + +#if defined(LWS_WITH_HTTP_DIGEST_AUTH) + +static const char *digest_toks[] = { + "Digest", // 1 << 0 + "username", // 1 << 1 + "realm", // 1 << 2 + "nonce", // 1 << 3 + "uri", // 1 << 4 optional + "response", // 1 << 5 + "opaque", // 1 << 6 + "qop", // 1 << 7 + "algorithm" // 1 << 8 + "nc", // 1 << 9 + "cnonce", // 1 << 10 + "domain", // 1 << 11 +}; + +#define PEND_NAME_EQ -1 +#define PEND_DELIM -2 + +enum lws_check_basic_auth_results +lws_http_digest_auth(struct lws* wsi) +{ + uint8_t nonce[128], response[LWS_GENHASH_LARGEST], qop[32]; + int seen = 0, n, pend = -1, skipping = 0; + struct lws_tokenize ts; + char resp_username[32]; + lws_tokenize_elem e; + char realm[64]; + char b64[512]; + int m, ml, fi; + + /* Did he send auth? */ + ml = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_WWW_AUTHENTICATE); + if (!ml) + return LCBA_FAILED_AUTH; + + /* Disallow fragmentation monkey business */ + + fi = wsi->http.ah->frag_index[WSI_TOKEN_HTTP_WWW_AUTHENTICATE]; + if (wsi->http.ah->frags[fi].nfrag) { + lwsl_wsi_err(wsi, "fragmented http auth header not allowed\n"); + return LCBA_FAILED_AUTH; + } + + m = lws_hdr_copy(wsi, b64, sizeof(b64), WSI_TOKEN_HTTP_WWW_AUTHENTICATE); + if (m < 7) { + lwsl_wsi_err(wsi, "HTTP auth length bad\n"); + return LCBA_END_TRANSACTION; + } + + /* + * We are expecting AUTHORIZATION to have something like this + * + * Authorization: Digest + * username="Mufasa", + * realm="testrealm@host.com", + * nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", + * uri="/dir/index.html", + * response="e966c932a9242554e42c8ee200cec7f6", + * opaque="5ccc069c403ebaf9f0171e9517f40e41" + * + * but the order, whitespace etc is quite open. uri is optional + */ + + ts.start = b64; + ts.len = (size_t)m; + ts.flags = LWS_TOKENIZE_F_MINUS_NONTERM | LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_RFC7230_DELIMS; + + do { + e = lws_tokenize(&ts); + switch (e) { + case LWS_TOKZE_TOKEN: + if (pend == 8) { + /* algorithm name */ + + if (strncasecmp(ts.token, "MD5", ts.token_len)) { + lwsl_wsi_err(wsi, "wrong alg %.*s\n", + (int)ts.token_len, + ts.token); + return LCBA_END_TRANSACTION; + } + pend = PEND_DELIM; + break; + } + if (strncasecmp(ts.token, "Digest", ts.token_len)) { + skipping = 1; + seen |= 1 << 0; + break; + } + if (seen) /* we must be first and one time */ + return LCBA_END_TRANSACTION; + + seen |= 1 << 15; + pend = PEND_NAME_EQ; + break; + + case LWS_TOKZE_TOKEN_NAME_EQUALS: + if (skipping) + break; + if (!(seen & (1 << 15)) || pend != -1) + /* no auth type token or disordered */ + return LCBA_END_TRANSACTION; + + for (n = 0; n < (int)LWS_ARRAY_SIZE(digest_toks); n++) + if (!strncmp(ts.token, digest_toks[n], ts.token_len)) + break; + + if (n == LWS_ARRAY_SIZE(digest_toks)) { + lwsl_wsi_notice(wsi, "c: '%.*s'\n", + (int)ts.token_len, + ts.token); + + return LCBA_END_TRANSACTION; + } + + if (seen & (1 << n) || !(seen & (1 << 15))) + /* dup or no auth type token */ + return LCBA_END_TRANSACTION; + + seen |= 1 << n; + pend = n; + break; + + case LWS_TOKZE_QUOTED_STRING: + if (skipping) + break; + if (pend < 0) + return LCBA_END_TRANSACTION; + + switch (pend) { + case 1: /* username */ + if (ts.token_len >= (int)sizeof(resp_username)) + return LCBA_END_TRANSACTION; + + strncpy(resp_username, ts.token, ts.token_len); + break; + case 2: /* realm */ + if (ts.token_len >= (int)sizeof(realm)) + return LCBA_END_TRANSACTION; + + strncpy(realm, ts.token, ts.token_len); + realm[ts.token_len] = 0; + break; + case 3: /* nonce */ + if (ts.token_len >= (int)sizeof(nonce)) + return LCBA_END_TRANSACTION; + + strncpy((char *)nonce, ts.token, ts.token_len); + nonce[ts.token_len] = 0; + break; + case 4: /* uri */ + break; + case 5: /* response */ + if (ts.token_len != + lws_genhash_size(LWS_GENHASH_TYPE_MD5) * 2) + return LCBA_END_TRANSACTION; + + if (lws_hex_len_to_byte_array(ts.token, ts.token_len, + response, + sizeof(response)) < 0) + return LCBA_END_TRANSACTION; + break; + case 6: /* opaque */ + break; + case 7: /* qop */ + if (strncmp(ts.token, "auth", ts.token_len)) + return LCBA_END_TRANSACTION; + + strncpy((char *)qop, ts.token, ts.token_len); + qop[ts.token_len] = 0; + break; + } + pend = PEND_DELIM; + break; + + case LWS_TOKZE_DELIMITER: + if (*ts.token == ',') { + if (skipping) + break; + if (pend != PEND_DELIM) + return LCBA_END_TRANSACTION; + + pend = PEND_NAME_EQ; + break; + } + if (*ts.token == ';') { + if (skipping) { + /* try again with this one */ + skipping = 0; + break; + } + /* it's the end */ + e = LWS_TOKZE_ENDED; + break; + } + break; + + case LWS_TOKZE_ENDED: + break; + + default: + lwsl_wsi_notice(wsi, "unexpected token %d\n", e); + return LCBA_END_TRANSACTION; + } + + } while (e > 0); + + if (e != LWS_TOKZE_ENDED) + return LCBA_END_TRANSACTION; + + /* we got all the parts we care about? */ + + // Realm, nonce + if ((seen & 0xc) != 0xc) { + lwsl_wsi_err(wsi, + "%s: Not all digest auth tokens found! " + "m: 0x%x\nServer sent: %s", + __func__, seen & 0x81ef, b64); + + return LCBA_END_TRANSACTION; + } + + lwsl_wsi_info(wsi, "HTTP digest auth realm %s nonce %s\n", realm, nonce); + + if (wsi->stash && wsi->stash->cis[CIS_METHOD] && + wsi->stash->cis[CIS_PATH]) { + char *username = wsi->stash->cis[CIS_USERNAME]; + char *password = wsi->stash->cis[CIS_PASSWORD]; + uint8_t digest[LWS_GENHASH_LARGEST * 2 + 1]; + char *uri = wsi->stash->cis[CIS_PATH]; + char a1[LWS_GENHASH_LARGEST * 2 + 1]; + char a2[LWS_GENHASH_LARGEST * 2 + 1]; + char nc[sizeof(int) * 2 + 1]; + struct lws_genhash_ctx hc; + int ncount = 1, ssl; + const char *a, *p; + struct lws *nwsi; + char cnonce[128]; + size_t l; + + if (!wsi->http.digest_auth_hdr) { + l = sizeof(a1) + sizeof(a2) + sizeof(nonce) + + (sizeof(ncount) *2) + sizeof(response) + + sizeof(cnonce) + sizeof(qop) + strlen(uri) + + strlen(username) + strlen(password) + + strlen(realm) + 111; + + wsi->http.digest_auth_hdr = lws_malloc(l, __func__); + if (!wsi->http.digest_auth_hdr) + return -1; + } + + n = lws_snprintf(wsi->http.digest_auth_hdr, l, "%s:%s:%s", + username, realm, password); + + if (lws_genhash_init(&hc, LWS_GENHASH_TYPE_MD5) || + lws_genhash_update(&hc, + wsi->http.digest_auth_hdr, + (size_t)n) || + lws_genhash_destroy(&hc, digest)) { + lws_genhash_destroy(&hc, NULL); + + goto bail; + } + + lws_hex_from_byte_array(digest, + lws_genhash_size(LWS_GENHASH_TYPE_MD5), + a1, sizeof(a1)); + lwsl_debug("A1: %s:%s:%s = %s\n", username, realm, password, a1); + + n = lws_snprintf(wsi->http.digest_auth_hdr, l, "%s:%s", + wsi->stash->cis[CIS_METHOD], uri); + + if (lws_genhash_init(&hc, LWS_GENHASH_TYPE_MD5) || + lws_genhash_update(&hc, + wsi->http.digest_auth_hdr, + (size_t)n) || + lws_genhash_destroy(&hc, digest)) { + lws_genhash_destroy(&hc, NULL); + lwsl_err("%s: hash failed\n", __func__); + + goto bail; + } + lws_hex_from_byte_array(digest, + lws_genhash_size(LWS_GENHASH_TYPE_MD5), + a2, sizeof(a2)); + lwsl_debug("A2: %s:%s = %s\n", wsi->stash->cis[CIS_METHOD], + uri, a2); + + lws_hex_random(lws_get_context(wsi), cnonce, sizeof(cnonce)); + lws_hex_from_byte_array((const uint8_t *)&ncount, + sizeof(ncount), nc, sizeof(nc)); + + n = lws_snprintf(wsi->http.digest_auth_hdr, l, "%s:%s:%08x:%s:%s:%s", a1, + nonce, ncount, cnonce, qop, a2); + + lwsl_wsi_debug(wsi, "digest response: %s\n", wsi->http.digest_auth_hdr); + + + if (lws_genhash_init(&hc, LWS_GENHASH_TYPE_MD5) || + lws_genhash_update(&hc, wsi->http.digest_auth_hdr, (size_t)n) || + lws_genhash_destroy(&hc, digest)) { + lws_genhash_destroy(&hc, NULL); + lwsl_wsi_err(wsi, "hash failed\n"); + + goto bail; + } + lws_hex_from_byte_array(digest, + lws_genhash_size(LWS_GENHASH_TYPE_MD5), + (char *)response, + lws_genhash_size(LWS_GENHASH_TYPE_MD5) * 2 + 1); + + n = lws_snprintf(wsi->http.digest_auth_hdr, l, + "Digest username=\"%s\", realm=\"%s\", " + "nonce=\"%s\", uri=\"%s\", qop=%s, nc=%08x, " + "cnonce=\"%s\", response=\"%s\", " + "algorithm=\"MD5\"", + username, realm, nonce, uri, qop, ncount, + cnonce, response); + + lwsl_hexdump(wsi->http.digest_auth_hdr, l); + + if (lws_hdr_simple_create(wsi, WSI_TOKEN_HTTP_AUTHORIZATION, + wsi->http.digest_auth_hdr)) { + lwsl_wsi_err(wsi, "Failed to add Digest auth header"); + goto bail; + } + + nwsi = lws_get_network_wsi(wsi); + ssl = nwsi->tls.use_ssl & LCCSCF_USE_SSL; + + a = wsi->stash->cis[CIS_ADDRESS]; + p = &wsi->stash->cis[CIS_PATH][1]; + + /* + * This prevents connection pipelining when two + * HTTP connection use the same tcp socket. + */ + wsi->keepalive_rejected = 1; + + if (!lws_client_reset(&wsi, ssl, a, wsi->c_port, p, a, 1)) { + lwsl_wsi_err(wsi, "Failed to reset WSI for Digest auth"); + + goto bail; + } + + wsi->client_pipeline = 0; + } + + return 0; + +bail: + lws_free(wsi->http.digest_auth_hdr); + wsi->http.digest_auth_hdr = NULL; + + return -1; +} +#endif + #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) int @@ -605,6 +967,7 @@ lws_client_interpret_server_handshake(struct lws *wsi) wsi->conmon.ciu_txn_resp = (lws_conmon_interval_us_t) (lws_now_usecs() - wsi->conmon_datum); #endif + // lws_free_set_NULL(wsi->stash); ah = wsi->http.ah; if (!wsi->do_ws) { @@ -692,6 +1055,32 @@ lws_client_interpret_server_handshake(struct lws *wsi) } #endif n = atoi(p); + +#if defined(LWS_WITH_HTTP_DIGEST_AUTH) + if (n == 401 && lws_hdr_simple_ptr(wsi, WSI_TOKEN_HTTP_WWW_AUTHENTICATE)) { + if (!(wsi->stash && wsi->stash->cis[CIS_USERNAME] && + wsi->stash->cis[CIS_PASSWORD])) { + lwsl_err( + "Digest auth requested by server but no credentials provided " + "by user\n"); + return LCBA_FAILED_AUTH; + } + + if (0 != lws_http_digest_auth(wsi)) { + if (wsi) + goto bail3; + return 1; + } + + opaque = wsi->a.opaque_user_data; + lws_close_free_wsi(wsi, LWS_CLOSE_STATUS_NOSTATUS, "digest_auth_step2"); + wsi->a.opaque_user_data = opaque; + + return -1; + } + + ah = wsi->http.ah; +#endif if (ah) ah->http_response = (unsigned int)n; @@ -1250,6 +1639,15 @@ lws_generate_client_handshake(struct lws *wsi, char *pkt) } #endif +#if defined(LWS_WITH_HTTP_DIGEST_AUTH) + if (wsi->http.digest_auth_hdr) { + p += lws_snprintf(p, 1024, "Authorization: %s\x0d\x0a", + wsi->http.digest_auth_hdr); + lws_free(wsi->http.digest_auth_hdr); + wsi->http.digest_auth_hdr = NULL; + } +#endif + #if defined(LWS_ROLE_WS) if (wsi->do_ws) { const char *conn1 = ""; @@ -1636,7 +2034,7 @@ lws_client_reset(struct lws **pwsi, int ssl, const char *address, int port, cisin[CIS_ALPN] = wsi->alpn; #endif - if (lws_client_stash_create(wsi, cisin)) + if (!wsi->stash && lws_client_stash_create(wsi, cisin)) return NULL; if (!port) { diff --git a/lib/roles/http/private-lib-roles-http.h b/lib/roles/http/private-lib-roles-http.h index 94ee87689..a27e7782e 100644 --- a/lib/roles/http/private-lib-roles-http.h +++ b/lib/roles/http/private-lib-roles-http.h @@ -279,6 +279,10 @@ struct _lws_http_mode_related { unsigned int multipart:1; unsigned int cgi_transaction_complete:1; unsigned int multipart_issue_boundary:1; + + char auth_username[64]; + char auth_password[64]; + char *digest_auth_hdr; };