/* * Copyright (c) 2018, 2021, 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.
*/
/** * A simple HTTP server that supports Basic or Digest authentication. * By default this server will echo back whatever is present * in the request body. Note that the Digest authentication is * a test implementation implemented only for tests purposes. * @author danielfuchs
*/ publicabstractclass DigestEchoServer implements HttpServerAdapters {
publicstaticclass HttpTestAuthenticator extends Authenticator { privatefinal String realm; privatefinal String username; // Used to prevent incrementation of 'count' when calling the // authenticator from the server side. privatefinal ThreadLocal<Boolean> skipCount = new ThreadLocal<>(); // count will be incremented every time getPasswordAuthentication() // is called from the client side. final AtomicInteger count = new AtomicInteger();
public HttpTestAuthenticator(String realm, String username) { this.realm = realm; this.username = username;
}
@Override protected PasswordAuthentication getPasswordAuthentication() { if (skipCount.get() == null || skipCount.get().booleanValue() == false) {
System.out.println("Authenticator called: " + count.incrementAndGet());
} returnnew PasswordAuthentication(getUserName(), newchar[] {'d','e','n', 't'});
} // Called by the server side to get the password of the user // being authentified. publicfinalchar[] getPassword(String user) { if (user.equals(username)) {
skipCount.set(Boolean.TRUE); try { return getPasswordAuthentication().getPassword();
} finally {
skipCount.set(Boolean.FALSE);
}
} thrownew SecurityException("User unknown: " + user);
} publicfinal String getUserName() { return username;
} publicfinal String getRealm() { return realm;
}
}
final HttpTestServer serverImpl; // this server endpoint final DigestEchoServer redirect; // the target server where to redirect 3xx final HttpTestHandler delegate; // unused final String key;
publicstatic DigestEchoServer create(Version version,
String protocol,
HttpAuthType authType,
HttpTestAuthenticator auth,
HttpAuthSchemeType schemeType,
HttpTestHandler delegate) throws IOException {
Objects.requireNonNull(authType);
Objects.requireNonNull(auth); switch(authType) { // A server that performs Server Digest authentication. case SERVER: return createServer(version, protocol, authType, auth,
schemeType, delegate, "/"); // 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(version, 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(version,
protocol,
HttpAuthType.SERVER,
auth, schemeType,
delegate, 307); // A server that sends 305 redirect to a proxy that performs // Digest authentication. // Note: this is not correctly stubbed/implemented in this test. case PROXY305: return createServerAndRedirect(version,
protocol,
HttpAuthType.PROXY,
auth, schemeType,
delegate, 305); default: thrownew 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 test client trying to reuse cached connections.
*/ privatestaticabstractclass SocketBindableFactory<B> { privatestaticfinalint MAX = 10; privatestaticfinal CopyOnWriteArrayList<String> addresses = new CopyOnWriteArrayList<>(); protected B createInternal() throws IOException { finalint max = addresses.size() + MAX; final List<B> toClose = new ArrayList<>(); try { for (int i = 1; i <= max; i++) {
B bindable = createBindable();
InetSocketAddress address = getAddress(bindable);
String key = "localhost:" + address.getPort(); 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 */ }
}
} thrownew IOException("Couldn't bind socket after " + max + " attempts: "
+ "addresses used before: " + addresses);
}
protectedabstract B createBindable() throws IOException;
/* * Used to create ServerSocket for a proxy.
*/ privatestaticfinalclass ServerSocketFactory extends SocketBindableFactory<ServerSocket> { privatestaticfinal ServerSocketFactory instance = new ServerSocketFactory();
void configureAuthentication(HttpTestContext ctxt,
HttpAuthSchemeType schemeType,
HttpTestAuthenticator auth,
HttpAuthType authType) { switch(schemeType) { case DIGEST: // DIGEST authentication is handled by the handler.
ctxt.addFilter(new HttpDigestFilter(key, auth, authType)); break; case BASIC: // BASIC authentication is handled by the filter.
ctxt.addFilter(new HttpBasicFilter(key, 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.addFilter(new HttpBasicFilter(key, auth, authType)); break; case SERVER: case SERVER307: if (ctxt.getVersion() == Version.HTTP_1_1) { // Basic authentication is handled by HttpServer // directly => the filter should not perform // authentication again.
setContextAuthenticator(ctxt, auth);
ctxt.addFilter(new HttpNoAuthFilter(key, authType));
} else {
ctxt.addFilter(new HttpBasicFilter(key, auth, authType));
} break; default: thrownew InternalError(key + ": Invalid combination scheme="
+ schemeType + " authType=" + authType);
} case NONE: // No authentication at all.
ctxt.addFilter(new HttpNoAuthFilter(key, authType)); break; default: thrownew InternalError(key + ": No such scheme: " + schemeType);
}
}
// Assert only a single value for Expect. Not directly related // to digest authentication, but verifies good client behaviour.
List<String> expectValues = he.getRequestHeaders().get("Expect"); if (expectValues != null && expectValues.size() > 1) { thrownew IOException("Expect: " + expectValues);
}
// WARNING: This is not a full fledged implementation of DIGEST. // It does contain bugs and inaccuracy. finalstaticclass 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;
@Override protectedboolean isAuthentified(HttpTestExchange 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) returnfalse;
String scheme = a.substring(0, sp); if (!"Basic".equalsIgnoreCase(scheme)) {
System.out.println(type + ": Unsupported scheme '"
+ scheme +"'"); returnfalse;
} if (a.length() <= sp+1) {
System.out.println(type + ": value too short for '"
+ scheme +"'"); returnfalse;
}
a = a.substring(sp+1); return validate(a);
} returnfalse;
} returnfalse;
}
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 BASIC authentication: " + type;
}
}
// An HTTP Filter that performs Digest authentication // WARNING: This is not a full fledged implementation of DIGEST. // It does contain bugs and inaccuracy. privatestaticclass 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. privatefinal HttpTestAuthenticator auth; privatefinalbyte[] nonce; privatefinal String ns; public HttpDigestFilter(String key, HttpTestAuthenticator auth, HttpAuthType authType) { super(authType, type(key, authType)); this.auth = auth;
nonce = newbyte[16]; new Random(Instant.now().toEpochMilli()).nextBytes(nonce);
ns = new BigInteger(1, nonce).toString(16);
}
boolean verify(URI uri, String reqMethod, DigestResponse dg, char[] pw) throws NoSuchAlgorithmException {
String response = DigestResponse.computeDigest(true, reqMethod, pw, dg); if (!dg.response.equals(response)) {
System.out.println(type + ": bad response returned by client: "
+ dg.response + " expected " + response); returnfalse;
} else { // A real server would also verify the uri=<request-uri> // parameter - but this is just a test...
System.out.println(type + ": verified response " + response);
} returntrue;
}
@Override public String description() { return"Filter for DIGEST authentication: " + type;
}
}
// true if this server is behind a proxy tunnel. finalboolean tunnelled; public HttpNoAuthHandler(String key, HttpAuthType authType, boolean tunnelled) { super(authType, stype("NoAuth", key, authType, tunnelled)); this.tunnelled = tunnelled;
}
@Override protectedvoid sendResponse(HttpTestExchange he) throws IOException { if (DEBUG) {
System.out.println(type + ": headers are: "
+ DigestEchoServer.toString(he.getRequestHeaders()));
} if (authType == HttpAuthType.SERVER && tunnelled) { // Verify that the client doesn't send us proxy-* headers // used to establish the proxy tunnel
Optional<String> proxyAuth = he.getRequestHeaders()
.keySet().stream()
.filter("proxy-authorization"::equalsIgnoreCase)
.findAny(); if (proxyAuth.isPresent()) {
System.out.println(type + " found "
+ proxyAuth.get() + ": failing!"); thrownew IOException(proxyAuth.get()
+ " found by " + type + " for "
+ he.getRequestURI());
}
}
DigestEchoServer.this.writeResponse(he);
}
}
// A dummy HTTP Handler that redirects all incoming requests // by sending a back 3xx response code (301, 305, 307 etc..) privateclass Http3xxHandler extends AbstractHttpHandler {
boolean verify(String type, String reqMethod, DigestResponse dg, char[] pw) throws NoSuchAlgorithmException {
String response = DigestResponse.computeDigest(true, reqMethod, pw, dg); if (!dg.response.equals(response)) {
System.out.println(type + ": bad response returned by client: "
+ dg.response + " expected " + response); returnfalse;
} else { // A real server would also verify the uri=<request-uri> // parameter - but this is just a test...
System.out.println(type + ": verified response " + response);
} returntrue;
}
// 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. staticclass HttpsProxyTunnel extends DigestEchoServer implements Runnable, TunnelingProxy {
final ServerSocket ss; final CopyOnWriteArrayList<CompletableFuture<Void>> connectionCFs
= new CopyOnWriteArrayList<>(); volatile ProxyAuthorization authorization; volatileboolean stopped; public HttpsProxyTunnel(String key, HttpTestServer server, DigestEchoServer target,
HttpTestHandler delegate) throws IOException { this(key, server, target, delegate, ServerSocketFactory.create());
} private HttpsProxyTunnel(String key, HttpTestServer server, DigestEchoServer target,
HttpTestHandler delegate, ServerSocket ss) throws IOException { super("HttpsProxyTunnel:" + ss.getLocalPort() + ":" + key,
server, target, delegate);
System.out.flush();
System.err.println("WARNING: HttpsProxyTunnel is an experimental test class"); this.ss = ss;
start();
}
@Override public Version getServerVersion() { // serverImpl is not null when this proxy // serves a single server. It will be null // if this proxy can serve multiple servers. if (serverImpl != null) return serverImpl.getVersion(); returnnull;
}
// Pipe the input stream to the output stream. privatesynchronizedThread pipe(InputStream is, OutputStream os, char tag, CompletableFuture<Void> end) { returnnewThread("TunnelPipe("+tag+")") {
@Override publicvoid run() { try { int c = 0; try { while ((c = is.read()) != -1) {
os.write(c);
os.flush(); // if DEBUG prints a + or a - for each transferred // character. if (DEBUG) System.out.print(tag);
}
is.close();
} catch (IOException ex) { if (DEBUG || !stopped && c > -1)
ex.printStackTrace(System.out);
end.completeExceptionally(ex);
} finally { try {os.close();} catch (Throwable t) {}
}
} finally {
end.complete(null);
}
}
};
}
@Override public InetSocketAddress getAddress() { returnnew InetSocketAddress(InetAddress.getLoopbackAddress(),
ss.getLocalPort());
}
@Override public InetSocketAddress getProxyAddress() { return getAddress();
}
@Override public InetSocketAddress getServerAddress() { // serverImpl can be null if this proxy can serve // multiple servers. if (serverImpl != null) { return serverImpl.getAddress();
} returnnull;
}
// 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.codePointAt(b.length() -1) == '\r') {
b.delete(b.length() -1, b.length());
} return b.toString();
}
@Override publicvoid run() {
Socket clientConnection = null;
Socket targetConnection = null; try { while (!stopped) {
System.out.println(now() + "Tunnel: Waiting for client");
Socket toClose;
targetConnection = clientConnection = null; try {
toClose = clientConnection = ss.accept(); if (NO_LINGER) { // can be useful to trigger "Connection reset by peer" // errors on the client side.
clientConnection.setOption(StandardSocketOptions.SO_LINGER, 0);
}
} catch (IOException io) { if (DEBUG || !stopped) io.printStackTrace(System.out); break;
}
System.out.println(now() + "Tunnel: Client accepted");
StringBuilder headers = new StringBuilder();
InputStream ccis = clientConnection.getInputStream();
OutputStream ccos = clientConnection.getOutputStream();
Writer w = new OutputStreamWriter(
clientConnection.getOutputStream(), "UTF-8");
PrintWriter pw = new PrintWriter(w);
System.out.println(now() + "Tunnel: Reading request line");
String requestLine = readLine(ccis);
System.out.println(now() + "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!
StringTokenizer tokenizer = new StringTokenizer(requestLine);
String connect = tokenizer.nextToken(); assert connect.equalsIgnoreCase("connect");
String hostport = tokenizer.nextToken();
InetSocketAddress targetAddress;
List<String> hosts = new ArrayList<>(); try {
URI uri = new URI("https", hostport, "/", null, null); int port = uri.getPort();
port = port == -1 ? 443 : port;
targetAddress = new InetSocketAddress(uri.getHost(), port); if (serverImpl != null) { assert targetAddress.getHostString()
.equalsIgnoreCase(serverImpl.getAddress().getHostString()); assert targetAddress.getPort() == serverImpl.getAddress().getPort();
}
} catch (Throwable x) {
System.err.printf("Bad target address: \"%s\" in \"%s\"%n",
hostport, requestLine);
toClose.close(); continue;
}
// Read all headers until we find the empty line that // signals the end of all headers.
String line = requestLine; while(!line.equals("")) {
System.out.println(now() + "Tunnel: Reading header: "
+ (line = readLine(ccis)));
headers.append(line).append("\r\n"); int index = line.indexOf(':'); if (index >= 0) {
String key = line.substring(0, index).trim(); if (key.equalsIgnoreCase("host")) {
hosts.add(line.substring(index+1).trim());
}
}
}
StringBuilder response = new StringBuilder(); if (TUNNEL_REQUIRES_HOST) { if (badRequest(response, hostport, hosts)) {
System.out.println(now() + "Tunnel: Sending " + response); // send the 400 response
pw.print(response.toString());
pw.flush();
toClose.close(); continue;
} else { assert hosts.size() == 1;
System.out.println(now()
+ "Tunnel: Host header verified " + hosts);
}
}
finalboolean authorize = authorize(response, requestLine, headers.toString()); if (!authorize) {
System.out.println(now() + "Tunnel: Sending "
+ response); // send the 407 response
pw.print(response.toString());
pw.flush();
toClose.close(); continue;
}
System.out.println(now()
+ "Tunnel connecting to target server at "
+ targetAddress.getAddress() + ":" + targetAddress.getPort());
targetConnection = new Socket(
targetAddress.getAddress(),
targetAddress.getPort());
// Then send the 200 OK response to the client
System.out.println(now() + "Tunnel: Sending "
+ response);
pw.print(response);
pw.flush();
} else { // This should not happen. If it does then just print an // error - both on out and err, and close the accepted // socket
System.out.println("WARNING: Tunnel: Unexpected status line: "
+ requestLine + " received by "
+ ss.getLocalSocketAddress()
+ " from "
+ toClose.getRemoteSocketAddress()
+ " - closing accepted socket"); // Print on err
System.err.println("WARNING: Tunnel: Unexpected status line: "
+ requestLine + " received by "
+ ss.getLocalSocketAddress()
+ " from "
+ toClose.getRemoteSocketAddress()); // close accepted socket.
toClose.close();
System.err.println("Tunnel: accepted socket closed."); continue;
}
// 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(now() + "Tunnel: Starting tunnel pipes");
CompletableFuture<Void> end, end1, end2; Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+',
end1 = new CompletableFuture<>()); Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-',
end2 = new CompletableFuture<>()); var end11 = end1.whenComplete((r, t) -> exceptionally(end2, t)); var end22 = end2.whenComplete((r, t) -> exceptionally(end1, t));
end = CompletableFuture.allOf(end11, end22);
Socket tc = targetConnection;
end.whenComplete(
(r,t) -> { try { toClose.close(); } catch (IOException x) { } try { tc.close(); } catch (IOException x) { } finally {connectionCFs.remove(end);}
});
connectionCFs.add(end);
targetConnection = clientConnection = null;
t1.start();
t2.start();
}
} catch (Throwable ex) {
close(clientConnection, ex);
close(targetConnection, ex);
close(ss, ex);
ex.printStackTrace(System.err);
} finally {
System.out.println(now() + "Tunnel: exiting (stopped=" + stopped + ")");
connectionCFs.forEach(cf -> cf.complete(null));
}
}
/** * Creates a TunnelingProxy that can serve multiple servers. * The server address is extracted from the CONNECT request line. * @param authScheme The authentication scheme supported by the proxy. * Typically one of DIGEST, BASIC, NONE. * @return A new TunnelingProxy able to serve multiple servers. * @throws IOException If the proxy could not be created.
*/ publicstatic TunnelingProxy createHttpsProxyTunnel(HttpAuthSchemeType authScheme) throws IOException {
HttpsProxyTunnel result = new HttpsProxyTunnel("", null, null, null); if (authScheme != HttpAuthSchemeType.NONE) {
result.configureAuthentication(null,
authScheme,
AUTHENTICATOR,
HttpAuthType.PROXY);
} return result;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.