Skip to content

Commit 4c919e1

Browse files
committed
RFC 7639 ALPN.
Add ConnectAlpnProvider and inject ALPN header in ConnectExec/AsyncConnectExec. Provide builder hooks for fixed list or provider-driven values.
1 parent 88c315d commit 4c919e1

File tree

8 files changed

+514
-68
lines changed

8 files changed

+514
-68
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http;
28+
29+
import java.util.List;
30+
31+
import org.apache.hc.core5.http.HttpHost;
32+
33+
/**
34+
* Supplies the Application-Layer Protocol Negotiation (ALPN) protocol IDs
35+
* to advertise in the HTTP {@code ALPN} header on a {@code CONNECT} request
36+
* (RFC 7639).
37+
*
38+
* <p>If this method returns {@code null} or an empty list, the client will
39+
* not add the {@code ALPN} header.</p>
40+
*
41+
* <p>Implementations should be fast and side-effect free; it may be invoked
42+
* for each CONNECT attempt.</p>
43+
*
44+
* @since 5.6
45+
*/
46+
@FunctionalInterface
47+
public interface ConnectAlpnProvider {
48+
49+
/**
50+
* Returns the ALPN protocol IDs to advertise for a tunnel to {@code target}
51+
* over the given {@code route}.
52+
*
53+
* @param target the origin server the tunnel will connect to (non-null)
54+
* @param route the planned connection route, including proxy info (non-null)
55+
* @return list of protocol IDs (e.g., {@code "h2"}, {@code "http/1.1"});
56+
* {@code null} or empty to omit the header
57+
*/
58+
List<String> getAlpnForTunnel(HttpHost target, HttpRoute route);
59+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.impl;
28+
29+
30+
import java.nio.charset.StandardCharsets;
31+
import java.util.ArrayList;
32+
import java.util.Collections;
33+
import java.util.List;
34+
35+
import org.apache.hc.core5.annotation.Contract;
36+
import org.apache.hc.core5.annotation.Internal;
37+
import org.apache.hc.core5.annotation.ThreadingBehavior;
38+
import org.apache.hc.core5.http.message.MessageSupport;
39+
import org.apache.hc.core5.http.message.ParserCursor;
40+
import org.apache.hc.core5.util.Args;
41+
42+
/**
43+
* Codec for the HTTP {@code ALPN} header field (RFC 7639).
44+
*
45+
* @since 5.4
46+
*/
47+
@Contract(threading = ThreadingBehavior.IMMUTABLE)
48+
@Internal
49+
public final class AlpnHeaderSupport {
50+
51+
private static final char[] HEXADECIMAL = "0123456789ABCDEF".toCharArray();
52+
53+
private AlpnHeaderSupport() {
54+
}
55+
56+
/**
57+
* Formats a list of raw ALPN protocol IDs into a single {@code ALPN} header value.
58+
*/
59+
public static String formatValue(final List<String> protocolIds) {
60+
Args.notEmpty(protocolIds, "protocolIds");
61+
final StringBuilder sb = new StringBuilder();
62+
boolean first = true;
63+
for (final String id : protocolIds) {
64+
if (!first) {
65+
sb.append(", ");
66+
}
67+
sb.append(encodeId(id));
68+
first = false;
69+
}
70+
return sb.toString();
71+
}
72+
73+
/**
74+
* Parses an {@code ALPN} header value into decoded protocol IDs.
75+
*/
76+
public static List<String> parseValue(final String value) {
77+
if (value == null || value.isEmpty()) {
78+
return Collections.emptyList();
79+
}
80+
final List<String> out = new ArrayList<>();
81+
final ParserCursor cursor = new ParserCursor(0, value.length());
82+
MessageSupport.parseTokens(value, cursor, token -> {
83+
if (!token.isEmpty()) {
84+
out.add(decodeId(token));
85+
}
86+
});
87+
return out;
88+
}
89+
90+
/**
91+
* Encodes a single raw protocol ID to canonical token form.
92+
*/
93+
public static String encodeId(final String id) {
94+
Args.notBlank(id, "id");
95+
final byte[] bytes = id.getBytes(StandardCharsets.UTF_8);
96+
final StringBuilder sb = new StringBuilder(bytes.length);
97+
for (final byte b0 : bytes) {
98+
final int b = b0 & 0xFF;
99+
if (b == '%' || !isTchar(b)) {
100+
appendPctEncoded(b, sb);
101+
} else {
102+
sb.append((char) b);
103+
}
104+
}
105+
return sb.toString();
106+
}
107+
108+
/**
109+
* Decodes percent-encoded token to raw ID using UTF-8.
110+
* Accepts lowercase hex; malformed/incomplete sequences are left literal.
111+
*/
112+
public static String decodeId(final String token) {
113+
Args.notBlank(token, "token");
114+
final byte[] buf = new byte[token.length()];
115+
int bi = 0;
116+
for (int i = 0; i < token.length(); ) {
117+
final char c = token.charAt(i);
118+
if (c == '%' && i + 2 < token.length()) {
119+
final int hi = hexVal(token.charAt(i + 1));
120+
final int lo = hexVal(token.charAt(i + 2));
121+
if (hi >= 0 && lo >= 0) {
122+
buf[bi++] = (byte) ((hi << 4) | lo);
123+
i += 3;
124+
continue;
125+
}
126+
}
127+
buf[bi++] = (byte) c;
128+
i++;
129+
}
130+
return new String(buf, 0, bi, StandardCharsets.UTF_8);
131+
}
132+
133+
// RFC7230 tchar minus '%' (RFC7639 requires '%' be percent-encoded)
134+
private static boolean isTchar(final int c) {
135+
if (c >= '0' && c <= '9') {
136+
return true;
137+
}
138+
if (c >= 'A' && c <= 'Z') {
139+
return true;
140+
}
141+
if (c >= 'a' && c <= 'z') {
142+
return true;
143+
}
144+
switch (c) {
145+
case '!':
146+
case '#':
147+
case '$':
148+
case '&':
149+
case '\'':
150+
case '*':
151+
case '+':
152+
case '-':
153+
case '.':
154+
case '^':
155+
case '_':
156+
case '`':
157+
case '|':
158+
case '~':
159+
return true;
160+
default:
161+
return false;
162+
}
163+
}
164+
165+
private static void appendPctEncoded(final int b, final StringBuilder sb) {
166+
sb.append('%');
167+
sb.append(HEXADECIMAL[(b >>> 4) & 0x0F]);
168+
sb.append(HEXADECIMAL[b & 0x0F]);
169+
}
170+
171+
private static int hexVal(final char c) {
172+
if (c >= '0' && c <= '9') {
173+
return c - '0';
174+
}
175+
if (c >= 'A' && c <= 'F') {
176+
return 10 + (c - 'A');
177+
}
178+
if (c >= 'a' && c <= 'f') {
179+
return 10 + (c - 'a');
180+
}
181+
return -1;
182+
}
183+
}

0 commit comments

Comments
 (0)