Path: blob/master/test/jdk/java/net/HttpURLConnection/SetAuthenticator/HTTPTestServer.java
41152 views
/*1* Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation.7*8* This code is distributed in the hope that it will be useful, but WITHOUT9* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or10* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License11* version 2 for more details (a copy is included in the LICENSE file that12* accompanied this code).13*14* You should have received a copy of the GNU General Public License version15* 2 along with this work; if not, write to the Free Software Foundation,16* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.17*18* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA19* or visit www.oracle.com if you need additional information or have any20* questions.21*/2223import com.sun.net.httpserver.BasicAuthenticator;24import com.sun.net.httpserver.Filter;25import com.sun.net.httpserver.Headers;26import com.sun.net.httpserver.HttpContext;27import com.sun.net.httpserver.HttpExchange;28import com.sun.net.httpserver.HttpHandler;29import com.sun.net.httpserver.HttpServer;30import com.sun.net.httpserver.HttpsConfigurator;31import com.sun.net.httpserver.HttpsParameters;32import com.sun.net.httpserver.HttpsServer;33import java.io.IOException;34import java.io.InputStream;35import java.io.OutputStream;36import java.io.OutputStreamWriter;37import java.io.PrintWriter;38import java.io.Writer;39import java.math.BigInteger;40import java.net.HttpURLConnection;41import java.net.InetAddress;42import java.net.InetSocketAddress;43import java.net.MalformedURLException;44import java.net.ServerSocket;45import java.net.Socket;46import java.net.SocketAddress;47import java.net.URL;48import java.security.MessageDigest;49import java.security.NoSuchAlgorithmException;50import java.time.Instant;51import java.util.ArrayList;52import java.util.Arrays;53import java.util.Base64;54import java.util.HexFormat;55import java.util.List;56import java.util.Objects;57import java.util.Random;58import java.util.concurrent.CopyOnWriteArrayList;59import java.util.stream.Collectors;60import javax.net.ssl.SSLContext;61import sun.net.www.HeaderParser;6263/**64* A simple HTTP server that supports Digest authentication.65* By default this server will echo back whatever is present66* in the request body.67* @author danielfuchs68*/69public class HTTPTestServer extends HTTPTest {7071final HttpServer serverImpl; // this server endpoint72final HTTPTestServer redirect; // the target server where to redirect 3xx73final HttpHandler delegate; // unused7475private HTTPTestServer(HttpServer server, HTTPTestServer target,76HttpHandler delegate) {77this.serverImpl = server;78this.redirect = target;79this.delegate = delegate;80}8182public static void main(String[] args)83throws IOException {8485HTTPTestServer server = create(HTTPTest.DEFAULT_PROTOCOL_TYPE,86HTTPTest.DEFAULT_HTTP_AUTH_TYPE,87HTTPTest.AUTHENTICATOR,88HTTPTest.DEFAULT_SCHEME_TYPE);89try {90System.out.println("Server created at " + server.getAddress());91System.out.println("Strike <Return> to exit");92System.in.read();93} finally {94System.out.println("stopping server");95server.stop();96}97}9899private static String toString(Headers headers) {100return headers.entrySet().stream()101.map((e) -> e.getKey() + ": " + e.getValue())102.collect(Collectors.joining("\n"));103}104105public static HTTPTestServer create(HttpProtocolType protocol,106HttpAuthType authType,107HttpTestAuthenticator auth,108HttpSchemeType schemeType)109throws IOException {110return create(protocol, authType, auth, schemeType, null);111}112113public static HTTPTestServer create(HttpProtocolType protocol,114HttpAuthType authType,115HttpTestAuthenticator auth,116HttpSchemeType schemeType,117HttpHandler delegate)118throws IOException {119Objects.requireNonNull(authType);120Objects.requireNonNull(auth);121switch(authType) {122// A server that performs Server Digest authentication.123case SERVER: return createServer(protocol, authType, auth,124schemeType, delegate, "/");125// A server that pretends to be a Proxy and performs126// Proxy Digest authentication. If protocol is HTTPS,127// then this will create a HttpsProxyTunnel that will128// handle the CONNECT request for tunneling.129case PROXY: return createProxy(protocol, authType, auth,130schemeType, delegate, "/");131// A server that sends 307 redirect to a server that performs132// Digest authentication.133// Note: 301 doesn't work here because it transforms POST into GET.134case SERVER307: return createServerAndRedirect(protocol,135HttpAuthType.SERVER,136auth, schemeType,137delegate, 307);138// A server that sends 305 redirect to a proxy that performs139// Digest authentication.140case PROXY305: return createServerAndRedirect(protocol,141HttpAuthType.PROXY,142auth, schemeType,143delegate, 305);144default:145throw new InternalError("Unknown server type: " + authType);146}147}148149/**150* The SocketBindableFactory ensures that the local port used by an HttpServer151* or a proxy ServerSocket previously created by the current test/VM will not152* get reused by a subsequent test in the same VM. This is to avoid having the153* AuthCache reuse credentials from previous tests - which would invalidate the154* assumptions made by the current test on when the default authenticator should155* be called.156*/157private static abstract class SocketBindableFactory<B> {158private static final int MAX = 10;159private static final CopyOnWriteArrayList<String> addresses =160new CopyOnWriteArrayList<>();161protected B createInternal() throws IOException {162final int max = addresses.size() + MAX;163final List<B> toClose = new ArrayList<>();164try {165for (int i = 1; i <= max; i++) {166B bindable = createBindable();167SocketAddress address = getAddress(bindable);168String key = toString(address);169if (addresses.addIfAbsent(key)) {170System.out.println("Socket bound to: " + key171+ " after " + i + " attempt(s)");172return bindable;173}174System.out.println("warning: address " + key175+ " already used. Retrying bind.");176// keep the port bound until we get a port that we haven't177// used already178toClose.add(bindable);179}180} finally {181// if we had to retry, then close the socket we're not182// going to use.183for (B b : toClose) {184try { close(b); } catch (Exception x) { /* ignore */ }185}186}187throw new IOException("Couldn't bind socket after " + max + " attempts: "188+ "addresses used before: " + addresses);189}190191private static String toString(SocketAddress address) {192// We don't rely on address.toString(): sometimes it can be193// "/127.0.0.1:port", sometimes it can be "localhost/127.0.0.1:port"194// Instead we compose our own string representation:195InetSocketAddress candidate = (InetSocketAddress) address;196String hostAddr = candidate.getAddress().getHostAddress();197if (hostAddr.contains(":")) hostAddr = "[" + hostAddr + "]";198return hostAddr + ":" + candidate.getPort();199}200201protected abstract B createBindable() throws IOException;202203protected abstract SocketAddress getAddress(B bindable);204205protected abstract void close(B bindable) throws IOException;206}207208/*209* Used to create ServerSocket for a proxy.210*/211private static final class ServerSocketFactory212extends SocketBindableFactory<ServerSocket> {213private static final ServerSocketFactory instance = new ServerSocketFactory();214215static ServerSocket create() throws IOException {216return instance.createInternal();217}218219@Override220protected ServerSocket createBindable() throws IOException {221InetAddress address = InetAddress.getLoopbackAddress();222return new ServerSocket(0, 0, address);223}224225@Override226protected SocketAddress getAddress(ServerSocket socket) {227return socket.getLocalSocketAddress();228}229230@Override231protected void close(ServerSocket socket) throws IOException {232socket.close();233}234}235236/*237* Used to create HttpServer for a NTLMTestServer.238*/239private static abstract class WebServerFactory<S extends HttpServer>240extends SocketBindableFactory<S> {241@Override242protected S createBindable() throws IOException {243S server = newHttpServer();244InetAddress address = InetAddress.getLoopbackAddress();245server.bind(new InetSocketAddress(address, 0), 0);246return server;247}248249@Override250protected SocketAddress getAddress(S server) {251return server.getAddress();252}253254@Override255protected void close(S server) throws IOException {256server.stop(1);257}258259/*260* Returns a HttpServer or a HttpsServer in different subclasses.261*/262protected abstract S newHttpServer() throws IOException;263}264265private static final class HttpServerFactory extends WebServerFactory<HttpServer> {266private static final HttpServerFactory instance = new HttpServerFactory();267268static HttpServer create() throws IOException {269return instance.createInternal();270}271272@Override273protected HttpServer newHttpServer() throws IOException {274return HttpServer.create();275}276}277278private static final class HttpsServerFactory extends WebServerFactory<HttpsServer> {279private static final HttpsServerFactory instance = new HttpsServerFactory();280281static HttpsServer create() throws IOException {282return instance.createInternal();283}284285@Override286protected HttpsServer newHttpServer() throws IOException {287return HttpsServer.create();288}289}290291static HttpServer createHttpServer(HttpProtocolType protocol) throws IOException {292switch (protocol) {293case HTTP: return HttpServerFactory.create();294case HTTPS: return configure(HttpsServerFactory.create());295default: throw new InternalError("Unsupported protocol " + protocol);296}297}298299static HttpsServer configure(HttpsServer server) throws IOException {300try {301SSLContext ctx = SSLContext.getDefault();302server.setHttpsConfigurator(new Configurator(ctx));303} catch (NoSuchAlgorithmException ex) {304throw new IOException(ex);305}306return server;307}308309310static void setContextAuthenticator(HttpContext ctxt,311HttpTestAuthenticator auth) {312final String realm = auth.getRealm();313com.sun.net.httpserver.Authenticator authenticator =314new BasicAuthenticator(realm) {315@Override316public boolean checkCredentials(String username, String pwd) {317return auth.getUserName().equals(username)318&& new String(auth.getPassword(username)).equals(pwd);319}320};321ctxt.setAuthenticator(authenticator);322}323324public static HTTPTestServer createServer(HttpProtocolType protocol,325HttpAuthType authType,326HttpTestAuthenticator auth,327HttpSchemeType schemeType,328HttpHandler delegate,329String path)330throws IOException {331Objects.requireNonNull(authType);332Objects.requireNonNull(auth);333334HttpServer impl = createHttpServer(protocol);335final HTTPTestServer server = new HTTPTestServer(impl, null, delegate);336final HttpHandler hh = server.createHandler(schemeType, auth, authType);337HttpContext ctxt = impl.createContext(path, hh);338server.configureAuthentication(ctxt, schemeType, auth, authType);339impl.start();340return server;341}342343public static HTTPTestServer createProxy(HttpProtocolType protocol,344HttpAuthType authType,345HttpTestAuthenticator auth,346HttpSchemeType schemeType,347HttpHandler delegate,348String path)349throws IOException {350Objects.requireNonNull(authType);351Objects.requireNonNull(auth);352353HttpServer impl = createHttpServer(protocol);354final HTTPTestServer server = protocol == HttpProtocolType.HTTPS355? new HttpsProxyTunnel(impl, null, delegate)356: new HTTPTestServer(impl, null, delegate);357final HttpHandler hh = server.createHandler(schemeType, auth, authType);358HttpContext ctxt = impl.createContext(path, hh);359server.configureAuthentication(ctxt, schemeType, auth, authType);360impl.start();361362return server;363}364365public static HTTPTestServer createServerAndRedirect(366HttpProtocolType protocol,367HttpAuthType targetAuthType,368HttpTestAuthenticator auth,369HttpSchemeType schemeType,370HttpHandler targetDelegate,371int code300)372throws IOException {373Objects.requireNonNull(targetAuthType);374Objects.requireNonNull(auth);375376// The connection between client and proxy can only377// be a plain connection: SSL connection to proxy378// is not supported by our client connection.379HttpProtocolType targetProtocol = targetAuthType == HttpAuthType.PROXY380? HttpProtocolType.HTTP381: protocol;382HTTPTestServer redirectTarget =383(targetAuthType == HttpAuthType.PROXY)384? createProxy(protocol, targetAuthType,385auth, schemeType, targetDelegate, "/")386: createServer(targetProtocol, targetAuthType,387auth, schemeType, targetDelegate, "/");388HttpServer impl = createHttpServer(protocol);389final HTTPTestServer redirectingServer =390new HTTPTestServer(impl, redirectTarget, null);391InetSocketAddress redirectAddr = redirectTarget.getAddress();392URL locationURL = url(targetProtocol, redirectAddr, "/");393final HttpHandler hh = redirectingServer.create300Handler(locationURL,394HttpAuthType.SERVER, code300);395impl.createContext("/", hh);396impl.start();397return redirectingServer;398}399400public InetSocketAddress getAddress() {401return serverImpl.getAddress();402}403404public InetSocketAddress getProxyAddress() {405return serverImpl.getAddress();406}407408public void stop() {409serverImpl.stop(0);410if (redirect != null) {411redirect.stop();412}413}414415protected void writeResponse(HttpExchange he) throws IOException {416if (delegate == null) {417he.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);418he.getResponseBody().write(he.getRequestBody().readAllBytes());419} else {420delegate.handle(he);421}422}423424private HttpHandler createHandler(HttpSchemeType schemeType,425HttpTestAuthenticator auth,426HttpAuthType authType) {427return new HttpNoAuthHandler(authType);428}429430private void configureAuthentication(HttpContext ctxt,431HttpSchemeType schemeType,432HttpTestAuthenticator auth,433HttpAuthType authType) {434switch(schemeType) {435case DIGEST:436// DIGEST authentication is handled by the handler.437ctxt.getFilters().add(new HttpDigestFilter(auth, authType));438break;439case BASIC:440// BASIC authentication is handled by the filter.441ctxt.getFilters().add(new HttpBasicFilter(auth, authType));442break;443case BASICSERVER:444switch(authType) {445case PROXY: case PROXY305:446// HttpServer can't support Proxy-type authentication447// => we do as if BASIC had been specified, and we will448// handle authentication in the handler.449ctxt.getFilters().add(new HttpBasicFilter(auth, authType));450break;451case SERVER: case SERVER307:452// Basic authentication is handled by HttpServer453// directly => the filter should not perform454// authentication again.455setContextAuthenticator(ctxt, auth);456ctxt.getFilters().add(new HttpNoAuthFilter(authType));457break;458default:459throw new InternalError("Invalid combination scheme="460+ schemeType + " authType=" + authType);461}462case NONE:463// No authentication at all.464ctxt.getFilters().add(new HttpNoAuthFilter(authType));465break;466default:467throw new InternalError("No such scheme: " + schemeType);468}469}470471private HttpHandler create300Handler(URL proxyURL,472HttpAuthType type, int code300) throws MalformedURLException {473return new Http3xxHandler(proxyURL, type, code300);474}475476// Abstract HTTP filter class.477private abstract static class AbstractHttpFilter extends Filter {478479final HttpAuthType authType;480final String type;481public AbstractHttpFilter(HttpAuthType authType, String type) {482this.authType = authType;483this.type = type;484}485486String getLocation() {487return "Location";488}489String getAuthenticate() {490return authType == HttpAuthType.PROXY491? "Proxy-Authenticate" : "WWW-Authenticate";492}493String getAuthorization() {494return authType == HttpAuthType.PROXY495? "Proxy-Authorization" : "Authorization";496}497int getUnauthorizedCode() {498return authType == HttpAuthType.PROXY499? HttpURLConnection.HTTP_PROXY_AUTH500: HttpURLConnection.HTTP_UNAUTHORIZED;501}502String getKeepAlive() {503return "keep-alive";504}505String getConnection() {506return authType == HttpAuthType.PROXY507? "Proxy-Connection" : "Connection";508}509protected abstract boolean isAuthentified(HttpExchange he) throws IOException;510protected abstract void requestAuthentication(HttpExchange he) throws IOException;511protected void accept(HttpExchange he, Chain chain) throws IOException {512chain.doFilter(he);513}514515@Override516public String description() {517return "Filter for " + type;518}519@Override520public void doFilter(HttpExchange he, Chain chain) throws IOException {521try {522System.out.println(type + ": Got " + he.getRequestMethod()523+ ": " + he.getRequestURI()524+ "\n" + HTTPTestServer.toString(he.getRequestHeaders()));525if (!isAuthentified(he)) {526try {527requestAuthentication(he);528he.sendResponseHeaders(getUnauthorizedCode(), 0);529System.out.println(type530+ ": Sent back " + getUnauthorizedCode());531} finally {532he.close();533}534} else {535accept(he, chain);536}537} catch (RuntimeException | Error | IOException t) {538System.err.println(type539+ ": Unexpected exception while handling request: " + t);540t.printStackTrace(System.err);541he.close();542throw t;543}544}545546}547548private final static class DigestResponse {549final String realm;550final String username;551final String nonce;552final String cnonce;553final String nc;554final String uri;555final String algorithm;556final String response;557final String qop;558final String opaque;559560public DigestResponse(String realm, String username, String nonce,561String cnonce, String nc, String uri,562String algorithm, String qop, String opaque,563String response) {564this.realm = realm;565this.username = username;566this.nonce = nonce;567this.cnonce = cnonce;568this.nc = nc;569this.uri = uri;570this.algorithm = algorithm;571this.qop = qop;572this.opaque = opaque;573this.response = response;574}575576String getAlgorithm(String defval) {577return algorithm == null ? defval : algorithm;578}579String getQoP(String defval) {580return qop == null ? defval : qop;581}582583// Code stolen from DigestAuthentication:584585private static String encode(String src, char[] passwd, MessageDigest md) {586try {587md.update(src.getBytes("ISO-8859-1"));588} catch (java.io.UnsupportedEncodingException uee) {589assert false;590}591if (passwd != null) {592byte[] passwdBytes = new byte[passwd.length];593for (int i=0; i<passwd.length; i++)594passwdBytes[i] = (byte)passwd[i];595md.update(passwdBytes);596Arrays.fill(passwdBytes, (byte)0x00);597}598byte[] digest = md.digest();599return HexFormat.of().formatHex(digest);600}601602public static String computeDigest(boolean isRequest,603String reqMethod,604char[] password,605DigestResponse params)606throws NoSuchAlgorithmException607{608609String A1, HashA1;610String algorithm = params.getAlgorithm("MD5");611boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");612613MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);614615if (params.username == null) {616throw new IllegalArgumentException("missing username");617}618if (params.realm == null) {619throw new IllegalArgumentException("missing realm");620}621if (params.uri == null) {622throw new IllegalArgumentException("missing uri");623}624if (params.nonce == null) {625throw new IllegalArgumentException("missing nonce");626}627628A1 = params.username + ":" + params.realm + ":";629HashA1 = encode(A1, password, md);630631String A2;632if (isRequest) {633A2 = reqMethod + ":" + params.uri;634} else {635A2 = ":" + params.uri;636}637String HashA2 = encode(A2, null, md);638String combo, finalHash;639640if ("auth".equals(params.qop)) { /* RRC2617 when qop=auth */641if (params.cnonce == null) {642throw new IllegalArgumentException("missing nonce");643}644if (params.nc == null) {645throw new IllegalArgumentException("missing nonce");646}647combo = HashA1+ ":" + params.nonce + ":" + params.nc + ":" +648params.cnonce + ":auth:" +HashA2;649650} else { /* for compatibility with RFC2069 */651combo = HashA1 + ":" +652params.nonce + ":" +653HashA2;654}655finalHash = encode(combo, null, md);656return finalHash;657}658659public static DigestResponse create(String raw) {660String username, realm, nonce, nc, uri, response, cnonce,661algorithm, qop, opaque;662HeaderParser parser = new HeaderParser(raw);663username = parser.findValue("username");664realm = parser.findValue("realm");665nonce = parser.findValue("nonce");666nc = parser.findValue("nc");667uri = parser.findValue("uri");668cnonce = parser.findValue("cnonce");669response = parser.findValue("response");670algorithm = parser.findValue("algorithm");671qop = parser.findValue("qop");672opaque = parser.findValue("opaque");673return new DigestResponse(realm, username, nonce, cnonce, nc, uri,674algorithm, qop, opaque, response);675}676677}678679private class HttpNoAuthFilter extends AbstractHttpFilter {680681public HttpNoAuthFilter(HttpAuthType authType) {682super(authType, authType == HttpAuthType.SERVER683? "NoAuth Server" : "NoAuth Proxy");684}685686@Override687protected boolean isAuthentified(HttpExchange he) throws IOException {688return true;689}690691@Override692protected void requestAuthentication(HttpExchange he) throws IOException {693throw new InternalError("Should not com here");694}695696@Override697public String description() {698return "Passthrough Filter";699}700701}702703// An HTTP Filter that performs Basic authentication704private class HttpBasicFilter extends AbstractHttpFilter {705706private final HttpTestAuthenticator auth;707public HttpBasicFilter(HttpTestAuthenticator auth, HttpAuthType authType) {708super(authType, authType == HttpAuthType.SERVER709? "Basic Server" : "Basic Proxy");710this.auth = auth;711}712713@Override714protected void requestAuthentication(HttpExchange he)715throws IOException {716he.getResponseHeaders().add(getAuthenticate(),717"Basic realm=\"" + auth.getRealm() + "\"");718System.out.println(type + ": Requesting Basic Authentication "719+ he.getResponseHeaders().getFirst(getAuthenticate()));720}721722@Override723protected boolean isAuthentified(HttpExchange he) {724if (he.getRequestHeaders().containsKey(getAuthorization())) {725List<String> authorization =726he.getRequestHeaders().get(getAuthorization());727for (String a : authorization) {728System.out.println(type + ": processing " + a);729int sp = a.indexOf(' ');730if (sp < 0) return false;731String scheme = a.substring(0, sp);732if (!"Basic".equalsIgnoreCase(scheme)) {733System.out.println(type + ": Unsupported scheme '"734+ scheme +"'");735return false;736}737if (a.length() <= sp+1) {738System.out.println(type + ": value too short for '"739+ scheme +"'");740return false;741}742a = a.substring(sp+1);743return validate(a);744}745return false;746}747return false;748}749750boolean validate(String a) {751byte[] b = Base64.getDecoder().decode(a);752String userpass = new String (b);753int colon = userpass.indexOf (':');754String uname = userpass.substring (0, colon);755String pass = userpass.substring (colon+1);756return auth.getUserName().equals(uname) &&757new String(auth.getPassword(uname)).equals(pass);758}759760@Override761public String description() {762return "Filter for " + type;763}764765}766767768// An HTTP Filter that performs Digest authentication769private class HttpDigestFilter extends AbstractHttpFilter {770771// This is a very basic DIGEST - used only for the purpose of testing772// the client implementation. Therefore we can get away with never773// updating the server nonce as it makes the implementation of the774// server side digest simpler.775private final HttpTestAuthenticator auth;776private final byte[] nonce;777private final String ns;778public HttpDigestFilter(HttpTestAuthenticator auth, HttpAuthType authType) {779super(authType, authType == HttpAuthType.SERVER780? "Digest Server" : "Digest Proxy");781this.auth = auth;782nonce = new byte[16];783new Random(Instant.now().toEpochMilli()).nextBytes(nonce);784ns = new BigInteger(1, nonce).toString(16);785}786787@Override788protected void requestAuthentication(HttpExchange he)789throws IOException {790he.getResponseHeaders().add(getAuthenticate(),791"Digest realm=\"" + auth.getRealm() + "\","792+ "\r\n qop=\"auth\","793+ "\r\n nonce=\"" + ns +"\"");794System.out.println(type + ": Requesting Digest Authentication "795+ he.getResponseHeaders().getFirst(getAuthenticate()));796}797798@Override799protected boolean isAuthentified(HttpExchange he) {800if (he.getRequestHeaders().containsKey(getAuthorization())) {801List<String> authorization = he.getRequestHeaders().get(getAuthorization());802for (String a : authorization) {803System.out.println(type + ": processing " + a);804int sp = a.indexOf(' ');805if (sp < 0) return false;806String scheme = a.substring(0, sp);807if (!"Digest".equalsIgnoreCase(scheme)) {808System.out.println(type + ": Unsupported scheme '" + scheme +"'");809return false;810}811if (a.length() <= sp+1) {812System.out.println(type + ": value too short for '" + scheme +"'");813return false;814}815a = a.substring(sp+1);816DigestResponse dgr = DigestResponse.create(a);817return validate(he.getRequestMethod(), dgr);818}819return false;820}821return false;822}823824boolean validate(String reqMethod, DigestResponse dg) {825if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) {826System.out.println(type + ": Unsupported algorithm "827+ dg.algorithm);828return false;829}830if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) {831System.out.println(type + ": Unsupported qop "832+ dg.qop);833return false;834}835try {836if (!dg.nonce.equals(ns)) {837System.out.println(type + ": bad nonce returned by client: "838+ nonce + " expected " + ns);839return false;840}841if (dg.response == null) {842System.out.println(type + ": missing digest response.");843return false;844}845char[] pa = auth.getPassword(dg.username);846return verify(reqMethod, dg, pa);847} catch(IllegalArgumentException | SecurityException848| NoSuchAlgorithmException e) {849System.out.println(type + ": " + e.getMessage());850return false;851}852}853854boolean verify(String reqMethod, DigestResponse dg, char[] pw)855throws NoSuchAlgorithmException {856String response = DigestResponse.computeDigest(true, reqMethod, pw, dg);857if (!dg.response.equals(response)) {858System.out.println(type + ": bad response returned by client: "859+ dg.response + " expected " + response);860return false;861} else {862System.out.println(type + ": verified response " + response);863}864return true;865}866867@Override868public String description() {869return "Filter for DIGEST authentication";870}871}872873// Abstract HTTP handler class.874private abstract static class AbstractHttpHandler implements HttpHandler {875876final HttpAuthType authType;877final String type;878public AbstractHttpHandler(HttpAuthType authType, String type) {879this.authType = authType;880this.type = type;881}882883String getLocation() {884return "Location";885}886887@Override888public void handle(HttpExchange he) throws IOException {889try {890sendResponse(he);891} catch (RuntimeException | Error | IOException t) {892System.err.println(type893+ ": Unexpected exception while handling request: " + t);894t.printStackTrace(System.err);895throw t;896} finally {897he.close();898}899}900901protected abstract void sendResponse(HttpExchange he) throws IOException;902903}904905private class HttpNoAuthHandler extends AbstractHttpHandler {906907public HttpNoAuthHandler(HttpAuthType authType) {908super(authType, authType == HttpAuthType.SERVER909? "NoAuth Server" : "NoAuth Proxy");910}911912@Override913protected void sendResponse(HttpExchange he) throws IOException {914HTTPTestServer.this.writeResponse(he);915}916917}918919// A dummy HTTP Handler that redirects all incoming requests920// by sending a back 3xx response code (301, 305, 307 etc..)921private class Http3xxHandler extends AbstractHttpHandler {922923private final URL redirectTargetURL;924private final int code3XX;925public Http3xxHandler(URL proxyURL, HttpAuthType authType, int code300) {926super(authType, "Server" + code300);927this.redirectTargetURL = proxyURL;928this.code3XX = code300;929}930931int get3XX() {932return code3XX;933}934935@Override936public void sendResponse(HttpExchange he) throws IOException {937System.out.println(type + ": Got " + he.getRequestMethod()938+ ": " + he.getRequestURI()939+ "\n" + HTTPTestServer.toString(he.getRequestHeaders()));940System.out.println(type + ": Redirecting to "941+ (authType == HttpAuthType.PROXY305942? "proxy" : "server"));943he.getResponseHeaders().add(getLocation(),944redirectTargetURL.toExternalForm().toString());945he.sendResponseHeaders(get3XX(), 0);946System.out.println(type + ": Sent back " + get3XX() + " "947+ getLocation() + ": " + redirectTargetURL.toExternalForm().toString());948}949}950951static class Configurator extends HttpsConfigurator {952public Configurator(SSLContext ctx) {953super(ctx);954}955956@Override957public void configure (HttpsParameters params) {958params.setSSLParameters (getSSLContext().getSupportedSSLParameters());959}960}961962// This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden963// behind a fake proxy that only understands CONNECT requests.964// The fake proxy is just a server socket that intercept the965// CONNECT and then redirect streams to the real server.966static class HttpsProxyTunnel extends HTTPTestServer967implements Runnable {968969final ServerSocket ss;970private volatile boolean stop;971972public HttpsProxyTunnel(HttpServer server, HTTPTestServer target,973HttpHandler delegate)974throws IOException {975super(server, target, delegate);976System.out.flush();977System.err.println("WARNING: HttpsProxyTunnel is an experimental test class");978ss = ServerSocketFactory.create();979start();980}981982final void start() throws IOException {983Thread t = new Thread(this, "ProxyThread");984t.setDaemon(true);985t.start();986}987988@Override989public void stop() {990try (var toClose = ss) {991stop = true;992System.out.println("Server " + ss + " stop requested");993super.stop();994} catch (IOException ex) {995if (DEBUG) ex.printStackTrace(System.out);996}997}998999// Pipe the input stream to the output stream.1000private synchronized Thread pipe(InputStream is, OutputStream os, char tag) {1001return new Thread("TunnelPipe("+tag+")") {1002@Override1003public void run() {1004try {1005try {1006int c;1007while ((c = is.read()) != -1) {1008os.write(c);1009os.flush();1010// if DEBUG prints a + or a - for each transferred1011// character.1012if (DEBUG) System.out.print(tag);1013}1014is.close();1015} finally {1016os.close();1017}1018} catch (IOException ex) {1019if (DEBUG) ex.printStackTrace(System.out);1020}1021}1022};1023}10241025@Override1026public InetSocketAddress getProxyAddress() {1027return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort());1028}10291030// This is a bit shaky. It doesn't handle continuation1031// lines, but our client shouldn't send any.1032// Read a line from the input stream, swallowing the final1033// \r\n sequence. Stops at the first \n, doesn't complain1034// if it wasn't preceded by '\r'.1035//1036String readLine(InputStream r) throws IOException {1037StringBuilder b = new StringBuilder();1038int c;1039while ((c = r.read()) != -1) {1040if (c == '\n') break;1041b.appendCodePoint(c);1042}1043if (b.length() == 0) {1044return "";1045}1046if (b.codePointAt(b.length() -1) == '\r') {1047b.delete(b.length() -1, b.length());1048}1049return b.toString();1050}10511052@Override1053public void run() {1054Socket clientConnection = null;1055while (!stop) {1056System.out.println("Tunnel: Waiting for client at: " + ss);1057final Socket previous = clientConnection;1058try {1059clientConnection = ss.accept();1060} catch (IOException io) {1061try {1062ss.close();1063} catch (IOException ex) {1064if (DEBUG) {1065ex.printStackTrace(System.out);1066}1067}1068// log the reason that caused the server to stop accepting connections1069if (!stop) {1070System.err.println("Server will stop accepting connections due to an exception:");1071io.printStackTrace();1072}1073break;1074} finally {1075// close the previous connection1076if (previous != null) {1077try {1078previous.close();1079} catch (IOException e) {1080// ignore1081if (DEBUG) {1082System.out.println("Ignoring exception that happened while closing " +1083"an older connection:");1084e.printStackTrace(System.out);1085}1086}1087}1088}1089System.out.println("Tunnel: Client accepted");1090try {1091// We have only 1 client... process the current client1092// request and wait until it has finished before1093// accepting a new connection request.1094processRequestAndWaitToComplete(clientConnection);1095} catch (IOException ioe) {1096// close the client connection1097try {1098clientConnection.close();1099} catch (IOException io) {1100// ignore1101if (DEBUG) {1102System.out.println("Ignoring exception that happened during client" +1103" connection close:");1104io.printStackTrace(System.out);1105}1106} finally {1107clientConnection = null;1108}1109} catch (Throwable t) {1110// don't close the client connection for non-IOExceptions, instead1111// just log it and move on to accept next connection1112if (!stop) {1113t.printStackTrace();1114}1115}1116}1117}11181119private void processRequestAndWaitToComplete(final Socket clientConnection)1120throws IOException, InterruptedException {1121final Socket targetConnection;1122InputStream ccis = clientConnection.getInputStream();1123OutputStream ccos = clientConnection.getOutputStream();1124Writer w = new OutputStreamWriter(1125clientConnection.getOutputStream(), "UTF-8");1126PrintWriter pw = new PrintWriter(w);1127System.out.println("Tunnel: Reading request line");1128String requestLine = readLine(ccis);1129System.out.println("Tunnel: Request line: " + requestLine);1130if (requestLine.startsWith("CONNECT ")) {1131// We should probably check that the next word following1132// CONNECT is the host:port of our HTTPS serverImpl.1133// Some improvement for a followup!11341135// Read all headers until we find the empty line that1136// signals the end of all headers.1137while(!requestLine.equals("")) {1138System.out.println("Tunnel: Reading header: "1139+ (requestLine = readLine(ccis)));1140}11411142targetConnection = new Socket(1143serverImpl.getAddress().getAddress(),1144serverImpl.getAddress().getPort());11451146// Then send the 200 OK response to the client1147System.out.println("Tunnel: Sending "1148+ "HTTP/1.1 200 OK\r\n\r\n");1149pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");1150pw.flush();1151} else {1152// This should not happen. If it does then consider it a1153// client error and throw an IOException1154System.out.println("Tunnel: Throwing an IOException due to unexpected" +1155" request line: " + requestLine);1156throw new IOException("Client request error - Unexpected request line");1157}11581159// Pipe the input stream of the client connection to the1160// output stream of the target connection and conversely.1161// Now the client and target will just talk to each other.1162System.out.println("Tunnel: Starting tunnel pipes");1163Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+');1164Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-');1165t1.start();1166t2.start();1167// wait for the request to complete1168t1.join();1169t2.join();1170}1171}1172}117311741175