Skip to content

Commit 42fd028

Browse files
committed
Add example
1 parent 6e4a83a commit 42fd028

File tree

1 file changed

+243
-0
lines changed

1 file changed

+243
-0
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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.examples;
28+
29+
import java.io.BufferedReader;
30+
import java.io.IOException;
31+
import java.io.InputStream;
32+
import java.io.InputStreamReader;
33+
import java.io.OutputStream;
34+
import java.net.InetSocketAddress;
35+
import java.net.ServerSocket;
36+
import java.net.Socket;
37+
import java.nio.charset.StandardCharsets;
38+
import java.util.Locale;
39+
import java.util.concurrent.ExecutorService;
40+
import java.util.concurrent.Executors;
41+
42+
import org.apache.hc.client5.http.HttpRoute;
43+
import org.apache.hc.client5.http.RouteInfo;
44+
import org.apache.hc.client5.http.classic.methods.HttpGet;
45+
import org.apache.hc.client5.http.config.RequestConfig;
46+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
47+
import org.apache.hc.client5.http.impl.classic.HttpClients;
48+
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
49+
import org.apache.hc.core5.http.ClassicHttpRequest;
50+
import org.apache.hc.core5.http.ClassicHttpResponse;
51+
import org.apache.hc.core5.http.ContentType;
52+
import org.apache.hc.core5.http.HttpHost;
53+
import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
54+
import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
55+
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
56+
import org.apache.hc.core5.http.io.HttpRequestHandler;
57+
import org.apache.hc.core5.http.io.entity.EntityUtils;
58+
import org.apache.hc.core5.http.io.entity.StringEntity;
59+
import org.apache.hc.core5.http.message.StatusLine;
60+
import org.apache.hc.core5.http.protocol.HttpContext;
61+
import org.apache.hc.core5.io.CloseMode;
62+
import org.apache.hc.core5.util.Timeout;
63+
64+
/**
65+
* Example: advertise ALPN on a proxy {@code CONNECT}.
66+
*
67+
* <p>This demo starts:
68+
* <ul>
69+
* <li>a tiny classic origin server on {@code localhost},</li>
70+
* <li>a minimal blocking proxy that prints the received {@code ALPN} header,</li>
71+
* <li>a classic client forced to tunnel via {@code CONNECT} and configured with
72+
* {@code .setConnectAlpn("h2","http/1.1")}.</li>
73+
* </ul>
74+
* The proxy logs a line like {@code ALPN: h2, http%2F1.1}, and the client receives {@code 200 OK}.
75+
*
76+
* <p><b>Tip:</b> Keep the request host consistent with the server’s canonical host to avoid
77+
* {@code 421 Misdirected Request}. This example uses {@code localhost} for both.</p>
78+
*
79+
* @since 5.6
80+
*/
81+
public final class ClassicConnectAlpnEndToEndDemo {
82+
83+
public static void main(final String[] args) throws Exception {
84+
// ---- Origin server (classic)
85+
final HttpServer origin = ServerBootstrap.bootstrap()
86+
.setListenerPort(0)
87+
.setCanonicalHostName("localhost")
88+
.register("/hello", new HelloHandler())
89+
.create();
90+
origin.start();
91+
final int originPort = origin.getLocalPort();
92+
93+
// ---- Tiny blocking proxy printing ALPN and tunneling bytes
94+
final ServerSocket proxyServer = new ServerSocket(0, 50, java.net.InetAddress.getByName("127.0.0.1"));
95+
final int proxyPort = proxyServer.getLocalPort();
96+
final ExecutorService proxyPool = Executors.newCachedThreadPool();
97+
proxyPool.submit(() -> {
98+
try {
99+
while (!proxyServer.isClosed()) {
100+
final Socket clientSock = proxyServer.accept(); // blocks
101+
proxyPool.submit(() -> handleConnectClient(clientSock));
102+
}
103+
} catch (final java.net.SocketException closed) {
104+
// server socket closed while blocking in accept() -> exit quietly
105+
if (!proxyServer.isClosed()) {
106+
System.out.println("[proxy] accept error: " + closed);
107+
}
108+
} catch (final Exception ex) {
109+
System.out.println("[proxy] error: " + ex);
110+
}
111+
return null;
112+
});
113+
114+
115+
// ---- Client forcing CONNECT even for HTTP (so the demo stays TLS-free)
116+
final HttpRoutePlanner alwaysTunnelPlanner = (target, context) -> new HttpRoute(
117+
target,
118+
null,
119+
new HttpHost("127.0.0.1", proxyPort),
120+
false,
121+
RouteInfo.TunnelType.TUNNELLED,
122+
RouteInfo.LayerType.PLAIN);
123+
124+
final RequestConfig reqCfg = RequestConfig.custom()
125+
.setResponseTimeout(Timeout.ofSeconds(10))
126+
.build();
127+
128+
try (final CloseableHttpClient client = HttpClients.custom()
129+
.setDefaultRequestConfig(reqCfg)
130+
.setRoutePlanner(alwaysTunnelPlanner)
131+
// Advertise ALPN on CONNECT
132+
.setConnectAlpn("h2", "http/1.1")
133+
.build()) {
134+
135+
final String url = "http://localhost:" + originPort + "/hello";
136+
final HttpGet get = new HttpGet(url);
137+
138+
final HttpClientResponseHandler<String> handler = response -> {
139+
System.out.println("[client] " + new StatusLine(response));
140+
final int code = response.getCode();
141+
if (code >= 200 && code < 300) {
142+
return EntityUtils.toString(response.getEntity());
143+
}
144+
throw new IOException("Unexpected response code " + code);
145+
};
146+
147+
final String body = client.execute(get, handler);
148+
System.out.println("[client] body: " + body);
149+
} finally {
150+
origin.close(CloseMode.GRACEFUL);
151+
proxyServer.close(); // triggers SocketException in accept()
152+
proxyPool.shutdown(); // let workers finish naturally
153+
proxyPool.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
154+
}
155+
}
156+
157+
// ---- Minimal CONNECT proxy (blocking) ----
158+
private static void handleConnectClient(final Socket client) {
159+
try (final Socket clientSock = client;
160+
final BufferedReader in = new BufferedReader(new InputStreamReader(clientSock.getInputStream(), StandardCharsets.ISO_8859_1));
161+
final OutputStream out = clientSock.getOutputStream()) {
162+
163+
final String requestLine = in.readLine();
164+
if (requestLine == null || !requestLine.toUpperCase(Locale.ROOT).startsWith("CONNECT ")) {
165+
writeSimple(out, "HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\n\r\n");
166+
return;
167+
}
168+
final String hostPort = requestLine.split("\\s+")[1];
169+
String alpnHeader = null;
170+
171+
String line;
172+
while ((line = in.readLine()) != null && !line.isEmpty()) {
173+
final int idx = line.indexOf(':');
174+
if (idx > 0) {
175+
final String name = line.substring(0, idx).trim();
176+
final String value = line.substring(idx + 1).trim();
177+
if ("ALPN".equalsIgnoreCase(name)) {
178+
alpnHeader = value;
179+
}
180+
}
181+
}
182+
183+
System.out.println("[proxy] CONNECT " + hostPort);
184+
System.out.println("[proxy] ALPN: " + (alpnHeader != null ? alpnHeader : "<none>"));
185+
186+
final String[] hp = hostPort.split(":");
187+
final String host = hp[0];
188+
final int port = Integer.parseInt(hp[1]);
189+
190+
final Socket origin = new Socket();
191+
origin.connect(new InetSocketAddress(host, port), 3000);
192+
193+
writeSimple(out, "HTTP/1.1 200 Connection Established\r\n\r\n");
194+
195+
final InputStream clientIn = clientSock.getInputStream();
196+
final OutputStream clientOut = clientSock.getOutputStream();
197+
final InputStream originIn = origin.getInputStream();
198+
final OutputStream originOut = origin.getOutputStream();
199+
200+
final Thread t1 = new Thread(() -> pump(clientIn, originOut));
201+
final Thread t2 = new Thread(() -> pump(originIn, clientOut));
202+
t1.start();
203+
t2.start();
204+
t1.join();
205+
t2.join();
206+
origin.close();
207+
208+
} catch (final Exception ex) {
209+
System.out.println("[proxy] error: " + ex);
210+
}
211+
}
212+
213+
private static void writeSimple(final OutputStream out, final String s) throws IOException {
214+
out.write(s.getBytes(StandardCharsets.ISO_8859_1));
215+
out.flush();
216+
}
217+
218+
private static void pump(final InputStream in, final OutputStream out) {
219+
final byte[] buf = new byte[8192];
220+
try {
221+
int n;
222+
while ((n = in.read(buf)) >= 0) {
223+
out.write(buf, 0, n);
224+
out.flush();
225+
}
226+
} catch (final IOException ignore) {
227+
}
228+
try {
229+
out.flush();
230+
} catch (final IOException ignore) {
231+
}
232+
}
233+
234+
private static final class HelloHandler implements HttpRequestHandler {
235+
@Override
236+
public void handle(final ClassicHttpRequest request,
237+
final ClassicHttpResponse response,
238+
final HttpContext context) {
239+
response.setCode(200);
240+
response.setEntity(new StringEntity("Hello through the tunnel!", ContentType.TEXT_PLAIN));
241+
}
242+
}
243+
}

0 commit comments

Comments
 (0)