fadcd65018
Reviewed-by: dfuchs, jpai
1194 lines
48 KiB
Java
1194 lines
48 KiB
Java
/*
|
|
* Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
import com.sun.net.httpserver.BasicAuthenticator;
|
|
import com.sun.net.httpserver.Filter;
|
|
import com.sun.net.httpserver.Headers;
|
|
import com.sun.net.httpserver.HttpContext;
|
|
import com.sun.net.httpserver.HttpExchange;
|
|
import com.sun.net.httpserver.HttpHandler;
|
|
import com.sun.net.httpserver.HttpServer;
|
|
import com.sun.net.httpserver.HttpsConfigurator;
|
|
import com.sun.net.httpserver.HttpsParameters;
|
|
import com.sun.net.httpserver.HttpsServer;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.io.OutputStreamWriter;
|
|
import java.io.PrintWriter;
|
|
import java.io.Writer;
|
|
import java.math.BigInteger;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.InetAddress;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.MalformedURLException;
|
|
import java.net.ServerSocket;
|
|
import java.net.Socket;
|
|
import java.net.SocketAddress;
|
|
import java.net.URL;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.time.Instant;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Base64;
|
|
import java.util.HexFormat;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.Random;
|
|
import java.util.concurrent.CopyOnWriteArrayList;
|
|
import java.util.stream.Collectors;
|
|
import javax.net.ssl.SSLContext;
|
|
import sun.net.www.HeaderParser;
|
|
|
|
/**
|
|
* A simple HTTP server that supports Digest authentication.
|
|
* By default this server will echo back whatever is present
|
|
* in the request body.
|
|
* @author danielfuchs
|
|
*/
|
|
public class HTTPTestServer extends HTTPTest {
|
|
|
|
final HttpServer serverImpl; // this server endpoint
|
|
final HTTPTestServer redirect; // the target server where to redirect 3xx
|
|
final HttpHandler delegate; // unused
|
|
|
|
private HTTPTestServer(HttpServer server, HTTPTestServer target,
|
|
HttpHandler delegate) {
|
|
this.serverImpl = server;
|
|
this.redirect = target;
|
|
this.delegate = delegate;
|
|
}
|
|
|
|
public static void main(String[] args)
|
|
throws IOException {
|
|
|
|
HTTPTestServer server = create(HTTPTest.DEFAULT_PROTOCOL_TYPE,
|
|
HTTPTest.DEFAULT_HTTP_AUTH_TYPE,
|
|
HTTPTest.AUTHENTICATOR,
|
|
HTTPTest.DEFAULT_SCHEME_TYPE);
|
|
try {
|
|
System.out.println("Server created at " + server.getAddress());
|
|
System.out.println("Strike <Return> to exit");
|
|
System.in.read();
|
|
} finally {
|
|
System.out.println("stopping server");
|
|
server.stop();
|
|
}
|
|
}
|
|
|
|
private static String toString(Headers headers) {
|
|
return headers.entrySet().stream()
|
|
.map((e) -> e.getKey() + ": " + e.getValue())
|
|
.collect(Collectors.joining("\n"));
|
|
}
|
|
|
|
public static HTTPTestServer create(HttpProtocolType protocol,
|
|
HttpAuthType authType,
|
|
HttpTestAuthenticator auth,
|
|
HttpSchemeType schemeType)
|
|
throws IOException {
|
|
return create(protocol, authType, auth, schemeType, null);
|
|
}
|
|
|
|
public static HTTPTestServer create(HttpProtocolType protocol,
|
|
HttpAuthType authType,
|
|
HttpTestAuthenticator auth,
|
|
HttpSchemeType schemeType,
|
|
HttpHandler delegate)
|
|
throws IOException {
|
|
return create(protocol, authType, auth, schemeType, null, "MD5");
|
|
}
|
|
|
|
public static HTTPTestServer create(HttpProtocolType protocol,
|
|
HttpAuthType authType,
|
|
HttpTestAuthenticator auth,
|
|
HttpSchemeType schemeType,
|
|
HttpHandler delegate,
|
|
String algorithm)
|
|
throws IOException {
|
|
Objects.requireNonNull(authType);
|
|
Objects.requireNonNull(auth);
|
|
switch(authType) {
|
|
// A server that performs Server Digest authentication.
|
|
case SERVER: return createServer(protocol, authType, auth,
|
|
schemeType, delegate, algorithm, "/");
|
|
// A server that pretends to be a Proxy and performs
|
|
// Proxy Digest authentication. If protocol is HTTPS,
|
|
// then this will create a HttpsProxyTunnel that will
|
|
// handle the CONNECT request for tunneling.
|
|
case PROXY: return createProxy(protocol, authType, auth,
|
|
schemeType, delegate, "/");
|
|
// A server that sends 307 redirect to a server that performs
|
|
// Digest authentication.
|
|
// Note: 301 doesn't work here because it transforms POST into GET.
|
|
case SERVER307: return createServerAndRedirect(protocol,
|
|
HttpAuthType.SERVER,
|
|
auth, schemeType,
|
|
delegate, 307);
|
|
// A server that sends 305 redirect to a proxy that performs
|
|
// Digest authentication.
|
|
case PROXY305: return createServerAndRedirect(protocol,
|
|
HttpAuthType.PROXY,
|
|
auth, schemeType,
|
|
delegate, 305);
|
|
default:
|
|
throw new InternalError("Unknown server type: " + authType);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The SocketBindableFactory ensures that the local port used by an HttpServer
|
|
* or a proxy ServerSocket previously created by the current test/VM will not
|
|
* get reused by a subsequent test in the same VM. This is to avoid having the
|
|
* AuthCache reuse credentials from previous tests - which would invalidate the
|
|
* assumptions made by the current test on when the default authenticator should
|
|
* be called.
|
|
*/
|
|
private static abstract class SocketBindableFactory<B> {
|
|
private static final int MAX = 10;
|
|
private static final CopyOnWriteArrayList<String> addresses =
|
|
new CopyOnWriteArrayList<>();
|
|
protected B createInternal() throws IOException {
|
|
final int max = addresses.size() + MAX;
|
|
final List<B> toClose = new ArrayList<>();
|
|
try {
|
|
for (int i = 1; i <= max; i++) {
|
|
B bindable = createBindable();
|
|
SocketAddress address = getAddress(bindable);
|
|
String key = toString(address);
|
|
if (addresses.addIfAbsent(key)) {
|
|
System.out.println("Socket bound to: " + key
|
|
+ " after " + i + " attempt(s)");
|
|
return bindable;
|
|
}
|
|
System.out.println("warning: address " + key
|
|
+ " already used. Retrying bind.");
|
|
// keep the port bound until we get a port that we haven't
|
|
// used already
|
|
toClose.add(bindable);
|
|
}
|
|
} finally {
|
|
// if we had to retry, then close the socket we're not
|
|
// going to use.
|
|
for (B b : toClose) {
|
|
try { close(b); } catch (Exception x) { /* ignore */ }
|
|
}
|
|
}
|
|
throw new IOException("Couldn't bind socket after " + max + " attempts: "
|
|
+ "addresses used before: " + addresses);
|
|
}
|
|
|
|
private static String toString(SocketAddress address) {
|
|
// We don't rely on address.toString(): sometimes it can be
|
|
// "/127.0.0.1:port", sometimes it can be "localhost/127.0.0.1:port"
|
|
// Instead we compose our own string representation:
|
|
InetSocketAddress candidate = (InetSocketAddress) address;
|
|
String hostAddr = candidate.getAddress().getHostAddress();
|
|
if (hostAddr.contains(":")) hostAddr = "[" + hostAddr + "]";
|
|
return hostAddr + ":" + candidate.getPort();
|
|
}
|
|
|
|
protected abstract B createBindable() throws IOException;
|
|
|
|
protected abstract SocketAddress getAddress(B bindable);
|
|
|
|
protected abstract void close(B bindable) throws IOException;
|
|
}
|
|
|
|
/*
|
|
* Used to create ServerSocket for a proxy.
|
|
*/
|
|
private static final class ServerSocketFactory
|
|
extends SocketBindableFactory<ServerSocket> {
|
|
private static final ServerSocketFactory instance = new ServerSocketFactory();
|
|
|
|
static ServerSocket create() throws IOException {
|
|
return instance.createInternal();
|
|
}
|
|
|
|
@Override
|
|
protected ServerSocket createBindable() throws IOException {
|
|
InetAddress address = InetAddress.getLoopbackAddress();
|
|
return new ServerSocket(0, 0, address);
|
|
}
|
|
|
|
@Override
|
|
protected SocketAddress getAddress(ServerSocket socket) {
|
|
return socket.getLocalSocketAddress();
|
|
}
|
|
|
|
@Override
|
|
protected void close(ServerSocket socket) throws IOException {
|
|
socket.close();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Used to create HttpServer for a NTLMTestServer.
|
|
*/
|
|
private static abstract class WebServerFactory<S extends HttpServer>
|
|
extends SocketBindableFactory<S> {
|
|
@Override
|
|
protected S createBindable() throws IOException {
|
|
S server = newHttpServer();
|
|
InetAddress address = InetAddress.getLoopbackAddress();
|
|
server.bind(new InetSocketAddress(address, 0), 0);
|
|
return server;
|
|
}
|
|
|
|
@Override
|
|
protected SocketAddress getAddress(S server) {
|
|
return server.getAddress();
|
|
}
|
|
|
|
@Override
|
|
protected void close(S server) throws IOException {
|
|
server.stop(1);
|
|
}
|
|
|
|
/*
|
|
* Returns a HttpServer or a HttpsServer in different subclasses.
|
|
*/
|
|
protected abstract S newHttpServer() throws IOException;
|
|
}
|
|
|
|
private static final class HttpServerFactory extends WebServerFactory<HttpServer> {
|
|
private static final HttpServerFactory instance = new HttpServerFactory();
|
|
|
|
static HttpServer create() throws IOException {
|
|
return instance.createInternal();
|
|
}
|
|
|
|
@Override
|
|
protected HttpServer newHttpServer() throws IOException {
|
|
return HttpServer.create();
|
|
}
|
|
}
|
|
|
|
private static final class HttpsServerFactory extends WebServerFactory<HttpsServer> {
|
|
private static final HttpsServerFactory instance = new HttpsServerFactory();
|
|
|
|
static HttpsServer create() throws IOException {
|
|
return instance.createInternal();
|
|
}
|
|
|
|
@Override
|
|
protected HttpsServer newHttpServer() throws IOException {
|
|
return HttpsServer.create();
|
|
}
|
|
}
|
|
|
|
static HttpServer createHttpServer(HttpProtocolType protocol) throws IOException {
|
|
switch (protocol) {
|
|
case HTTP: return HttpServerFactory.create();
|
|
case HTTPS: return configure(HttpsServerFactory.create());
|
|
default: throw new InternalError("Unsupported protocol " + protocol);
|
|
}
|
|
}
|
|
|
|
static HttpsServer configure(HttpsServer server) throws IOException {
|
|
try {
|
|
SSLContext ctx = SSLContext.getDefault();
|
|
server.setHttpsConfigurator(new Configurator(ctx));
|
|
} catch (NoSuchAlgorithmException ex) {
|
|
throw new IOException(ex);
|
|
}
|
|
return server;
|
|
}
|
|
|
|
|
|
static void setContextAuthenticator(HttpContext ctxt,
|
|
HttpTestAuthenticator auth) {
|
|
final String realm = auth.getRealm();
|
|
com.sun.net.httpserver.Authenticator authenticator =
|
|
new BasicAuthenticator(realm) {
|
|
@Override
|
|
public boolean checkCredentials(String username, String pwd) {
|
|
return auth.getUserName().equals(username)
|
|
&& new String(auth.getPassword(username)).equals(pwd);
|
|
}
|
|
};
|
|
ctxt.setAuthenticator(authenticator);
|
|
}
|
|
|
|
public static HTTPTestServer createServer(HttpProtocolType protocol,
|
|
HttpAuthType authType,
|
|
HttpTestAuthenticator auth,
|
|
HttpSchemeType schemeType,
|
|
HttpHandler delegate,
|
|
String algorithm,
|
|
String path)
|
|
throws IOException {
|
|
Objects.requireNonNull(authType);
|
|
Objects.requireNonNull(auth);
|
|
|
|
HttpServer impl = createHttpServer(protocol);
|
|
final HTTPTestServer server = new HTTPTestServer(impl, null, delegate);
|
|
final HttpHandler hh = server.createHandler(schemeType, auth, authType);
|
|
HttpContext ctxt = impl.createContext(path, hh);
|
|
server.configureAuthentication(ctxt, schemeType, auth, authType, algorithm);
|
|
impl.start();
|
|
return server;
|
|
}
|
|
|
|
public static HTTPTestServer createProxy(HttpProtocolType protocol,
|
|
HttpAuthType authType,
|
|
HttpTestAuthenticator auth,
|
|
HttpSchemeType schemeType,
|
|
HttpHandler delegate,
|
|
String path)
|
|
throws IOException {
|
|
Objects.requireNonNull(authType);
|
|
Objects.requireNonNull(auth);
|
|
|
|
HttpServer impl = createHttpServer(protocol);
|
|
final HTTPTestServer server = protocol == HttpProtocolType.HTTPS
|
|
? new HttpsProxyTunnel(impl, null, delegate)
|
|
: new HTTPTestServer(impl, null, delegate);
|
|
final HttpHandler hh = server.createHandler(schemeType, auth, authType);
|
|
HttpContext ctxt = impl.createContext(path, hh);
|
|
server.configureAuthentication(ctxt, schemeType, auth, authType, null);
|
|
impl.start();
|
|
|
|
return server;
|
|
}
|
|
|
|
public static HTTPTestServer createServerAndRedirect(
|
|
HttpProtocolType protocol,
|
|
HttpAuthType targetAuthType,
|
|
HttpTestAuthenticator auth,
|
|
HttpSchemeType schemeType,
|
|
HttpHandler targetDelegate,
|
|
int code300)
|
|
throws IOException {
|
|
Objects.requireNonNull(targetAuthType);
|
|
Objects.requireNonNull(auth);
|
|
|
|
// The connection between client and proxy can only
|
|
// be a plain connection: SSL connection to proxy
|
|
// is not supported by our client connection.
|
|
HttpProtocolType targetProtocol = targetAuthType == HttpAuthType.PROXY
|
|
? HttpProtocolType.HTTP
|
|
: protocol;
|
|
HTTPTestServer redirectTarget =
|
|
(targetAuthType == HttpAuthType.PROXY)
|
|
? createProxy(protocol, targetAuthType,
|
|
auth, schemeType, targetDelegate, "/")
|
|
: createServer(targetProtocol, targetAuthType,
|
|
auth, schemeType, targetDelegate, "MD5", "/");
|
|
HttpServer impl = createHttpServer(protocol);
|
|
final HTTPTestServer redirectingServer =
|
|
new HTTPTestServer(impl, redirectTarget, null);
|
|
InetSocketAddress redirectAddr = redirectTarget.getAddress();
|
|
URL locationURL = url(targetProtocol, redirectAddr, "/");
|
|
final HttpHandler hh = redirectingServer.create300Handler(locationURL,
|
|
HttpAuthType.SERVER, code300);
|
|
impl.createContext("/", hh);
|
|
impl.start();
|
|
return redirectingServer;
|
|
}
|
|
|
|
public InetSocketAddress getAddress() {
|
|
return serverImpl.getAddress();
|
|
}
|
|
|
|
public InetSocketAddress getProxyAddress() {
|
|
return serverImpl.getAddress();
|
|
}
|
|
|
|
public void stop() {
|
|
serverImpl.stop(0);
|
|
if (redirect != null) {
|
|
redirect.stop();
|
|
}
|
|
}
|
|
|
|
protected void writeResponse(HttpExchange he) throws IOException {
|
|
if (delegate == null) {
|
|
he.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
|
|
he.getResponseBody().write(he.getRequestBody().readAllBytes());
|
|
} else {
|
|
delegate.handle(he);
|
|
}
|
|
}
|
|
|
|
private HttpHandler createHandler(HttpSchemeType schemeType,
|
|
HttpTestAuthenticator auth,
|
|
HttpAuthType authType) {
|
|
return new HttpNoAuthHandler(authType);
|
|
}
|
|
|
|
private void configureAuthentication(HttpContext ctxt,
|
|
HttpSchemeType schemeType,
|
|
HttpTestAuthenticator auth,
|
|
HttpAuthType authType, String algorithm) {
|
|
switch(schemeType) {
|
|
case DIGEST:
|
|
// DIGEST authentication is handled by the handler.
|
|
ctxt.getFilters().add(new HttpDigestFilter(auth, authType, algorithm));
|
|
break;
|
|
case BASIC:
|
|
// BASIC authentication is handled by the filter.
|
|
ctxt.getFilters().add(new HttpBasicFilter(auth, authType));
|
|
break;
|
|
case BASICSERVER:
|
|
switch(authType) {
|
|
case PROXY: case PROXY305:
|
|
// HttpServer can't support Proxy-type authentication
|
|
// => we do as if BASIC had been specified, and we will
|
|
// handle authentication in the handler.
|
|
ctxt.getFilters().add(new HttpBasicFilter(auth, authType));
|
|
break;
|
|
case SERVER: case SERVER307:
|
|
// Basic authentication is handled by HttpServer
|
|
// directly => the filter should not perform
|
|
// authentication again.
|
|
setContextAuthenticator(ctxt, auth);
|
|
ctxt.getFilters().add(new HttpNoAuthFilter(authType));
|
|
break;
|
|
default:
|
|
throw new InternalError("Invalid combination scheme="
|
|
+ schemeType + " authType=" + authType);
|
|
}
|
|
case NONE:
|
|
// No authentication at all.
|
|
ctxt.getFilters().add(new HttpNoAuthFilter(authType));
|
|
break;
|
|
default:
|
|
throw new InternalError("No such scheme: " + schemeType);
|
|
}
|
|
}
|
|
|
|
private HttpHandler create300Handler(URL proxyURL,
|
|
HttpAuthType type, int code300) throws MalformedURLException {
|
|
return new Http3xxHandler(proxyURL, type, code300);
|
|
}
|
|
|
|
// Abstract HTTP filter class.
|
|
private abstract static class AbstractHttpFilter extends Filter {
|
|
|
|
final HttpAuthType authType;
|
|
final String type;
|
|
public AbstractHttpFilter(HttpAuthType authType, String type) {
|
|
this.authType = authType;
|
|
this.type = type;
|
|
}
|
|
|
|
String getLocation() {
|
|
return "Location";
|
|
}
|
|
String getAuthenticate() {
|
|
return authType == HttpAuthType.PROXY
|
|
? "Proxy-Authenticate" : "WWW-Authenticate";
|
|
}
|
|
String getAuthorization() {
|
|
return authType == HttpAuthType.PROXY
|
|
? "Proxy-Authorization" : "Authorization";
|
|
}
|
|
int getUnauthorizedCode() {
|
|
return authType == HttpAuthType.PROXY
|
|
? HttpURLConnection.HTTP_PROXY_AUTH
|
|
: HttpURLConnection.HTTP_UNAUTHORIZED;
|
|
}
|
|
String getKeepAlive() {
|
|
return "keep-alive";
|
|
}
|
|
String getConnection() {
|
|
return authType == HttpAuthType.PROXY
|
|
? "Proxy-Connection" : "Connection";
|
|
}
|
|
protected abstract boolean isAuthentified(HttpExchange he) throws IOException;
|
|
protected abstract void requestAuthentication(HttpExchange he) throws IOException;
|
|
protected void accept(HttpExchange he, Chain chain) throws IOException {
|
|
chain.doFilter(he);
|
|
}
|
|
|
|
@Override
|
|
public String description() {
|
|
return "Filter for " + type;
|
|
}
|
|
@Override
|
|
public void doFilter(HttpExchange he, Chain chain) throws IOException {
|
|
try {
|
|
System.out.println(type + ": Got " + he.getRequestMethod()
|
|
+ ": " + he.getRequestURI()
|
|
+ "\n" + HTTPTestServer.toString(he.getRequestHeaders()));
|
|
if (!isAuthentified(he)) {
|
|
try {
|
|
requestAuthentication(he);
|
|
he.sendResponseHeaders(getUnauthorizedCode(), 0);
|
|
System.out.println(type
|
|
+ ": Sent back " + getUnauthorizedCode());
|
|
} finally {
|
|
he.close();
|
|
}
|
|
} else {
|
|
accept(he, chain);
|
|
}
|
|
} catch (RuntimeException | Error | IOException t) {
|
|
System.err.println(type
|
|
+ ": Unexpected exception while handling request: " + t);
|
|
t.printStackTrace(System.err);
|
|
he.close();
|
|
throw t;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private final static class DigestResponse {
|
|
final String realm;
|
|
final String username;
|
|
final String nonce;
|
|
final String cnonce;
|
|
final String nc;
|
|
final String uri;
|
|
final String algorithm;
|
|
final String response;
|
|
final String qop;
|
|
final String opaque;
|
|
|
|
public DigestResponse(String realm, String username, String nonce,
|
|
String cnonce, String nc, String uri,
|
|
String algorithm, String qop, String opaque,
|
|
String response) {
|
|
this.realm = realm;
|
|
this.username = username;
|
|
this.nonce = nonce;
|
|
this.cnonce = cnonce;
|
|
this.nc = nc;
|
|
this.uri = uri;
|
|
this.algorithm = algorithm;
|
|
this.qop = qop;
|
|
this.opaque = opaque;
|
|
this.response = response;
|
|
}
|
|
|
|
String getAlgorithm(String defval) {
|
|
return algorithm == null ? defval : algorithm;
|
|
}
|
|
String getQoP(String defval) {
|
|
return qop == null ? defval : qop;
|
|
}
|
|
|
|
// Code stolen from DigestAuthentication:
|
|
|
|
private static String encode(String src, char[] passwd, MessageDigest md) {
|
|
try {
|
|
md.update(src.getBytes("ISO-8859-1"));
|
|
} catch (java.io.UnsupportedEncodingException uee) {
|
|
assert false;
|
|
}
|
|
if (passwd != null) {
|
|
byte[] passwdBytes = new byte[passwd.length];
|
|
for (int i=0; i<passwd.length; i++)
|
|
passwdBytes[i] = (byte)passwd[i];
|
|
md.update(passwdBytes);
|
|
Arrays.fill(passwdBytes, (byte)0x00);
|
|
}
|
|
byte[] digest = md.digest();
|
|
return HexFormat.of().formatHex(digest);
|
|
}
|
|
|
|
public static String computeDigest(boolean isRequest,
|
|
String reqMethod,
|
|
char[] password,
|
|
String expectedAlgorithm,
|
|
DigestResponse params)
|
|
throws NoSuchAlgorithmException
|
|
{
|
|
|
|
String A1, HashA1;
|
|
String algorithm = params.getAlgorithm("MD5");
|
|
if (algorithm.endsWith("-sess")) {
|
|
algorithm = algorithm.substring(0, algorithm.length() - 5);
|
|
}
|
|
if (!algorithm.equalsIgnoreCase(expectedAlgorithm)) {
|
|
throw new IllegalArgumentException("unexpected algorithm");
|
|
}
|
|
|
|
MessageDigest md = MessageDigest.getInstance(algorithm);
|
|
|
|
if (params.username == null) {
|
|
throw new IllegalArgumentException("missing username");
|
|
}
|
|
if (params.realm == null) {
|
|
throw new IllegalArgumentException("missing realm");
|
|
}
|
|
if (params.uri == null) {
|
|
throw new IllegalArgumentException("missing uri");
|
|
}
|
|
if (params.nonce == null) {
|
|
throw new IllegalArgumentException("missing nonce");
|
|
}
|
|
|
|
A1 = params.username + ":" + params.realm + ":";
|
|
HashA1 = encode(A1, password, md);
|
|
|
|
String A2;
|
|
if (isRequest) {
|
|
A2 = reqMethod + ":" + params.uri;
|
|
} else {
|
|
A2 = ":" + params.uri;
|
|
}
|
|
String HashA2 = encode(A2, null, md);
|
|
String combo, finalHash;
|
|
|
|
if ("auth".equals(params.qop)) { /* RRC2617 when qop=auth */
|
|
if (params.cnonce == null) {
|
|
throw new IllegalArgumentException("missing nonce");
|
|
}
|
|
if (params.nc == null) {
|
|
throw new IllegalArgumentException("missing nonce");
|
|
}
|
|
combo = HashA1+ ":" + params.nonce + ":" + params.nc + ":" +
|
|
params.cnonce + ":auth:" +HashA2;
|
|
|
|
} else { /* for compatibility with RFC2069 */
|
|
combo = HashA1 + ":" +
|
|
params.nonce + ":" +
|
|
HashA2;
|
|
}
|
|
finalHash = encode(combo, null, md);
|
|
return finalHash;
|
|
}
|
|
|
|
public static DigestResponse create(String raw) {
|
|
String username, realm, nonce, nc, uri, response, cnonce,
|
|
algorithm, qop, opaque;
|
|
HeaderParser parser = new HeaderParser(raw);
|
|
username = parser.findValue("username");
|
|
realm = parser.findValue("realm");
|
|
nonce = parser.findValue("nonce");
|
|
nc = parser.findValue("nc");
|
|
uri = parser.findValue("uri");
|
|
cnonce = parser.findValue("cnonce");
|
|
response = parser.findValue("response");
|
|
algorithm = parser.findValue("algorithm");
|
|
qop = parser.findValue("qop");
|
|
opaque = parser.findValue("opaque");
|
|
return new DigestResponse(realm, username, nonce, cnonce, nc, uri,
|
|
algorithm, qop, opaque, response);
|
|
}
|
|
|
|
}
|
|
|
|
private class HttpNoAuthFilter extends AbstractHttpFilter {
|
|
|
|
public HttpNoAuthFilter(HttpAuthType authType) {
|
|
super(authType, authType == HttpAuthType.SERVER
|
|
? "NoAuth Server" : "NoAuth Proxy");
|
|
}
|
|
|
|
@Override
|
|
protected boolean isAuthentified(HttpExchange he) throws IOException {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
protected void requestAuthentication(HttpExchange he) throws IOException {
|
|
throw new InternalError("Should not com here");
|
|
}
|
|
|
|
@Override
|
|
public String description() {
|
|
return "Passthrough Filter";
|
|
}
|
|
|
|
}
|
|
|
|
// An HTTP Filter that performs Basic authentication
|
|
private class HttpBasicFilter extends AbstractHttpFilter {
|
|
|
|
private final HttpTestAuthenticator auth;
|
|
public HttpBasicFilter(HttpTestAuthenticator auth, HttpAuthType authType) {
|
|
super(authType, authType == HttpAuthType.SERVER
|
|
? "Basic Server" : "Basic Proxy");
|
|
this.auth = auth;
|
|
}
|
|
|
|
@Override
|
|
protected void requestAuthentication(HttpExchange he)
|
|
throws IOException {
|
|
he.getResponseHeaders().add(getAuthenticate(),
|
|
"Basic realm=\"" + auth.getRealm() + "\"");
|
|
System.out.println(type + ": Requesting Basic Authentication "
|
|
+ he.getResponseHeaders().getFirst(getAuthenticate()));
|
|
}
|
|
|
|
@Override
|
|
protected boolean isAuthentified(HttpExchange he) {
|
|
if (he.getRequestHeaders().containsKey(getAuthorization())) {
|
|
List<String> authorization =
|
|
he.getRequestHeaders().get(getAuthorization());
|
|
for (String a : authorization) {
|
|
System.out.println(type + ": processing " + a);
|
|
int sp = a.indexOf(' ');
|
|
if (sp < 0) return false;
|
|
String scheme = a.substring(0, sp);
|
|
if (!"Basic".equalsIgnoreCase(scheme)) {
|
|
System.out.println(type + ": Unsupported scheme '"
|
|
+ scheme +"'");
|
|
return false;
|
|
}
|
|
if (a.length() <= sp+1) {
|
|
System.out.println(type + ": value too short for '"
|
|
+ scheme +"'");
|
|
return false;
|
|
}
|
|
a = a.substring(sp+1);
|
|
return validate(a);
|
|
}
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
boolean validate(String a) {
|
|
byte[] b = Base64.getDecoder().decode(a);
|
|
String userpass = new String (b);
|
|
int colon = userpass.indexOf (':');
|
|
String uname = userpass.substring (0, colon);
|
|
String pass = userpass.substring (colon+1);
|
|
return auth.getUserName().equals(uname) &&
|
|
new String(auth.getPassword(uname)).equals(pass);
|
|
}
|
|
|
|
@Override
|
|
public String description() {
|
|
return "Filter for " + type;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// An HTTP Filter that performs Digest authentication
|
|
private class HttpDigestFilter extends AbstractHttpFilter {
|
|
|
|
// This is a very basic DIGEST - used only for the purpose of testing
|
|
// the client implementation. Therefore we can get away with never
|
|
// updating the server nonce as it makes the implementation of the
|
|
// server side digest simpler.
|
|
private final HttpTestAuthenticator auth;
|
|
private final byte[] nonce;
|
|
private final String ns;
|
|
private final String algorithm;
|
|
public HttpDigestFilter(HttpTestAuthenticator auth, HttpAuthType authType, String algorithm) {
|
|
super(authType, authType == HttpAuthType.SERVER
|
|
? "Digest Server" : "Digest Proxy");
|
|
this.auth = auth;
|
|
nonce = new byte[16];
|
|
new Random(Instant.now().toEpochMilli()).nextBytes(nonce);
|
|
ns = new BigInteger(1, nonce).toString(16);
|
|
this.algorithm = (algorithm == null) ? "MD5" : algorithm;
|
|
}
|
|
|
|
@Override
|
|
protected void requestAuthentication(HttpExchange he)
|
|
throws IOException {
|
|
he.getResponseHeaders().add(getAuthenticate(),
|
|
"Digest realm=\"" + auth.getRealm() + "\","
|
|
+ "\r\n qop=\"auth\", " + "algorithm=\"" + algorithm + "\", "
|
|
+ "\r\n nonce=\"" + ns +"\"");
|
|
System.out.println(type + ": Requesting Digest Authentication "
|
|
+ he.getResponseHeaders().getFirst(getAuthenticate()));
|
|
}
|
|
|
|
@Override
|
|
protected boolean isAuthentified(HttpExchange he) {
|
|
if (he.getRequestHeaders().containsKey(getAuthorization())) {
|
|
List<String> authorization = he.getRequestHeaders().get(getAuthorization());
|
|
for (String a : authorization) {
|
|
System.out.println(type + ": processing " + a);
|
|
int sp = a.indexOf(' ');
|
|
if (sp < 0) return false;
|
|
String scheme = a.substring(0, sp);
|
|
if (!"Digest".equalsIgnoreCase(scheme)) {
|
|
System.out.println(type + ": Unsupported scheme '" + scheme +"'");
|
|
return false;
|
|
}
|
|
if (a.length() <= sp+1) {
|
|
System.out.println(type + ": value too short for '" + scheme +"'");
|
|
return false;
|
|
}
|
|
a = a.substring(sp+1);
|
|
DigestResponse dgr = DigestResponse.create(a);
|
|
return validate(he.getRequestMethod(), dgr);
|
|
}
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
boolean validate(String reqMethod, DigestResponse dg) {
|
|
if (!this.algorithm.equalsIgnoreCase(dg.getAlgorithm("MD5"))) {
|
|
System.out.println(type + ": Unsupported algorithm "
|
|
+ dg.algorithm);
|
|
return false;
|
|
}
|
|
if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) {
|
|
System.out.println(type + ": Unsupported qop "
|
|
+ dg.qop);
|
|
return false;
|
|
}
|
|
try {
|
|
if (!dg.nonce.equals(ns)) {
|
|
System.out.println(type + ": bad nonce returned by client: "
|
|
+ nonce + " expected " + ns);
|
|
return false;
|
|
}
|
|
if (dg.response == null) {
|
|
System.out.println(type + ": missing digest response.");
|
|
return false;
|
|
}
|
|
char[] pa = auth.getPassword(dg.username);
|
|
return verify(reqMethod, dg, pa);
|
|
} catch(IllegalArgumentException | SecurityException
|
|
| NoSuchAlgorithmException e) {
|
|
System.out.println(type + ": " + e.getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
boolean verify(String reqMethod, DigestResponse dg, char[] pw)
|
|
throws NoSuchAlgorithmException {
|
|
String response = DigestResponse.computeDigest(true, reqMethod, pw, algorithm, dg);
|
|
if (!dg.response.equals(response)) {
|
|
System.out.println(type + ": bad response returned by client: "
|
|
+ dg.response + " expected " + response);
|
|
return false;
|
|
} else {
|
|
System.out.println(type + ": verified response " + response);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public String description() {
|
|
return "Filter for DIGEST authentication";
|
|
}
|
|
}
|
|
|
|
// Abstract HTTP handler class.
|
|
private abstract static class AbstractHttpHandler implements HttpHandler {
|
|
|
|
final HttpAuthType authType;
|
|
final String type;
|
|
public AbstractHttpHandler(HttpAuthType authType, String type) {
|
|
this.authType = authType;
|
|
this.type = type;
|
|
}
|
|
|
|
String getLocation() {
|
|
return "Location";
|
|
}
|
|
|
|
@Override
|
|
public void handle(HttpExchange he) throws IOException {
|
|
try {
|
|
sendResponse(he);
|
|
} catch (RuntimeException | Error | IOException t) {
|
|
System.err.println(type
|
|
+ ": Unexpected exception while handling request: " + t);
|
|
t.printStackTrace(System.err);
|
|
throw t;
|
|
} finally {
|
|
he.close();
|
|
}
|
|
}
|
|
|
|
protected abstract void sendResponse(HttpExchange he) throws IOException;
|
|
|
|
}
|
|
|
|
private class HttpNoAuthHandler extends AbstractHttpHandler {
|
|
|
|
public HttpNoAuthHandler(HttpAuthType authType) {
|
|
super(authType, authType == HttpAuthType.SERVER
|
|
? "NoAuth Server" : "NoAuth Proxy");
|
|
}
|
|
|
|
@Override
|
|
protected void sendResponse(HttpExchange he) throws IOException {
|
|
HTTPTestServer.this.writeResponse(he);
|
|
}
|
|
|
|
}
|
|
|
|
// A dummy HTTP Handler that redirects all incoming requests
|
|
// by sending a back 3xx response code (301, 305, 307 etc..)
|
|
private class Http3xxHandler extends AbstractHttpHandler {
|
|
|
|
private final URL redirectTargetURL;
|
|
private final int code3XX;
|
|
public Http3xxHandler(URL proxyURL, HttpAuthType authType, int code300) {
|
|
super(authType, "Server" + code300);
|
|
this.redirectTargetURL = proxyURL;
|
|
this.code3XX = code300;
|
|
}
|
|
|
|
int get3XX() {
|
|
return code3XX;
|
|
}
|
|
|
|
@Override
|
|
public void sendResponse(HttpExchange he) throws IOException {
|
|
System.out.println(type + ": Got " + he.getRequestMethod()
|
|
+ ": " + he.getRequestURI()
|
|
+ "\n" + HTTPTestServer.toString(he.getRequestHeaders()));
|
|
System.out.println(type + ": Redirecting to "
|
|
+ (authType == HttpAuthType.PROXY305
|
|
? "proxy" : "server"));
|
|
he.getResponseHeaders().add(getLocation(),
|
|
redirectTargetURL.toExternalForm().toString());
|
|
he.sendResponseHeaders(get3XX(), 0);
|
|
System.out.println(type + ": Sent back " + get3XX() + " "
|
|
+ getLocation() + ": " + redirectTargetURL.toExternalForm().toString());
|
|
}
|
|
}
|
|
|
|
static class Configurator extends HttpsConfigurator {
|
|
public Configurator(SSLContext ctx) {
|
|
super(ctx);
|
|
}
|
|
|
|
@Override
|
|
public void configure (HttpsParameters params) {
|
|
params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
|
|
}
|
|
}
|
|
|
|
// This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden
|
|
// behind a fake proxy that only understands CONNECT requests.
|
|
// The fake proxy is just a server socket that intercept the
|
|
// CONNECT and then redirect streams to the real server.
|
|
static class HttpsProxyTunnel extends HTTPTestServer
|
|
implements Runnable {
|
|
|
|
final ServerSocket ss;
|
|
private volatile boolean stop;
|
|
|
|
public HttpsProxyTunnel(HttpServer server, HTTPTestServer target,
|
|
HttpHandler delegate)
|
|
throws IOException {
|
|
super(server, target, delegate);
|
|
System.out.flush();
|
|
System.err.println("WARNING: HttpsProxyTunnel is an experimental test class");
|
|
ss = ServerSocketFactory.create();
|
|
start();
|
|
}
|
|
|
|
final void start() throws IOException {
|
|
Thread t = new Thread(this, "ProxyThread");
|
|
t.setDaemon(true);
|
|
t.start();
|
|
}
|
|
|
|
@Override
|
|
public void stop() {
|
|
try (var toClose = ss) {
|
|
stop = true;
|
|
System.out.println("Server " + ss + " stop requested");
|
|
super.stop();
|
|
} catch (IOException ex) {
|
|
if (DEBUG) ex.printStackTrace(System.out);
|
|
}
|
|
}
|
|
|
|
// Pipe the input stream to the output stream.
|
|
private synchronized Thread pipe(InputStream is, OutputStream os, char tag) {
|
|
return new Thread("TunnelPipe("+tag+")") {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
try {
|
|
int len;
|
|
byte[] buf = new byte[16 * 1024];
|
|
while ((len = is.read(buf)) != -1) {
|
|
os.write(buf, 0, len);
|
|
os.flush();
|
|
// if DEBUG prints a + or a - for each transferred
|
|
// character.
|
|
if (DEBUG) System.out.print(String.valueOf(tag).repeat(len));
|
|
}
|
|
is.close();
|
|
} finally {
|
|
os.close();
|
|
}
|
|
} catch (IOException ex) {
|
|
if (DEBUG) ex.printStackTrace(System.out);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
@Override
|
|
public InetSocketAddress getProxyAddress() {
|
|
return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort());
|
|
}
|
|
|
|
// This is a bit shaky. It doesn't handle continuation
|
|
// lines, but our client shouldn't send any.
|
|
// Read a line from the input stream, swallowing the final
|
|
// \r\n sequence. Stops at the first \n, doesn't complain
|
|
// if it wasn't preceded by '\r'.
|
|
//
|
|
String readLine(InputStream r) throws IOException {
|
|
StringBuilder b = new StringBuilder();
|
|
int c;
|
|
while ((c = r.read()) != -1) {
|
|
if (c == '\n') break;
|
|
b.appendCodePoint(c);
|
|
}
|
|
if (b.length() == 0) {
|
|
return "";
|
|
}
|
|
if (b.codePointAt(b.length() -1) == '\r') {
|
|
b.delete(b.length() -1, b.length());
|
|
}
|
|
return b.toString();
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
Socket clientConnection = null;
|
|
while (!stop) {
|
|
System.out.println("Tunnel: Waiting for client at: " + ss);
|
|
final Socket previous = clientConnection;
|
|
try {
|
|
clientConnection = ss.accept();
|
|
} catch (IOException io) {
|
|
try {
|
|
ss.close();
|
|
} catch (IOException ex) {
|
|
if (DEBUG) {
|
|
ex.printStackTrace(System.out);
|
|
}
|
|
}
|
|
// log the reason that caused the server to stop accepting connections
|
|
if (!stop) {
|
|
System.err.println("Server will stop accepting connections due to an exception:");
|
|
io.printStackTrace();
|
|
}
|
|
break;
|
|
} finally {
|
|
// close the previous connection
|
|
if (previous != null) {
|
|
try {
|
|
previous.close();
|
|
} catch (IOException e) {
|
|
// ignore
|
|
if (DEBUG) {
|
|
System.out.println("Ignoring exception that happened while closing " +
|
|
"an older connection:");
|
|
e.printStackTrace(System.out);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
System.out.println("Tunnel: Client accepted");
|
|
try {
|
|
// We have only 1 client... process the current client
|
|
// request and wait until it has finished before
|
|
// accepting a new connection request.
|
|
processRequestAndWaitToComplete(clientConnection);
|
|
} catch (IOException ioe) {
|
|
// close the client connection
|
|
try {
|
|
clientConnection.close();
|
|
} catch (IOException io) {
|
|
// ignore
|
|
if (DEBUG) {
|
|
System.out.println("Ignoring exception that happened during client" +
|
|
" connection close:");
|
|
io.printStackTrace(System.out);
|
|
}
|
|
} finally {
|
|
clientConnection = null;
|
|
}
|
|
} catch (Throwable t) {
|
|
// don't close the client connection for non-IOExceptions, instead
|
|
// just log it and move on to accept next connection
|
|
if (!stop) {
|
|
t.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void processRequestAndWaitToComplete(final Socket clientConnection)
|
|
throws IOException, InterruptedException {
|
|
final Socket targetConnection;
|
|
InputStream ccis = clientConnection.getInputStream();
|
|
OutputStream ccos = clientConnection.getOutputStream();
|
|
Writer w = new OutputStreamWriter(
|
|
clientConnection.getOutputStream(), "UTF-8");
|
|
PrintWriter pw = new PrintWriter(w);
|
|
System.out.println("Tunnel: Reading request line");
|
|
String requestLine = readLine(ccis);
|
|
System.out.println("Tunnel: Request line: " + requestLine);
|
|
if (requestLine.startsWith("CONNECT ")) {
|
|
// We should probably check that the next word following
|
|
// CONNECT is the host:port of our HTTPS serverImpl.
|
|
// Some improvement for a followup!
|
|
|
|
// Read all headers until we find the empty line that
|
|
// signals the end of all headers.
|
|
while(!requestLine.equals("")) {
|
|
System.out.println("Tunnel: Reading header: "
|
|
+ (requestLine = readLine(ccis)));
|
|
}
|
|
|
|
targetConnection = new Socket(
|
|
serverImpl.getAddress().getAddress(),
|
|
serverImpl.getAddress().getPort());
|
|
|
|
// Then send the 200 OK response to the client
|
|
System.out.println("Tunnel: Sending "
|
|
+ "HTTP/1.1 200 OK\r\n\r\n");
|
|
pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
|
|
pw.flush();
|
|
} else {
|
|
// This should not happen. If it does then consider it a
|
|
// client error and throw an IOException
|
|
System.out.println("Tunnel: Throwing an IOException due to unexpected" +
|
|
" request line: " + requestLine);
|
|
throw new IOException("Client request error - Unexpected request line");
|
|
}
|
|
|
|
// Pipe the input stream of the client connection to the
|
|
// output stream of the target connection and conversely.
|
|
// Now the client and target will just talk to each other.
|
|
System.out.println("Tunnel: Starting tunnel pipes");
|
|
Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+');
|
|
Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-');
|
|
t1.start();
|
|
t2.start();
|
|
// wait for the request to complete
|
|
t1.join();
|
|
t2.join();
|
|
}
|
|
}
|
|
}
|