public class TestUtils { public static final String USER = "******"; public static final String ADMIN = "admin"; public static final String TEXT_HTML_CONTENT_TYPE_WITH_UTF_8_CHARSET = "text/html; charset=UTF-8"; public static final String TEXT_HTML_CONTENT_TYPE_WITH_ISO_8859_1_CHARSET = "text/html; charset=ISO-8859-1"; private static final File TMP_DIR = new File( System.getProperty("java.io.tmpdir"), "ahc-tests-" + UUID.randomUUID().toString().substring(0, 8)); public static final byte[] PATTERN_BYTES = "FooBarBazQixFooBarBazQixFooBarBazQixFooBarBazQixFooBarBazQixFooBarBazQix" .getBytes(Charset.forName("UTF-16")); public static final File LARGE_IMAGE_FILE; public static byte[] LARGE_IMAGE_BYTES; public static final File SIMPLE_TEXT_FILE; public static final String SIMPLE_TEXT_FILE_STRING; private static final LoginService LOGIN_SERVICE = new HashLoginService("MyRealm", "src/test/resources/realm.properties"); static { try { TMP_DIR.mkdirs(); TMP_DIR.deleteOnExit(); LARGE_IMAGE_FILE = new File(TestUtils.class.getClassLoader().getResource("300k.png").toURI()); LARGE_IMAGE_BYTES = FileUtils.readFileToByteArray(LARGE_IMAGE_FILE); SIMPLE_TEXT_FILE = new File(TestUtils.class.getClassLoader().getResource("SimpleTextFile.txt").toURI()); SIMPLE_TEXT_FILE_STRING = FileUtils.readFileToString(SIMPLE_TEXT_FILE, UTF_8); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static synchronized int findFreePort() throws IOException { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); } } public static File createTempFile(int approxSize) throws IOException { long repeats = approxSize / TestUtils.PATTERN_BYTES.length + 1; File tmpFile = File.createTempFile("tmpfile-", ".data", TMP_DIR); tmpFile.deleteOnExit(); try (FileOutputStream out = new FileOutputStream(tmpFile)) { for (int i = 0; i < repeats; i++) { out.write(PATTERN_BYTES); } long expectedFileSize = PATTERN_BYTES.length * repeats; assertEquals(tmpFile.length(), expectedFileSize, "Invalid file length"); return tmpFile; } } public static Server newJettyHttpServer(int port) { Server server = new Server(); addHttpConnector(server, port); return server; } public static void addHttpConnector(Server server, int port) { ServerConnector connector = new ServerConnector(server); connector.setPort(port); server.addConnector(connector); } public static Server newJettyHttpsServer(int port) throws URISyntaxException { Server server = new Server(); addHttpsConnector(server, port); return server; } public static void addHttpsConnector(Server server, int port) throws URISyntaxException { ClassLoader cl = TestUtils.class.getClassLoader(); URL keystoreUrl = cl.getResource("ssltest-keystore.jks"); String keyStoreFile = new File(keystoreUrl.toURI()).getAbsolutePath(); SslContextFactory sslContextFactory = new SslContextFactory(keyStoreFile); sslContextFactory.setKeyStorePassword("changeit"); String trustStoreFile = new File(cl.getResource("ssltest-cacerts.jks").toURI()).getAbsolutePath(); sslContextFactory.setTrustStorePath(trustStoreFile); sslContextFactory.setTrustStorePassword("changeit"); HttpConfiguration httpsConfig = new HttpConfiguration(); httpsConfig.setSecureScheme("https"); httpsConfig.setSecurePort(port); httpsConfig.addCustomizer(new SecureRequestCustomizer()); ServerConnector connector = new ServerConnector( server, new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(httpsConfig)); connector.setPort(port); server.addConnector(connector); } public static void addBasicAuthHandler(Server server, Handler handler) { addAuthHandler(server, Constraint.__BASIC_AUTH, new BasicAuthenticator(), handler); } public static void addDigestAuthHandler(Server server, Handler handler) { addAuthHandler(server, Constraint.__DIGEST_AUTH, new DigestAuthenticator(), handler); } private static void addAuthHandler( Server server, String auth, LoginAuthenticator authenticator, Handler handler) { server.addBean(LOGIN_SERVICE); Constraint constraint = new Constraint(); constraint.setName(auth); constraint.setRoles(new String[] {USER, ADMIN}); constraint.setAuthenticate(true); ConstraintMapping mapping = new ConstraintMapping(); mapping.setConstraint(constraint); mapping.setPathSpec("/*"); Set<String> knownRoles = new HashSet<>(); knownRoles.add(USER); knownRoles.add(ADMIN); List<ConstraintMapping> cm = new ArrayList<>(); cm.add(mapping); ConstraintSecurityHandler security = new ConstraintSecurityHandler(); security.setConstraintMappings(cm, knownRoles); security.setAuthenticator(authenticator); security.setLoginService(LOGIN_SERVICE); security.setHandler(handler); server.setHandler(security); } private static KeyManager[] createKeyManagers() throws GeneralSecurityException, IOException { InputStream keyStoreStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("ssltest-cacerts.jks"); char[] keyStorePassword = "******".toCharArray(); KeyStore ks = KeyStore.getInstance("JKS"); ks.load(keyStoreStream, keyStorePassword); assert (ks.size() > 0); // Set up key manager factory to use our key store char[] certificatePassword = "******".toCharArray(); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks, certificatePassword); // Initialize the SSLContext to work with our key managers. return kmf.getKeyManagers(); } private static TrustManager[] createTrustManagers() throws GeneralSecurityException, IOException { InputStream keyStoreStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("ssltest-keystore.jks"); char[] keyStorePassword = "******".toCharArray(); KeyStore ks = KeyStore.getInstance("JKS"); ks.load(keyStoreStream, keyStorePassword); assert (ks.size() > 0); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ks); return tmf.getTrustManagers(); } public static SSLContext createSSLContext(AtomicBoolean trust) { try { KeyManager[] keyManagers = createKeyManagers(); TrustManager[] trustManagers = new TrustManager[] { dummyTrustManager(trust, (X509TrustManager) createTrustManagers()[0]) }; SecureRandom secureRandom = new SecureRandom(); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, trustManagers, secureRandom); return sslContext; } catch (Exception e) { throw new Error("Failed to initialize the server-side SSLContext", e); } } public static class DummyTrustManager implements X509TrustManager { private final X509TrustManager tm; private final AtomicBoolean trust; public DummyTrustManager(final AtomicBoolean trust, final X509TrustManager tm) { this.trust = trust; this.tm = tm; } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { tm.checkClientTrusted(chain, authType); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { if (!trust.get()) { throw new CertificateException("Server certificate not trusted."); } tm.checkServerTrusted(chain, authType); } @Override public X509Certificate[] getAcceptedIssuers() { return tm.getAcceptedIssuers(); } } private static TrustManager dummyTrustManager( final AtomicBoolean trust, final X509TrustManager tm) { return new DummyTrustManager(trust, tm); } public static File getClasspathFile(String file) throws FileNotFoundException { ClassLoader cl = null; try { cl = Thread.currentThread().getContextClassLoader(); } catch (Throwable ex) { } if (cl == null) { cl = TestUtils.class.getClassLoader(); } URL resourceUrl = cl.getResource(file); try { return new File(new URI(resourceUrl.toString()).getSchemeSpecificPart()); } catch (URISyntaxException e) { throw new FileNotFoundException(file); } } }
/** A builder for {@link Realm} */ public static class RealmBuilder { private static final ThreadLocal<MessageDigest> DIGEST_TL = new ThreadLocal<MessageDigest>() { @Override protected MessageDigest initialValue() { try { return MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }; private String principal; private String password; private AuthScheme scheme; private String realmName; private String nonce; private String algorithm; private String response; private String opaque; private String qop; private String nc = DEFAULT_NC; private String cnonce; private Uri uri; private String methodName = "GET"; private boolean usePreemptive; private String ntlmDomain = System.getProperty("http.auth.ntlm.domain"); private Charset charset = UTF_8; private String ntlmHost = "localhost"; private boolean useAbsoluteURI = false; private boolean omitQuery; public RealmBuilder ntlmDomain(String ntlmDomain) { this.ntlmDomain = ntlmDomain; return this; } public RealmBuilder ntlmHost(String host) { this.ntlmHost = host; return this; } public RealmBuilder principal(String principal) { this.principal = principal; return this; } public RealmBuilder password(String password) { this.password = password; return this; } public RealmBuilder scheme(AuthScheme scheme) { this.scheme = scheme; return this; } public RealmBuilder realmName(String realmName) { this.realmName = realmName; return this; } public RealmBuilder nonce(String nonce) { this.nonce = nonce; return this; } public RealmBuilder algorithm(String algorithm) { this.algorithm = algorithm; return this; } public RealmBuilder response(String response) { this.response = response; return this; } public RealmBuilder opaque(String opaque) { this.opaque = opaque; return this; } public RealmBuilder qop(String qop) { if (isNonEmpty(qop)) { this.qop = qop; } return this; } public RealmBuilder nc(String nc) { this.nc = nc; return this; } public RealmBuilder uri(Uri uri) { this.uri = uri; return this; } public RealmBuilder methodName(String methodName) { this.methodName = methodName; return this; } public RealmBuilder usePreemptiveAuth(boolean usePreemptiveAuth) { this.usePreemptive = usePreemptiveAuth; return this; } public RealmBuilder useAbsoluteURI(boolean useAbsoluteURI) { this.useAbsoluteURI = useAbsoluteURI; return this; } public RealmBuilder omitQuery(boolean omitQuery) { this.omitQuery = omitQuery; return this; } public RealmBuilder charset(Charset charset) { this.charset = charset; return this; } private String parseRawQop(String rawQop) { String[] rawServerSupportedQops = rawQop.split(","); String[] serverSupportedQops = new String[rawServerSupportedQops.length]; for (int i = 0; i < rawServerSupportedQops.length; i++) { serverSupportedQops[i] = rawServerSupportedQops[i].trim(); } // prefer auth over auth-int for (String rawServerSupportedQop : serverSupportedQops) { if (rawServerSupportedQop.equals("auth")) return rawServerSupportedQop; } for (String rawServerSupportedQop : serverSupportedQops) { if (rawServerSupportedQop.equals("auth-int")) return rawServerSupportedQop; } return null; } public RealmBuilder parseWWWAuthenticateHeader(String headerLine) { realmName(match(headerLine, "realm")) // .nonce(match(headerLine, "nonce")) // .opaque(match(headerLine, "opaque")) // .scheme(isNonEmpty(nonce) ? AuthScheme.DIGEST : AuthScheme.BASIC); String algorithm = match(headerLine, "algorithm"); if (isNonEmpty(algorithm)) { algorithm(algorithm); } // FIXME qop is different with proxy? String rawQop = match(headerLine, "qop"); if (rawQop != null) { qop(parseRawQop(rawQop)); } return this; } public RealmBuilder parseProxyAuthenticateHeader(String headerLine) { realmName(match(headerLine, "realm")) // .nonce(match(headerLine, "nonce")) // .opaque(match(headerLine, "opaque")) // .scheme(isNonEmpty(nonce) ? AuthScheme.DIGEST : AuthScheme.BASIC); String algorithm = match(headerLine, "algorithm"); if (isNonEmpty(algorithm)) { algorithm(algorithm); } // FIXME qop is different with proxy? qop(match(headerLine, "qop")); return this; } private void newCnonce(MessageDigest md) { byte[] b = new byte[8]; ThreadLocalRandom.current().nextBytes(b); b = md.digest(b); cnonce = toHexString(b); } /** TODO: A Pattern/Matcher may be better. */ private String match(String headerLine, String token) { if (headerLine == null) { return null; } int match = headerLine.indexOf(token); if (match <= 0) return null; // = to skip match += token.length() + 1; int trailingComa = headerLine.indexOf(",", match); String value = headerLine.substring(match, trailingComa > 0 ? trailingComa : headerLine.length()); value = value.length() > 0 && value.charAt(value.length() - 1) == '"' ? value.substring(0, value.length() - 1) : value; return value.charAt(0) == '"' ? value.substring(1) : value; } private byte[] md5FromRecycledStringBuilder(StringBuilder sb, MessageDigest md) { md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1)); sb.setLength(0); return md.digest(); } private byte[] secretDigest(StringBuilder sb, MessageDigest md) { sb.append(principal).append(':').append(realmName).append(':').append(password); byte[] ha1 = md5FromRecycledStringBuilder(sb, md); if (algorithm == null || algorithm.equals("MD5")) { return ha1; } else if ("MD5-sess".equals(algorithm)) { appendBase16(sb, ha1); sb.append(':').append(nonce).append(':').append(cnonce); return md5FromRecycledStringBuilder(sb, md); } throw new UnsupportedOperationException("Digest algorithm not supported: " + algorithm); } private byte[] dataDigest(StringBuilder sb, String digestUri, MessageDigest md) { sb.append(methodName).append(':').append(digestUri); if ("auth-int".equals(qop)) { sb.append(':').append(EMPTY_ENTITY_MD5); } else if (qop != null && !qop.equals("auth")) { throw new UnsupportedOperationException("Digest qop not supported: " + qop); } return md5FromRecycledStringBuilder(sb, md); } private void appendDataBase(StringBuilder sb) { sb.append(':').append(nonce).append(':'); if ("auth".equals(qop) || "auth-int".equals(qop)) { sb.append(nc).append(':').append(cnonce).append(':').append(qop).append(':'); } } private void newResponse(MessageDigest md) { // BEWARE: compute first as it used the cached StringBuilder String digestUri = AuthenticatorUtils.computeRealmURI(uri, useAbsoluteURI, omitQuery); StringBuilder sb = StringUtils.stringBuilder(); // WARNING: DON'T MOVE, BUFFER IS RECYCLED!!!! byte[] secretDigest = secretDigest(sb, md); byte[] dataDigest = dataDigest(sb, digestUri, md); appendBase16(sb, secretDigest); appendDataBase(sb); appendBase16(sb, dataDigest); byte[] responseDigest = md5FromRecycledStringBuilder(sb, md); response = toHexString(responseDigest); } private static String toHexString(byte[] data) { StringBuilder buffer = StringUtils.stringBuilder(); for (int i = 0; i < data.length; i++) { buffer.append(Integer.toHexString((data[i] & 0xf0) >>> 4)); buffer.append(Integer.toHexString(data[i] & 0x0f)); } return buffer.toString(); } private static void appendBase16(StringBuilder buf, byte[] bytes) { int base = 16; for (byte b : bytes) { int bi = 0xff & b; int c = '0' + (bi / base) % base; if (c > '9') c = 'a' + (c - '0' - 10); buf.append((char) c); c = '0' + bi % base; if (c > '9') c = 'a' + (c - '0' - 10); buf.append((char) c); } } /** * Build a {@link Realm} * * @return a {@link Realm} */ public Realm build() { // Avoid generating if (isNonEmpty(nonce)) { MessageDigest md = DIGEST_TL.get(); newCnonce(md); newResponse(md); } return new Realm( scheme, principal, password, realmName, nonce, algorithm, response, qop, nc, cnonce, uri, methodName, usePreemptive, ntlmDomain, charset, ntlmHost, opaque, useAbsoluteURI, omitQuery); } }