Java tutorial
/** * Copyright (c) 2012 to original author or authors * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package io.takari.aether.connector.test.mockwebserver; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.Authenticator; import java.net.HttpURLConnection; import java.net.PasswordAuthentication; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Inject; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.internal.test.util.TestFileProcessor; import org.eclipse.aether.internal.test.util.TestFileUtils; import org.eclipse.aether.internal.test.util.TestUtils; import org.eclipse.aether.repository.Authentication; import org.eclipse.aether.repository.Proxy; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.repository.RepositoryPolicy; import org.eclipse.aether.spi.connector.ArtifactDownload; import org.eclipse.aether.spi.connector.RepositoryConnector; import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; import org.eclipse.aether.spi.io.FileProcessor; import org.eclipse.aether.transfer.NoRepositoryConnectorException; import org.eclipse.aether.util.repository.AuthenticationBuilder; import org.eclipse.sisu.launch.InjectedTestCase; import org.junit.Assert; import org.slf4j.ILoggerFactory; import org.slf4j.impl.SimpleLoggerFactory; import com.google.common.base.Charsets; import com.google.common.io.Files; import com.google.inject.Binder; import com.squareup.okhttp.Headers; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.OkUrlFactory; import com.squareup.okhttp.internal.SslContextBuilder; import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockWebServer; import com.squareup.okhttp.mockwebserver.RecordedRequest; import com.squareup.okhttp.mockwebserver.SocketPolicy; // // 6 cases: // - http direct, // - https direct, // - http through http proxy, // - http through https proxy, // - https through http proxy (not allowed) // - https through https proxy // // Combination with authentication // // client to http://target // client to https://target // client to http://target via http://proxy // client to http://target via https://proxy // client to https://target via http://proxy // client to https://target via https://proxy // // Used for CONNECT messages to tunnel SSL over an HTTP proxy. // // CLIENT ---[HTTP]---> PROXY ---[HTTPS]---> TARGET // // public class AetherMockWebserverConnectorTest extends InjectedTestCase { private static final String CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ="; // base64("username:password") private static final String ARTIFACT_CONTENT = "i am a secret binary full of magical powers. don't eat me. you will grow a tail."; private static final String USERNAME = "username"; private static final String PASSWORD = "password"; private static final String PROXY_USERNAME = "pusername"; private static final String PROXY_PASSWORD = "ppassword"; private static final RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); private static final SSLContext sslContext = SslContextBuilder.localhost(); private MockWebServer server = new MockWebServer(); private boolean enableSsl; private boolean enableAuth; private boolean enableProxy; private boolean enableProxyWithAuth; private String proxyTarget = "repo1.maven.org"; private RemoteRepository repository; private DefaultRepositorySystemSession session; private RepositoryConnector connector; @Inject protected RepositoryConnectorFactory repositoryConnectorFactory; protected String protocol() { if (enableSsl) { return "https"; } else { return "http"; } } public String url(String path) { String url; if (enableProxy) { url = String.format("%s://%s/", protocol(), proxyTarget) + path; } else { url = server.getUrl("/" + path).toExternalForm(); } return url; } @Override protected void tearDown() throws Exception { Authenticator.setDefault(null); System.clearProperty("proxyHost"); System.clearProperty("proxyPort"); System.clearProperty("http.proxyHost"); System.clearProperty("http.proxyPort"); System.clearProperty("https.proxyHost"); System.clearProperty("https.proxyPort"); enableAuth = false; enableProxy = false; enableProxyWithAuth = false; enableSsl = false; server.shutdown(); super.tearDown(); } protected int port() { return server.getPort(); } protected String hostname() { return server.getHostName(); } public void testArtifactDownload() throws Exception { enqueueServerWithSingleArtifactResponse(); downloadArtifact(); } // // Testing only with authentication really doesn't make sense in the absence of SSL because // sending your credentials in the clear is a bad idea. // public void testArtifactDownloadWithBasicAuth() throws Exception { enableAuthRequests(); enqueueServerWithSingleArtifactResponse(); downloadArtifact(); // once challenged, the client is expected to send credentials eagerly assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization"); assertContains(server.takeRequest().getHeaders(), "Authorization", "Basic " + CREDENTIALS); assertContains(server.takeRequest().getHeaders(), "Authorization", "Basic " + CREDENTIALS); } public void testArtifactDownloadWithBasicAuthAndSystemAuthenticator() throws Exception { // the point of this test is to validate that default system authenticator is not called final AtomicBoolean defaultAuthenticationRequested = new AtomicBoolean(false); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { defaultAuthenticationRequested.set(true); throw new UnsupportedOperationException(); } }); try { enableAuthRequests(); enqueueServerWithSingleArtifactResponse(); downloadArtifact(); assertFalse(defaultAuthenticationRequested.get()); } finally { Authenticator.setDefault(null); } } // http://www.squid-cache.org/mail-archive/squid-users/199811/0488.html public void testArtifactDownloadViaProxy() throws Exception { enableProxyRequests(); enqueueServerWithSingleArtifactResponse(); downloadArtifact(); // // Make sure that the client is sending the correct headers for BASIC authentication // for (int i = 0; i < 2; i++) { RecordedRequest request = server.takeRequest(); assertRequestMatches(request.getRequestLine(), String.format("GET http://%s/repo(.*) HTTP/1.1", proxyTarget)); assertContains(request.getHeaders(), "Host", proxyTarget); } } public void testArtifactDownloadViaSsl() throws Exception { enableSslRequests(); enqueueServerWithSingleArtifactResponse(); downloadArtifact(); } public void XXXtestArtifactDownloadViaHttpProxyToHttpsEndPoint() throws Exception { // these are not quite right, it's not sslTargetRequests and enableHttpProxy enableProxyRequests(); enableSslRequests(); enqueueServerWithSingleArtifactResponse(); downloadArtifact(); // // Make sure that the client is sending the correct headers for BASIC authentication // for (int i = 0; i < 3; i++) { RecordedRequest connect = server.takeRequest(); assertEquals("Connect line failure on proxy", String.format("CONNECT %s:443 HTTP/1.1", proxyTarget), connect.getRequestLine()); assertContains(connect.getHeaders(), "Host", proxyTarget); RecordedRequest get = server.takeRequest(); assertRequestMatches(get.getRequestLine(), String.format("GET /repo(.*) HTTP/1.1", proxyTarget)); assertContains(get.getHeaders(), "Host", proxyTarget); assertEquals(Arrays.asList(String.format("verify %s", proxyTarget)), hostnameVerifier.calls()); } } // // We setup the server to respond to a request for a primary artifact and its // corresponding SHA1 and MD5 checksums. // protected void enqueueServerWithSingleArtifactResponse() throws Exception { addResponseForTunnelingSslOverAnHttpProxy(); if (enableAuth) { MockResponse pleaseAuthenticate = new MockResponse() // .setResponseCode(401) // .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") // .setBody("Please authenticate."); server.enqueue(pleaseAuthenticate); } server.enqueue(new MockResponse().setBody(ARTIFACT_CONTENT)); addResponseForTunnelingSslOverAnHttpProxy(); server.enqueue(new MockResponse().setBody(sha1(ARTIFACT_CONTENT))); server.play(); } private void addResponseForTunnelingSslOverAnHttpProxy() { if (enableSsl && (enableProxy || enableProxyWithAuth)) { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders()); } } protected void downloadArtifact() throws Exception { File artifactFile = TestFileUtils.createTempFile(""); Artifact artifact = artifact(ARTIFACT_CONTENT); ArtifactDownload download = new ArtifactDownload(artifact, null, artifactFile, RepositoryPolicy.CHECKSUM_POLICY_FAIL); Collection<? extends ArtifactDownload> downloads = Arrays.asList(download); // // // RepositoryConnector aetherConnector = connector(); aetherConnector.get(downloads, null); assertNull(String.valueOf(download.getException()), download.getException()); Assert.assertEquals(ARTIFACT_CONTENT, Files.toString(artifactFile, Charsets.UTF_8)); } private void enableAuthRequests() { enableAuth = true; } private void enableProxyRequests() { enableProxy = true; } private void enableSslRequests() { enableSsl = true; server.useHttps(sslContext.getSocketFactory(), enableProxy); } private Artifact artifact(String content) throws IOException { Artifact artifact = new DefaultArtifact("gid", "aid", "classifier", "extension", "version", null); artifact.setFile(TestFileUtils.createTempFile(content)); return artifact; } // // Helper test methods // private void assertContains(Headers headers, String header, String value) { assertTrue(headers.toString(), headers.values(header).contains(value)); } private void assertContainsNoneMatching(Headers headers, String header) { assertTrue(headers.values(header).isEmpty()); } private void assertRequestMatches(String request, String pattern) { if (!request.matches(pattern)) { fail("Request does not match pattern " + pattern); } } // private final OkUrlFactory client = new OkUrlFactory(new OkHttpClient()); private HttpURLConnection connection; /** * Reads at most {@code limit} characters from {@code in} and asserts that * content equals {@code expected}. */ private void assertContent(String expected, HttpURLConnection connection, int limit) throws IOException { connection.connect(); assertEquals(expected, readAscii(connection.getInputStream(), limit)); } private void assertContent(String expected, HttpURLConnection connection) throws IOException { assertContent(expected, connection, Integer.MAX_VALUE); } /** * Reads {@code count} characters from the stream. If the stream is * exhausted before {@code count} characters can be read, the remaining * characters are returned and the stream is closed. */ private String readAscii(InputStream in, int count) throws IOException { StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) { int value = in.read(); if (value == -1) { in.close(); break; } result.append((char) value); } return result.toString(); } // // Converation for proxy auth // // --> GET http://server.com/foo HTTP/1.1 // // <-- HTTP/1.1 407 Proxy Authorization Required // <-- Proxy-Authenticate: Basic realm="Secure Realm" // // --> GET http://server.com/foo HTTP/1.1 // --> Proxy-Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ // // <-- HTTP/1.1 200 OK // public void testProxyAuthenticateOnConnect() throws Exception { Authenticator.setDefault(new RecordingAuthenticator()); server.enqueue( new MockResponse().setResponseCode(407).addHeader("Proxy-Authenticate: Basic realm=\"localhost\"")); server.enqueue(new MockResponse().setBody("A")); server.play(); client.client().setProxy(server.toProxyAddress()); URL url = new URL("http://server.com/foo"); connection = client.open(url); assertContent("A", connection); assertEquals(200, connection.getResponseCode()); RecordedRequest connect1 = server.takeRequest(); assertEquals("GET http://server.com/foo HTTP/1.1", connect1.getRequestLine()); assertContainsNoneMatching(connect1.getHeaders(), "Proxy-Authorization"); RecordedRequest connect2 = server.takeRequest(); assertEquals("GET http://server.com/foo HTTP/1.1", connect2.getRequestLine()); assertContains(connect2.getHeaders(), "Proxy-Authorization", "Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS); } // // Converation for proxy auth over SSL // public void testProxyAuthenticateOnConnectOverSSL() throws Exception { Authenticator.setDefault(new RecordingAuthenticator()); server.useHttps(sslContext.getSocketFactory(), true); server.enqueue( new MockResponse().setResponseCode(407).addHeader("Proxy-Authenticate: Basic realm=\"localhost\"")); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders()); server.enqueue(new MockResponse().setBody("A")); server.play(); client.client().setProxy(server.toProxyAddress()); URL url = new URL("https://android.com/foo"); client.client().setSslSocketFactory(sslContext.getSocketFactory()); client.client().setHostnameVerifier(new RecordingHostnameVerifier()); connection = client.open(url); assertContent("A", connection); RecordedRequest connect1 = server.takeRequest(); assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine()); assertContainsNoneMatching(connect1.getHeaders(), "Proxy-Authorization"); RecordedRequest connect2 = server.takeRequest(); assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine()); assertContains(connect2.getHeaders(), "Proxy-Authorization", "Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS); RecordedRequest get = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", get.getRequestLine()); assertContainsNoneMatching(get.getHeaders(), "Proxy-Authorization"); } // // // protected DefaultRepositorySystemSession session() { if (session == null) { session = TestUtils.newSession(); } return session; } protected RepositoryConnector connector() throws NoRepositoryConnectorException, IOException { return connector(false); } protected RepositoryConnector connector(boolean forceNew) throws NoRepositoryConnectorException, IOException { if (connector == null || forceNew) { connector = repositoryConnectorFactory.newInstance(session(), remoteRepository()); } return connector; } private RemoteRepository remoteRepository() { if (repository == null) { RemoteRepository.Builder builder = new RemoteRepository.Builder("repo", "default", url("repo")); if (enableProxyWithAuth) { Authentication auth = new AuthenticationBuilder().addUsername(PROXY_USERNAME) .addPassword(PROXY_PASSWORD).build(); Proxy proxy = new Proxy(protocol(), hostname(), port(), auth); builder.setProxy(proxy); } else if (enableProxy) { Proxy proxy = new Proxy(protocol(), hostname(), port()); builder.setProxy(proxy); } if (enableAuth) { Authentication auth = new AuthenticationBuilder().addUsername(USERNAME).addPassword(PASSWORD) .build(); builder.setAuthentication(auth); } repository = builder.build(); } return repository; } // // Checksums // protected String sha1(String string) throws NoSuchAlgorithmException, UnsupportedEncodingException { return digest(string, "SHA-1"); } protected String md5(String string) throws NoSuchAlgorithmException, UnsupportedEncodingException { String algo = "MD5"; return digest(string, algo); } private String digest(String string, String algo) throws NoSuchAlgorithmException, UnsupportedEncodingException { MessageDigest digest = MessageDigest.getInstance(algo); byte[] bytes = digest.digest(string.getBytes("UTF-8")); StringBuilder buffer = new StringBuilder(64); for (int i = 0; i < bytes.length; i++) { int b = bytes[i] & 0xFF; if (b < 0x10) { buffer.append('0'); } buffer.append(Integer.toHexString(b)); } return buffer.toString(); } // // Right now all all tests will have an SSLSocketFactory binding. Ideally we configure this and // have a conditional binding but right now I use methods to configure the test setup and at this // point it's too late to control the binding. Not ideal, but not horrible by any means. // @Override public void configure(Binder binder) { binder.bind(FileProcessor.class).to(TestFileProcessor.class); binder.bind(ILoggerFactory.class).to(SimpleLoggerFactory.class); binder.bind(SSLSocketFactory.class).toInstance(sslContext.getSocketFactory()); } }