Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/mobile
Path: blob/master/test/jdk/java/net/HttpURLConnection/SetAuthenticator/HTTPTestServer.java
41152 views
1
/*
2
* Copyright (c) 2016, 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.Filter;
26
import com.sun.net.httpserver.Headers;
27
import com.sun.net.httpserver.HttpContext;
28
import com.sun.net.httpserver.HttpExchange;
29
import com.sun.net.httpserver.HttpHandler;
30
import com.sun.net.httpserver.HttpServer;
31
import com.sun.net.httpserver.HttpsConfigurator;
32
import com.sun.net.httpserver.HttpsParameters;
33
import com.sun.net.httpserver.HttpsServer;
34
import java.io.IOException;
35
import java.io.InputStream;
36
import java.io.OutputStream;
37
import java.io.OutputStreamWriter;
38
import java.io.PrintWriter;
39
import java.io.Writer;
40
import java.math.BigInteger;
41
import java.net.HttpURLConnection;
42
import java.net.InetAddress;
43
import java.net.InetSocketAddress;
44
import java.net.MalformedURLException;
45
import java.net.ServerSocket;
46
import java.net.Socket;
47
import java.net.SocketAddress;
48
import java.net.URL;
49
import java.security.MessageDigest;
50
import java.security.NoSuchAlgorithmException;
51
import java.time.Instant;
52
import java.util.ArrayList;
53
import java.util.Arrays;
54
import java.util.Base64;
55
import java.util.HexFormat;
56
import java.util.List;
57
import java.util.Objects;
58
import java.util.Random;
59
import java.util.concurrent.CopyOnWriteArrayList;
60
import java.util.stream.Collectors;
61
import javax.net.ssl.SSLContext;
62
import sun.net.www.HeaderParser;
63
64
/**
65
* A simple HTTP server that supports Digest authentication.
66
* By default this server will echo back whatever is present
67
* in the request body.
68
* @author danielfuchs
69
*/
70
public class HTTPTestServer extends HTTPTest {
71
72
final HttpServer serverImpl; // this server endpoint
73
final HTTPTestServer redirect; // the target server where to redirect 3xx
74
final HttpHandler delegate; // unused
75
76
private HTTPTestServer(HttpServer server, HTTPTestServer target,
77
HttpHandler delegate) {
78
this.serverImpl = server;
79
this.redirect = target;
80
this.delegate = delegate;
81
}
82
83
public static void main(String[] args)
84
throws IOException {
85
86
HTTPTestServer server = create(HTTPTest.DEFAULT_PROTOCOL_TYPE,
87
HTTPTest.DEFAULT_HTTP_AUTH_TYPE,
88
HTTPTest.AUTHENTICATOR,
89
HTTPTest.DEFAULT_SCHEME_TYPE);
90
try {
91
System.out.println("Server created at " + server.getAddress());
92
System.out.println("Strike <Return> to exit");
93
System.in.read();
94
} finally {
95
System.out.println("stopping server");
96
server.stop();
97
}
98
}
99
100
private static String toString(Headers headers) {
101
return headers.entrySet().stream()
102
.map((e) -> e.getKey() + ": " + e.getValue())
103
.collect(Collectors.joining("\n"));
104
}
105
106
public static HTTPTestServer create(HttpProtocolType protocol,
107
HttpAuthType authType,
108
HttpTestAuthenticator auth,
109
HttpSchemeType schemeType)
110
throws IOException {
111
return create(protocol, authType, auth, schemeType, null);
112
}
113
114
public static HTTPTestServer create(HttpProtocolType protocol,
115
HttpAuthType authType,
116
HttpTestAuthenticator auth,
117
HttpSchemeType schemeType,
118
HttpHandler delegate)
119
throws IOException {
120
Objects.requireNonNull(authType);
121
Objects.requireNonNull(auth);
122
switch(authType) {
123
// A server that performs Server Digest authentication.
124
case SERVER: return createServer(protocol, authType, auth,
125
schemeType, delegate, "/");
126
// A server that pretends to be a Proxy and performs
127
// Proxy Digest authentication. If protocol is HTTPS,
128
// then this will create a HttpsProxyTunnel that will
129
// handle the CONNECT request for tunneling.
130
case PROXY: return createProxy(protocol, authType, auth,
131
schemeType, delegate, "/");
132
// A server that sends 307 redirect to a server that performs
133
// Digest authentication.
134
// Note: 301 doesn't work here because it transforms POST into GET.
135
case SERVER307: return createServerAndRedirect(protocol,
136
HttpAuthType.SERVER,
137
auth, schemeType,
138
delegate, 307);
139
// A server that sends 305 redirect to a proxy that performs
140
// Digest authentication.
141
case PROXY305: return createServerAndRedirect(protocol,
142
HttpAuthType.PROXY,
143
auth, schemeType,
144
delegate, 305);
145
default:
146
throw new InternalError("Unknown server type: " + authType);
147
}
148
}
149
150
/**
151
* The SocketBindableFactory ensures that the local port used by an HttpServer
152
* or a proxy ServerSocket previously created by the current test/VM will not
153
* get reused by a subsequent test in the same VM. This is to avoid having the
154
* AuthCache reuse credentials from previous tests - which would invalidate the
155
* assumptions made by the current test on when the default authenticator should
156
* be called.
157
*/
158
private static abstract class SocketBindableFactory<B> {
159
private static final int MAX = 10;
160
private static final CopyOnWriteArrayList<String> addresses =
161
new CopyOnWriteArrayList<>();
162
protected B createInternal() throws IOException {
163
final int max = addresses.size() + MAX;
164
final List<B> toClose = new ArrayList<>();
165
try {
166
for (int i = 1; i <= max; i++) {
167
B bindable = createBindable();
168
SocketAddress address = getAddress(bindable);
169
String key = toString(address);
170
if (addresses.addIfAbsent(key)) {
171
System.out.println("Socket bound to: " + key
172
+ " after " + i + " attempt(s)");
173
return bindable;
174
}
175
System.out.println("warning: address " + key
176
+ " already used. Retrying bind.");
177
// keep the port bound until we get a port that we haven't
178
// used already
179
toClose.add(bindable);
180
}
181
} finally {
182
// if we had to retry, then close the socket we're not
183
// going to use.
184
for (B b : toClose) {
185
try { close(b); } catch (Exception x) { /* ignore */ }
186
}
187
}
188
throw new IOException("Couldn't bind socket after " + max + " attempts: "
189
+ "addresses used before: " + addresses);
190
}
191
192
private static String toString(SocketAddress address) {
193
// We don't rely on address.toString(): sometimes it can be
194
// "/127.0.0.1:port", sometimes it can be "localhost/127.0.0.1:port"
195
// Instead we compose our own string representation:
196
InetSocketAddress candidate = (InetSocketAddress) address;
197
String hostAddr = candidate.getAddress().getHostAddress();
198
if (hostAddr.contains(":")) hostAddr = "[" + hostAddr + "]";
199
return hostAddr + ":" + candidate.getPort();
200
}
201
202
protected abstract B createBindable() throws IOException;
203
204
protected abstract SocketAddress getAddress(B bindable);
205
206
protected abstract void close(B bindable) throws IOException;
207
}
208
209
/*
210
* Used to create ServerSocket for a proxy.
211
*/
212
private static final class ServerSocketFactory
213
extends SocketBindableFactory<ServerSocket> {
214
private static final ServerSocketFactory instance = new ServerSocketFactory();
215
216
static ServerSocket create() throws IOException {
217
return instance.createInternal();
218
}
219
220
@Override
221
protected ServerSocket createBindable() throws IOException {
222
InetAddress address = InetAddress.getLoopbackAddress();
223
return new ServerSocket(0, 0, address);
224
}
225
226
@Override
227
protected SocketAddress getAddress(ServerSocket socket) {
228
return socket.getLocalSocketAddress();
229
}
230
231
@Override
232
protected void close(ServerSocket socket) throws IOException {
233
socket.close();
234
}
235
}
236
237
/*
238
* Used to create HttpServer for a NTLMTestServer.
239
*/
240
private static abstract class WebServerFactory<S extends HttpServer>
241
extends SocketBindableFactory<S> {
242
@Override
243
protected S createBindable() throws IOException {
244
S server = newHttpServer();
245
InetAddress address = InetAddress.getLoopbackAddress();
246
server.bind(new InetSocketAddress(address, 0), 0);
247
return server;
248
}
249
250
@Override
251
protected SocketAddress getAddress(S server) {
252
return server.getAddress();
253
}
254
255
@Override
256
protected void close(S server) throws IOException {
257
server.stop(1);
258
}
259
260
/*
261
* Returns a HttpServer or a HttpsServer in different subclasses.
262
*/
263
protected abstract S newHttpServer() throws IOException;
264
}
265
266
private static final class HttpServerFactory extends WebServerFactory<HttpServer> {
267
private static final HttpServerFactory instance = new HttpServerFactory();
268
269
static HttpServer create() throws IOException {
270
return instance.createInternal();
271
}
272
273
@Override
274
protected HttpServer newHttpServer() throws IOException {
275
return HttpServer.create();
276
}
277
}
278
279
private static final class HttpsServerFactory extends WebServerFactory<HttpsServer> {
280
private static final HttpsServerFactory instance = new HttpsServerFactory();
281
282
static HttpsServer create() throws IOException {
283
return instance.createInternal();
284
}
285
286
@Override
287
protected HttpsServer newHttpServer() throws IOException {
288
return HttpsServer.create();
289
}
290
}
291
292
static HttpServer createHttpServer(HttpProtocolType protocol) throws IOException {
293
switch (protocol) {
294
case HTTP: return HttpServerFactory.create();
295
case HTTPS: return configure(HttpsServerFactory.create());
296
default: throw new InternalError("Unsupported protocol " + protocol);
297
}
298
}
299
300
static HttpsServer configure(HttpsServer server) throws IOException {
301
try {
302
SSLContext ctx = SSLContext.getDefault();
303
server.setHttpsConfigurator(new Configurator(ctx));
304
} catch (NoSuchAlgorithmException ex) {
305
throw new IOException(ex);
306
}
307
return server;
308
}
309
310
311
static void setContextAuthenticator(HttpContext ctxt,
312
HttpTestAuthenticator auth) {
313
final String realm = auth.getRealm();
314
com.sun.net.httpserver.Authenticator authenticator =
315
new BasicAuthenticator(realm) {
316
@Override
317
public boolean checkCredentials(String username, String pwd) {
318
return auth.getUserName().equals(username)
319
&& new String(auth.getPassword(username)).equals(pwd);
320
}
321
};
322
ctxt.setAuthenticator(authenticator);
323
}
324
325
public static HTTPTestServer createServer(HttpProtocolType protocol,
326
HttpAuthType authType,
327
HttpTestAuthenticator auth,
328
HttpSchemeType schemeType,
329
HttpHandler delegate,
330
String path)
331
throws IOException {
332
Objects.requireNonNull(authType);
333
Objects.requireNonNull(auth);
334
335
HttpServer impl = createHttpServer(protocol);
336
final HTTPTestServer server = new HTTPTestServer(impl, null, delegate);
337
final HttpHandler hh = server.createHandler(schemeType, auth, authType);
338
HttpContext ctxt = impl.createContext(path, hh);
339
server.configureAuthentication(ctxt, schemeType, auth, authType);
340
impl.start();
341
return server;
342
}
343
344
public static HTTPTestServer createProxy(HttpProtocolType protocol,
345
HttpAuthType authType,
346
HttpTestAuthenticator auth,
347
HttpSchemeType schemeType,
348
HttpHandler delegate,
349
String path)
350
throws IOException {
351
Objects.requireNonNull(authType);
352
Objects.requireNonNull(auth);
353
354
HttpServer impl = createHttpServer(protocol);
355
final HTTPTestServer server = protocol == HttpProtocolType.HTTPS
356
? new HttpsProxyTunnel(impl, null, delegate)
357
: new HTTPTestServer(impl, null, delegate);
358
final HttpHandler hh = server.createHandler(schemeType, auth, authType);
359
HttpContext ctxt = impl.createContext(path, hh);
360
server.configureAuthentication(ctxt, schemeType, auth, authType);
361
impl.start();
362
363
return server;
364
}
365
366
public static HTTPTestServer createServerAndRedirect(
367
HttpProtocolType protocol,
368
HttpAuthType targetAuthType,
369
HttpTestAuthenticator auth,
370
HttpSchemeType schemeType,
371
HttpHandler targetDelegate,
372
int code300)
373
throws IOException {
374
Objects.requireNonNull(targetAuthType);
375
Objects.requireNonNull(auth);
376
377
// The connection between client and proxy can only
378
// be a plain connection: SSL connection to proxy
379
// is not supported by our client connection.
380
HttpProtocolType targetProtocol = targetAuthType == HttpAuthType.PROXY
381
? HttpProtocolType.HTTP
382
: protocol;
383
HTTPTestServer redirectTarget =
384
(targetAuthType == HttpAuthType.PROXY)
385
? createProxy(protocol, targetAuthType,
386
auth, schemeType, targetDelegate, "/")
387
: createServer(targetProtocol, targetAuthType,
388
auth, schemeType, targetDelegate, "/");
389
HttpServer impl = createHttpServer(protocol);
390
final HTTPTestServer redirectingServer =
391
new HTTPTestServer(impl, redirectTarget, null);
392
InetSocketAddress redirectAddr = redirectTarget.getAddress();
393
URL locationURL = url(targetProtocol, redirectAddr, "/");
394
final HttpHandler hh = redirectingServer.create300Handler(locationURL,
395
HttpAuthType.SERVER, code300);
396
impl.createContext("/", hh);
397
impl.start();
398
return redirectingServer;
399
}
400
401
public InetSocketAddress getAddress() {
402
return serverImpl.getAddress();
403
}
404
405
public InetSocketAddress getProxyAddress() {
406
return serverImpl.getAddress();
407
}
408
409
public void stop() {
410
serverImpl.stop(0);
411
if (redirect != null) {
412
redirect.stop();
413
}
414
}
415
416
protected void writeResponse(HttpExchange he) throws IOException {
417
if (delegate == null) {
418
he.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
419
he.getResponseBody().write(he.getRequestBody().readAllBytes());
420
} else {
421
delegate.handle(he);
422
}
423
}
424
425
private HttpHandler createHandler(HttpSchemeType schemeType,
426
HttpTestAuthenticator auth,
427
HttpAuthType authType) {
428
return new HttpNoAuthHandler(authType);
429
}
430
431
private void configureAuthentication(HttpContext ctxt,
432
HttpSchemeType schemeType,
433
HttpTestAuthenticator auth,
434
HttpAuthType authType) {
435
switch(schemeType) {
436
case DIGEST:
437
// DIGEST authentication is handled by the handler.
438
ctxt.getFilters().add(new HttpDigestFilter(auth, authType));
439
break;
440
case BASIC:
441
// BASIC authentication is handled by the filter.
442
ctxt.getFilters().add(new HttpBasicFilter(auth, authType));
443
break;
444
case BASICSERVER:
445
switch(authType) {
446
case PROXY: case PROXY305:
447
// HttpServer can't support Proxy-type authentication
448
// => we do as if BASIC had been specified, and we will
449
// handle authentication in the handler.
450
ctxt.getFilters().add(new HttpBasicFilter(auth, authType));
451
break;
452
case SERVER: case SERVER307:
453
// Basic authentication is handled by HttpServer
454
// directly => the filter should not perform
455
// authentication again.
456
setContextAuthenticator(ctxt, auth);
457
ctxt.getFilters().add(new HttpNoAuthFilter(authType));
458
break;
459
default:
460
throw new InternalError("Invalid combination scheme="
461
+ schemeType + " authType=" + authType);
462
}
463
case NONE:
464
// No authentication at all.
465
ctxt.getFilters().add(new HttpNoAuthFilter(authType));
466
break;
467
default:
468
throw new InternalError("No such scheme: " + schemeType);
469
}
470
}
471
472
private HttpHandler create300Handler(URL proxyURL,
473
HttpAuthType type, int code300) throws MalformedURLException {
474
return new Http3xxHandler(proxyURL, type, code300);
475
}
476
477
// Abstract HTTP filter class.
478
private abstract static class AbstractHttpFilter extends Filter {
479
480
final HttpAuthType authType;
481
final String type;
482
public AbstractHttpFilter(HttpAuthType authType, String type) {
483
this.authType = authType;
484
this.type = type;
485
}
486
487
String getLocation() {
488
return "Location";
489
}
490
String getAuthenticate() {
491
return authType == HttpAuthType.PROXY
492
? "Proxy-Authenticate" : "WWW-Authenticate";
493
}
494
String getAuthorization() {
495
return authType == HttpAuthType.PROXY
496
? "Proxy-Authorization" : "Authorization";
497
}
498
int getUnauthorizedCode() {
499
return authType == HttpAuthType.PROXY
500
? HttpURLConnection.HTTP_PROXY_AUTH
501
: HttpURLConnection.HTTP_UNAUTHORIZED;
502
}
503
String getKeepAlive() {
504
return "keep-alive";
505
}
506
String getConnection() {
507
return authType == HttpAuthType.PROXY
508
? "Proxy-Connection" : "Connection";
509
}
510
protected abstract boolean isAuthentified(HttpExchange he) throws IOException;
511
protected abstract void requestAuthentication(HttpExchange he) throws IOException;
512
protected void accept(HttpExchange he, Chain chain) throws IOException {
513
chain.doFilter(he);
514
}
515
516
@Override
517
public String description() {
518
return "Filter for " + type;
519
}
520
@Override
521
public void doFilter(HttpExchange he, Chain chain) throws IOException {
522
try {
523
System.out.println(type + ": Got " + he.getRequestMethod()
524
+ ": " + he.getRequestURI()
525
+ "\n" + HTTPTestServer.toString(he.getRequestHeaders()));
526
if (!isAuthentified(he)) {
527
try {
528
requestAuthentication(he);
529
he.sendResponseHeaders(getUnauthorizedCode(), 0);
530
System.out.println(type
531
+ ": Sent back " + getUnauthorizedCode());
532
} finally {
533
he.close();
534
}
535
} else {
536
accept(he, chain);
537
}
538
} catch (RuntimeException | Error | IOException t) {
539
System.err.println(type
540
+ ": Unexpected exception while handling request: " + t);
541
t.printStackTrace(System.err);
542
he.close();
543
throw t;
544
}
545
}
546
547
}
548
549
private final static class DigestResponse {
550
final String realm;
551
final String username;
552
final String nonce;
553
final String cnonce;
554
final String nc;
555
final String uri;
556
final String algorithm;
557
final String response;
558
final String qop;
559
final String opaque;
560
561
public DigestResponse(String realm, String username, String nonce,
562
String cnonce, String nc, String uri,
563
String algorithm, String qop, String opaque,
564
String response) {
565
this.realm = realm;
566
this.username = username;
567
this.nonce = nonce;
568
this.cnonce = cnonce;
569
this.nc = nc;
570
this.uri = uri;
571
this.algorithm = algorithm;
572
this.qop = qop;
573
this.opaque = opaque;
574
this.response = response;
575
}
576
577
String getAlgorithm(String defval) {
578
return algorithm == null ? defval : algorithm;
579
}
580
String getQoP(String defval) {
581
return qop == null ? defval : qop;
582
}
583
584
// Code stolen from DigestAuthentication:
585
586
private static String encode(String src, char[] passwd, MessageDigest md) {
587
try {
588
md.update(src.getBytes("ISO-8859-1"));
589
} catch (java.io.UnsupportedEncodingException uee) {
590
assert false;
591
}
592
if (passwd != null) {
593
byte[] passwdBytes = new byte[passwd.length];
594
for (int i=0; i<passwd.length; i++)
595
passwdBytes[i] = (byte)passwd[i];
596
md.update(passwdBytes);
597
Arrays.fill(passwdBytes, (byte)0x00);
598
}
599
byte[] digest = md.digest();
600
return HexFormat.of().formatHex(digest);
601
}
602
603
public static String computeDigest(boolean isRequest,
604
String reqMethod,
605
char[] password,
606
DigestResponse params)
607
throws NoSuchAlgorithmException
608
{
609
610
String A1, HashA1;
611
String algorithm = params.getAlgorithm("MD5");
612
boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
613
614
MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
615
616
if (params.username == null) {
617
throw new IllegalArgumentException("missing username");
618
}
619
if (params.realm == null) {
620
throw new IllegalArgumentException("missing realm");
621
}
622
if (params.uri == null) {
623
throw new IllegalArgumentException("missing uri");
624
}
625
if (params.nonce == null) {
626
throw new IllegalArgumentException("missing nonce");
627
}
628
629
A1 = params.username + ":" + params.realm + ":";
630
HashA1 = encode(A1, password, md);
631
632
String A2;
633
if (isRequest) {
634
A2 = reqMethod + ":" + params.uri;
635
} else {
636
A2 = ":" + params.uri;
637
}
638
String HashA2 = encode(A2, null, md);
639
String combo, finalHash;
640
641
if ("auth".equals(params.qop)) { /* RRC2617 when qop=auth */
642
if (params.cnonce == null) {
643
throw new IllegalArgumentException("missing nonce");
644
}
645
if (params.nc == null) {
646
throw new IllegalArgumentException("missing nonce");
647
}
648
combo = HashA1+ ":" + params.nonce + ":" + params.nc + ":" +
649
params.cnonce + ":auth:" +HashA2;
650
651
} else { /* for compatibility with RFC2069 */
652
combo = HashA1 + ":" +
653
params.nonce + ":" +
654
HashA2;
655
}
656
finalHash = encode(combo, null, md);
657
return finalHash;
658
}
659
660
public static DigestResponse create(String raw) {
661
String username, realm, nonce, nc, uri, response, cnonce,
662
algorithm, qop, opaque;
663
HeaderParser parser = new HeaderParser(raw);
664
username = parser.findValue("username");
665
realm = parser.findValue("realm");
666
nonce = parser.findValue("nonce");
667
nc = parser.findValue("nc");
668
uri = parser.findValue("uri");
669
cnonce = parser.findValue("cnonce");
670
response = parser.findValue("response");
671
algorithm = parser.findValue("algorithm");
672
qop = parser.findValue("qop");
673
opaque = parser.findValue("opaque");
674
return new DigestResponse(realm, username, nonce, cnonce, nc, uri,
675
algorithm, qop, opaque, response);
676
}
677
678
}
679
680
private class HttpNoAuthFilter extends AbstractHttpFilter {
681
682
public HttpNoAuthFilter(HttpAuthType authType) {
683
super(authType, authType == HttpAuthType.SERVER
684
? "NoAuth Server" : "NoAuth Proxy");
685
}
686
687
@Override
688
protected boolean isAuthentified(HttpExchange he) throws IOException {
689
return true;
690
}
691
692
@Override
693
protected void requestAuthentication(HttpExchange he) throws IOException {
694
throw new InternalError("Should not com here");
695
}
696
697
@Override
698
public String description() {
699
return "Passthrough Filter";
700
}
701
702
}
703
704
// An HTTP Filter that performs Basic authentication
705
private class HttpBasicFilter extends AbstractHttpFilter {
706
707
private final HttpTestAuthenticator auth;
708
public HttpBasicFilter(HttpTestAuthenticator auth, HttpAuthType authType) {
709
super(authType, authType == HttpAuthType.SERVER
710
? "Basic Server" : "Basic Proxy");
711
this.auth = auth;
712
}
713
714
@Override
715
protected void requestAuthentication(HttpExchange he)
716
throws IOException {
717
he.getResponseHeaders().add(getAuthenticate(),
718
"Basic realm=\"" + auth.getRealm() + "\"");
719
System.out.println(type + ": Requesting Basic Authentication "
720
+ he.getResponseHeaders().getFirst(getAuthenticate()));
721
}
722
723
@Override
724
protected boolean isAuthentified(HttpExchange he) {
725
if (he.getRequestHeaders().containsKey(getAuthorization())) {
726
List<String> authorization =
727
he.getRequestHeaders().get(getAuthorization());
728
for (String a : authorization) {
729
System.out.println(type + ": processing " + a);
730
int sp = a.indexOf(' ');
731
if (sp < 0) return false;
732
String scheme = a.substring(0, sp);
733
if (!"Basic".equalsIgnoreCase(scheme)) {
734
System.out.println(type + ": Unsupported scheme '"
735
+ scheme +"'");
736
return false;
737
}
738
if (a.length() <= sp+1) {
739
System.out.println(type + ": value too short for '"
740
+ scheme +"'");
741
return false;
742
}
743
a = a.substring(sp+1);
744
return validate(a);
745
}
746
return false;
747
}
748
return false;
749
}
750
751
boolean validate(String a) {
752
byte[] b = Base64.getDecoder().decode(a);
753
String userpass = new String (b);
754
int colon = userpass.indexOf (':');
755
String uname = userpass.substring (0, colon);
756
String pass = userpass.substring (colon+1);
757
return auth.getUserName().equals(uname) &&
758
new String(auth.getPassword(uname)).equals(pass);
759
}
760
761
@Override
762
public String description() {
763
return "Filter for " + type;
764
}
765
766
}
767
768
769
// An HTTP Filter that performs Digest authentication
770
private class HttpDigestFilter extends AbstractHttpFilter {
771
772
// This is a very basic DIGEST - used only for the purpose of testing
773
// the client implementation. Therefore we can get away with never
774
// updating the server nonce as it makes the implementation of the
775
// server side digest simpler.
776
private final HttpTestAuthenticator auth;
777
private final byte[] nonce;
778
private final String ns;
779
public HttpDigestFilter(HttpTestAuthenticator auth, HttpAuthType authType) {
780
super(authType, authType == HttpAuthType.SERVER
781
? "Digest Server" : "Digest Proxy");
782
this.auth = auth;
783
nonce = new byte[16];
784
new Random(Instant.now().toEpochMilli()).nextBytes(nonce);
785
ns = new BigInteger(1, nonce).toString(16);
786
}
787
788
@Override
789
protected void requestAuthentication(HttpExchange he)
790
throws IOException {
791
he.getResponseHeaders().add(getAuthenticate(),
792
"Digest realm=\"" + auth.getRealm() + "\","
793
+ "\r\n qop=\"auth\","
794
+ "\r\n nonce=\"" + ns +"\"");
795
System.out.println(type + ": Requesting Digest Authentication "
796
+ he.getResponseHeaders().getFirst(getAuthenticate()));
797
}
798
799
@Override
800
protected boolean isAuthentified(HttpExchange he) {
801
if (he.getRequestHeaders().containsKey(getAuthorization())) {
802
List<String> authorization = he.getRequestHeaders().get(getAuthorization());
803
for (String a : authorization) {
804
System.out.println(type + ": processing " + a);
805
int sp = a.indexOf(' ');
806
if (sp < 0) return false;
807
String scheme = a.substring(0, sp);
808
if (!"Digest".equalsIgnoreCase(scheme)) {
809
System.out.println(type + ": Unsupported scheme '" + scheme +"'");
810
return false;
811
}
812
if (a.length() <= sp+1) {
813
System.out.println(type + ": value too short for '" + scheme +"'");
814
return false;
815
}
816
a = a.substring(sp+1);
817
DigestResponse dgr = DigestResponse.create(a);
818
return validate(he.getRequestMethod(), dgr);
819
}
820
return false;
821
}
822
return false;
823
}
824
825
boolean validate(String reqMethod, DigestResponse dg) {
826
if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) {
827
System.out.println(type + ": Unsupported algorithm "
828
+ dg.algorithm);
829
return false;
830
}
831
if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) {
832
System.out.println(type + ": Unsupported qop "
833
+ dg.qop);
834
return false;
835
}
836
try {
837
if (!dg.nonce.equals(ns)) {
838
System.out.println(type + ": bad nonce returned by client: "
839
+ nonce + " expected " + ns);
840
return false;
841
}
842
if (dg.response == null) {
843
System.out.println(type + ": missing digest response.");
844
return false;
845
}
846
char[] pa = auth.getPassword(dg.username);
847
return verify(reqMethod, dg, pa);
848
} catch(IllegalArgumentException | SecurityException
849
| NoSuchAlgorithmException e) {
850
System.out.println(type + ": " + e.getMessage());
851
return false;
852
}
853
}
854
855
boolean verify(String reqMethod, DigestResponse dg, char[] pw)
856
throws NoSuchAlgorithmException {
857
String response = DigestResponse.computeDigest(true, reqMethod, pw, dg);
858
if (!dg.response.equals(response)) {
859
System.out.println(type + ": bad response returned by client: "
860
+ dg.response + " expected " + response);
861
return false;
862
} else {
863
System.out.println(type + ": verified response " + response);
864
}
865
return true;
866
}
867
868
@Override
869
public String description() {
870
return "Filter for DIGEST authentication";
871
}
872
}
873
874
// Abstract HTTP handler class.
875
private abstract static class AbstractHttpHandler implements HttpHandler {
876
877
final HttpAuthType authType;
878
final String type;
879
public AbstractHttpHandler(HttpAuthType authType, String type) {
880
this.authType = authType;
881
this.type = type;
882
}
883
884
String getLocation() {
885
return "Location";
886
}
887
888
@Override
889
public void handle(HttpExchange he) throws IOException {
890
try {
891
sendResponse(he);
892
} catch (RuntimeException | Error | IOException t) {
893
System.err.println(type
894
+ ": Unexpected exception while handling request: " + t);
895
t.printStackTrace(System.err);
896
throw t;
897
} finally {
898
he.close();
899
}
900
}
901
902
protected abstract void sendResponse(HttpExchange he) throws IOException;
903
904
}
905
906
private class HttpNoAuthHandler extends AbstractHttpHandler {
907
908
public HttpNoAuthHandler(HttpAuthType authType) {
909
super(authType, authType == HttpAuthType.SERVER
910
? "NoAuth Server" : "NoAuth Proxy");
911
}
912
913
@Override
914
protected void sendResponse(HttpExchange he) throws IOException {
915
HTTPTestServer.this.writeResponse(he);
916
}
917
918
}
919
920
// A dummy HTTP Handler that redirects all incoming requests
921
// by sending a back 3xx response code (301, 305, 307 etc..)
922
private class Http3xxHandler extends AbstractHttpHandler {
923
924
private final URL redirectTargetURL;
925
private final int code3XX;
926
public Http3xxHandler(URL proxyURL, HttpAuthType authType, int code300) {
927
super(authType, "Server" + code300);
928
this.redirectTargetURL = proxyURL;
929
this.code3XX = code300;
930
}
931
932
int get3XX() {
933
return code3XX;
934
}
935
936
@Override
937
public void sendResponse(HttpExchange he) throws IOException {
938
System.out.println(type + ": Got " + he.getRequestMethod()
939
+ ": " + he.getRequestURI()
940
+ "\n" + HTTPTestServer.toString(he.getRequestHeaders()));
941
System.out.println(type + ": Redirecting to "
942
+ (authType == HttpAuthType.PROXY305
943
? "proxy" : "server"));
944
he.getResponseHeaders().add(getLocation(),
945
redirectTargetURL.toExternalForm().toString());
946
he.sendResponseHeaders(get3XX(), 0);
947
System.out.println(type + ": Sent back " + get3XX() + " "
948
+ getLocation() + ": " + redirectTargetURL.toExternalForm().toString());
949
}
950
}
951
952
static class Configurator extends HttpsConfigurator {
953
public Configurator(SSLContext ctx) {
954
super(ctx);
955
}
956
957
@Override
958
public void configure (HttpsParameters params) {
959
params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
960
}
961
}
962
963
// This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden
964
// behind a fake proxy that only understands CONNECT requests.
965
// The fake proxy is just a server socket that intercept the
966
// CONNECT and then redirect streams to the real server.
967
static class HttpsProxyTunnel extends HTTPTestServer
968
implements Runnable {
969
970
final ServerSocket ss;
971
private volatile boolean stop;
972
973
public HttpsProxyTunnel(HttpServer server, HTTPTestServer target,
974
HttpHandler delegate)
975
throws IOException {
976
super(server, target, delegate);
977
System.out.flush();
978
System.err.println("WARNING: HttpsProxyTunnel is an experimental test class");
979
ss = ServerSocketFactory.create();
980
start();
981
}
982
983
final void start() throws IOException {
984
Thread t = new Thread(this, "ProxyThread");
985
t.setDaemon(true);
986
t.start();
987
}
988
989
@Override
990
public void stop() {
991
try (var toClose = ss) {
992
stop = true;
993
System.out.println("Server " + ss + " stop requested");
994
super.stop();
995
} catch (IOException ex) {
996
if (DEBUG) ex.printStackTrace(System.out);
997
}
998
}
999
1000
// Pipe the input stream to the output stream.
1001
private synchronized Thread pipe(InputStream is, OutputStream os, char tag) {
1002
return new Thread("TunnelPipe("+tag+")") {
1003
@Override
1004
public void run() {
1005
try {
1006
try {
1007
int c;
1008
while ((c = is.read()) != -1) {
1009
os.write(c);
1010
os.flush();
1011
// if DEBUG prints a + or a - for each transferred
1012
// character.
1013
if (DEBUG) System.out.print(tag);
1014
}
1015
is.close();
1016
} finally {
1017
os.close();
1018
}
1019
} catch (IOException ex) {
1020
if (DEBUG) ex.printStackTrace(System.out);
1021
}
1022
}
1023
};
1024
}
1025
1026
@Override
1027
public InetSocketAddress getProxyAddress() {
1028
return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort());
1029
}
1030
1031
// This is a bit shaky. It doesn't handle continuation
1032
// lines, but our client shouldn't send any.
1033
// Read a line from the input stream, swallowing the final
1034
// \r\n sequence. Stops at the first \n, doesn't complain
1035
// if it wasn't preceded by '\r'.
1036
//
1037
String readLine(InputStream r) throws IOException {
1038
StringBuilder b = new StringBuilder();
1039
int c;
1040
while ((c = r.read()) != -1) {
1041
if (c == '\n') break;
1042
b.appendCodePoint(c);
1043
}
1044
if (b.length() == 0) {
1045
return "";
1046
}
1047
if (b.codePointAt(b.length() -1) == '\r') {
1048
b.delete(b.length() -1, b.length());
1049
}
1050
return b.toString();
1051
}
1052
1053
@Override
1054
public void run() {
1055
Socket clientConnection = null;
1056
while (!stop) {
1057
System.out.println("Tunnel: Waiting for client at: " + ss);
1058
final Socket previous = clientConnection;
1059
try {
1060
clientConnection = ss.accept();
1061
} catch (IOException io) {
1062
try {
1063
ss.close();
1064
} catch (IOException ex) {
1065
if (DEBUG) {
1066
ex.printStackTrace(System.out);
1067
}
1068
}
1069
// log the reason that caused the server to stop accepting connections
1070
if (!stop) {
1071
System.err.println("Server will stop accepting connections due to an exception:");
1072
io.printStackTrace();
1073
}
1074
break;
1075
} finally {
1076
// close the previous connection
1077
if (previous != null) {
1078
try {
1079
previous.close();
1080
} catch (IOException e) {
1081
// ignore
1082
if (DEBUG) {
1083
System.out.println("Ignoring exception that happened while closing " +
1084
"an older connection:");
1085
e.printStackTrace(System.out);
1086
}
1087
}
1088
}
1089
}
1090
System.out.println("Tunnel: Client accepted");
1091
try {
1092
// We have only 1 client... process the current client
1093
// request and wait until it has finished before
1094
// accepting a new connection request.
1095
processRequestAndWaitToComplete(clientConnection);
1096
} catch (IOException ioe) {
1097
// close the client connection
1098
try {
1099
clientConnection.close();
1100
} catch (IOException io) {
1101
// ignore
1102
if (DEBUG) {
1103
System.out.println("Ignoring exception that happened during client" +
1104
" connection close:");
1105
io.printStackTrace(System.out);
1106
}
1107
} finally {
1108
clientConnection = null;
1109
}
1110
} catch (Throwable t) {
1111
// don't close the client connection for non-IOExceptions, instead
1112
// just log it and move on to accept next connection
1113
if (!stop) {
1114
t.printStackTrace();
1115
}
1116
}
1117
}
1118
}
1119
1120
private void processRequestAndWaitToComplete(final Socket clientConnection)
1121
throws IOException, InterruptedException {
1122
final Socket targetConnection;
1123
InputStream ccis = clientConnection.getInputStream();
1124
OutputStream ccos = clientConnection.getOutputStream();
1125
Writer w = new OutputStreamWriter(
1126
clientConnection.getOutputStream(), "UTF-8");
1127
PrintWriter pw = new PrintWriter(w);
1128
System.out.println("Tunnel: Reading request line");
1129
String requestLine = readLine(ccis);
1130
System.out.println("Tunnel: Request line: " + requestLine);
1131
if (requestLine.startsWith("CONNECT ")) {
1132
// We should probably check that the next word following
1133
// CONNECT is the host:port of our HTTPS serverImpl.
1134
// Some improvement for a followup!
1135
1136
// Read all headers until we find the empty line that
1137
// signals the end of all headers.
1138
while(!requestLine.equals("")) {
1139
System.out.println("Tunnel: Reading header: "
1140
+ (requestLine = readLine(ccis)));
1141
}
1142
1143
targetConnection = new Socket(
1144
serverImpl.getAddress().getAddress(),
1145
serverImpl.getAddress().getPort());
1146
1147
// Then send the 200 OK response to the client
1148
System.out.println("Tunnel: Sending "
1149
+ "HTTP/1.1 200 OK\r\n\r\n");
1150
pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
1151
pw.flush();
1152
} else {
1153
// This should not happen. If it does then consider it a
1154
// client error and throw an IOException
1155
System.out.println("Tunnel: Throwing an IOException due to unexpected" +
1156
" request line: " + requestLine);
1157
throw new IOException("Client request error - Unexpected request line");
1158
}
1159
1160
// Pipe the input stream of the client connection to the
1161
// output stream of the target connection and conversely.
1162
// Now the client and target will just talk to each other.
1163
System.out.println("Tunnel: Starting tunnel pipes");
1164
Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+');
1165
Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-');
1166
t1.start();
1167
t2.start();
1168
// wait for the request to complete
1169
t1.join();
1170
t2.join();
1171
}
1172
}
1173
}
1174
1175