diff --git a/doc/ref/sess.xml b/doc/ref/sess.xml
index 31135f99..4bd3770d 100644
--- a/doc/ref/sess.xml
+++ b/doc/ref/sess.xml
@@ -64,14 +64,18 @@ perform any global initialization needed by any libraries used by
linkend="ne_ssl_trust_default_ca"/>).
The host parameter must follow
- the definition of 'host' in host in RFC 3986,
which can be an IP-literal or registered (DNS) hostname. Valid
examples of each: "198.51.100.42" (IPv4
literal address), "[2001:db8::42]" (IPv6
literal, which MUST be enclosed in square
brackets), or "www.example.com" (DNS
- hostname).
+ hostname). The RFC 6874
+ syntax for scoped IPv6 link-local literal addresses is also
+ permitted, for example "[fe80::1%25eth0]".
+
The scheme parameter is used to
determine the URI for resources identified during request
diff --git a/doc/using.xml b/doc/using.xml
index f38d63d7..abc9bbc6 100644
--- a/doc/using.xml
+++ b/doc/using.xml
@@ -103,6 +103,24 @@
header with a value other than identity
or
chunked
.
+
+ RFC 3986 Uniform Resource Identifier (URI): Generic Syntax and RFC 6874, Representing IPv6 Zone Identifiers in Address Literals and Uniform Resource Identifiers
+
+ &neon; parses and handles scoped IPv6 link local literal
+ addresses passed to since version
+ 0.34, following the syntax in RFC 6874. An
+ example host argument would be
+ "[fe80::cafe%25eth0]" where
+ "eth0" is the scope ID. Since RFC
+ 9110 does not reference the extended syntax of scoped
+ IPv6 literals, and a scope ID has no meaningful interpretation
+ outside of the client host, it is omitted from the
+ Host header sent over the wire. So the
+ example URI used here translates to an HTTP/1.1 header field
+ of Host: [fe80::cafe].
+
+
RFC 7616, HTTP Digest Access Authentication
diff --git a/src/ne_session.c b/src/ne_session.c
index 5e1ee363..9d7aff25 100644
--- a/src/ne_session.c
+++ b/src/ne_session.c
@@ -148,8 +148,21 @@ static void set_hostport(struct host_info *host, unsigned int defaultport)
}
}
-/* Stores the hostname/port in *info, setting up the "hostport"
- * segment correctly. */
+#define V6_ADDR_MINLEN strlen("[::1]") /* "[::]" never valid */
+#define V6_SCOPE_SEP "%25"
+#define V6_SCOPE_SEPLEN (strlen(V6_SCOPE_SEP))
+/* Minimum length of link-local address with scope. */
+#define V6_SCOPE_MINLEN (strlen("[fe80::%251]"))
+
+/* Stores the hostname/port in *HI, setting up the "hostport" segment
+ * correctly. RFC 6874 syntax is allowed here but the scope ID is
+ * stripped from the hostname which is used in the Host header. RFC
+ * 9110's Host header uses uri-host, which references RFC 3986 and not
+ * RFC 6874, so it is pedantically correct; the scope ID also has no
+ * possible interpretation outside of the client host.
+ *
+ * TODO: This function also does not propagate parse failures or scope
+ * mapping failures, which is bad. */
static void set_hostinfo(struct host_info *hi, enum proxy_type type,
const char *hostname, unsigned int port)
{
@@ -162,12 +175,48 @@ static void set_hostinfo(struct host_info *hi, enum proxy_type type,
hlen = strlen(hi->hostname);
- /* IP literal parsing */
+ /* IP literal parsing. */
ia = ne_iaddr_parse(hi->hostname, ne_iaddr_ipv4);
- if (!ia && hlen > 4
+ if (!ia && hlen >= V6_ADDR_MINLEN
&& hi->hostname[0] == '[' && hi->hostname[hlen-1] == ']') {
- char *v6lit = ne_strndup(hi->hostname + 1, hlen-2);
+ const char *v6end, *v6start = hi->hostname + 1;
+ char *v6lit, *scope;
+
+ /* Parse here, see if there is a Zone ID:
+ * IPv6addrzb => v6start = IPv6address "%25" ZoneID */
+
+ if (hlen >= V6_SCOPE_MINLEN
+ && (scope = strstr(v6start, V6_SCOPE_SEP)) != NULL)
+ v6end = scope;
+ else
+ v6end = hi->hostname + hlen - 1; /* trailing ']' */
+
+ /* Extract the IPv6-literal part. */
+ v6lit = ne_strndup(v6start, v6end - v6start);
ia = ne_iaddr_parse(v6lit, ne_iaddr_ipv6);
+ if (ia && scope) {
+ /* => scope = "%25" scope "]" */
+ char *v6scope = ne_strndup(scope + V6_SCOPE_SEPLEN,
+ strlen(scope) - (V6_SCOPE_SEPLEN + 1));
+
+ if (ne_iaddr_set_scope(ia, v6scope) == 0) {
+ /* Strip scope from hostname since it's used in Host:
+ * headers and will be rejected. This is safe since
+ * strlen(scope) is assured by strstr() above. */
+ *scope++ = ']';
+ *scope = '\0';
+ NE_DEBUG(NE_DBG_HTTP, "sess: Using IPv6 scope '%s', "
+ "hostname rewritten to %s.\n", v6scope,
+ hi->hostname);
+ }
+ else {
+ NE_DEBUG(NE_DBG_HTTP, "sess: Failed to set IPv6 scope '%s' "
+ "for address %s.\n", v6scope, v6lit);
+ }
+
+ ne_free(v6scope);
+ }
+
ne_free(v6lit);
}
diff --git a/src/ne_session.h b/src/ne_session.h
index 992aaecd..e32bedf8 100644
--- a/src/ne_session.h
+++ b/src/ne_session.h
@@ -38,7 +38,8 @@ typedef struct ne_session_s ne_session;
* server. The host string must follow the definition of 'host' in RFC
* 3986, which can be an IP-literal or registered (DNS) hostname. An
* IPv6 literal address must be enclosed in square brackets (for
- * example "[::1]"). */
+ * example "[::1]"). The RFC 6874 syntax for IPv6 link-local literal
+ * addresses is also supported, for example "[fe80::1%25eth0]". */
ne_session *ne_session_create(const char *scheme, const char *host,
unsigned int port);