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 invalidMessageHandler = INVALID_MSG_HANDLER; - private Handler alternativeServicesHandler; + private Handler alternativeServicesHandler; private boolean wantClose; private boolean isConnect; private int keepAliveTimeout; @@ -150,7 +150,7 @@ public io.vertx.core.http.impl.HttpClientConnection invalidMessageHandler(Handle } @Override - public HttpClientConnection alternativeServicesHandler(Handler handler) { + public HttpClientConnection alternativeServicesHandler(Handler handler) { alternativeServicesHandler = handler; return this; } @@ -906,14 +906,15 @@ private void handleResponseBegin(Stream stream, HttpVersion version, io.vertx.co } } String altSvcHeader = response.headers.get(ALT_SVC); - Handler handler; - if (altSvcHeader != null && (handler = alternativeServicesHandler) != null) { + Handler handler; + AltSvc altSvc; + if (altSvcHeader != null && (altSvc = AltSvc.parseAltSvc(altSvcHeader)) != null && (handler = alternativeServicesHandler) != null) { int port = authority.port(); if (port == -1) { port = ssl ? 443 : 80; } Origin origin = new Origin(ssl ? "https" : "http", authority.host(), port); - context.emit(new AltSvc(origin, altSvcHeader), handler); + context.emit(new AltSvcEvent(origin, altSvc), handler); } } } diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http1x/Http2UpgradeClientConnection.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http1x/Http2UpgradeClientConnection.java index eb264452bf4..071adb54881 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http1x/Http2UpgradeClientConnection.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http1x/Http2UpgradeClientConnection.java @@ -62,7 +62,7 @@ public class Http2UpgradeClientConnection implements io.vertx.core.http.impl.Htt private Handler evictionHandler; private Handler invalidMessageHandler; private Handler concurrencyChangeHandler; - private Handler alternativeServicesHandler; + private Handler alternativeServicesHandler; private Handler remoteSettingsHandler; public Http2UpgradeClientConnection(Http1xClientConnection connection, long maxLifetimeMillis, ClientMetrics metrics, Http2ChannelUpgrade upgrade) { @@ -812,7 +812,7 @@ public io.vertx.core.http.impl.HttpClientConnection concurrencyChangeHandler(Han } @Override - public HttpClientConnection alternativeServicesHandler(Handler handler) { + public HttpClientConnection alternativeServicesHandler(Handler handler) { if (current instanceof Http1xClientConnection) { alternativeServicesHandler = handler; } diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/codec/Http2ClientConnectionImpl.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/codec/Http2ClientConnectionImpl.java index 54825810df9..c3d560d5780 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/codec/Http2ClientConnectionImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/codec/Http2ClientConnectionImpl.java @@ -29,8 +29,6 @@ import io.vertx.core.spi.metrics.HttpClientMetrics; import io.vertx.core.spi.metrics.TransportMetrics; -import java.nio.charset.StandardCharsets; - /** * @author Julien Viet */ @@ -43,7 +41,7 @@ public class Http2ClientConnectionImpl extends Http2ConnectionImpl implements Ht private final long lifetimeEvictionTimestamp; private Handler evictionHandler = DEFAULT_EVICTION_HANDLER; private Handler concurrencyChangeHandler = DEFAULT_CONCURRENCY_CHANGE_HANDLER; - private Handler alternativeServicesHandler; + private Handler alternativeServicesHandler; private long expirationTimestamp; private boolean evicted; private final VertxHttp2ConnectionHandler handler; @@ -92,7 +90,7 @@ public Http2ClientConnectionImpl concurrencyChangeHandler(Handler handler) } @Override - public HttpClientConnection alternativeServicesHandler(Handler handler) { + public HttpClientConnection alternativeServicesHandler(Handler handler) { alternativeServicesHandler = handler; return this; } @@ -259,8 +257,8 @@ public synchronized void onPushPromiseRead(ChannelHandlerContext ctx, int stream public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf payload) { if (frameType == 0xA) { io.vertx.core.http.impl.http2.Http2Stream stream = stream(streamId); - AltSvc event = HttpUtils.parseAltSvcFrame(payload); - Handler handler; + AltSvcEvent event = HttpUtils.parseAltSvcFrame(payload); + Handler handler; if (event != null && (handler = alternativeServicesHandler) != null) { if (stream != null) { String scheme = stream.scheme(); @@ -277,7 +275,7 @@ public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int stream break; } } - event = new AltSvc(new Origin(scheme, host, port), event.value); + event = new AltSvcEvent(new Origin(scheme, host, port), event.altSvc); } if (event.origin != null) { context.emit(event, handler); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/multiplex/Http2MultiplexClientConnection.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/multiplex/Http2MultiplexClientConnection.java index d5353b2c0d0..0e91df67dfc 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/multiplex/Http2MultiplexClientConnection.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/multiplex/Http2MultiplexClientConnection.java @@ -10,7 +10,6 @@ */ package io.vertx.core.http.impl.http2.multiplex; -import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.Headers; import io.netty.handler.codec.http2.*; @@ -21,7 +20,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.http.Http2Settings; import io.vertx.core.http.StreamPriority; -import io.vertx.core.http.impl.AltSvc; +import io.vertx.core.http.impl.AltSvcEvent; import io.vertx.core.http.impl.HttpClientConnection; import io.vertx.core.http.impl.HttpClientStream; import io.vertx.core.http.impl.headers.HttpResponseHeaders; @@ -33,8 +32,6 @@ import io.vertx.core.spi.metrics.ClientMetrics; import io.vertx.core.spi.metrics.HttpClientMetrics; -import static io.vertx.core.http.HttpHeaders.ALT_SVC; - public class Http2MultiplexClientConnection extends Http2MultiplexConnection implements HttpClientConnection, Http2ClientConnection { private final boolean decompressionSupported; @@ -43,7 +40,7 @@ public class Http2MultiplexClientConnection extends Http2MultiplexConnection completion; private long concurrency; private Handler concurrencyChangeHandler; - private Handler alternativeServicesHandler; + private Handler alternativeServicesHandler; private Handler evictionHandler; private final long maxConcurrency; private final long keepAliveTimeoutMillis; @@ -175,7 +172,7 @@ public HttpClientConnection concurrencyChangeHandler(Handler handler) { } @Override - public HttpClientConnection alternativeServicesHandler(Handler handler) { + public HttpClientConnection alternativeServicesHandler(Handler handler) { alternativeServicesHandler = handler; return this; } @@ -213,8 +210,8 @@ public void writePriorityFrame(int streamId, StreamPriority priority, Promise handler = alternativeServicesHandler; + void onAltSvc(AltSvcEvent event) { + Handler handler = alternativeServicesHandler; if (handler != null) { context.emit(event, handler); } diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/multiplex/Http2MultiplexConnection.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/multiplex/Http2MultiplexConnection.java index 9b6937427a1..d7595b78048 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/multiplex/Http2MultiplexConnection.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/multiplex/Http2MultiplexConnection.java @@ -39,7 +39,7 @@ import io.vertx.core.VertxException; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.*; -import io.vertx.core.http.impl.AltSvc; +import io.vertx.core.http.impl.AltSvcEvent; import io.vertx.core.http.impl.HttpUtils; import io.vertx.core.http.impl.Origin; import io.vertx.core.http.impl.http2.Http2Stream; @@ -126,12 +126,12 @@ void receiveUnknownFrame(int streamId, int type, int flags, ByteBuf content) { private void receiveUnknownFrame(StreamChannel channel, int streamId, int type, int flags, ByteBuf content) { if (type == 0xA) { - AltSvc altSvc = HttpUtils.parseAltSvcFrame(content); - if (altSvc != null) { + AltSvcEvent altSvcEvt = HttpUtils.parseAltSvcFrame(content); + if (altSvcEvt != null) { if (streamId == 0) { // Assume the event contains an origin } else if (channel == null) { - altSvc = null; + altSvcEvt = null; } else { String scheme = channel.stream.scheme(); HostAndPort authority = channel.stream.authority(); @@ -148,11 +148,11 @@ private void receiveUnknownFrame(StreamChannel channel, int streamId, int type, } if (port > 0) { String host = authority.host(); - altSvc = new AltSvc(new Origin(scheme, host, port), altSvc.value); + altSvcEvt = new AltSvcEvent(new Origin(scheme, host, port), altSvcEvt.altSvc); } } - if (altSvc != null && altSvc.origin != null) { - onAltSvc(altSvc); + if (altSvcEvt != null && altSvcEvt.origin != null) { + onAltSvc(altSvcEvt); } } } @@ -167,7 +167,7 @@ void receiveResetFrame(int streamId, long code) { channel.stream.onReset(code); } - void onAltSvc(AltSvc event) { + void onAltSvc(AltSvcEvent event) { } void onWritabilityChanged(int streamId) { diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/HostAndPortImpl.java b/vertx-core/src/main/java/io/vertx/core/net/impl/HostAndPortImpl.java index 2bd534b3439..93778769fda 100644 --- a/vertx-core/src/main/java/io/vertx/core/net/impl/HostAndPortImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/net/impl/HostAndPortImpl.java @@ -2,144 +2,8 @@ import io.vertx.core.net.HostAndPort; -import java.util.Arrays; - public class HostAndPortImpl implements HostAndPort { - // digits lookup table to speed-up parsing - private static final byte[] DIGITS = new byte[128]; - - static { - Arrays.fill(DIGITS, (byte) -1); - for (int i = '0';i <= '9';i++) { - DIGITS[i] = (byte) (i - '0'); - } - } - - public static int parseHost(String val, int from, int to) { - int pos; - if ((pos = parseIPLiteral(val, from, to)) != -1) { - return pos; - } else if ((pos = parseIPv4Address(val, from, to)) != -1) { - return pos; - } else if ((pos = parseRegName(val, from, to)) != -1) { - return pos; - } - return -1; - } - - private static int foo(int v) { - return v == -1 ? -1 : v + 1; - } - - public static int parseIPv4Address(String s, int from, int to) { - for (int i = 0;i < 4;i++) { - if (i > 0 && from < to && s.charAt(from++) != '.') { - return -1; - } - from = parseDecOctet(s, from, to); - if (from == -1) { - return -1; - } - } - // from is the next position to parse: whatever come before is a valid IPv4 address - if (from == to) { - // we're done - return from; - } - assert from < to; - // we have more characters, let's check if it has enough space for a port - if (from + 1 == s.length()) { - // just a single character left, we don't care what it is - return -1; - } - // we have more characters - if (s.charAt(from) != ':') { - // we need : to start a port - return -1; - } - // we (maybe) have a port - even with a single digit; the ipv4 addr is fineFi - return from; - } - - public static int parseDecOctet(String s, int from, int to) { - int val = parseDigit(s, from++, to); - if (val == 0) { - return from; - } - if (val < 0 || val > 9) { - return -1; - } - int n = parseDigit(s, from, to); - if (n != -1) { - val = val * 10 + n; - n = parseDigit(s, ++from, to); - if (n != -1) { - from++; - val = val * 10 + n; - } - } - if (val < 256) { - return from; - } - return -1; - } - - private static int parseDigit(String s, int from, int to) { - if (from >= to) { - return -1; - } - char ch = s.charAt(from); - // a very predictable condition - if (ch < 128) { - // negative short values are still positive ints - return DIGITS[ch]; - } - return -1; - } - - public static int parseIPLiteral(String s, int from, int to) { - return from + 2 < to && s.charAt(from) == '[' ? foo(s.indexOf(']', from + 2)) : -1; - } - - public static int parseRegName(String s, int from, int to) { - while (from < to) { - char c = s.charAt(from); - if (isUnreserved(c) || isSubDelims(c)) { - from++; - } else if (c == '%' && (from + 2) < to && isHEXDIG(s.charAt(c + 1)) && isHEXDIG(s.charAt(c + 2))) { - from += 3; - } else { - break; - } - } - return from; - } - - private static boolean isUnreserved(char ch) { - return isALPHA(ch) || isDIGIT(ch) || ch == '-' || ch == '.' || ch == '_' || ch == '~'; - } - - private static boolean isALPHA(char ch) { - return ('A' <= ch && ch <= 'Z') - || ('a'<= ch && ch <= 'z'); - } - - private static boolean isDIGIT(char ch) { - if (ch < 128) { - return DIGITS[ch] != -1; - } - return false; - } - - private static boolean isSubDelims(char ch) { - return ch == '!' || ch == '$' || ch == '&' || ch == '\'' || ch == '(' || ch == ')' || ch == '*' || ch == '+' || ch == ',' || ch == ';' || ch == '='; - } - - static boolean isHEXDIG(char ch) { - return isDIGIT(ch) || ('A' <= ch && ch <= 'F') || ('a' <= ch && ch <= 'f'); - } - /** * Validate an authority HTTP header, that is host [':' port]
* This method should behave like {@link #parseAuthority(String, int)}, @@ -151,12 +15,12 @@ static boolean isHEXDIG(char ch) { * @throws NullPointerException when the string is {@code null} */ public static boolean isValidAuthority(String s) { - int pos = parseHost(s, 0, s.length()); + int pos = UriParser.parseHost(s, 0, s.length()); if (pos == s.length()) { return true; } if (pos < s.length() && s.charAt(pos) == ':') { - return parsePort(s, pos) != -1; + return UriParser.parseAndEvalPort(s, pos) != -1; } return false; } @@ -168,13 +32,13 @@ public static boolean isValidAuthority(String s) { * @return the parsed value or {@code null} when the string cannot be parsed */ public static HostAndPortImpl parseAuthority(String s, int schemePort) { - int pos = parseHost(s, 0, s.length()); + int pos = UriParser.parseHost(s, 0, s.length()); if (pos == s.length()) { return new HostAndPortImpl(s, schemePort); } if (pos < s.length() && s.charAt(pos) == ':') { String host = s.substring(0, pos); - int port = parsePort(s, pos); + int port = UriParser.parseAndEvalPort(s, pos); if (port == -1) { return null; } @@ -183,21 +47,6 @@ public static HostAndPortImpl parseAuthority(String s, int schemePort) { return null; } - private static int parsePort(String s, int pos) { - int port = 0; - while (++pos < s.length()) { - int digit = parseDigit(s, pos, s.length()); - if (digit == -1) { - return -1; - } - port = port * 10 + digit; - if (port > 65535) { - return -1; - } - } - return port; - } - private final String host; private final int port; diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/Rfc5234Parser.java b/vertx-core/src/main/java/io/vertx/core/net/impl/Rfc5234Parser.java new file mode 100644 index 00000000000..9f83b0bb5fd --- /dev/null +++ b/vertx-core/src/main/java/io/vertx/core/net/impl/Rfc5234Parser.java @@ -0,0 +1,61 @@ +/* + * 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.net.impl; + +import java.util.Arrays; + +/** + * Various parsing methods as defined by ABNF + */ +public class Rfc5234Parser { + + // digits lookup table to speed-up parsing + private static final byte[] DIGITS = new byte[128]; + + static { + Arrays.fill(DIGITS, (byte) -1); + for (int i = '0';i <= '9';i++) { + DIGITS[i] = (byte) (i - '0'); + } + } + + public static boolean isSP(char c) { + return c == 0x20; + } + + public static boolean isHTAB(char c) { + return c == 0x09; + } + + public static boolean isDQUOTE(char c) { + return c == '"'; + } + + public static boolean isVCHAR(char c) { + return 0x21 <= c && c <= 0x7E; + } + + public static boolean isDIGIT(char ch) { + if (ch < 128) { + return DIGITS[ch] != -1; + } + return false; + } + + public static boolean isHEXDIG(char ch) { + return isDIGIT(ch) || ('A' <= ch && ch <= 'F') || ('a' <= ch && ch <= 'f'); + } + + public static boolean isALPHA(char ch) { + return ('A' <= ch && ch <= 'Z') + || ('a'<= ch && ch <= 'z'); + } +} diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/UriParser.java b/vertx-core/src/main/java/io/vertx/core/net/impl/UriParser.java new file mode 100644 index 00000000000..c0703c6b4d7 --- /dev/null +++ b/vertx-core/src/main/java/io/vertx/core/net/impl/UriParser.java @@ -0,0 +1,409 @@ +/* + * 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.net.impl; + +import java.util.Arrays; + +import static io.vertx.core.net.impl.Rfc5234Parser.*; + +/** + * URI related parsing, as in RFC3986 + */ +public class UriParser { + + // digits lookup table to speed-up parsing + private static final byte[] DIGITS = new byte[128]; + + static { + Arrays.fill(DIGITS, (byte) -1); + for (int i = '0';i <= '9';i++) { + DIGITS[i] = (byte) (i - '0'); + } + } + + /** + * Try to parse the host rul as per + * RFC3986. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the index after parsing or {@code -1} if the value is invalid + */ + public static int parseHost(String s, int from, int to) { + int pos; + if ((pos = parseIPLiteral(s, from, to)) != -1) { + return pos; + } else if ((pos = parseIPv4Address(s, from, to)) != -1) { + return pos; + } else if ((pos = parseRegName(s, from, to)) != -1) { + return pos; + } + return -1; + } + + /** + * Try to parse the reg-name rul as per + * RFC3986. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the index after parsing or {@code -1} if the value is invalid + */ + public static int parseRegName(String s, int from, int to) { + while (from < to) { + char c = s.charAt(from); + if (isUnreserved(c) || isSubDelims(c)) { + from++; + } else if (c == '%' && (from + 2) < to && isHEXDIG(s.charAt(c + 1)) && isHEXDIG(s.charAt(c + 2))) { + from += 3; + } else { + break; + } + } + return from; + } + + /** + * Try to parse the IP-literal rule: "[" ( IPv6address / IPvFuture ) "]" as per + * RFC3986. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the index after parsing or {@code -1} if the value is invalid + */ + public static int parseIPLiteral(String s, int from, int to) { + if (from < to && s.charAt(from++) == '[') { + int idx = parseIPv6Address(s, from, to); + if (idx == -1) { + idx = parseIPvFuture(s, from, to); + } + if (idx != -1 && idx < to && s.charAt(idx++) == ']') { + return idx; + } + } + return -1; + } + + private static int foo(int v) { + return v == -1 ? -1 : v + 1; + } + + /** + * Try to parse the IPv4address rule as per + * RFC3986. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the index after parsing or {@code -1} if the value is invalid + */ + public static int parseIPv4Address(String s, int from, int to) { + for (int i = 0;i < 4;i++) { + if (i > 0 && from < to && s.charAt(from++) != '.') { + return -1; + } + from = parseDecOctet(s, from, to); + if (from == -1) { + return -1; + } + } + // from is the next position to parse: whatever come before is a valid IPv4 address + if (from == to) { + // we're done + return from; + } + assert from < to; + // we have more characters, let's check if it has enough space for a port + if (from + 1 == s.length()) { + // just a single character left, we don't care what it is + return -1; + } + // we have more characters + if (s.charAt(from) != ':') { + // we need : to start a port + return -1; + } + // we (maybe) have a port - even with a single digit; the ipv4 addr is fine + return from; + } + + /** + * Try to parse the IPv6address rule as per + * RFC3986. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the index after parsing or {@code -1} if the value is invalid + */ + public static int parseIPv6Address(String s, int from, int to) { + int doubleColonIdx = s.indexOf("::", from); + if (doubleColonIdx == -1 || (doubleColonIdx + 2) > to) { + // Rule1 + for (int i = 0;i < 6;i++) { + from = parseH16(s, from, to); + if (from == -1) { + return -1; + } + from = _parseColon(s, from, to); + if (from == -1) { + return -1; + } + } + return parseLs32(s, from, to); + } + + // To ease parsing we use the first colon (:) or double colon (::) + // e.g. "0::" = "0:" + ":" + // "0:1:2:3::" = "0:1:2:3:" + ":" + int a = doubleColonIdx + 1; + int count1 = 0; + while (true) { + from = parseH16(s, from, a); + if (from == -1) { + break; + } + from = _parseColon(s, from, a); + if (from == -1) { + return -1; + } + count1++; + } + + int count2 = 0; + from = doubleColonIdx + 2; + while (true) { + int idx = parseH16(s, from, to); + if (idx == -1) { + break; + } + idx = _parseColon(s, idx, to); + if (idx == -1) { + break; + } + from = idx; + count2++; + } + int idx; + if ((idx = parseIPv4Address(s, from, to)) != -1) { + // + } else if ((idx = parseH16(s, from, to)) != -1) { + count2--; + } else { + // Rule 9 + return count1 <= 7 ? from : -1; + } + from = idx; + + + switch (count2) { + case 5: + // Rule 2 + return count1 == 0 ? from : -1; + case 4: + // Rule 3 + return count1 <= 1 ? from : -1; + case 3: + // Rule 4 + return count1 <= 2 ? from : -1; + case 2: + // Rule 5 + return count1 <= 3 ? from : -1; + case 1: + // Rule 6 + return count1 <= 4 ? from : -1; + case 0: + // Rule 7 + return count1 <= 5 ? from : -1; + case -1: + // Rule 8 + return count1 <= 6 ? from : -1; + default: + throw new AssertionError(); + } + } + + public static int _parseColon(String s, int from, int to) { + return from < to && s.charAt(from) == ':' ? from + 1 : -1; + } + + public static int _parseDoubleColon(String s, int from, int to) { + return from + 1 < to && s.charAt(from) == ':' && s.charAt(from + 1) == ':' ? from + 2 : -1; + } + + /** + * Try to parse the ls32 rule as per + * RFC3986. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the index after parsing or {@code -1} if the value is invalid + */ + public static int parseLs32(String s, int from, int to) { + int idx = _parseH16ColonH16(s, from, to); + if (idx == -1) { + idx = parseIPv4Address(s, from, to); + } + return idx; + } + + public static int _parseH16ColonH16(String s, int from, int to) { + int idx = parseH16(s, from, to); + if (idx == -1) { + return -1; + } + from = idx; + if (from >= to || s.charAt(from++) != ':') { + return -1; + } + return parseH16(s, from, to); + } + + /** + * Try to parse the h16 rule as per + * RFC3986. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the index after parsing or {@code -1} if the value is invalid + */ + public static int parseH16(String s, int from, int to) { + if (from >= to || !isHEXDIG(s.charAt(from++))) { + return -1; + } + for (int i = 0;i < 3;i++) { + if (from < to && isHEXDIG(s.charAt(from))) { + from++; + } + } + return from; + } + + /** + * Try to parse the IPvFuture rule as per + * RFC3986. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the index after parsing or {@code -1} if the value is invalid + */ + public static int parseIPvFuture(String s, int from, int to) { + if (from >= to || s.charAt(from++) != 'v') { + return -1; + } + if (from >= to || !isHEXDIG(s.charAt(from++))) { + return -1; + } + while (from < to) { + if (isHEXDIG(s.charAt(from))) { + // Continue + from++; + } else { + break; + } + } + if (from >= to || s.charAt(from++) != '.') { + return -1; + } + int count = 0; + char c; + while ((from + count) < to && (isUnreserved(c = s.charAt(from + count)) || isSubDelims(c) || c == ':')) { + count++; + } + if (count == 0) { + return -1; + } + return from + count; + } + + /** + * Try to parse the dec-octet rule as per + * RFC3986. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the index after parsing or {@code -1} if the value is invalid + */ + public static int parseDecOctet(String s, int from, int to) { + int val = parseAndEvalDigit(s, from++, to); + if (val < 0 || val > 9) { + return -1; + } + if (val == 0) { + return from; + } + int n = parseAndEvalDigit(s, from, to); + if (n != -1) { + val = val * 10 + n; + n = parseAndEvalDigit(s, ++from, to); + if (n != -1) { + from++; + val = val * 10 + n; + } + } + if (val < 256) { + return from; + } + return -1; + } + + /** + * Try to parse the DIGIT rule as per + * Value Range Alternatives. + * + * @param s a string containing the value to parse + * @param from the index at which parsing begins + * @param to the index at which parsing ends + * @return the parsed value or {@code -1} if parsing did not occur + */ + public static int parseAndEvalDigit(String s, int from, int to) { + if (from >= to) { + return -1; + } + char ch = s.charAt(from); + // a very predictable condition + if (ch < 128) { + // negative short values are still positive ints + return DIGITS[ch]; + } + return -1; + } + + private static boolean isUnreserved(char ch) { + return isALPHA(ch) || isDIGIT(ch) || ch == '-' || ch == '.' || ch == '_' || ch == '~'; + } + + private static boolean isSubDelims(char ch) { + return ch == '!' || ch == '$' || ch == '&' || ch == '\'' || ch == '(' || ch == ')' || ch == '*' || ch == '+' || ch == ',' || ch == ';' || ch == '='; + } + + public static int parseAndEvalPort(String s, int pos) { + int port = 0; + while (++pos < s.length()) { + int digit = parseAndEvalDigit(s, pos, s.length()); + if (digit == -1) { + return -1; + } + port = port * 10 + digit; + if (port > 65535) { + return -1; + } + } + return port; + } +} diff --git a/vertx-core/src/test/java/io/vertx/benchmarks/HostAndPortBenchmark.java b/vertx-core/src/test/java/io/vertx/benchmarks/HostAndPortBenchmark.java index 05a97896fa2..2d324694c34 100644 --- a/vertx-core/src/test/java/io/vertx/benchmarks/HostAndPortBenchmark.java +++ b/vertx-core/src/test/java/io/vertx/benchmarks/HostAndPortBenchmark.java @@ -11,6 +11,7 @@ package io.vertx.benchmarks; +import io.vertx.core.net.impl.UriParser; import io.vertx.core.net.impl.HostAndPortImpl; import org.openjdk.jmh.annotations.*; @@ -35,13 +36,13 @@ public void setup() { @Benchmark public int parseIPv4Address() { String host = this.host; - return HostAndPortImpl.parseIPv4Address(host, 0, host.length()); + return UriParser.parseIPv4Address(host, 0, host.length()); } @Benchmark public int parseHost() { String host = this.host; - return HostAndPortImpl.parseHost(host, 0, host.length()); + return UriParser.parseHost(host, 0, host.length()); } @Benchmark diff --git a/vertx-core/src/test/java/io/vertx/test/core/TestParser.java b/vertx-core/src/test/java/io/vertx/test/core/TestParser.java new file mode 100644 index 00000000000..47b13869bd7 --- /dev/null +++ b/vertx-core/src/test/java/io/vertx/test/core/TestParser.java @@ -0,0 +1,22 @@ +package io.vertx.test.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@FunctionalInterface +public interface TestParser { + + int parse(String s, int from, int to); + + default void assertParse(String s) { + int res = parse(s, 0, s.length()); + assertEquals(s.length(), res); + } + + default void assertFailParse(String s) { + int res = parse(s, 0, s.length()); + if (res == s.length()) { + fail(); + } + } +} diff --git a/vertx-core/src/test/java/io/vertx/tests/http/connection/HttpClientConnectionTest.java b/vertx-core/src/test/java/io/vertx/tests/http/connection/HttpClientConnectionTest.java index 5f88ee3b943..988bff4bf84 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/connection/HttpClientConnectionTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/connection/HttpClientConnectionTest.java @@ -10,9 +10,9 @@ */ package io.vertx.tests.http.connection; -import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.*; +import io.vertx.core.http.impl.AltSvc; import io.vertx.core.http.impl.Origin; import io.vertx.core.http.impl.http2.Http2Connection; import io.vertx.core.internal.buffer.BufferInternal; @@ -133,7 +133,7 @@ public void testAlternateServiceHandler() throws Exception { } void testAlternateServiceHandler(Origin origin) throws Exception { - String expected = "Alt-Svc: h2=\":443\"; ma=2592000;"; + String expected = "h2=\"192.168.0.1:443\"; ma=2592000"; server.requestHandler(request -> { HttpServerResponse response = request.response(); if (request.version() == HttpVersion.HTTP_2) { @@ -159,19 +159,26 @@ void testAlternateServiceHandler(Origin origin) throws Exception { }); startServer(testAddress); HttpClientConnection connection = client.connect(new HttpConnectOptions().setServer(testAddress).setHost(requestOptions.getHost()).setPort(requestOptions.getPort())).await(); - ((io.vertx.core.http.impl.UnpooledHttpClientConnection)connection).unwrap().alternativeServicesHandler(altSvc -> { - assertNotNull(altSvc); - assertNotNull(altSvc.origin); + ((io.vertx.core.http.impl.UnpooledHttpClientConnection)connection).unwrap().alternativeServicesHandler(evt -> { + assertNotNull(evt); + assertNotNull(evt.origin); if (origin != null) { - assertEquals(origin.scheme, altSvc.origin.scheme); - assertEquals(origin.host, altSvc.origin.host); - assertEquals(origin.port, altSvc.origin.port); + assertEquals(origin.scheme, evt.origin.scheme); + assertEquals(origin.host, evt.origin.host); + assertEquals(origin.port, evt.origin.port); } else { - assertEquals("http", altSvc.origin.scheme); - assertEquals(testAddress.host(), altSvc.origin.host); - assertEquals(testAddress.port(), altSvc.origin.port); + assertEquals("http", evt.origin.scheme); + assertEquals(testAddress.host(), evt.origin.host); + assertEquals(testAddress.port(), evt.origin.port); } - assertEquals(expected, altSvc.value); + assertEquals(AltSvc.ListOfValue.class, evt.altSvc.getClass()); + AltSvc.ListOfValue list = (AltSvc.ListOfValue)evt.altSvc; + assertEquals(1, list.size()); + AltSvc.Value value = list.get(0); + assertEquals("h2", value.protocolId()); + assertEquals("192.168.0.1", value.altAuthority().host()); + assertEquals(443, value.altAuthority().port()); + assertEquals("2592000", value.parameters().get("ma")); testComplete(); }); Buffer response = connection diff --git a/vertx-core/src/test/java/io/vertx/tests/http/impl/AltSvcParserTest.java b/vertx-core/src/test/java/io/vertx/tests/http/impl/AltSvcParserTest.java new file mode 100644 index 00000000000..745d0a73ef1 --- /dev/null +++ b/vertx-core/src/test/java/io/vertx/tests/http/impl/AltSvcParserTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 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.tests.http.impl; + +import io.vertx.core.http.impl.AltSvc; +import io.vertx.core.net.HostAndPort; +import io.vertx.test.core.TestParser; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.*; + +public class AltSvcParserTest { + + public static TestParser token = AltSvc::parseParameter; + public static TestParser alternative = AltSvc::parseAlternative; + public static TestParser altValue = AltSvc::parseAltValue; + + @Test + public void testParseRawAltValue() { + altValue.assertParse("abc=\"def\""); + altValue.assertFailParse("abc=\"def\" "); + altValue.assertFailParse("abc=\"def\" ;"); + altValue.assertFailParse("abc=\"def\" ; "); + altValue.assertParse("abc=\"def\";a=b"); + altValue.assertParse("abc=\"def\" ;a=b"); + altValue.assertParse("abc=\"def\"; a=b"); + altValue.assertParse("abc=\"def\" ; a=b"); + altValue.assertFailParse("abc=\"def\";a=b "); + } + + @Test + public void testParseAltValue() { + AltSvc.Value value = AltSvc.parseAltValue("abc=\":10\""); + assertNotNull(value); + assertEquals("abc", value.protocolId()); + assertEquals(HostAndPort.authority("", 10), value.altAuthority()); + assertEquals(Map.of(), value.parameters()); + value = AltSvc.parseAltValue("abc=\"foo:10\""); + assertNotNull(value); + assertEquals("abc", value.protocolId()); + assertEquals(HostAndPort.authority("foo", 10), value.altAuthority()); + assertEquals(Map.of(), value.parameters()); + value = AltSvc.parseAltValue("abc=\"192.168.0.1:10\""); + assertNotNull(value); + assertEquals("abc", value.protocolId()); + assertEquals(HostAndPort.authority("192.168.0.1", 10), value.altAuthority()); + assertEquals(Map.of(), value.parameters()); + value = AltSvc.parseAltValue("abc=\"[::]:10\""); + assertNotNull(value); + assertEquals("abc", value.protocolId()); + assertEquals(HostAndPort.authority("[::]", 10), value.altAuthority()); + assertEquals(Map.of(), value.parameters()); + value = AltSvc.parseAltValue("abc=\"foo:10\";a=b"); + assertNotNull(value); + assertEquals("abc", value.protocolId()); + assertEquals(HostAndPort.authority("foo", 10), value.altAuthority()); + assertEquals(Map.of("a", "b"), value.parameters()); + value = AltSvc.parseAltValue("abc=\"foo:10\";a=\"b\""); + assertNotNull(value); + assertEquals("abc", value.protocolId()); + assertEquals(HostAndPort.authority("foo", 10), value.altAuthority()); + assertEquals(Map.of("a", "b"), value.parameters()); + assertNull(AltSvc.parseAltValue("abc=\"def\";a")); + assertNull(AltSvc.parseAltValue("abc=\"def\";a=")); + } + + @Test + public void testParseAlternative() { + alternative.assertParse("abc=\"def\""); + alternative.assertFailParse("abc=def"); + } + + @Test + public void testParseParameter() { + token.assertParse("abc=def"); + token.assertParse("abc=\"def\""); + token.assertFailParse(""); + token.assertFailParse("abc"); + token.assertFailParse("abc="); + token.assertFailParse("abc=\"def"); + token.assertFailParse("abc=def\""); + token.assertFailParse("abc =def"); + token.assertFailParse("abc= def"); + } + + @Test + public void testParseAltSvc() { + AltSvc.Clear clear = (AltSvc.Clear)AltSvc.parseAltSvc("clear"); + assertNotNull(clear); + AltSvc.ListOfValue values = (AltSvc.ListOfValue)AltSvc.parseAltSvc("abc=\":10\"" + "," + "abc=\":12\""); + assertEquals(2, values.size()); + values = (AltSvc.ListOfValue)AltSvc.parseAltSvc("abc=\":10\"" + ","); + assertNull(values); + values = (AltSvc.ListOfValue)AltSvc.parseAltSvc("abc=\":10\"" + " a"); + assertNull(values); + } +} diff --git a/vertx-core/src/test/java/io/vertx/tests/http/impl/HttpParserTest.java b/vertx-core/src/test/java/io/vertx/tests/http/impl/HttpParserTest.java new file mode 100644 index 00000000000..f12ddd7dd27 --- /dev/null +++ b/vertx-core/src/test/java/io/vertx/tests/http/impl/HttpParserTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 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.tests.http.impl; + +import io.vertx.core.http.impl.HttpParser; +import io.vertx.test.core.TestParser; +import org.junit.Test; + +public class HttpParserTest { + + public static TestParser OWS = HttpParser::parseOWS; + public static TestParser quotedString = HttpParser::parseQuotedString; + public static TestParser token = HttpParser::parseToken; + + @Test + public void testParseOWS() { + OWS.assertParse(""); + OWS.assertParse(" \t"); + OWS.assertParse("\t "); + OWS.assertParse("\t"); + } + + @Test + public void testParseQuotedString() { + quotedString.assertParse("\"ABC\""); + quotedString.assertParse("\"\t\""); + quotedString.assertParse("\" \""); + quotedString.assertParse("\"!\""); + for (char c = '\u005D';c <= '\u007E';c++) { + quotedString.assertParse("\"" + c + "\""); + } + for (char c = '\u0080';c <= '\u00FF';c++) { + quotedString.assertParse("\"" + c + "\""); + } + quotedString.assertParse("\"\\\t\""); + quotedString.assertParse("\"\\ \""); + for (char c = '\u0021';c <= '\u007E';c++) { + quotedString.assertParse("\"\\" + c + "\""); + } + for (char c = '\u0080';c <= '\u00FF';c++) { + quotedString.assertParse("\"\\" + c + "\""); + } + } + + @Test + public void testParseToken() { + token.assertParse("!"); + token.assertParse("#"); + token.assertParse("$"); + token.assertParse("%"); + token.assertParse("&"); + token.assertParse("'"); + token.assertParse("*"); + token.assertParse("+"); + token.assertParse("-"); + token.assertParse("."); + token.assertParse("^"); + token.assertParse("_"); + token.assertParse("`"); + token.assertParse("|"); + token.assertParse("~"); + for (char c = '0';c <= '9';c++) { + token.assertParse("" + c); + } + for (char c = 'A';c <= 'Z';c++) { + token.assertParse("" + c); + } + for (char c = 'a';c <= 'z';c++) { + token.assertParse("" + c); + } + token.assertFailParse(""); + token.assertFailParse(" "); + token.assertFailParse("?"); + } +} diff --git a/vertx-core/src/test/java/io/vertx/tests/net/HostAndPortTest.java b/vertx-core/src/test/java/io/vertx/tests/net/HostAndPortTest.java index b9915d4ae93..43e4d2a904a 100644 --- a/vertx-core/src/test/java/io/vertx/tests/net/HostAndPortTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/net/HostAndPortTest.java @@ -1,5 +1,6 @@ package io.vertx.tests.net; +import io.vertx.core.net.impl.UriParser; import io.vertx.core.json.JsonObject; import io.vertx.core.net.HostAndPort; import io.vertx.core.net.impl.HostAndPortImpl; @@ -12,56 +13,173 @@ public class HostAndPortTest { @Test public void testParseIPLiteral() { - Assert.assertEquals(-1, HostAndPortImpl.parseIPLiteral("", 0, 0)); - assertEquals(-1, HostAndPortImpl.parseIPLiteral("[", 0, 1)); - assertEquals(-1, HostAndPortImpl.parseIPLiteral("[]", 0, 2)); - assertEquals(3, HostAndPortImpl.parseIPLiteral("[0]", 0, 3)); - assertEquals(-1, HostAndPortImpl.parseIPLiteral("[0", 0, 2)); + Assert.assertEquals(-1, UriParser.parseIPLiteral("", 0, 0)); + assertEquals(-1, UriParser.parseIPLiteral("[", 0, 1)); + assertEquals(-1, UriParser.parseIPLiteral("[]", 0, 2)); + assertEquals(4, UriParser.parseIPLiteral("[::]", 0, 4)); + assertEquals(-1, UriParser.parseIPLiteral("[::", 0, 3)); } @Test public void testParseDecOctet() { - assertEquals(-1, HostAndPortImpl.parseDecOctet("", 0, 0)); - assertEquals(1, HostAndPortImpl.parseDecOctet("0", 0, 1)); - assertEquals(1, HostAndPortImpl.parseDecOctet("9", 0, 1)); - assertEquals(1, HostAndPortImpl.parseDecOctet("01", 0, 2)); - assertEquals(2, HostAndPortImpl.parseDecOctet("19", 0, 2)); - assertEquals(3, HostAndPortImpl.parseDecOctet("192", 0, 3)); - assertEquals(3, HostAndPortImpl.parseDecOctet("1234", 0, 4)); - assertEquals(-1, HostAndPortImpl.parseDecOctet("256", 0, 3)); + assertEquals(-1, UriParser.parseDecOctet("", 0, 0)); + assertEquals(1, UriParser.parseDecOctet("0", 0, 1)); + assertEquals(1, UriParser.parseDecOctet("9", 0, 1)); + assertEquals(1, UriParser.parseDecOctet("01", 0, 2)); + assertEquals(2, UriParser.parseDecOctet("19", 0, 2)); + assertEquals(3, UriParser.parseDecOctet("192", 0, 3)); + assertEquals(3, UriParser.parseDecOctet("1234", 0, 4)); + assertEquals(-1, UriParser.parseDecOctet("256", 0, 3)); } @Test public void testParseIPV4Address() { - assertEquals(-1, HostAndPortImpl.parseIPv4Address("0.0.0", 0, 5)); - assertEquals(-1, HostAndPortImpl.parseIPv4Address("0.0.0#0", 0, 7)); - assertEquals(7, HostAndPortImpl.parseIPv4Address("0.0.0.0", 0, 7)); - assertEquals(11, HostAndPortImpl.parseIPv4Address("192.168.0.0", 0, 11)); - assertEquals(-1, HostAndPortImpl.parseIPv4Address("011.168.0.0", 0, 11)); - assertEquals(-1, HostAndPortImpl.parseIPv4Address("10.0.0.1.nip.io", 0, 15)); - assertEquals(-1, HostAndPortImpl.parseIPv4Address("10.0.0.1.nip.io", 0, 9)); - assertEquals(8, HostAndPortImpl.parseIPv4Address("10.0.0.1.nip.io", 0, 8)); - assertEquals(-1, HostAndPortImpl.parseIPv4Address("10.0.0.1:", 0, 9)); - assertEquals(8, HostAndPortImpl.parseIPv4Address("10.0.0.1:0", 0, 10)); + assertEquals(-1, UriParser.parseIPv4Address("0.0.0", 0, 5)); + assertEquals(-1, UriParser.parseIPv4Address("0.0.0#0", 0, 7)); + assertEquals(7, UriParser.parseIPv4Address("0.0.0.0", 0, 7)); + assertEquals(11, UriParser.parseIPv4Address("192.168.0.0", 0, 11)); + assertEquals(-1, UriParser.parseIPv4Address("011.168.0.0", 0, 11)); + assertEquals(-1, UriParser.parseIPv4Address("10.0.0.1.nip.io", 0, 15)); + assertEquals(-1, UriParser.parseIPv4Address("10.0.0.1.nip.io", 0, 9)); + assertEquals(8, UriParser.parseIPv4Address("10.0.0.1.nip.io", 0, 8)); + assertEquals(-1, UriParser.parseIPv4Address("10.0.0.1:", 0, 9)); + assertEquals(8, UriParser.parseIPv4Address("10.0.0.1:0", 0, 10)); } @Test public void testParseRegName() { - assertEquals(5, HostAndPortImpl.parseRegName("abcdef", 0, 5)); - assertEquals(5, HostAndPortImpl.parseRegName("abcdef:1234", 0, 5)); - assertEquals(11, HostAndPortImpl.parseRegName("example.com", 0, 11)); - assertEquals(14, HostAndPortImpl.parseRegName("example-fr.com", 0, 14)); - assertEquals(15, HostAndPortImpl.parseRegName("10.0.0.1.nip.io", 0, 15)); + assertEquals(5, UriParser.parseRegName("abcdef", 0, 5)); + assertEquals(5, UriParser.parseRegName("abcdef:1234", 0, 5)); + assertEquals(11, UriParser.parseRegName("example.com", 0, 11)); + assertEquals(14, UriParser.parseRegName("example-fr.com", 0, 14)); + assertEquals(15, UriParser.parseRegName("10.0.0.1.nip.io", 0, 15)); + } + + @Test + public void testParseIPvFuture() { + assertEquals(6, UriParser.parseIPvFuture("v123.a", 0, 6)); + assertEquals(6, UriParser.parseIPvFuture("v123.:", 0, 6)); + assertEquals(6, UriParser.parseIPvFuture("v123.!", 0, 6)); + } + + @Test + public void testParseIPv6Address() { + // Rule 1 + assertIPv6Address("0:1:2:3:4:5:1.1.1.1"); + assertIPv6Address("0:1:2:3:4:5:7:8"); + // Rule 2 + assertIPv6Address("::0:1:2:3:4:1.1.1.1"); + assertIPv6Address("::0:1:2:3:4:5:6"); + // Rule 3 + assertIPv6Address("0::0:1:2:3:1.1.1.1"); + assertIPv6Address("0::0:1:2:3:4:5"); + assertIPv6Address("::0:1:2:3:1.1.1.1"); + assertIPv6Address("::0:1:2:3:4:5"); + // Rule 4 + assertIPv6Address("0:1::0:1:2:1.1.1.1"); + assertIPv6Address("0:1::0:1:2:3:4"); + assertIPv6Address("0::0:1:2:1.1.1.1"); + assertIPv6Address("0::0:1:2:3:4"); + assertIPv6Address("::0:1:2:1.1.1.1"); + assertIPv6Address("::0:1:2:3:4"); + // Rule 5 + assertIPv6Address("0:1:2::0:1:1.1.1.1"); + assertIPv6Address("0:1:2::0:1:2:3"); + assertIPv6Address("0:1::0:1:1.1.1.1"); + assertIPv6Address("0:1::0:1:2:3"); + assertIPv6Address("0::0:1:1.1.1.1"); + assertIPv6Address("0::0:1:2:3"); + assertIPv6Address("::0:1:1.1.1.1"); + assertIPv6Address("::0:1:2:3"); + // Rule 6 + assertIPv6Address("0:1:2:3::0:1.1.1.1"); + assertIPv6Address("0:1:2:3::0:1:2"); + assertIPv6Address("0:1:2::0:1.1.1.1"); + assertIPv6Address("0:1:2::0:1:2"); + assertIPv6Address("0:1::0:1.1.1.1"); + assertIPv6Address("0:1::0:1:2"); + assertIPv6Address("0::0:1.1.1.1"); + assertIPv6Address("0::0:1:2"); + assertIPv6Address("::0:1.1.1.1"); + assertIPv6Address("::0:1:2"); + // Rule 7 + assertIPv6Address("0:1:2:3:4::1.1.1.1"); + assertIPv6Address("0:1:2:3:4::0:1"); + assertIPv6Address("0:1:2:3::1.1.1.1"); + assertIPv6Address("0:1:2:3::0:1"); + assertIPv6Address("0:1:2::1.1.1.1"); + assertIPv6Address("0:1:2::0:1"); + assertIPv6Address("0:1::1.1.1.1"); + assertIPv6Address("0:1::0:1"); + assertIPv6Address("0::1.1.1.1"); + assertIPv6Address("0::0:1"); + assertIPv6Address("::1.1.1.1"); + assertIPv6Address("::0:1"); + // Rule 8 + assertIPv6Address("0:1:2:3:4:5::0"); + assertIPv6Address("0:1:2:3:4::0"); + assertIPv6Address("0:1:2:3::0"); + assertIPv6Address("0:1:2::0"); + assertIPv6Address("0:1::0"); + assertIPv6Address("0::0"); + assertIPv6Address("::0"); + // Rule 9 + assertIPv6Address("0:1:2:3:4:5:6::"); + assertIPv6Address("0:1:2:3:4:5::"); + assertIPv6Address("0:1:2:3:4::"); + assertIPv6Address("0:1:2:3::"); + assertIPv6Address("0:1:2::"); + assertIPv6Address("0:1::"); + assertIPv6Address("0::"); + assertIPv6Address("::"); + } + + private void assertIPv6Address(String address) { + int len = UriParser.parseIPv6Address(address, 0, address.length()); + assertEquals(len, address.length()); } @Test public void testParseHost() { - assertEquals(14, HostAndPortImpl.parseHost("example-fr.com", 0, 14)); - assertEquals(5, HostAndPortImpl.parseHost("[0::]", 0, 5)); - assertEquals(7, HostAndPortImpl.parseHost("0.0.0.0", 0, 7)); - assertEquals(8, HostAndPortImpl.parseHost("10.0.0.1.nip.io", 0, 8)); - assertEquals(15, HostAndPortImpl.parseHost("10.0.0.1.nip.io", 0, 15)); - assertEquals(8, HostAndPortImpl.parseHost("10.0.0.1:8080", 0, 15)); + assertEquals(14, UriParser.parseHost("example-fr.com", 0, 14)); + assertEquals(5, UriParser.parseHost("[0::]", 0, 5)); + assertEquals(7, UriParser.parseHost("0.0.0.0", 0, 7)); + assertEquals(8, UriParser.parseHost("10.0.0.1.nip.io", 0, 8)); + assertEquals(15, UriParser.parseHost("10.0.0.1.nip.io", 0, 15)); + assertEquals(8, UriParser.parseHost("10.0.0.1:8080", 0, 15)); + } + + @Test + public void testParseH16() { + assertEquals(-1, UriParser.parseH16("", 0, 0)); + assertEquals(-1, UriParser.parseH16("Z", 0, 1)); + for (int i = 0;i < 0xFFFF;i++) { + String s = Integer.toHexString(i); + assertEquals(s.length(), UriParser.parseH16(s, 0, s.length())); + } + assertEquals(4, UriParser.parseH16("FFFFF", 0, 5)); + } + + @Test + public void testParseDoubleColon() { + assertEquals(2, UriParser._parseDoubleColon("::", 0, 2)); + assertEquals(-1, UriParser._parseDoubleColon(":", 0, 1)); + assertEquals(-1, UriParser._parseDoubleColon(":a", 0, 2)); + } + + @Test + public void testParseH16ColonH16() { + assertEquals(3, UriParser._parseH16ColonH16("0:0", 0, 3)); + assertEquals(-1, UriParser._parseH16ColonH16("0:", 0, 2)); + assertEquals(6, UriParser._parseH16ColonH16("0:FFFF", 0, 6)); + } + + @Test + public void testParseLs32() { + assertEquals(3, UriParser.parseLs32("0:0", 0, 3)); + assertEquals(6, UriParser.parseLs32("0:FFFF", 0, 6)); + assertEquals(7, UriParser.parseLs32("0.0.0.0", 0, 7)); + assertEquals(11, UriParser.parseLs32("192.168.0.1", 0, 11)); } @Test