Skip to content

Commit dc9bfe6

Browse files
committed
proxy support, including https tunnel
1 parent 01fdbae commit dc9bfe6

File tree

10 files changed

+277
-25
lines changed

10 files changed

+277
-25
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ It adds websocket support using modified source from nanohttpd.
66

77
It has basic server-side proxy support using [ProxyHandler](https://github.com/robaho/httpserver/blob/main/src/main/java/robaho/net/httpserver/extras/ProxyHandler.java).
88

9+
ProxyHandler also supports tunneling proxies using CONNECT for https.
910

1011
All async functionality has been removed. Most synchronized blocks were removed in favor of other Java concurrency concepts.
1112

build.gradle

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,15 @@ task runSimpleFileServer(type: Test) {
134134
}
135135
}
136136

137-
task run(type: Test) {
137+
task run(type: JavaExec) {
138+
classpath sourceSets.testMains.runtimeClasspath
138139
dependsOn testMainsClasses
139-
doLast {
140-
def props = systemProperties
141-
javaexec {
142-
classpath sourceSets.testMains.runtimeClasspath
143-
main runClassName
144-
systemProperties = props
145-
}
146-
}
147140
}
148141

142+
publish {
143+
dependsOn test
144+
dependsOn testMainsTest
145+
}
149146

150147
publishing {
151148
publications {

logging.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ handlers = java.util.logging.ConsoleHandler
33
java.util.logging.ConsoleHandler.level = ALL
44
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
55
java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] [%2$s] %5$s %6$s %n
6-
robaho.net.level=INFO
6+
robaho.net.level=FINEST

src/main/java/robaho/net/httpserver/ExchangeImpl.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class ExchangeImpl {
6565
}
6666

6767
private static final String HEAD = "HEAD";
68+
private static final String CONNECT = "CONNECT";
6869

6970
/*
7071
* streams which take care of the HTTP protocol framing
@@ -127,6 +128,10 @@ private boolean isHeadRequest() {
127128
return HEAD.equals(getRequestMethod());
128129
}
129130

131+
private boolean isConnectRequest() {
132+
return CONNECT.equals(getRequestMethod());
133+
}
134+
130135
public void close() {
131136
if (closed) {
132137
return;
@@ -161,8 +166,8 @@ public InputStream getRequestBody() {
161166
if (uis != null) {
162167
return uis;
163168
}
164-
if (websocket) {
165-
// websocket connection cannot be re-used
169+
if (websocket || isConnectRequest()) {
170+
// connection cannot be re-used
166171
uis = ris;
167172
} else if (reqContentLen == -1L) {
168173
uis_orig = new ChunkedInputStream(this, ris);
@@ -266,7 +271,7 @@ public void sendResponseHeaders(int rCode, long contentLen)
266271
o.setWrappedStream(new FixedLengthOutputStream(this, ros, contentLen));
267272
} else { /* not a HEAD request or 304 response */
268273
if (contentLen == 0) {
269-
if (websocket) {
274+
if (websocket || isConnectRequest()) {
270275
o.setWrappedStream(ros);
271276
close = true;
272277
}

src/main/java/robaho/net/httpserver/ServerImpl.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,16 @@
4545
import java.util.Collections;
4646
import java.util.HashSet;
4747
import java.util.List;
48+
import java.util.Optional;
4849
import java.util.Set;
4950
import java.util.Timer;
5051
import java.util.TimerTask;
5152
import java.util.concurrent.ConcurrentHashMap;
5253
import java.util.concurrent.Executor;
5354
import java.util.concurrent.ExecutorService;
5455
import java.util.concurrent.Executors;
56+
import java.util.logging.LogManager;
57+
import java.util.logging.LogRecord;
5558

5659
import javax.net.ssl.SSLSocket;
5760
import javax.net.ssl.SSLSocketFactory;
@@ -109,21 +112,30 @@ class ServerImpl {
109112
}
110113

111114
private Timer timer;
112-
private final Logger logger;
115+
private Logger logger;
113116
private Thread dispatcherThread;
114117

115118
ServerImpl(HttpServer wrapper, String protocol, InetSocketAddress addr, int backlog) throws IOException {
116119

117120
this.protocol = protocol;
118121
this.wrapper = wrapper;
119-
this.logger = System.getLogger("robaho.net.httpserver");
122+
123+
this.logger = System.getLogger("robaho.net.httpserver."+System.identityHashCode(this));
124+
LogManager.getLogManager().getLogger(this.logger.getName()).setFilter(new java.util.logging.Filter(){
125+
@Override
126+
public boolean isLoggable(LogRecord record) {
127+
record.setMessage("["+protocol+":"+socket.getLocalPort()+"] "+record.getMessage());
128+
return true;
129+
}
130+
});
120131

121132
https = protocol.equalsIgnoreCase("https");
122133
contexts = new ContextList();
123134
socket = new ServerSocket();
124135
if (addr != null) {
125136
socket.bind(addr, backlog);
126137
bound = true;
138+
logger.log(Level.INFO,"server bound to "+socket.getLocalSocketAddress());
127139
}
128140
dispatcher = new Dispatcher();
129141
timer = new Timer("connection-cleaner", true);
@@ -139,6 +151,7 @@ public void bind(InetSocketAddress addr, int backlog) throws IOException {
139151
throw new NullPointerException("null address");
140152
}
141153
socket.bind(addr, backlog);
154+
logger.log(Level.INFO,"server bound to "+socket.getLocalSocketAddress());
142155
bound = true;
143156
}
144157

@@ -491,7 +504,9 @@ private void runPerRequest() throws IOException {
491504
}
492505
}
493506
}
494-
ctx = contexts.findContext(protocol, uri.getPath());
507+
logger.log(Level.INFO,"protocol "+protocol+" uri "+uri+" headers "+headers);
508+
String uriPath = Optional.ofNullable(uri.getPath()).orElse("/");
509+
ctx = contexts.findContext(protocol, uriPath);
495510
if (ctx == null) {
496511
reject(Code.HTTP_NOT_FOUND,
497512
requestLine, "No context found for request");

src/main/java/robaho/net/httpserver/extras/ProxyHandler.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package robaho.net.httpserver.extras;
22

33
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.io.OutputStream;
46
import java.net.InetSocketAddress;
57
import java.net.Proxy;
68
import java.net.ProxySelector;
9+
import java.net.Socket;
710
import java.net.SocketAddress;
811
import java.net.URI;
912
import java.net.URISyntaxException;
@@ -12,10 +15,14 @@
1215
import java.net.http.HttpResponse;
1316
import java.util.ArrayList;
1417
import java.util.List;
18+
import java.util.Objects;
1519
import java.util.Optional;
1620
import java.util.Set;
1721
import java.util.concurrent.ConcurrentHashMap;
1822
import java.util.concurrent.ConcurrentMap;
23+
import java.util.logging.Logger;
24+
25+
import javax.net.SocketFactory;
1926

2027
import com.sun.net.httpserver.Headers;
2128
import com.sun.net.httpserver.HttpExchange;
@@ -31,6 +38,8 @@ public record HostPort(String server,int port,String scheme){}
3138
private final HttpClient proxyClient;
3239
private final Optional<HostPort> defaultProxy;
3340

41+
protected final Logger logger = Logger.getLogger("robaho.net.httpserver.ProxyHandler");
42+
3443
public ProxyHandler() {
3544
this(Optional.empty());
3645
}
@@ -55,6 +64,36 @@ public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
5564

5665
@Override
5766
public void handle(HttpExchange exchange) throws IOException {
67+
if(exchange.getRequestMethod().equals("CONNECT")) {
68+
if(!authorizeConnect(exchange)) return;
69+
try (Socket s = SocketFactory.getDefault().createSocket()) {
70+
var uri = exchange.getRequestURI();
71+
var addr = new InetSocketAddress(uri.getScheme(),Integer.parseInt(uri.getSchemeSpecificPart()));
72+
try {
73+
s.connect(addr);
74+
} catch(Exception e) {
75+
logger.warning("failed to connect to "+addr);
76+
exchange.sendResponseHeaders(500,-1);
77+
return;
78+
}
79+
logger.fine("connected to "+s.getRemoteSocketAddress());
80+
exchange.sendResponseHeaders(200,0);
81+
82+
try {
83+
exchange.getHttpContext().getServer().getExecutor().execute(() -> {
84+
try {
85+
transfer(s.getInputStream(),exchange.getResponseBody());
86+
} catch (IOException ex) {
87+
ex.printStackTrace();
88+
}
89+
});
90+
transfer(exchange.getRequestBody(),s.getOutputStream());
91+
} finally {
92+
logger.fine("proxy connection to "+s.getRemoteSocketAddress()+" ended");
93+
return;
94+
}
95+
}
96+
}
5897
var proxy = proxyTo(exchange).orElseThrow(() -> new IOException("proxy not configured for "+exchange.getRequestURI()));
5998
var uri = exchange.getRequestURI();
6099
if(uri.getScheme()==null) {
@@ -70,13 +109,43 @@ public void handle(HttpExchange exchange) throws IOException {
70109
exchange.getResponseHeaders().putAll(response.headers().map());
71110
exchange.sendResponseHeaders(response.statusCode(),0);
72111
try (var os = exchange.getResponseBody ()) {
73-
response.body().transferTo(os);
112+
transfer(response.body(),os);
74113
}
75114
} catch (InterruptedException ex) {
76115
throw new IOException("unable to proxy request to "+exchange.getRequestURI(),ex);
77116
}
78117
}
79118

119+
/**
120+
* override to check authorization headers. if returning false,
121+
* the implementation must call exchange.sendResponseHeaders() with the appropriate code.
122+
*
123+
* @return true if the CONNECT should proceed, else false
124+
*/
125+
protected boolean authorizeConnect(HttpExchange exchange) {
126+
return true;
127+
}
128+
129+
private static int DEFAULT_BUFFER_SIZE = 16384;
130+
private static long transfer(InputStream in, OutputStream out) throws IOException {
131+
Objects.requireNonNull(out, "out");
132+
long transferred = 0;
133+
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
134+
int read;
135+
while ((read = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
136+
out.write(buffer, 0, read);
137+
out.flush();
138+
if (transferred < Long.MAX_VALUE) {
139+
try {
140+
transferred = Math.addExact(transferred, read);
141+
} catch (ArithmeticException ignore) {
142+
transferred = Long.MAX_VALUE;
143+
}
144+
}
145+
}
146+
return transferred;
147+
}
148+
80149
private static final Set<String> restrictedHeaders = Set.of("CONNECTION","HOST","UPGRADE","CONTENT-LENGTH");
81150

82151
private static String[] headers(Headers headers) {

src/main/java/robaho/net/httpserver/websockets/WebSocketHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public void handle(HttpExchange exchange) throws IOException {
1616
Headers headers = exchange.getRequestHeaders();
1717

1818
if (!isWebsocketRequested(headers)) {
19-
exchange.sendResponseHeaders(Code.HTTP_BAD_REQUEST, 0l);
19+
exchange.sendResponseHeaders(Code.HTTP_BAD_REQUEST, -1l);
2020
return;
2121
}
2222

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/**
25+
* @test
26+
* @summary test that handler can send response asynchronously
27+
* @library /test/lib
28+
* @modules java.base/sun.net.www
29+
* @run main/othervm AsyncHandlerTest
30+
*/
31+
32+
import java.io.IOException;
33+
import java.io.InputStream;
34+
import java.net.HttpURLConnection;
35+
import java.net.InetAddress;
36+
import java.net.InetSocketAddress;
37+
import java.net.URL;
38+
39+
import com.sun.net.httpserver.HttpExchange;
40+
import com.sun.net.httpserver.HttpHandler;
41+
import com.sun.net.httpserver.HttpServer;
42+
import jdk.test.lib.net.URIBuilder;
43+
44+
public class AsyncHandlerTest implements HttpHandler {
45+
private final HttpServer server;
46+
47+
48+
public AsyncHandlerTest(HttpServer server) {
49+
this.server = server;
50+
}
51+
52+
@Override
53+
public void handle(HttpExchange ex) throws IOException {
54+
ex.sendResponseHeaders(200, 0L);
55+
server.getExecutor().execute(() -> {
56+
try (var os = ex.getResponseBody()) {
57+
os.write("hello".getBytes());
58+
} catch(IOException e) {
59+
e.printStackTrace();
60+
}
61+
});
62+
}
63+
64+
public static void main(String[] args) throws IOException, InterruptedException {
65+
HttpServer server = HttpServer.create(
66+
new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
67+
68+
try {
69+
server.createContext("/context", new AsyncHandlerTest(server));
70+
server.start();
71+
72+
URL url = URIBuilder.newBuilder()
73+
.scheme("http")
74+
.loopback()
75+
.port(server.getAddress().getPort())
76+
.path("/context")
77+
.toURLUnchecked();
78+
79+
HttpURLConnection urlc = (HttpURLConnection) url.openConnection();
80+
System.out.println("Client: Response code received: " + urlc.getResponseCode());
81+
try (InputStream is = urlc.getInputStream()) {
82+
String body = new String(is.readAllBytes());
83+
if(!"hello".equals(body)) throw new IllegalStateException("incorrect body "+body);
84+
}
85+
} finally {
86+
server.stop(0);
87+
}
88+
}
89+
}

src/test/test_mains/ProxyHandlerTest.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,16 @@ public static void main(String[] args) throws Exception {
4242
try {
4343
var client = HttpClient.newHttpClient();
4444

45-
var direct_uri = URIBuilder.newBuilder().scheme("http").host(server1.getAddress().getHostName()).port(server1.getAddress().getPort()).path("/test").build();
46-
var response = client.send(HttpRequest.newBuilder(direct_uri).build(),HttpResponse.BodyHandlers.ofString());
45+
var uri = URIBuilder.newBuilder().scheme("http").host(server1.getAddress().getHostName()).port(server1.getAddress().getPort()).path("/test").build();
46+
var response = client.send(HttpRequest.newBuilder(uri).build(),HttpResponse.BodyHandlers.ofString());
4747
if(!response.body().equals("hello")) throw new IllegalStateException("incorrect body "+response.body());
4848

49-
var uri = URIBuilder.newBuilder().scheme("http").host(proxy.getAddress().getHostName()).port(proxy.getAddress().getPort()).path("/test").build();
50-
var proxied_response = client.send(HttpRequest.newBuilder(uri).build(),HttpResponse.BodyHandlers.ofString());
51-
if(!proxied_response.body().equals("hello")) throw new IllegalStateException("incorrect body "+response.body());
49+
uri = URIBuilder.newBuilder().scheme("http").host(proxy.getAddress().getHostName()).port(proxy.getAddress().getPort()).path("/test").build();
50+
response = client.send(HttpRequest.newBuilder(uri).build(),HttpResponse.BodyHandlers.ofString());
51+
if(!response.body().equals("hello")) throw new IllegalStateException("incorrect body "+response.body());
5252

53-
var post_response = client.send(HttpRequest.newBuilder(uri).POST(HttpRequest.BodyPublishers.ofString("senditback")).build(),HttpResponse.BodyHandlers.ofString());
54-
if(!post_response.body().equals("senditback")) throw new IllegalStateException("incorrect body "+response.body());
53+
response = client.send(HttpRequest.newBuilder(uri).POST(HttpRequest.BodyPublishers.ofString("senditback")).build(),HttpResponse.BodyHandlers.ofString());
54+
if(!response.body().equals("senditback")) throw new IllegalStateException("incorrect body "+response.body());
5555

5656

5757
} finally {

0 commit comments

Comments
 (0)