Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP Client Digest Auth #2785

Open
wants to merge 1 commit into
base: v4.3-stable
Choose a base branch
from

Conversation

ilsme
Copy link
Contributor

@ilsme ilsme commented Nov 21, 2022

Based on #1635

Just noticed #2764 and realized I did it already in the past, just sharing, hope it will be helpful.

@ilsme ilsme closed this Nov 21, 2022
@ilsme ilsme reopened this Nov 21, 2022
@lgtm-com
Copy link

lgtm-com bot commented Nov 21, 2022

This pull request introduces 1 alert when merging b6a4080 into 58af7b4 - view on LGTM.com

new alerts:

  • 1 for Cleartext transmission of sensitive information

Heads-up: LGTM.com's PR analysis will be disabled on the 5th of December, and LGTM.com will be shut down ⏻ completely on the 16th of December 2022. Please enable GitHub code scanning, which uses the same CodeQL engine ⚙️ that powers LGTM.com. For more information, please check out our post on the GitHub blog.

@simon0356
Copy link

Hello,
Is it missing something for this to be merged ?

Thanks

Simon

@lws-team
Copy link
Member

lws-team commented Dec 8, 2023

There are a few things... new features can't go on a stable branch. So it can only go on main.

There's no test for it provided.

The cmake bit is missing.

I fixed these things except the test, please try this on main and either propose a patch on an existing test to confirm it or paste the active bits necessary for a test

Author: Ilya Smelykh <ismelykh@gmail.com>
Date:   Fri Dec 8 07:30:50 2023 +0000

    http: auth digest

diff --git a/CMakeLists-implied-options.txt b/CMakeLists-implied-options.txt
index dc82bba9..e3e80041 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 5156f67f..bce26a79 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 dfd25d52..3a1ec0d7 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 dd474a9c..9108484d 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 9dd3e6c2..db625171 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 3712b424..35250236 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/private-lib-core-net.h b/lib/core-net/private-lib-core-net.h
index 34408110..a3c5b3c9 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 70373bbc..411cc803 100644
--- a/lib/roles/http/client/client-http.c
+++ b/lib/roles/http/client/client-http.c
@@ -579,6 +579,353 @@ 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 */
+				lwsl_notice("%s: repeated auth type\n", __func__);
+				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 nbuf[strlen(uri) + 256];
+		char nc[sizeof(int) * 2 + 1];
+		struct lws_genhash_ctx hc;
+		int ncount = 1, ssl;
+		char response[512];
+		const char *a, *p;
+		struct lws *nwsi;
+		char cnonce[128];
+
+		n = lws_snprintf(nbuf, sizeof(nbuf), "%s:%s:%s",
+				 username, realm, password);
+
+		if (lws_genhash_init(&hc, LWS_GENHASH_TYPE_MD5) ||
+				lws_genhash_update(&hc, nbuf, (size_t)n) ||
+				lws_genhash_destroy(&hc, digest)) {
+			lws_genhash_destroy(&hc, NULL);
+			lwsl_err("%s: hash failed\n", __func__);
+
+			return -1;
+		}
+		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((char *)nbuf, sizeof(nbuf), "%s:%s",
+				wsi->stash->cis[CIS_METHOD],
+				uri);
+
+		if (lws_genhash_init(&hc, LWS_GENHASH_TYPE_MD5) ||
+				     lws_genhash_update(&hc, nbuf, (size_t)n) ||
+				     lws_genhash_destroy(&hc, digest)) {
+			lws_genhash_destroy(&hc, NULL);
+			lwsl_err("%s: hash failed\n", __func__);
+
+			return -1;
+		}
+		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(nbuf, sizeof(nbuf), "%s:%s:%08x:%s:%s:%s", a1,
+				nonce, ncount, cnonce, qop, a2);
+		if (lws_genhash_init(&hc, LWS_GENHASH_TYPE_MD5) ||
+				lws_genhash_update(&hc, nbuf, (size_t)n) ||
+				lws_genhash_destroy(&hc, digest)) {
+			lws_genhash_destroy(&hc, NULL);
+			lwsl_wsi_err(wsi, "hash failed\n");
+
+			return -1;
+		}
+
+		lwsl_wsi_debug(wsi, "digest response: %s\n", nbuf);
+
+		lws_hex_from_byte_array(digest,
+					lws_genhash_size(LWS_GENHASH_TYPE_MD5),
+					response,
+					lws_genhash_size(LWS_GENHASH_TYPE_MD5) * 2 + 1);
+
+		// Authorization header
+		n = lws_snprintf(nbuf, sizeof(nbuf),
+				"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(nbuf, (size_t)n);
+
+		wsi->http.pending_digest_auth_hdr = 1;
+		strncpy(wsi->http.digest_auth_hdr, nbuf, (size_t)n);
+
+		if (lws_hdr_simple_create(wsi, WSI_TOKEN_HTTP_AUTHORIZATION,
+					  nbuf)) {
+			lwsl_wsi_err(wsi, "Failed to add Digest auth header");
+			return -1;
+		}
+
+		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");
+
+			return -1;
+		}
+
+		wsi->client_pipeline = 0;
+	}
+
+	return 0;
+}
+#endif
+
 #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
 
 int
@@ -605,6 +952,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 +1040,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 +1624,13 @@ lws_generate_client_handshake(struct lws *wsi, char *pkt)
 	}
 #endif
 
+#if defined(LWS_WITH_HTTP_DIGEST_AUTH)
+    if (wsi->http.pending_digest_auth_hdr) {
+        p += lws_snprintf(p, 1024, "Authorization: %s\x0d\x0a",
+                          wsi->http.digest_auth_hdr);
+    }
+#endif
+
 #if defined(LWS_ROLE_WS)
 	if (wsi->do_ws) {
 		const char *conn1 = "";
@@ -1636,7 +2017,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 94ee8768..75c628e8 100644
--- a/lib/roles/http/private-lib-roles-http.h
+++ b/lib/roles/http/private-lib-roles-http.h
@@ -279,6 +279,11 @@ 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];
+	unsigned int pending_digest_auth_hdr:1;
+	char digest_auth_hdr[4096];
 };
 
 

@lws-team
Copy link
Member

lws-team commented Dec 8, 2023

This tries to improve it to allocate the header buffer rather than cost 4K per wsi.

Author: Ilya Smelykh <ismelykh@gmail.com>
Date:   Fri Dec 8 07:30:50 2023 +0000

    http: auth digest

diff --git a/CMakeLists-implied-options.txt b/CMakeLists-implied-options.txt
index dc82bba9..e3e80041 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 5156f67f..bce26a79 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 dfd25d52..3a1ec0d7 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 dd474a9c..9108484d 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 9dd3e6c2..db625171 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 3712b424..35250236 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 a8dc9527..354767b0 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 34408110..a3c5b3c9 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 70373bbc..42cd7e14 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 94ee8768..a27e7782 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;
 };

@simon0356
Copy link

Hi,

As i understand you merged your last patch into main.
I'm currently trying it with an Nginx server with auth digest plugin : https://github.com/atomx/nginx-http-auth-digest
It is not working so far, i will try to identify the issue (probably in lws_tokenize) and if i'm able to fix it, i will send patch.

Thanks
Simon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants