diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/AltSvc.java b/vertx-core/src/main/java/io/vertx/core/http/impl/AltSvc.java
index 8cc003e8093..d807b20e3a5 100644
--- a/vertx-core/src/main/java/io/vertx/core/http/impl/AltSvc.java
+++ b/vertx-core/src/main/java/io/vertx/core/http/impl/AltSvc.java
@@ -10,16 +10,224 @@
*/
package io.vertx.core.http.impl;
+import io.vertx.core.net.HostAndPort;
+import io.vertx.core.net.impl.UriParser;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+
/**
- * An event signalling an alternative service of an origin.
+ * Alt-svc parsed value.
*/
-public class AltSvc {
+public interface AltSvc {
+
+ /**
+ * Alt-Svc: Clear.
+ */
+ class Clear implements AltSvc {
+ public static final Clear INSTANCE = new Clear();
+ private Clear() {
+ }
+ }
+
+ class ListOfValue extends ArrayList implements AltSvc {
+ public ListOfValue() {
+ super(3);
+ }
+ }
+
+ /**
+ * Alt-Svc: Value.
+ */
+ public static class Value {
+ private String protocolId;
+ private String rawAltAuthority;
+ private HostAndPort altAuthority;
+ private Map parameters = Map.of();
+ private Value() {
+ }
+
+ /**
+ * @return the protocol id
+ */
+ public String protocolId() {
+ return protocolId;
+ }
+
+ /**
+ * @return the alt authority
+ */
+ public HostAndPort altAuthority() {
+ return altAuthority;
+ }
- public final Origin origin;
- public final String value;
+ /**
+ * @return the parameter map
+ */
+ public Map parameters() {
+ return parameters;
+ }
+
+ private void setAlternative(String protocolId, String altAuthority) {
+ this.protocolId = protocolId;
+ this.rawAltAuthority = altAuthority;
+ }
+
+ private void addParameter(String name, String value) {
+ if (parameters.isEmpty()) {
+ parameters = new HashMap<>(2);
+ }
+ parameters.put(name, value);
+ }
+ }
+
+ public static AltSvc parseAltSvc(String s) {
+ if (s.equals("clear")) {
+ return Clear.INSTANCE;
+ }
+ int from = 0;
+ int to = s.length();
+ ListOfValue values = new ListOfValue();
+ while (true) {
+ Value value = new Value();
+ from = parseAltValue(s, from, to, value);
+ if (from == -1) {
+ return null;
+ }
+ values.add(value);
+ if (from == to) {
+ break;
+ }
+ from = HttpParser.parseOWS(s, from, to);
+ if (from >= to || s.charAt(from++) != ',') {
+ return null;
+ }
+ from = HttpParser.parseOWS(s, from, to);
+ }
+
+ return values.isEmpty() ? null : values;
+ }
+
+ public static Value parseAltValue(String s) {
+ Value value = new Value();
+ int res = parseAltValue(s, 0, s.length(), value);
+ return res == s.length() ? value : null;
+ }
+
+ public static int parseAltValue(String s, int from, int to, Value value) {
+ int res = parseAltValue(s, from, to, value::setAlternative, value::addParameter);
+ if (res == s.length()) {
+ // Now parse raw alt-authority into an HostAndPort
+ String raw = value.rawAltAuthority;
+ String host;
+ int idx;
+ if (raw.charAt(0) == ':') {
+ idx = 0;
+ host = "";
+ } else {
+ idx = UriParser.parseHost(raw, 0, raw.length());
+ if (idx == -1 || idx >= raw.length() || raw.charAt(idx) != ':') {
+ return -1;
+ }
+ host = raw.substring(0, idx);
+ }
+ int port = UriParser.parseAndEvalPort(raw, idx);
+ if (port != -1) {
+ value.altAuthority = HostAndPort.create(host, port);
+ }
+ }
+ return res;
+ }
+
+ public static int parseAltValue(String s, int from, int to) {
+ return parseAltValue(s, from, to, null, null);
+ }
+
+ public static int parseAltValue(String s, int from, int to, BiConsumer alternative, BiConsumer parameter) {
+ from = parseAlternative(s, from, to, alternative);
+ if (from == -1) {
+ return -1;
+ }
+ while (true) {
+ int idx = HttpParser.parseOWS(s, from, to);
+ if (idx == -1 || idx >= to || s.charAt(idx++) != ';') {
+ return from;
+ }
+ idx = HttpParser.parseOWS(s, idx, to);
+ if (idx == -1) {
+ return from;
+ }
+ idx = parseParameter(s, idx, to, parameter);
+ if (idx == -1) {
+ return from;
+ }
+ from = idx;
+ }
+ }
+
+ public static int parseParameter(String s, int from, int to) {
+ return parseParameter(s, from, to, null);
+ }
+
+ public static int parseParameter(String s, int from, int to, BiConsumer parameter) {
+ if (from >= to) {
+ return -1;
+ }
+ int endOfName = HttpParser.parseToken(s, from, to);
+ if (endOfName == -1 || endOfName >= to || s.charAt(endOfName) != '=') {
+ return -1;
+ }
+ int endOfValue = HttpParser.parseToken(s, endOfName + 1, to);
+ if (endOfValue == -1) {
+ endOfValue = HttpParser.parseQuotedString(s, endOfName + 1, to);
+ }
+ if (endOfValue == -1) {
+ return -1;
+ }
+ if (parameter != null) {
+ String name = s.substring(from, endOfName);
+ String value;
+ if (s.charAt(endOfName + 1) == '\"') {
+ value = s.substring(endOfName + 2, endOfValue - 1);
+ } else {
+ value = s.substring(endOfName + 1, endOfValue);
+ }
+ parameter.accept(name, value);
+ }
+ return endOfValue;
+ }
+
+ public static int parseAlternative(String s, int from, int to) {
+ return parseAlternative(s, from, to, null);
+ }
+
+ public static int parseAlternative(String s, int from, int to, BiConsumer alternative) {
+ int endOfProtocolId = parseProtocolId(s, from, to);
+ if (endOfProtocolId == -1 || endOfProtocolId >= to || s.charAt(endOfProtocolId) != '=') {
+ return -1;
+ }
+ int endOfAltAuthority = parseAltAuthority(s, endOfProtocolId + 1, to);
+ if (endOfAltAuthority == -1) {
+ return -1;
+ }
+ if (alternative != null) {
+ String protocolId = s.substring(from, endOfProtocolId);
+ String altAuthority;
+ if (s.charAt(endOfProtocolId + 1) == '\"') {
+ altAuthority = s.substring(endOfProtocolId + 2, endOfAltAuthority - 1);
+ } else {
+ altAuthority = s.substring(endOfProtocolId + 1, endOfAltAuthority);
+ }
+ alternative.accept(protocolId, altAuthority);
+ }
+ return endOfAltAuthority;
+ }
+
+ public static int parseAltAuthority(String s, int from, int to) {
+ return HttpParser.parseQuotedString(s, from, to);
+ }
- public AltSvc(Origin origin, String value) {
- this.origin = origin;
- this.value = value;
+ public static int parseProtocolId(String s, int from, int to) {
+ return HttpParser.parseToken(s, from, to);
}
}
diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/AltSvcEvent.java b/vertx-core/src/main/java/io/vertx/core/http/impl/AltSvcEvent.java
new file mode 100644
index 00000000000..e8550072bcb
--- /dev/null
+++ b/vertx-core/src/main/java/io/vertx/core/http/impl/AltSvcEvent.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2011-2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ */
+package io.vertx.core.http.impl;
+
+/**
+ * An event signalling an alternative service of an origin.
+ */
+public class AltSvcEvent {
+
+ public final Origin origin;
+ public final AltSvc altSvc;
+
+ public AltSvcEvent(Origin origin, AltSvc altSvc) {
+ this.origin = origin;
+ this.altSvc = altSvc;
+ }
+}
diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientConnection.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientConnection.java
index 808d7c9ec02..88be2315e02 100644
--- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientConnection.java
+++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientConnection.java
@@ -81,7 +81,7 @@ public interface HttpClientConnection extends HttpConnection {
* @param handler
* @return
*/
- default HttpClientConnection alternativeServicesHandler(Handler handler) {
+ default HttpClientConnection alternativeServicesHandler(Handler handler) {
return this;
}
diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpParser.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpParser.java
new file mode 100644
index 00000000000..e7a68c35616
--- /dev/null
+++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpParser.java
@@ -0,0 +1,63 @@
+package io.vertx.core.http.impl;
+
+import static io.vertx.core.net.impl.Rfc5234Parser.*;
+
+public class HttpParser {
+
+ public static int parseQuotedString(String s, int from, int to) {
+ if (from + 1 >= to || !isDQUOTE(s.charAt(from++))) {
+ return -1;
+ }
+ while (true) {
+ if (from < to && isQDText(s.charAt(from))) {
+ from++;
+ } else if (isQuotedPair(s, from, to)) {
+ from += 2;
+ } else {
+ break;
+ }
+ }
+ return from < to && isDQUOTE(s.charAt(from++)) ? from : -1;
+ }
+
+ public static boolean isQDText(char c) {
+ return isHTAB(c) || isSP(c) || c == 0x21 || (0x23 <= c && c <= 0x5B) || (0x5D <= c && c <= 0x7E) || isObsText(c);
+ }
+
+ public static boolean isObsText(char c) {
+ return 0x80 <= c && c <= 0xFF;
+ }
+
+ public static boolean isQuotedPair(String s, int from, int to) {
+ if (from + 1 >= to || s.charAt(from++) != '\\') {
+ return false;
+ }
+ char c = s.charAt(from);
+ return isHTAB(c) || isSP(c) || isVCHAR(c) || isObsText(c);
+ }
+
+ public static int parseToken(String s, int from, int to) {
+ if (from >= to || !isTChar(s.charAt(from++))) {
+ return -1;
+ }
+ while (from < to && isTChar(s.charAt(from))) {
+ from++;
+ }
+ return from;
+ }
+
+ public static boolean isTChar(char c) {
+ return c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' ||
+ c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~' || isDIGIT(c) || isALPHA(c);
+ }
+
+ public static int parseOWS(String s, int from, int to) {
+ if (from > to) {
+ throw new IllegalArgumentException();
+ }
+ while (from < to && (isSP(s.charAt(from)) || isHTAB(s.charAt(from)))) {
+ from++;
+ }
+ return from;
+ }
+}
diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java
index a68144b6719..538a9c62d87 100644
--- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java
+++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java
@@ -20,7 +20,6 @@
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;
import io.vertx.core.Future;
-import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.AsyncFile;
@@ -34,6 +33,7 @@
import io.vertx.core.internal.net.RFC3986;
import io.vertx.core.net.HostAndPort;
import io.vertx.core.net.impl.HostAndPortImpl;
+import io.vertx.core.net.impl.UriParser;
import io.vertx.core.spi.tracing.TagExtractor;
import io.vertx.core.spi.observability.HttpRequest;
import io.vertx.core.spi.observability.HttpResponse;
@@ -937,7 +937,7 @@ public static boolean isKeepAlive(io.netty.handler.codec.http.HttpRequest reques
public static boolean isValidHostAuthority(String host) {
int len = host.length();
- return HostAndPortImpl.parseHost(host, 0, len) == len;
+ return UriParser.parseHost(host, 0, len) == len;
}
public static boolean canUpgradeToWebSocket(HttpServerRequest req) {
@@ -1014,7 +1014,7 @@ public static List toHttpAlpnVersions(List= 2) {
int idx = payload.readerIndex();
try {
@@ -1039,7 +1039,10 @@ public static AltSvc parseAltSvcFrame(ByteBuf payload) {
origin = null;
}
String value = payload.readString(payload.readableBytes(), StandardCharsets.US_ASCII);
- return new AltSvc(origin, value);
+ AltSvc altSvc;
+ if (value != null && (altSvc = AltSvc.parseAltSvc(value)) != null) {
+ return new AltSvcEvent(origin, altSvc);
+ }
} finally {
payload.readerIndex(idx);
}
diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http1x/Http1xClientConnection.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http1x/Http1xClientConnection.java
index c247acf4d2d..1eaa74f8d1c 100644
--- a/vertx-core/src/main/java/io/vertx/core/http/impl/http1x/Http1xClientConnection.java
+++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http1x/Http1xClientConnection.java
@@ -92,7 +92,7 @@ public class Http1xClientConnection extends Http1xConnection implements io.vertx
private Handler evictionHandler = DEFAULT_EVICTION_HANDLER;
private Handler