/* * 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'. //
--> --------------------
--> maximum size reached
--> --------------------
¤ Dauer der Verarbeitung: 0.35 Sekunden
(vorverarbeitet)
¤
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 ist noch experimentell.