Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/mobile
Path: blob/master/test/jdk/java/net/httpclient/DigestEchoServer.java
41149 views
1
/*
2
* Copyright (c) 2018, 2021, 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
import com.sun.net.httpserver.BasicAuthenticator;
25
import com.sun.net.httpserver.HttpServer;
26
import com.sun.net.httpserver.HttpsConfigurator;
27
import com.sun.net.httpserver.HttpsParameters;
28
import com.sun.net.httpserver.HttpsServer;
29
30
import java.io.Closeable;
31
import java.io.IOException;
32
import java.io.InputStream;
33
import java.io.OutputStream;
34
import java.io.OutputStreamWriter;
35
import java.io.PrintWriter;
36
import java.io.Writer;
37
import java.math.BigInteger;
38
import java.net.Authenticator;
39
import java.net.HttpURLConnection;
40
import java.net.InetAddress;
41
import java.net.InetSocketAddress;
42
import java.net.MalformedURLException;
43
import java.net.PasswordAuthentication;
44
import java.net.ServerSocket;
45
import java.net.Socket;
46
import java.net.StandardSocketOptions;
47
import java.net.URI;
48
import java.net.URISyntaxException;
49
import java.net.URL;
50
import java.nio.charset.StandardCharsets;
51
import java.security.MessageDigest;
52
import java.security.NoSuchAlgorithmException;
53
import java.time.Instant;
54
import java.util.ArrayList;
55
import java.util.Arrays;
56
import java.util.Base64;
57
import java.util.HexFormat;
58
import java.util.List;
59
import java.util.Locale;
60
import java.util.Objects;
61
import java.util.Optional;
62
import java.util.Random;
63
import java.util.StringTokenizer;
64
import java.util.concurrent.CompletableFuture;
65
import java.util.concurrent.CopyOnWriteArrayList;
66
import java.util.concurrent.atomic.AtomicInteger;
67
import java.util.stream.Collectors;
68
import java.util.stream.Stream;
69
import javax.net.ssl.SSLContext;
70
import sun.net.www.HeaderParser;
71
import java.net.http.HttpClient.Version;
72
73
/**
74
* A simple HTTP server that supports Basic or Digest authentication.
75
* By default this server will echo back whatever is present
76
* in the request body. Note that the Digest authentication is
77
* a test implementation implemented only for tests purposes.
78
* @author danielfuchs
79
*/
80
public abstract class DigestEchoServer implements HttpServerAdapters {
81
82
public static final boolean DEBUG =
83
Boolean.parseBoolean(System.getProperty("test.debug", "false"));
84
public static final boolean NO_LINGER =
85
Boolean.parseBoolean(System.getProperty("test.nolinger", "false"));
86
public static final boolean TUNNEL_REQUIRES_HOST =
87
Boolean.parseBoolean(System.getProperty("test.requiresHost", "false"));
88
public enum HttpAuthType {
89
SERVER, PROXY, SERVER307, PROXY305
90
/* add PROXY_AND_SERVER and SERVER_PROXY_NONE */
91
};
92
public enum HttpAuthSchemeType { NONE, BASICSERVER, BASIC, DIGEST };
93
public static final HttpAuthType DEFAULT_HTTP_AUTH_TYPE = HttpAuthType.SERVER;
94
public static final String DEFAULT_PROTOCOL_TYPE = "https";
95
public static final HttpAuthSchemeType DEFAULT_SCHEME_TYPE = HttpAuthSchemeType.DIGEST;
96
97
public static class HttpTestAuthenticator extends Authenticator {
98
private final String realm;
99
private final String username;
100
// Used to prevent incrementation of 'count' when calling the
101
// authenticator from the server side.
102
private final ThreadLocal<Boolean> skipCount = new ThreadLocal<>();
103
// count will be incremented every time getPasswordAuthentication()
104
// is called from the client side.
105
final AtomicInteger count = new AtomicInteger();
106
107
public HttpTestAuthenticator(String realm, String username) {
108
this.realm = realm;
109
this.username = username;
110
}
111
@Override
112
protected PasswordAuthentication getPasswordAuthentication() {
113
if (skipCount.get() == null || skipCount.get().booleanValue() == false) {
114
System.out.println("Authenticator called: " + count.incrementAndGet());
115
}
116
return new PasswordAuthentication(getUserName(),
117
new char[] {'d','e','n', 't'});
118
}
119
// Called by the server side to get the password of the user
120
// being authentified.
121
public final char[] getPassword(String user) {
122
if (user.equals(username)) {
123
skipCount.set(Boolean.TRUE);
124
try {
125
return getPasswordAuthentication().getPassword();
126
} finally {
127
skipCount.set(Boolean.FALSE);
128
}
129
}
130
throw new SecurityException("User unknown: " + user);
131
}
132
public final String getUserName() {
133
return username;
134
}
135
public final String getRealm() {
136
return realm;
137
}
138
}
139
140
public static final HttpTestAuthenticator AUTHENTICATOR;
141
static {
142
AUTHENTICATOR = new HttpTestAuthenticator("earth", "arthur");
143
}
144
145
146
final HttpTestServer serverImpl; // this server endpoint
147
final DigestEchoServer redirect; // the target server where to redirect 3xx
148
final HttpTestHandler delegate; // unused
149
final String key;
150
151
DigestEchoServer(String key,
152
HttpTestServer server,
153
DigestEchoServer target,
154
HttpTestHandler delegate) {
155
this.key = key;
156
this.serverImpl = server;
157
this.redirect = target;
158
this.delegate = delegate;
159
}
160
161
public static void main(String[] args)
162
throws IOException {
163
164
DigestEchoServer server = create(Version.HTTP_1_1,
165
DEFAULT_PROTOCOL_TYPE,
166
DEFAULT_HTTP_AUTH_TYPE,
167
AUTHENTICATOR,
168
DEFAULT_SCHEME_TYPE);
169
try {
170
System.out.println("Server created at " + server.getAddress());
171
System.out.println("Strike <Return> to exit");
172
System.in.read();
173
} finally {
174
System.out.println("stopping server");
175
server.stop();
176
}
177
}
178
179
private static String toString(HttpTestRequestHeaders headers) {
180
return headers.entrySet().stream()
181
.map((e) -> e.getKey() + ": " + e.getValue())
182
.collect(Collectors.joining("\n"));
183
}
184
185
public static DigestEchoServer create(Version version,
186
String protocol,
187
HttpAuthType authType,
188
HttpAuthSchemeType schemeType)
189
throws IOException {
190
return create(version, protocol, authType, AUTHENTICATOR, schemeType);
191
}
192
193
public static DigestEchoServer create(Version version,
194
String protocol,
195
HttpAuthType authType,
196
HttpTestAuthenticator auth,
197
HttpAuthSchemeType schemeType)
198
throws IOException {
199
return create(version, protocol, authType, auth, schemeType, null);
200
}
201
202
public static DigestEchoServer create(Version version,
203
String protocol,
204
HttpAuthType authType,
205
HttpTestAuthenticator auth,
206
HttpAuthSchemeType schemeType,
207
HttpTestHandler delegate)
208
throws IOException {
209
Objects.requireNonNull(authType);
210
Objects.requireNonNull(auth);
211
switch(authType) {
212
// A server that performs Server Digest authentication.
213
case SERVER: return createServer(version, protocol, authType, auth,
214
schemeType, delegate, "/");
215
// A server that pretends to be a Proxy and performs
216
// Proxy Digest authentication. If protocol is HTTPS,
217
// then this will create a HttpsProxyTunnel that will
218
// handle the CONNECT request for tunneling.
219
case PROXY: return createProxy(version, protocol, authType, auth,
220
schemeType, delegate, "/");
221
// A server that sends 307 redirect to a server that performs
222
// Digest authentication.
223
// Note: 301 doesn't work here because it transforms POST into GET.
224
case SERVER307: return createServerAndRedirect(version,
225
protocol,
226
HttpAuthType.SERVER,
227
auth, schemeType,
228
delegate, 307);
229
// A server that sends 305 redirect to a proxy that performs
230
// Digest authentication.
231
// Note: this is not correctly stubbed/implemented in this test.
232
case PROXY305: return createServerAndRedirect(version,
233
protocol,
234
HttpAuthType.PROXY,
235
auth, schemeType,
236
delegate, 305);
237
default:
238
throw new InternalError("Unknown server type: " + authType);
239
}
240
}
241
242
243
/**
244
* The SocketBindableFactory ensures that the local port used by an HttpServer
245
* or a proxy ServerSocket previously created by the current test/VM will not
246
* get reused by a subsequent test in the same VM.
247
* This is to avoid having the test client trying to reuse cached connections.
248
*/
249
private static abstract class SocketBindableFactory<B> {
250
private static final int MAX = 10;
251
private static final CopyOnWriteArrayList<String> addresses =
252
new CopyOnWriteArrayList<>();
253
protected B createInternal() throws IOException {
254
final int max = addresses.size() + MAX;
255
final List<B> toClose = new ArrayList<>();
256
try {
257
for (int i = 1; i <= max; i++) {
258
B bindable = createBindable();
259
InetSocketAddress address = getAddress(bindable);
260
String key = "localhost:" + address.getPort();
261
if (addresses.addIfAbsent(key)) {
262
System.out.println("Socket bound to: " + key
263
+ " after " + i + " attempt(s)");
264
return bindable;
265
}
266
System.out.println("warning: address " + key
267
+ " already used. Retrying bind.");
268
// keep the port bound until we get a port that we haven't
269
// used already
270
toClose.add(bindable);
271
}
272
} finally {
273
// if we had to retry, then close the socket we're not
274
// going to use.
275
for (B b : toClose) {
276
try { close(b); } catch (Exception x) { /* ignore */ }
277
}
278
}
279
throw new IOException("Couldn't bind socket after " + max + " attempts: "
280
+ "addresses used before: " + addresses);
281
}
282
283
protected abstract B createBindable() throws IOException;
284
285
protected abstract InetSocketAddress getAddress(B bindable);
286
287
protected abstract void close(B bindable) throws IOException;
288
}
289
290
/*
291
* Used to create ServerSocket for a proxy.
292
*/
293
private static final class ServerSocketFactory
294
extends SocketBindableFactory<ServerSocket> {
295
private static final ServerSocketFactory instance = new ServerSocketFactory();
296
297
static ServerSocket create() throws IOException {
298
return instance.createInternal();
299
}
300
301
@Override
302
protected ServerSocket createBindable() throws IOException {
303
ServerSocket ss = new ServerSocket();
304
ss.setReuseAddress(false);
305
ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
306
return ss;
307
}
308
309
@Override
310
protected InetSocketAddress getAddress(ServerSocket socket) {
311
return new InetSocketAddress(socket.getInetAddress(), socket.getLocalPort());
312
}
313
314
@Override
315
protected void close(ServerSocket socket) throws IOException {
316
socket.close();
317
}
318
}
319
320
/*
321
* Used to create HttpServer
322
*/
323
private static abstract class H1ServerFactory<S extends HttpServer>
324
extends SocketBindableFactory<S> {
325
@Override
326
protected S createBindable() throws IOException {
327
S server = newHttpServer();
328
server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
329
return server;
330
}
331
332
@Override
333
protected InetSocketAddress getAddress(S server) {
334
return server.getAddress();
335
}
336
337
@Override
338
protected void close(S server) throws IOException {
339
server.stop(1);
340
}
341
342
/*
343
* Returns a HttpServer or a HttpsServer in different subclasses.
344
*/
345
protected abstract S newHttpServer() throws IOException;
346
}
347
348
/*
349
* Used to create Http2TestServer
350
*/
351
private static abstract class H2ServerFactory<S extends Http2TestServer>
352
extends SocketBindableFactory<S> {
353
@Override
354
protected S createBindable() throws IOException {
355
final S server;
356
try {
357
server = newHttpServer();
358
} catch (IOException io) {
359
throw io;
360
} catch (Exception x) {
361
throw new IOException(x);
362
}
363
return server;
364
}
365
366
@Override
367
protected InetSocketAddress getAddress(S server) {
368
return server.getAddress();
369
}
370
371
@Override
372
protected void close(S server) throws IOException {
373
server.stop();
374
}
375
376
/*
377
* Returns a HttpServer or a HttpsServer in different subclasses.
378
*/
379
protected abstract S newHttpServer() throws Exception;
380
}
381
382
private static final class Http2ServerFactory extends H2ServerFactory<Http2TestServer> {
383
private static final Http2ServerFactory instance = new Http2ServerFactory();
384
385
static Http2TestServer create() throws IOException {
386
return instance.createInternal();
387
}
388
389
@Override
390
protected Http2TestServer newHttpServer() throws Exception {
391
return new Http2TestServer("localhost", false, 0);
392
}
393
}
394
395
private static final class Https2ServerFactory extends H2ServerFactory<Http2TestServer> {
396
private static final Https2ServerFactory instance = new Https2ServerFactory();
397
398
static Http2TestServer create() throws IOException {
399
return instance.createInternal();
400
}
401
402
@Override
403
protected Http2TestServer newHttpServer() throws Exception {
404
return new Http2TestServer("localhost", true, 0);
405
}
406
}
407
408
private static final class Http1ServerFactory extends H1ServerFactory<HttpServer> {
409
private static final Http1ServerFactory instance = new Http1ServerFactory();
410
411
static HttpServer create() throws IOException {
412
return instance.createInternal();
413
}
414
415
@Override
416
protected HttpServer newHttpServer() throws IOException {
417
return HttpServer.create();
418
}
419
}
420
421
private static final class Https1ServerFactory extends H1ServerFactory<HttpsServer> {
422
private static final Https1ServerFactory instance = new Https1ServerFactory();
423
424
static HttpsServer create() throws IOException {
425
return instance.createInternal();
426
}
427
428
@Override
429
protected HttpsServer newHttpServer() throws IOException {
430
return HttpsServer.create();
431
}
432
}
433
434
static Http2TestServer createHttp2Server(String protocol) throws IOException {
435
final Http2TestServer server;
436
if ("http".equalsIgnoreCase(protocol)) {
437
server = Http2ServerFactory.create();
438
} else if ("https".equalsIgnoreCase(protocol)) {
439
server = Https2ServerFactory.create();
440
} else {
441
throw new InternalError("unsupported protocol: " + protocol);
442
}
443
return server;
444
}
445
446
static HttpTestServer createHttpServer(Version version, String protocol)
447
throws IOException
448
{
449
switch(version) {
450
case HTTP_1_1:
451
return HttpTestServer.of(createHttp1Server(protocol));
452
case HTTP_2:
453
return HttpTestServer.of(createHttp2Server(protocol));
454
default:
455
throw new InternalError("Unexpected version: " + version);
456
}
457
}
458
459
static HttpServer createHttp1Server(String protocol) throws IOException {
460
final HttpServer server;
461
if ("http".equalsIgnoreCase(protocol)) {
462
server = Http1ServerFactory.create();
463
} else if ("https".equalsIgnoreCase(protocol)) {
464
server = configure(Https1ServerFactory.create());
465
} else {
466
throw new InternalError("unsupported protocol: " + protocol);
467
}
468
return server;
469
}
470
471
static HttpsServer configure(HttpsServer server) throws IOException {
472
try {
473
SSLContext ctx = SSLContext.getDefault();
474
server.setHttpsConfigurator(new Configurator(ctx));
475
} catch (NoSuchAlgorithmException ex) {
476
throw new IOException(ex);
477
}
478
return server;
479
}
480
481
482
static void setContextAuthenticator(HttpTestContext ctxt,
483
HttpTestAuthenticator auth) {
484
final String realm = auth.getRealm();
485
com.sun.net.httpserver.Authenticator authenticator =
486
new BasicAuthenticator(realm) {
487
@Override
488
public boolean checkCredentials(String username, String pwd) {
489
return auth.getUserName().equals(username)
490
&& new String(auth.getPassword(username)).equals(pwd);
491
}
492
};
493
ctxt.setAuthenticator(authenticator);
494
}
495
496
public static DigestEchoServer createServer(Version version,
497
String protocol,
498
HttpAuthType authType,
499
HttpTestAuthenticator auth,
500
HttpAuthSchemeType schemeType,
501
HttpTestHandler delegate,
502
String path)
503
throws IOException {
504
Objects.requireNonNull(authType);
505
Objects.requireNonNull(auth);
506
507
HttpTestServer impl = createHttpServer(version, protocol);
508
String key = String.format("DigestEchoServer[PID=%s,PORT=%s]:%s:%s:%s:%s",
509
ProcessHandle.current().pid(),
510
impl.getAddress().getPort(),
511
version, protocol, authType, schemeType);
512
final DigestEchoServer server = new DigestEchoServerImpl(key, impl, null, delegate);
513
final HttpTestHandler handler =
514
server.createHandler(schemeType, auth, authType, false);
515
HttpTestContext context = impl.addHandler(handler, path);
516
server.configureAuthentication(context, schemeType, auth, authType);
517
impl.start();
518
return server;
519
}
520
521
public static DigestEchoServer createProxy(Version version,
522
String protocol,
523
HttpAuthType authType,
524
HttpTestAuthenticator auth,
525
HttpAuthSchemeType schemeType,
526
HttpTestHandler delegate,
527
String path)
528
throws IOException {
529
Objects.requireNonNull(authType);
530
Objects.requireNonNull(auth);
531
532
if (version == Version.HTTP_2 && protocol.equalsIgnoreCase("http")) {
533
System.out.println("WARNING: can't use HTTP/1.1 proxy with unsecure HTTP/2 server");
534
version = Version.HTTP_1_1;
535
}
536
HttpTestServer impl = createHttpServer(version, protocol);
537
String key = String.format("DigestEchoServer[PID=%s,PORT=%s]:%s:%s:%s:%s",
538
ProcessHandle.current().pid(),
539
impl.getAddress().getPort(),
540
version, protocol, authType, schemeType);
541
final DigestEchoServer server = "https".equalsIgnoreCase(protocol)
542
? new HttpsProxyTunnel(key, impl, null, delegate)
543
: new DigestEchoServerImpl(key, impl, null, delegate);
544
545
final HttpTestHandler hh = server.createHandler(HttpAuthSchemeType.NONE,
546
null, HttpAuthType.SERVER,
547
server instanceof HttpsProxyTunnel);
548
HttpTestContext ctxt = impl.addHandler(hh, path);
549
server.configureAuthentication(ctxt, schemeType, auth, authType);
550
impl.start();
551
552
return server;
553
}
554
555
public static DigestEchoServer createServerAndRedirect(
556
Version version,
557
String protocol,
558
HttpAuthType targetAuthType,
559
HttpTestAuthenticator auth,
560
HttpAuthSchemeType schemeType,
561
HttpTestHandler targetDelegate,
562
int code300)
563
throws IOException {
564
Objects.requireNonNull(targetAuthType);
565
Objects.requireNonNull(auth);
566
567
// The connection between client and proxy can only
568
// be a plain connection: SSL connection to proxy
569
// is not supported by our client connection.
570
String targetProtocol = targetAuthType == HttpAuthType.PROXY
571
? "http"
572
: protocol;
573
DigestEchoServer redirectTarget =
574
(targetAuthType == HttpAuthType.PROXY)
575
? createProxy(version, protocol, targetAuthType,
576
auth, schemeType, targetDelegate, "/")
577
: createServer(version, targetProtocol, targetAuthType,
578
auth, schemeType, targetDelegate, "/");
579
HttpTestServer impl = createHttpServer(version, protocol);
580
String key = String.format("RedirectingServer[PID=%s,PORT=%s]:%s:%s:%s:%s",
581
ProcessHandle.current().pid(),
582
impl.getAddress().getPort(),
583
version, protocol,
584
HttpAuthType.SERVER, code300)
585
+ "->" + redirectTarget.key;
586
final DigestEchoServer redirectingServer =
587
new DigestEchoServerImpl(key, impl, redirectTarget, null);
588
InetSocketAddress redirectAddr = redirectTarget.getAddress();
589
URL locationURL = url(targetProtocol, redirectAddr, "/");
590
final HttpTestHandler hh = redirectingServer.create300Handler(key, locationURL,
591
HttpAuthType.SERVER, code300);
592
impl.addHandler(hh,"/");
593
impl.start();
594
return redirectingServer;
595
}
596
597
public abstract InetSocketAddress getServerAddress();
598
public abstract InetSocketAddress getProxyAddress();
599
public abstract InetSocketAddress getAddress();
600
public abstract void stop();
601
public abstract Version getServerVersion();
602
603
private static class DigestEchoServerImpl extends DigestEchoServer {
604
DigestEchoServerImpl(String key,
605
HttpTestServer server,
606
DigestEchoServer target,
607
HttpTestHandler delegate) {
608
super(key, Objects.requireNonNull(server), target, delegate);
609
}
610
611
public InetSocketAddress getAddress() {
612
return new InetSocketAddress(InetAddress.getLoopbackAddress(),
613
serverImpl.getAddress().getPort());
614
}
615
616
public InetSocketAddress getServerAddress() {
617
return new InetSocketAddress(InetAddress.getLoopbackAddress(),
618
serverImpl.getAddress().getPort());
619
}
620
621
public InetSocketAddress getProxyAddress() {
622
return new InetSocketAddress(InetAddress.getLoopbackAddress(),
623
serverImpl.getAddress().getPort());
624
}
625
626
public Version getServerVersion() {
627
return serverImpl.getVersion();
628
}
629
630
public void stop() {
631
serverImpl.stop();
632
if (redirect != null) {
633
redirect.stop();
634
}
635
}
636
}
637
638
protected void writeResponse(HttpTestExchange he) throws IOException {
639
if (delegate == null) {
640
he.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1);
641
he.getResponseBody().write(he.getRequestBody().readAllBytes());
642
} else {
643
delegate.handle(he);
644
}
645
}
646
647
private HttpTestHandler createHandler(HttpAuthSchemeType schemeType,
648
HttpTestAuthenticator auth,
649
HttpAuthType authType,
650
boolean tunelled) {
651
return new HttpNoAuthHandler(key, authType, tunelled);
652
}
653
654
void configureAuthentication(HttpTestContext ctxt,
655
HttpAuthSchemeType schemeType,
656
HttpTestAuthenticator auth,
657
HttpAuthType authType) {
658
switch(schemeType) {
659
case DIGEST:
660
// DIGEST authentication is handled by the handler.
661
ctxt.addFilter(new HttpDigestFilter(key, auth, authType));
662
break;
663
case BASIC:
664
// BASIC authentication is handled by the filter.
665
ctxt.addFilter(new HttpBasicFilter(key, auth, authType));
666
break;
667
case BASICSERVER:
668
switch(authType) {
669
case PROXY: case PROXY305:
670
// HttpServer can't support Proxy-type authentication
671
// => we do as if BASIC had been specified, and we will
672
// handle authentication in the handler.
673
ctxt.addFilter(new HttpBasicFilter(key, auth, authType));
674
break;
675
case SERVER: case SERVER307:
676
if (ctxt.getVersion() == Version.HTTP_1_1) {
677
// Basic authentication is handled by HttpServer
678
// directly => the filter should not perform
679
// authentication again.
680
setContextAuthenticator(ctxt, auth);
681
ctxt.addFilter(new HttpNoAuthFilter(key, authType));
682
} else {
683
ctxt.addFilter(new HttpBasicFilter(key, auth, authType));
684
}
685
break;
686
default:
687
throw new InternalError(key + ": Invalid combination scheme="
688
+ schemeType + " authType=" + authType);
689
}
690
case NONE:
691
// No authentication at all.
692
ctxt.addFilter(new HttpNoAuthFilter(key, authType));
693
break;
694
default:
695
throw new InternalError(key + ": No such scheme: " + schemeType);
696
}
697
}
698
699
private HttpTestHandler create300Handler(String key, URL proxyURL,
700
HttpAuthType type, int code300)
701
throws MalformedURLException
702
{
703
return new Http3xxHandler(key, proxyURL, type, code300);
704
}
705
706
// Abstract HTTP filter class.
707
private abstract static class AbstractHttpFilter extends HttpTestFilter {
708
709
final HttpAuthType authType;
710
final String type;
711
public AbstractHttpFilter(HttpAuthType authType, String type) {
712
this.authType = authType;
713
this.type = type;
714
}
715
716
String getLocation() {
717
return "Location";
718
}
719
String getAuthenticate() {
720
return authType == HttpAuthType.PROXY
721
? "Proxy-Authenticate" : "WWW-Authenticate";
722
}
723
String getAuthorization() {
724
return authType == HttpAuthType.PROXY
725
? "Proxy-Authorization" : "Authorization";
726
}
727
int getUnauthorizedCode() {
728
return authType == HttpAuthType.PROXY
729
? HttpURLConnection.HTTP_PROXY_AUTH
730
: HttpURLConnection.HTTP_UNAUTHORIZED;
731
}
732
String getKeepAlive() {
733
return "keep-alive";
734
}
735
String getConnection() {
736
return authType == HttpAuthType.PROXY
737
? "Proxy-Connection" : "Connection";
738
}
739
protected abstract boolean isAuthentified(HttpTestExchange he) throws IOException;
740
protected abstract void requestAuthentication(HttpTestExchange he) throws IOException;
741
protected void accept(HttpTestExchange he, HttpChain chain) throws IOException {
742
chain.doFilter(he);
743
}
744
745
@Override
746
public String description() {
747
return "Filter for " + type;
748
}
749
@Override
750
public void doFilter(HttpTestExchange he, HttpChain chain) throws IOException {
751
try {
752
System.out.println(type + ": Got " + he.getRequestMethod()
753
+ ": " + he.getRequestURI()
754
+ "\n" + DigestEchoServer.toString(he.getRequestHeaders()));
755
756
// Assert only a single value for Expect. Not directly related
757
// to digest authentication, but verifies good client behaviour.
758
List<String> expectValues = he.getRequestHeaders().get("Expect");
759
if (expectValues != null && expectValues.size() > 1) {
760
throw new IOException("Expect: " + expectValues);
761
}
762
763
if (!isAuthentified(he)) {
764
try {
765
requestAuthentication(he);
766
he.sendResponseHeaders(getUnauthorizedCode(), -1);
767
System.out.println(type
768
+ ": Sent back " + getUnauthorizedCode());
769
} finally {
770
he.close();
771
}
772
} else {
773
accept(he, chain);
774
}
775
} catch (RuntimeException | Error | IOException t) {
776
System.err.println(type
777
+ ": Unexpected exception while handling request: " + t);
778
t.printStackTrace(System.err);
779
he.close();
780
throw t;
781
}
782
}
783
784
}
785
786
// WARNING: This is not a full fledged implementation of DIGEST.
787
// It does contain bugs and inaccuracy.
788
final static class DigestResponse {
789
final String realm;
790
final String username;
791
final String nonce;
792
final String cnonce;
793
final String nc;
794
final String uri;
795
final String algorithm;
796
final String response;
797
final String qop;
798
final String opaque;
799
800
public DigestResponse(String realm, String username, String nonce,
801
String cnonce, String nc, String uri,
802
String algorithm, String qop, String opaque,
803
String response) {
804
this.realm = realm;
805
this.username = username;
806
this.nonce = nonce;
807
this.cnonce = cnonce;
808
this.nc = nc;
809
this.uri = uri;
810
this.algorithm = algorithm;
811
this.qop = qop;
812
this.opaque = opaque;
813
this.response = response;
814
}
815
816
String getAlgorithm(String defval) {
817
return algorithm == null ? defval : algorithm;
818
}
819
String getQoP(String defval) {
820
return qop == null ? defval : qop;
821
}
822
823
// Code stolen from DigestAuthentication:
824
825
private static String encode(String src, char[] passwd, MessageDigest md) {
826
try {
827
md.update(src.getBytes("ISO-8859-1"));
828
} catch (java.io.UnsupportedEncodingException uee) {
829
assert false;
830
}
831
if (passwd != null) {
832
byte[] passwdBytes = new byte[passwd.length];
833
for (int i=0; i<passwd.length; i++)
834
passwdBytes[i] = (byte)passwd[i];
835
md.update(passwdBytes);
836
Arrays.fill(passwdBytes, (byte)0x00);
837
}
838
byte[] digest = md.digest();
839
return HexFormat.of().formatHex(digest);
840
}
841
842
public static String computeDigest(boolean isRequest,
843
String reqMethod,
844
char[] password,
845
DigestResponse params)
846
throws NoSuchAlgorithmException
847
{
848
849
String A1, HashA1;
850
String algorithm = params.getAlgorithm("MD5");
851
boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
852
853
MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
854
855
if (params.username == null) {
856
throw new IllegalArgumentException("missing username");
857
}
858
if (params.realm == null) {
859
throw new IllegalArgumentException("missing realm");
860
}
861
if (params.uri == null) {
862
throw new IllegalArgumentException("missing uri");
863
}
864
if (params.nonce == null) {
865
throw new IllegalArgumentException("missing nonce");
866
}
867
868
A1 = params.username + ":" + params.realm + ":";
869
HashA1 = encode(A1, password, md);
870
871
String A2;
872
if (isRequest) {
873
A2 = reqMethod + ":" + params.uri;
874
} else {
875
A2 = ":" + params.uri;
876
}
877
String HashA2 = encode(A2, null, md);
878
String combo, finalHash;
879
880
if ("auth".equals(params.qop)) { /* RRC2617 when qop=auth */
881
if (params.cnonce == null) {
882
throw new IllegalArgumentException("missing nonce");
883
}
884
if (params.nc == null) {
885
throw new IllegalArgumentException("missing nonce");
886
}
887
combo = HashA1+ ":" + params.nonce + ":" + params.nc + ":" +
888
params.cnonce + ":auth:" +HashA2;
889
890
} else { /* for compatibility with RFC2069 */
891
combo = HashA1 + ":" +
892
params.nonce + ":" +
893
HashA2;
894
}
895
finalHash = encode(combo, null, md);
896
return finalHash;
897
}
898
899
public static DigestResponse create(String raw) {
900
String username, realm, nonce, nc, uri, response, cnonce,
901
algorithm, qop, opaque;
902
HeaderParser parser = new HeaderParser(raw);
903
username = parser.findValue("username");
904
realm = parser.findValue("realm");
905
nonce = parser.findValue("nonce");
906
nc = parser.findValue("nc");
907
uri = parser.findValue("uri");
908
cnonce = parser.findValue("cnonce");
909
response = parser.findValue("response");
910
algorithm = parser.findValue("algorithm");
911
qop = parser.findValue("qop");
912
opaque = parser.findValue("opaque");
913
return new DigestResponse(realm, username, nonce, cnonce, nc, uri,
914
algorithm, qop, opaque, response);
915
}
916
917
}
918
919
private static class HttpNoAuthFilter extends AbstractHttpFilter {
920
921
static String type(String key, HttpAuthType authType) {
922
String type = authType == HttpAuthType.SERVER
923
? "NoAuth Server Filter" : "NoAuth Proxy Filter";
924
return "["+type+"]:"+key;
925
}
926
927
public HttpNoAuthFilter(String key, HttpAuthType authType) {
928
super(authType, type(key, authType));
929
}
930
931
@Override
932
protected boolean isAuthentified(HttpTestExchange he) throws IOException {
933
return true;
934
}
935
936
@Override
937
protected void requestAuthentication(HttpTestExchange he) throws IOException {
938
throw new InternalError("Should not com here");
939
}
940
941
@Override
942
public String description() {
943
return "Passthrough Filter";
944
}
945
946
}
947
948
// An HTTP Filter that performs Basic authentication
949
private static class HttpBasicFilter extends AbstractHttpFilter {
950
951
static String type(String key, HttpAuthType authType) {
952
String type = authType == HttpAuthType.SERVER
953
? "Basic Server Filter" : "Basic Proxy Filter";
954
return "["+type+"]:"+key;
955
}
956
957
private final HttpTestAuthenticator auth;
958
public HttpBasicFilter(String key, HttpTestAuthenticator auth,
959
HttpAuthType authType) {
960
super(authType, type(key, authType));
961
this.auth = auth;
962
}
963
964
@Override
965
protected void requestAuthentication(HttpTestExchange he)
966
throws IOException
967
{
968
String headerName = getAuthenticate();
969
String headerValue = "Basic realm=\"" + auth.getRealm() + "\"";
970
he.getResponseHeaders().addHeader(headerName, headerValue);
971
System.out.println(type + ": Requesting Basic Authentication, "
972
+ headerName + " : "+ headerValue);
973
}
974
975
@Override
976
protected boolean isAuthentified(HttpTestExchange he) {
977
if (he.getRequestHeaders().containsKey(getAuthorization())) {
978
List<String> authorization =
979
he.getRequestHeaders().get(getAuthorization());
980
for (String a : authorization) {
981
System.out.println(type + ": processing " + a);
982
int sp = a.indexOf(' ');
983
if (sp < 0) return false;
984
String scheme = a.substring(0, sp);
985
if (!"Basic".equalsIgnoreCase(scheme)) {
986
System.out.println(type + ": Unsupported scheme '"
987
+ scheme +"'");
988
return false;
989
}
990
if (a.length() <= sp+1) {
991
System.out.println(type + ": value too short for '"
992
+ scheme +"'");
993
return false;
994
}
995
a = a.substring(sp+1);
996
return validate(a);
997
}
998
return false;
999
}
1000
return false;
1001
}
1002
1003
boolean validate(String a) {
1004
byte[] b = Base64.getDecoder().decode(a);
1005
String userpass = new String (b);
1006
int colon = userpass.indexOf (':');
1007
String uname = userpass.substring (0, colon);
1008
String pass = userpass.substring (colon+1);
1009
return auth.getUserName().equals(uname) &&
1010
new String(auth.getPassword(uname)).equals(pass);
1011
}
1012
1013
@Override
1014
public String description() {
1015
return "Filter for BASIC authentication: " + type;
1016
}
1017
1018
}
1019
1020
1021
// An HTTP Filter that performs Digest authentication
1022
// WARNING: This is not a full fledged implementation of DIGEST.
1023
// It does contain bugs and inaccuracy.
1024
private static class HttpDigestFilter extends AbstractHttpFilter {
1025
1026
static String type(String key, HttpAuthType authType) {
1027
String type = authType == HttpAuthType.SERVER
1028
? "Digest Server Filter" : "Digest Proxy Filter";
1029
return "["+type+"]:"+key;
1030
}
1031
1032
// This is a very basic DIGEST - used only for the purpose of testing
1033
// the client implementation. Therefore we can get away with never
1034
// updating the server nonce as it makes the implementation of the
1035
// server side digest simpler.
1036
private final HttpTestAuthenticator auth;
1037
private final byte[] nonce;
1038
private final String ns;
1039
public HttpDigestFilter(String key, HttpTestAuthenticator auth, HttpAuthType authType) {
1040
super(authType, type(key, authType));
1041
this.auth = auth;
1042
nonce = new byte[16];
1043
new Random(Instant.now().toEpochMilli()).nextBytes(nonce);
1044
ns = new BigInteger(1, nonce).toString(16);
1045
}
1046
1047
@Override
1048
protected void requestAuthentication(HttpTestExchange he)
1049
throws IOException {
1050
String separator;
1051
Version v = he.getExchangeVersion();
1052
if (v == Version.HTTP_1_1) {
1053
separator = "\r\n ";
1054
} else if (v == Version.HTTP_2) {
1055
separator = " ";
1056
} else {
1057
throw new InternalError(String.valueOf(v));
1058
}
1059
String headerName = getAuthenticate();
1060
String headerValue = "Digest realm=\"" + auth.getRealm() + "\","
1061
+ separator + "qop=\"auth\","
1062
+ separator + "nonce=\"" + ns +"\"";
1063
he.getResponseHeaders().addHeader(headerName, headerValue);
1064
System.out.println(type + ": Requesting Digest Authentication, "
1065
+ headerName + " : " + headerValue);
1066
}
1067
1068
@Override
1069
protected boolean isAuthentified(HttpTestExchange he) {
1070
if (he.getRequestHeaders().containsKey(getAuthorization())) {
1071
List<String> authorization = he.getRequestHeaders().get(getAuthorization());
1072
for (String a : authorization) {
1073
System.out.println(type + ": processing " + a);
1074
int sp = a.indexOf(' ');
1075
if (sp < 0) return false;
1076
String scheme = a.substring(0, sp);
1077
if (!"Digest".equalsIgnoreCase(scheme)) {
1078
System.out.println(type + ": Unsupported scheme '" + scheme +"'");
1079
return false;
1080
}
1081
if (a.length() <= sp+1) {
1082
System.out.println(type + ": value too short for '" + scheme +"'");
1083
return false;
1084
}
1085
a = a.substring(sp+1);
1086
DigestResponse dgr = DigestResponse.create(a);
1087
return validate(he.getRequestURI(), he.getRequestMethod(), dgr);
1088
}
1089
return false;
1090
}
1091
return false;
1092
}
1093
1094
boolean validate(URI uri, String reqMethod, DigestResponse dg) {
1095
if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) {
1096
System.out.println(type + ": Unsupported algorithm "
1097
+ dg.algorithm);
1098
return false;
1099
}
1100
if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) {
1101
System.out.println(type + ": Unsupported qop "
1102
+ dg.qop);
1103
return false;
1104
}
1105
try {
1106
if (!dg.nonce.equals(ns)) {
1107
System.out.println(type + ": bad nonce returned by client: "
1108
+ nonce + " expected " + ns);
1109
return false;
1110
}
1111
if (dg.response == null) {
1112
System.out.println(type + ": missing digest response.");
1113
return false;
1114
}
1115
char[] pa = auth.getPassword(dg.username);
1116
return verify(uri, reqMethod, dg, pa);
1117
} catch(IllegalArgumentException | SecurityException
1118
| NoSuchAlgorithmException e) {
1119
System.out.println(type + ": " + e.getMessage());
1120
return false;
1121
}
1122
}
1123
1124
1125
boolean verify(URI uri, String reqMethod, DigestResponse dg, char[] pw)
1126
throws NoSuchAlgorithmException {
1127
String response = DigestResponse.computeDigest(true, reqMethod, pw, dg);
1128
if (!dg.response.equals(response)) {
1129
System.out.println(type + ": bad response returned by client: "
1130
+ dg.response + " expected " + response);
1131
return false;
1132
} else {
1133
// A real server would also verify the uri=<request-uri>
1134
// parameter - but this is just a test...
1135
System.out.println(type + ": verified response " + response);
1136
}
1137
return true;
1138
}
1139
1140
1141
@Override
1142
public String description() {
1143
return "Filter for DIGEST authentication: " + type;
1144
}
1145
}
1146
1147
// Abstract HTTP handler class.
1148
private abstract static class AbstractHttpHandler implements HttpTestHandler {
1149
1150
final HttpAuthType authType;
1151
final String type;
1152
public AbstractHttpHandler(HttpAuthType authType, String type) {
1153
this.authType = authType;
1154
this.type = type;
1155
}
1156
1157
String getLocation() {
1158
return "Location";
1159
}
1160
1161
@Override
1162
public void handle(HttpTestExchange he) throws IOException {
1163
try {
1164
sendResponse(he);
1165
} catch (RuntimeException | Error | IOException t) {
1166
System.err.println(type
1167
+ ": Unexpected exception while handling request: " + t);
1168
t.printStackTrace(System.err);
1169
throw t;
1170
} finally {
1171
he.close();
1172
}
1173
}
1174
1175
protected abstract void sendResponse(HttpTestExchange he) throws IOException;
1176
1177
}
1178
1179
static String stype(String type, String key, HttpAuthType authType, boolean tunnelled) {
1180
type = type + (authType == HttpAuthType.SERVER
1181
? " Server" : " Proxy")
1182
+ (tunnelled ? " Tunnelled" : "");
1183
return "["+type+"]:"+key;
1184
}
1185
1186
private class HttpNoAuthHandler extends AbstractHttpHandler {
1187
1188
// true if this server is behind a proxy tunnel.
1189
final boolean tunnelled;
1190
public HttpNoAuthHandler(String key, HttpAuthType authType, boolean tunnelled) {
1191
super(authType, stype("NoAuth", key, authType, tunnelled));
1192
this.tunnelled = tunnelled;
1193
}
1194
1195
@Override
1196
protected void sendResponse(HttpTestExchange he) throws IOException {
1197
if (DEBUG) {
1198
System.out.println(type + ": headers are: "
1199
+ DigestEchoServer.toString(he.getRequestHeaders()));
1200
}
1201
if (authType == HttpAuthType.SERVER && tunnelled) {
1202
// Verify that the client doesn't send us proxy-* headers
1203
// used to establish the proxy tunnel
1204
Optional<String> proxyAuth = he.getRequestHeaders()
1205
.keySet().stream()
1206
.filter("proxy-authorization"::equalsIgnoreCase)
1207
.findAny();
1208
if (proxyAuth.isPresent()) {
1209
System.out.println(type + " found "
1210
+ proxyAuth.get() + ": failing!");
1211
throw new IOException(proxyAuth.get()
1212
+ " found by " + type + " for "
1213
+ he.getRequestURI());
1214
}
1215
}
1216
DigestEchoServer.this.writeResponse(he);
1217
}
1218
1219
}
1220
1221
// A dummy HTTP Handler that redirects all incoming requests
1222
// by sending a back 3xx response code (301, 305, 307 etc..)
1223
private class Http3xxHandler extends AbstractHttpHandler {
1224
1225
private final URL redirectTargetURL;
1226
private final int code3XX;
1227
public Http3xxHandler(String key, URL proxyURL, HttpAuthType authType, int code300) {
1228
super(authType, stype("Server" + code300, key, authType, false));
1229
this.redirectTargetURL = proxyURL;
1230
this.code3XX = code300;
1231
}
1232
1233
int get3XX() {
1234
return code3XX;
1235
}
1236
1237
@Override
1238
public void sendResponse(HttpTestExchange he) throws IOException {
1239
System.out.println(type + ": Got " + he.getRequestMethod()
1240
+ ": " + he.getRequestURI()
1241
+ "\n" + DigestEchoServer.toString(he.getRequestHeaders()));
1242
System.out.println(type + ": Redirecting to "
1243
+ (authType == HttpAuthType.PROXY305
1244
? "proxy" : "server"));
1245
he.getResponseHeaders().addHeader(getLocation(),
1246
redirectTargetURL.toExternalForm().toString());
1247
he.sendResponseHeaders(get3XX(), -1);
1248
System.out.println(type + ": Sent back " + get3XX() + " "
1249
+ getLocation() + ": " + redirectTargetURL.toExternalForm().toString());
1250
}
1251
}
1252
1253
static class Configurator extends HttpsConfigurator {
1254
public Configurator(SSLContext ctx) {
1255
super(ctx);
1256
}
1257
1258
@Override
1259
public void configure (HttpsParameters params) {
1260
params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
1261
}
1262
}
1263
1264
static final long start = System.nanoTime();
1265
public static String now() {
1266
long now = System.nanoTime() - start;
1267
long secs = now / 1000_000_000;
1268
long mill = (now % 1000_000_000) / 1000_000;
1269
long nan = now % 1000_000;
1270
return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
1271
}
1272
1273
static class ProxyAuthorization {
1274
final HttpAuthSchemeType schemeType;
1275
final HttpTestAuthenticator authenticator;
1276
private final byte[] nonce;
1277
private final String ns;
1278
private final String key;
1279
1280
ProxyAuthorization(String key, HttpAuthSchemeType schemeType, HttpTestAuthenticator auth) {
1281
this.key = key;
1282
this.schemeType = schemeType;
1283
this.authenticator = auth;
1284
nonce = new byte[16];
1285
new Random(Instant.now().toEpochMilli()).nextBytes(nonce);
1286
ns = new BigInteger(1, nonce).toString(16);
1287
}
1288
1289
String doBasic(Optional<String> authorization) {
1290
String offset = "proxy-authorization: basic ";
1291
String authstring = authorization.orElse("");
1292
if (!authstring.toLowerCase(Locale.US).startsWith(offset)) {
1293
return "Proxy-Authenticate: BASIC " + "realm=\""
1294
+ authenticator.getRealm() +"\"";
1295
}
1296
authstring = authstring
1297
.substring(offset.length())
1298
.trim();
1299
byte[] base64 = Base64.getDecoder().decode(authstring);
1300
String up = new String(base64, StandardCharsets.UTF_8);
1301
int colon = up.indexOf(':');
1302
if (colon < 1) {
1303
return "Proxy-Authenticate: BASIC " + "realm=\""
1304
+ authenticator.getRealm() +"\"";
1305
}
1306
String u = up.substring(0, colon);
1307
String p = up.substring(colon+1);
1308
char[] pw = authenticator.getPassword(u);
1309
if (!p.equals(new String(pw))) {
1310
return "Proxy-Authenticate: BASIC " + "realm=\""
1311
+ authenticator.getRealm() +"\"";
1312
}
1313
System.out.println(now() + key + " Proxy basic authentication success");
1314
return null;
1315
}
1316
1317
String doDigest(Optional<String> authorization) {
1318
String offset = "proxy-authorization: digest ";
1319
String authstring = authorization.orElse("");
1320
if (!authstring.toLowerCase(Locale.US).startsWith(offset)) {
1321
return "Proxy-Authenticate: " +
1322
"Digest realm=\"" + authenticator.getRealm() + "\","
1323
+ "\r\n qop=\"auth\","
1324
+ "\r\n nonce=\"" + ns +"\"";
1325
}
1326
authstring = authstring
1327
.substring(offset.length())
1328
.trim();
1329
boolean validated = false;
1330
try {
1331
DigestResponse dgr = DigestResponse.create(authstring);
1332
validated = validate("CONNECT", dgr);
1333
} catch (Throwable t) {
1334
t.printStackTrace();
1335
}
1336
if (!validated) {
1337
return "Proxy-Authenticate: " +
1338
"Digest realm=\"" + authenticator.getRealm() + "\","
1339
+ "\r\n qop=\"auth\","
1340
+ "\r\n nonce=\"" + ns +"\"";
1341
}
1342
return null;
1343
}
1344
1345
1346
1347
1348
boolean validate(String reqMethod, DigestResponse dg) {
1349
String type = now() + this.getClass().getSimpleName() + ":" + key;
1350
if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) {
1351
System.out.println(type + ": Unsupported algorithm "
1352
+ dg.algorithm);
1353
return false;
1354
}
1355
if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) {
1356
System.out.println(type + ": Unsupported qop "
1357
+ dg.qop);
1358
return false;
1359
}
1360
try {
1361
if (!dg.nonce.equals(ns)) {
1362
System.out.println(type + ": bad nonce returned by client: "
1363
+ nonce + " expected " + ns);
1364
return false;
1365
}
1366
if (dg.response == null) {
1367
System.out.println(type + ": missing digest response.");
1368
return false;
1369
}
1370
char[] pa = authenticator.getPassword(dg.username);
1371
return verify(type, reqMethod, dg, pa);
1372
} catch(IllegalArgumentException | SecurityException
1373
| NoSuchAlgorithmException e) {
1374
System.out.println(type + ": " + e.getMessage());
1375
return false;
1376
}
1377
}
1378
1379
1380
boolean verify(String type, String reqMethod, DigestResponse dg, char[] pw)
1381
throws NoSuchAlgorithmException {
1382
String response = DigestResponse.computeDigest(true, reqMethod, pw, dg);
1383
if (!dg.response.equals(response)) {
1384
System.out.println(type + ": bad response returned by client: "
1385
+ dg.response + " expected " + response);
1386
return false;
1387
} else {
1388
// A real server would also verify the uri=<request-uri>
1389
// parameter - but this is just a test...
1390
System.out.println(type + ": verified response " + response);
1391
}
1392
return true;
1393
}
1394
1395
public boolean authorize(StringBuilder response, String requestLine, String headers) {
1396
String message = "<html><body><p>Authorization Failed%s</p></body></html>\r\n";
1397
if (authenticator == null && schemeType != HttpAuthSchemeType.NONE) {
1398
message = String.format(message, " No Authenticator Set");
1399
response.append("HTTP/1.1 407 Proxy Authentication Failed\r\n");
1400
response.append("Content-Length: ")
1401
.append(message.getBytes(StandardCharsets.UTF_8).length)
1402
.append("\r\n\r\n");
1403
response.append(message);
1404
return false;
1405
}
1406
Optional<String> authorization = Stream.of(headers.split("\r\n"))
1407
.filter((k) -> k.toLowerCase(Locale.US).startsWith("proxy-authorization:"))
1408
.findFirst();
1409
String authenticate = null;
1410
switch(schemeType) {
1411
case BASIC:
1412
case BASICSERVER:
1413
authenticate = doBasic(authorization);
1414
break;
1415
case DIGEST:
1416
authenticate = doDigest(authorization);
1417
break;
1418
case NONE:
1419
response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
1420
return true;
1421
default:
1422
throw new InternalError("Unknown scheme type: " + schemeType);
1423
}
1424
if (authenticate != null) {
1425
message = String.format(message, "");
1426
response.append("HTTP/1.1 407 Proxy Authentication Required\r\n");
1427
response.append("Content-Length: ")
1428
.append(message.getBytes(StandardCharsets.UTF_8).length)
1429
.append("\r\n")
1430
.append(authenticate)
1431
.append("\r\n\r\n");
1432
response.append(message);
1433
return false;
1434
}
1435
response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
1436
return true;
1437
}
1438
}
1439
1440
public interface TunnelingProxy {
1441
InetSocketAddress getProxyAddress();
1442
void stop();
1443
}
1444
1445
// This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden
1446
// behind a fake proxy that only understands CONNECT requests.
1447
// The fake proxy is just a server socket that intercept the
1448
// CONNECT and then redirect streams to the real server.
1449
static class HttpsProxyTunnel extends DigestEchoServer
1450
implements Runnable, TunnelingProxy {
1451
1452
final ServerSocket ss;
1453
final CopyOnWriteArrayList<CompletableFuture<Void>> connectionCFs
1454
= new CopyOnWriteArrayList<>();
1455
volatile ProxyAuthorization authorization;
1456
volatile boolean stopped;
1457
public HttpsProxyTunnel(String key, HttpTestServer server, DigestEchoServer target,
1458
HttpTestHandler delegate)
1459
throws IOException {
1460
this(key, server, target, delegate, ServerSocketFactory.create());
1461
}
1462
private HttpsProxyTunnel(String key, HttpTestServer server, DigestEchoServer target,
1463
HttpTestHandler delegate, ServerSocket ss)
1464
throws IOException {
1465
super("HttpsProxyTunnel:" + ss.getLocalPort() + ":" + key,
1466
server, target, delegate);
1467
System.out.flush();
1468
System.err.println("WARNING: HttpsProxyTunnel is an experimental test class");
1469
this.ss = ss;
1470
start();
1471
}
1472
1473
final void start() throws IOException {
1474
Thread t = new Thread(this, "ProxyThread");
1475
t.setDaemon(true);
1476
t.start();
1477
}
1478
1479
@Override
1480
public Version getServerVersion() {
1481
// serverImpl is not null when this proxy
1482
// serves a single server. It will be null
1483
// if this proxy can serve multiple servers.
1484
if (serverImpl != null) return serverImpl.getVersion();
1485
return null;
1486
}
1487
1488
@Override
1489
public void stop() {
1490
stopped = true;
1491
if (serverImpl != null) {
1492
serverImpl.stop();
1493
}
1494
if (redirect != null) {
1495
redirect.stop();
1496
}
1497
try {
1498
ss.close();
1499
} catch (IOException ex) {
1500
if (DEBUG) ex.printStackTrace(System.out);
1501
}
1502
}
1503
1504
1505
@Override
1506
void configureAuthentication(HttpTestContext ctxt,
1507
HttpAuthSchemeType schemeType,
1508
HttpTestAuthenticator auth,
1509
HttpAuthType authType) {
1510
if (authType == HttpAuthType.PROXY || authType == HttpAuthType.PROXY305) {
1511
authorization = new ProxyAuthorization(key, schemeType, auth);
1512
} else {
1513
super.configureAuthentication(ctxt, schemeType, auth, authType);
1514
}
1515
}
1516
1517
boolean badRequest(StringBuilder response, String hostport, List<String> hosts) {
1518
String message = null;
1519
if (hosts.isEmpty()) {
1520
message = "No host header provided\r\n";
1521
} else if (hosts.size() > 1) {
1522
message = "Multiple host headers provided\r\n";
1523
for (String h : hosts) {
1524
message = message + "host: " + h + "\r\n";
1525
}
1526
} else {
1527
String h = hosts.get(0);
1528
if (!hostport.equalsIgnoreCase(h)
1529
&& !hostport.equalsIgnoreCase(h + ":80")
1530
&& !hostport.equalsIgnoreCase(h + ":443")) {
1531
message = "Bad host provided: [" + h
1532
+ "] doesnot match [" + hostport + "]\r\n";
1533
}
1534
}
1535
if (message != null) {
1536
int length = message.getBytes(StandardCharsets.UTF_8).length;
1537
response.append("HTTP/1.1 400 BadRequest\r\n")
1538
.append("Content-Length: " + length)
1539
.append("\r\n\r\n")
1540
.append(message);
1541
return true;
1542
}
1543
1544
return false;
1545
}
1546
1547
boolean authorize(StringBuilder response, String requestLine, String headers) {
1548
if (authorization != null) {
1549
return authorization.authorize(response, requestLine, headers);
1550
}
1551
response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
1552
return true;
1553
}
1554
1555
// Pipe the input stream to the output stream.
1556
private synchronized Thread pipe(InputStream is, OutputStream os, char tag, CompletableFuture<Void> end) {
1557
return new Thread("TunnelPipe("+tag+")") {
1558
@Override
1559
public void run() {
1560
try {
1561
int c = 0;
1562
try {
1563
while ((c = is.read()) != -1) {
1564
os.write(c);
1565
os.flush();
1566
// if DEBUG prints a + or a - for each transferred
1567
// character.
1568
if (DEBUG) System.out.print(tag);
1569
}
1570
is.close();
1571
} catch (IOException ex) {
1572
if (DEBUG || !stopped && c > -1)
1573
ex.printStackTrace(System.out);
1574
end.completeExceptionally(ex);
1575
} finally {
1576
try {os.close();} catch (Throwable t) {}
1577
}
1578
} finally {
1579
end.complete(null);
1580
}
1581
}
1582
};
1583
}
1584
1585
@Override
1586
public InetSocketAddress getAddress() {
1587
return new InetSocketAddress(InetAddress.getLoopbackAddress(),
1588
ss.getLocalPort());
1589
}
1590
@Override
1591
public InetSocketAddress getProxyAddress() {
1592
return getAddress();
1593
}
1594
@Override
1595
public InetSocketAddress getServerAddress() {
1596
// serverImpl can be null if this proxy can serve
1597
// multiple servers.
1598
if (serverImpl != null) {
1599
return serverImpl.getAddress();
1600
}
1601
return null;
1602
}
1603
1604
1605
// This is a bit shaky. It doesn't handle continuation
1606
// lines, but our client shouldn't send any.
1607
// Read a line from the input stream, swallowing the final
1608
// \r\n sequence. Stops at the first \n, doesn't complain
1609
// if it wasn't preceded by '\r'.
1610
//
1611
String readLine(InputStream r) throws IOException {
1612
StringBuilder b = new StringBuilder();
1613
int c;
1614
while ((c = r.read()) != -1) {
1615
if (c == '\n') break;
1616
b.appendCodePoint(c);
1617
}
1618
if (b.codePointAt(b.length() -1) == '\r') {
1619
b.delete(b.length() -1, b.length());
1620
}
1621
return b.toString();
1622
}
1623
1624
@Override
1625
public void run() {
1626
Socket clientConnection = null;
1627
Socket targetConnection = null;
1628
try {
1629
while (!stopped) {
1630
System.out.println(now() + "Tunnel: Waiting for client");
1631
Socket toClose;
1632
targetConnection = clientConnection = null;
1633
try {
1634
toClose = clientConnection = ss.accept();
1635
if (NO_LINGER) {
1636
// can be useful to trigger "Connection reset by peer"
1637
// errors on the client side.
1638
clientConnection.setOption(StandardSocketOptions.SO_LINGER, 0);
1639
}
1640
} catch (IOException io) {
1641
if (DEBUG || !stopped) io.printStackTrace(System.out);
1642
break;
1643
}
1644
System.out.println(now() + "Tunnel: Client accepted");
1645
StringBuilder headers = new StringBuilder();
1646
InputStream ccis = clientConnection.getInputStream();
1647
OutputStream ccos = clientConnection.getOutputStream();
1648
Writer w = new OutputStreamWriter(
1649
clientConnection.getOutputStream(), "UTF-8");
1650
PrintWriter pw = new PrintWriter(w);
1651
System.out.println(now() + "Tunnel: Reading request line");
1652
String requestLine = readLine(ccis);
1653
System.out.println(now() + "Tunnel: Request line: " + requestLine);
1654
if (requestLine.startsWith("CONNECT ")) {
1655
// We should probably check that the next word following
1656
// CONNECT is the host:port of our HTTPS serverImpl.
1657
// Some improvement for a followup!
1658
StringTokenizer tokenizer = new StringTokenizer(requestLine);
1659
String connect = tokenizer.nextToken();
1660
assert connect.equalsIgnoreCase("connect");
1661
String hostport = tokenizer.nextToken();
1662
InetSocketAddress targetAddress;
1663
List<String> hosts = new ArrayList<>();
1664
try {
1665
URI uri = new URI("https", hostport, "/", null, null);
1666
int port = uri.getPort();
1667
port = port == -1 ? 443 : port;
1668
targetAddress = new InetSocketAddress(uri.getHost(), port);
1669
if (serverImpl != null) {
1670
assert targetAddress.getHostString()
1671
.equalsIgnoreCase(serverImpl.getAddress().getHostString());
1672
assert targetAddress.getPort() == serverImpl.getAddress().getPort();
1673
}
1674
} catch (Throwable x) {
1675
System.err.printf("Bad target address: \"%s\" in \"%s\"%n",
1676
hostport, requestLine);
1677
toClose.close();
1678
continue;
1679
}
1680
1681
// Read all headers until we find the empty line that
1682
// signals the end of all headers.
1683
String line = requestLine;
1684
while(!line.equals("")) {
1685
System.out.println(now() + "Tunnel: Reading header: "
1686
+ (line = readLine(ccis)));
1687
headers.append(line).append("\r\n");
1688
int index = line.indexOf(':');
1689
if (index >= 0) {
1690
String key = line.substring(0, index).trim();
1691
if (key.equalsIgnoreCase("host")) {
1692
hosts.add(line.substring(index+1).trim());
1693
}
1694
}
1695
}
1696
StringBuilder response = new StringBuilder();
1697
if (TUNNEL_REQUIRES_HOST) {
1698
if (badRequest(response, hostport, hosts)) {
1699
System.out.println(now() + "Tunnel: Sending " + response);
1700
// send the 400 response
1701
pw.print(response.toString());
1702
pw.flush();
1703
toClose.close();
1704
continue;
1705
} else {
1706
assert hosts.size() == 1;
1707
System.out.println(now()
1708
+ "Tunnel: Host header verified " + hosts);
1709
}
1710
}
1711
1712
final boolean authorize = authorize(response, requestLine, headers.toString());
1713
if (!authorize) {
1714
System.out.println(now() + "Tunnel: Sending "
1715
+ response);
1716
// send the 407 response
1717
pw.print(response.toString());
1718
pw.flush();
1719
toClose.close();
1720
continue;
1721
}
1722
System.out.println(now()
1723
+ "Tunnel connecting to target server at "
1724
+ targetAddress.getAddress() + ":" + targetAddress.getPort());
1725
targetConnection = new Socket(
1726
targetAddress.getAddress(),
1727
targetAddress.getPort());
1728
1729
// Then send the 200 OK response to the client
1730
System.out.println(now() + "Tunnel: Sending "
1731
+ response);
1732
pw.print(response);
1733
pw.flush();
1734
} else {
1735
// This should not happen. If it does then just print an
1736
// error - both on out and err, and close the accepted
1737
// socket
1738
System.out.println("WARNING: Tunnel: Unexpected status line: "
1739
+ requestLine + " received by "
1740
+ ss.getLocalSocketAddress()
1741
+ " from "
1742
+ toClose.getRemoteSocketAddress()
1743
+ " - closing accepted socket");
1744
// Print on err
1745
System.err.println("WARNING: Tunnel: Unexpected status line: "
1746
+ requestLine + " received by "
1747
+ ss.getLocalSocketAddress()
1748
+ " from "
1749
+ toClose.getRemoteSocketAddress());
1750
// close accepted socket.
1751
toClose.close();
1752
System.err.println("Tunnel: accepted socket closed.");
1753
continue;
1754
}
1755
1756
// Pipe the input stream of the client connection to the
1757
// output stream of the target connection and conversely.
1758
// Now the client and target will just talk to each other.
1759
System.out.println(now() + "Tunnel: Starting tunnel pipes");
1760
CompletableFuture<Void> end, end1, end2;
1761
Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+',
1762
end1 = new CompletableFuture<>());
1763
Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-',
1764
end2 = new CompletableFuture<>());
1765
var end11 = end1.whenComplete((r, t) -> exceptionally(end2, t));
1766
var end22 = end2.whenComplete((r, t) -> exceptionally(end1, t));
1767
end = CompletableFuture.allOf(end11, end22);
1768
Socket tc = targetConnection;
1769
end.whenComplete(
1770
(r,t) -> {
1771
try { toClose.close(); } catch (IOException x) { }
1772
try { tc.close(); } catch (IOException x) { }
1773
finally {connectionCFs.remove(end);}
1774
});
1775
connectionCFs.add(end);
1776
targetConnection = clientConnection = null;
1777
t1.start();
1778
t2.start();
1779
}
1780
} catch (Throwable ex) {
1781
close(clientConnection, ex);
1782
close(targetConnection, ex);
1783
close(ss, ex);
1784
ex.printStackTrace(System.err);
1785
} finally {
1786
System.out.println(now() + "Tunnel: exiting (stopped=" + stopped + ")");
1787
connectionCFs.forEach(cf -> cf.complete(null));
1788
}
1789
}
1790
1791
void exceptionally(CompletableFuture<?> cf, Throwable t) {
1792
if (t != null) cf.completeExceptionally(t);
1793
}
1794
1795
void close(Closeable c, Throwable e) {
1796
if (c == null) return;
1797
try {
1798
c.close();
1799
} catch (IOException x) {
1800
e.addSuppressed(x);
1801
}
1802
}
1803
}
1804
1805
/**
1806
* Creates a TunnelingProxy that can serve multiple servers.
1807
* The server address is extracted from the CONNECT request line.
1808
* @param authScheme The authentication scheme supported by the proxy.
1809
* Typically one of DIGEST, BASIC, NONE.
1810
* @return A new TunnelingProxy able to serve multiple servers.
1811
* @throws IOException If the proxy could not be created.
1812
*/
1813
public static TunnelingProxy createHttpsProxyTunnel(HttpAuthSchemeType authScheme)
1814
throws IOException {
1815
HttpsProxyTunnel result = new HttpsProxyTunnel("", null, null, null);
1816
if (authScheme != HttpAuthSchemeType.NONE) {
1817
result.configureAuthentication(null,
1818
authScheme,
1819
AUTHENTICATOR,
1820
HttpAuthType.PROXY);
1821
}
1822
return result;
1823
}
1824
1825
private static String protocol(String protocol) {
1826
if ("http".equalsIgnoreCase(protocol)) return "http";
1827
else if ("https".equalsIgnoreCase(protocol)) return "https";
1828
else throw new InternalError("Unsupported protocol: " + protocol);
1829
}
1830
1831
public static URL url(String protocol, InetSocketAddress address,
1832
String path) throws MalformedURLException {
1833
return new URL(protocol(protocol),
1834
address.getHostString(),
1835
address.getPort(), path);
1836
}
1837
1838
public static URI uri(String protocol, InetSocketAddress address,
1839
String path) throws URISyntaxException {
1840
return new URI(protocol(protocol) + "://" +
1841
address.getHostString() + ":" +
1842
address.getPort() + path);
1843
}
1844
}
1845
1846