diff --git a/build.gradle b/build.gradle index bad6e6b992..5340839127 100644 --- a/build.gradle +++ b/build.gradle @@ -46,9 +46,10 @@ subprojects { // For Google libraries, check <http-client-bom.version>, <google.auth.version>, <guava.version>, // ... in https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-clients/pom.xml // for best compatibility. - GOOGLE_HTTP_CLIENT: '1.27.0', - GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP: '0.16.2', - GUAVA: '28.0-jre', + GOOGLE_HTTP_CLIENT: '1.33.0', + GOOGLE_HTTP_CLIENT_APACHE_V2: '1.33.0', + GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP: '0.18.0', + GUAVA: '28.1-jre', COMMONS_COMPRESS: '1.19', JACKSON_DATABIND: '2.9.10', @@ -64,7 +65,10 @@ subprojects { // Use this to ensure we correctly override transitive dependencies // TODO: There might be a plugin that does this task ensureTransitiveDependencyOverrides { - def rules = ["google-http-client": dependencyVersions.GOOGLE_HTTP_CLIENT] + def rules = [ + "google-http-client": dependencyVersions.GOOGLE_HTTP_CLIENT, + "google-http-client-apache-v2": dependencyVersions.GOOGLE_HTTP_CLIENT_APACHE_V2, + ] doLast { configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> def dependency = artifact.moduleVersion.id diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 624a319697..672ab9f3ba 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -5,9 +5,8 @@ plugins { dependencies { implementation "com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}" - implementation("com.google.auth:google-auth-library-oauth2-http:${dependencyVersions.GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP}") { - exclude group: "com.google.http-client", module: "google-http-client" - } + implementation "com.google.http-client:google-http-client-apache-v2:${dependencyVersions.GOOGLE_HTTP_CLIENT_APACHE_V2}" + implementation "com.google.auth:google-auth-library-oauth2-http:${dependencyVersions.GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP}" implementation "org.apache.commons:commons-compress:${dependencyVersions.COMMONS_COMPRESS}" implementation "com.google.guava:guava:${dependencyVersions.GUAVA}" diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java index 46bf2615ee..78e073c5ca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java @@ -22,10 +22,10 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.apache.ApacheHttpTransport; +import com.google.api.client.http.apache.v2.ApacheHttpTransport; +import com.google.api.client.util.SslUtils; import com.google.cloud.tools.jib.api.LogEvent; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import java.io.IOException; import java.net.ConnectException; import java.net.URL; @@ -33,9 +33,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; import javax.net.ssl.SSLException; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.HttpClientBuilder; /** * Thread-safe HTTP client that can automatically failover from secure HTTPS to insecure HTTPS or @@ -86,55 +85,23 @@ private static HttpTransport getSecureHttpTransport() { // // A new ApacheHttpTransport needs to be created for each connection because otherwise HTTP // connection persistence causes the connection to throw NoHttpResponseException. - ApacheHttpTransport transport = new ApacheHttpTransport(); - addProxyCredentials(transport); - return transport; + return new ApacheHttpTransport(); } private static HttpTransport getInsecureHttpTransport() { try { - ApacheHttpTransport insecureTransport = - new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); - addProxyCredentials(insecureTransport); - return insecureTransport; + HttpClientBuilder httpClientBuilder = + ApacheHttpTransport.newDefaultHttpClientBuilder() + .setSSLSocketFactory(null) // creates new factory with the SSLContext given below + .setSSLContext(SslUtils.trustAllSSLContext()) + .setSSLHostnameVerifier(new NoopHostnameVerifier()); + // Do not use NetHttpTransport. See comments in getConnectionFactory for details. + return new ApacheHttpTransport(httpClientBuilder.build()); } catch (GeneralSecurityException ex) { throw new RuntimeException("platform does not support TLS protocol", ex); } } - /** - * Registers proxy credentials onto transport client, in order to deal with proxies that require - * basic authentication. - * - * @param transport Apache HTTP transport - */ - @VisibleForTesting - static void addProxyCredentials(ApacheHttpTransport transport) { - addProxyCredentials(transport, "https"); - addProxyCredentials(transport, "http"); - } - - private static void addProxyCredentials(ApacheHttpTransport transport, String protocol) { - Preconditions.checkArgument(protocol.equals("http") || protocol.equals("https")); - - String proxyHost = System.getProperty(protocol + ".proxyHost"); - String proxyUser = System.getProperty(protocol + ".proxyUser"); - String proxyPassword = System.getProperty(protocol + ".proxyPassword"); - if (proxyHost == null || proxyUser == null || proxyPassword == null) { - return; - } - - String defaultProxyPort = protocol.equals("http") ? "80" : "443"; - int proxyPort = Integer.parseInt(System.getProperty(protocol + ".proxyPort", defaultProxyPort)); - - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - httpClient - .getCredentialsProvider() - .setCredentials( - new AuthScope(proxyHost, proxyPort), - new UsernamePasswordCredentials(proxyUser, proxyPassword)); - } - private final boolean enableHttpAndInsecureFailover; private final boolean sendAuthorizationOverHttp; private final Consumer<LogEvent> logger; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index eb5d0fd98a..debfbd4737 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -164,10 +164,11 @@ private T call(URL url) throws IOException, RegistryException { throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); } - } else if (responseException.getStatusCode() - == HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT - || responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY - || responseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { + // 301 (Moved Permanently), 302 (Found), 303 (See Other), and 307 (Temporary Redirect) are + // automatically followed by Google HTTP Client (setFollowRedirects(true)), but 308 isn't. + // https://github.com/googleapis/google-http-java-client/issues/873 + // TODO: remove this when the bug is fixed. + } else if (responseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { // 'Location' header can be relative or absolute. URL redirectLocation = new URL(url, responseException.getHeaders().getLocation()); return call(redirectLocation); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java deleted file mode 100644 index 615f3f0cfb..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import com.google.api.client.http.apache.ApacheHttpTransport; -import com.google.common.collect.ImmutableList; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.Credentials; -import org.apache.http.impl.client.DefaultHttpClient; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.contrib.java.lang.system.RestoreSystemProperties; - -/** Tests for {@link FailoverHttpClient} with setting proxy credentials. */ -public class FailoverHttpClientProxyCredentialsTest { - - private static final ImmutableList<String> proxyProperties = - ImmutableList.of( - "http.proxyHost", - "http.proxyPort", - "http.proxyUser", - "http.proxyPassword", - "https.proxyHost", - "https.proxyPort", - "https.proxyUser", - "https.proxyPassword"); - - @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); - - private final ApacheHttpTransport transport = new ApacheHttpTransport(); - - @Before - public void setUp() { - proxyProperties.stream().forEach(key -> System.clearProperty(key)); - } - - @Test - public void testAddProxyCredentials_undefined() { - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } - - @Test - public void testAddProxyCredentials() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyPort", "1080"); - System.setProperty("http.proxyUser", "user"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyPort", "1443"); - System.setProperty("https.proxyUser", "s-user"); - System.setProperty("https.proxyPassword", "s-pass"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials httpCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 1080)); - Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); - Assert.assertEquals("pass", httpCredentials.getPassword()); - - Credentials httpsCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 1443)); - Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); - Assert.assertEquals("s-pass", httpsCredentials.getPassword()); - } - - @Test - public void testAddProxyCredentials_defaultPorts() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyUser", "user"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyUser", "s-user"); - System.setProperty("https.proxyPassword", "s-pass"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials httpCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 80)); - Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); - Assert.assertEquals("pass", httpCredentials.getPassword()); - - Credentials httpsCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 443)); - Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); - Assert.assertEquals("s-pass", httpsCredentials.getPassword()); - } - - @Test - public void testAddProxyCredentials_hostUndefined() { - System.setProperty("http.proxyUser", "user"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyUser", "s-user"); - System.setProperty("https.proxyPassword", "s-pass"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } - - @Test - public void testAddProxyCredentials_userUndefined() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyPassword", "s-pass"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } - - @Test - public void testAddProxyCredentials_passwordUndefined() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyUser", "user"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyUser", "s-user"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java index 471b74002a..72e8baa979 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java @@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Arrays; +import java.util.List; import java.util.function.Consumer; import javax.net.ssl.SSLException; import org.hamcrest.CoreMatchers; @@ -149,4 +150,45 @@ public void testProxyCredentialProperties() } } } + + @Test + public void testRedirectionUrls() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + // Sample query strings from + // https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104 + String url1 = "?id=301&_auth_=exp=1572285389~hmac=f0a387f0"; + String url2 = "?id=302&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614"; + String url3 = "?id=303&_auth_=exp=1572285389~hmac=f0a387f0"; + String url4 = "?id=307&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614"; + + String redirect301 = + "HTTP/1.1 301 Moved Permanently\nLocation: " + url1 + "\nContent-Length: 0\n\n"; + String redirect302 = "HTTP/1.1 302 Found\nLocation: " + url2 + "\nContent-Length: 0\n\n"; + String redirect303 = "HTTP/1.1 303 See Other\nLocation: " + url3 + "\nContent-Length: 0\n\n"; + String redirect307 = + "HTTP/1.1 307 Temporary Redirect\nLocation: " + url4 + "\nContent-Length: 0\n\n"; + String ok200 = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; + List<String> responses = + Arrays.asList(redirect301, redirect302, redirect303, redirect307, ok200); + + FailoverHttpClient httpClient = new FailoverHttpClient(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(false, responses, 1)) { + httpClient.get(new URL(server.getEndpoint()), request); + + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString("GET /?id=301&_auth_=exp%3D1572285389~hmac%3Df0a387f0 ")); + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString( + "GET /?id=302&Signature=2wYOD0a%2BDAkK/9lQJUOuIpYti8o%3D&Expires=1569997614 ")); + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString("GET /?id=303&_auth_=exp%3D1572285389~hmac%3Df0a387f0 ")); + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString( + "GET /?id=307&Signature=2wYOD0a%2BDAkK/9lQJUOuIpYti8o%3D&Expires=1569997614 ")); + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 2179cd5b2b..3487df0b58 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -242,18 +242,22 @@ public void testCall_unknown() throws IOException, RegistryException { } @Test - public void testCall_temporaryRedirect() throws IOException, RegistryException { - verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT); - } + public void testCall_permanentRedirect() throws IOException, RegistryException { + ResponseException redirectException = + mockResponseException( + RegistryEndpointCaller.STATUS_CODE_PERMANENT_REDIRECT, + new HttpHeaders().setLocation("https://newlocation")); - @Test - public void testCall_movedPermanently() throws IOException, RegistryException { - verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY); - } + // Make httpClient.call() throw first, then succeed. + setUpRegistryResponse(redirectException); + Mockito.when( + mockHttpClient.call( + Mockito.eq("httpMethod"), + Mockito.eq(new URL("https://newlocation")), + Mockito.any())) + .thenReturn(mockResponse); - @Test - public void testCall_permanentRedirect() throws IOException, RegistryException { - verifyRetriesWithNewLocation(RegistryEndpointCaller.STATUS_CODE_PERMANENT_REDIRECT); + Assert.assertEquals("body", endpointCaller.call()); } @Test @@ -471,28 +475,6 @@ private void verifyThrowsRegistryErrorException(int httpStatusCode) } } - /** - * Verifies that a response with {@code httpStatusCode} retries the request with the {@code - * Location} header. - */ - private void verifyRetriesWithNewLocation(int httpStatusCode) - throws IOException, RegistryException { - // Mocks a response for temporary redirect to a new location. - ResponseException redirectException = - mockResponseException(httpStatusCode, new HttpHeaders().setLocation("https://newlocation")); - - // Make httpClient.call() throw first, then succeed. - setUpRegistryResponse(redirectException); - Mockito.when( - mockHttpClient.call( - Mockito.eq("httpMethod"), - Mockito.eq(new URL("https://newlocation")), - Mockito.any())) - .thenReturn(mockResponse); - - Assert.assertEquals("body", endpointCaller.call()); - } - private void setUpRegistryResponse(Exception exceptionToThrow) throws MalformedURLException, IOException { Mockito.when(