Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 185 additions & 7 deletions vertx-core/src/main/java/io/vertx/core/http/impl/AltSvc.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,194 @@
*/
package io.vertx.core.http.impl;

import io.vertx.core.net.HostAndPort;
import io.vertx.core.net.impl.UriParser;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

/**
* An event signalling an <a href="https://datatracker.ietf.org/doc/html/rfc7838">alternative service</a> of an origin.
* Alt-svc parsed value.
*/
public class AltSvc {
public abstract class AltSvc {

protected AltSvc() {
}

/**
* Alt-Svc: Clear.
*/
public static class Clear extends AltSvc {
public static final Clear INSTANCE = new Clear();
private Clear() {
}
}

/**
* Alt-Svc: Value.
*/
public static class Value extends AltSvc {
private String protocolId;
private String rawAltAuthority;
private HostAndPort altAuthority;
private Map<String, String> parameters = Map.of();
private Value() {
}

/**
* @return the protocol id
*/
public String protocolId() {
return protocolId;
}

/**
* @return the alt authority
*/
public HostAndPort altAuthority() {
return altAuthority;
}

/**
* @return the parameter map
*/
public Map<String, String> 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 parse(String s) {
if (s.equals("clear")) {
return Clear.INSTANCE;
}
Value altSvc = new Value();
int res = parseAltValue(s, 0, s.length(), altSvc::setAlternative, altSvc::addParameter);
if (res == s.length()) {
// Now parse raw alt-authority into an HostAndPort
String raw = altSvc.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 null;
}
host = raw.substring(0, idx);
}
int port = UriParser.parseAndEvalPort(raw, idx);
if (port != -1) {
altSvc.altAuthority = HostAndPort.create(host, port);
return altSvc;
}
}
return null;
}

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<String, String> alternative, BiConsumer<String, String> 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 final Origin origin;
public final String value;
public static int parseParameter(String s, int from, int to, BiConsumer<String, String> 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<String, String> 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);
}
}
25 changes: 25 additions & 0 deletions vertx-core/src/main/java/io/vertx/core/http/impl/AltSvcEvent.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://datatracker.ietf.org/doc/html/rfc7838">alternative service</a> of an origin.
*/
public class AltSvcEvent {

public final Origin origin;
public final String value;

public AltSvcEvent(Origin origin, String value) {
this.origin = origin;
this.value = value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public interface HttpClientConnection extends HttpConnection {
* @param handler
* @return
*/
default HttpClientConnection alternativeServicesHandler(Handler<AltSvc> handler) {
default HttpClientConnection alternativeServicesHandler(Handler<AltSvcEvent> handler) {
return this;
}

Expand Down
63 changes: 63 additions & 0 deletions vertx-core/src/main/java/io/vertx/core/http/impl/HttpParser.java
Original file line number Diff line number Diff line change
@@ -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) {
return -1;
}
while (from < to && (isSP(s.charAt(from)) || isHTAB(s.charAt(from)))) {
from++;
}
return from;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1014,7 +1014,7 @@ public static List<io.vertx.core.http.HttpVersion> toHttpAlpnVersions(List<Strin
.collect(Collectors.toList());
}

public static AltSvc parseAltSvcFrame(ByteBuf payload) {
public static AltSvcEvent parseAltSvcFrame(ByteBuf payload) {
if (payload.readableBytes() >= 2) {
int idx = payload.readerIndex();
try {
Expand All @@ -1039,7 +1039,7 @@ public static AltSvc parseAltSvcFrame(ByteBuf payload) {
origin = null;
}
String value = payload.readString(payload.readableBytes(), StandardCharsets.US_ASCII);
return new AltSvc(origin, value);
return new AltSvcEvent(origin, value);
} finally {
payload.readerIndex(idx);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public class Http1xClientConnection extends Http1xConnection implements io.vertx

private Handler<Void> evictionHandler = DEFAULT_EVICTION_HANDLER;
private Handler<Object> invalidMessageHandler = INVALID_MSG_HANDLER;
private Handler<AltSvc> alternativeServicesHandler;
private Handler<AltSvcEvent> alternativeServicesHandler;
private boolean wantClose;
private boolean isConnect;
private int keepAliveTimeout;
Expand Down Expand Up @@ -150,7 +150,7 @@ public io.vertx.core.http.impl.HttpClientConnection invalidMessageHandler(Handle
}

@Override
public HttpClientConnection alternativeServicesHandler(Handler<AltSvc> handler) {
public HttpClientConnection alternativeServicesHandler(Handler<AltSvcEvent> handler) {
alternativeServicesHandler = handler;
return this;
}
Expand Down Expand Up @@ -906,14 +906,14 @@ private void handleResponseBegin(Stream stream, HttpVersion version, io.vertx.co
}
}
String altSvcHeader = response.headers.get(ALT_SVC);
Handler<AltSvc> handler;
Handler<AltSvcEvent> handler;
if (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, altSvcHeader), handler);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public class Http2UpgradeClientConnection implements io.vertx.core.http.impl.Htt
private Handler<Void> evictionHandler;
private Handler<Object> invalidMessageHandler;
private Handler<Long> concurrencyChangeHandler;
private Handler<AltSvc> alternativeServicesHandler;
private Handler<AltSvcEvent> alternativeServicesHandler;
private Handler<Http2Settings> remoteSettingsHandler;

public Http2UpgradeClientConnection(Http1xClientConnection connection, long maxLifetimeMillis, ClientMetrics<?, ?, ?> metrics, Http2ChannelUpgrade upgrade) {
Expand Down Expand Up @@ -812,7 +812,7 @@ public io.vertx.core.http.impl.HttpClientConnection concurrencyChangeHandler(Han
}

@Override
public HttpClientConnection alternativeServicesHandler(Handler<AltSvc> handler) {
public HttpClientConnection alternativeServicesHandler(Handler<AltSvcEvent> handler) {
if (current instanceof Http1xClientConnection) {
alternativeServicesHandler = handler;
}
Expand Down
Loading
Loading