View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.transport.apache;
20  
21  import java.io.File;
22  import java.net.URI;
23  import java.nio.charset.StandardCharsets;
24  import java.util.Base64;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.Map;
28  import java.util.Queue;
29  import java.util.concurrent.atomic.AtomicInteger;
30  import java.util.stream.Stream;
31  
32  import org.apache.http.Header;
33  import org.apache.http.HttpHost;
34  import org.apache.http.HttpVersion;
35  import org.apache.http.auth.AuthOption;
36  import org.apache.http.auth.AuthSchemeProvider;
37  import org.apache.http.auth.UsernamePasswordCredentials;
38  import org.apache.http.client.config.AuthSchemes;
39  import org.apache.http.client.config.RequestConfig;
40  import org.apache.http.client.protocol.HttpClientContext;
41  import org.apache.http.config.RegistryBuilder;
42  import org.apache.http.conn.routing.HttpRoute;
43  import org.apache.http.impl.auth.BasicSchemeFactory;
44  import org.apache.http.impl.client.BasicCredentialsProvider;
45  import org.apache.http.message.BasicHeader;
46  import org.apache.http.message.BasicHttpResponse;
47  import org.apache.http.pool.ConnPoolControl;
48  import org.apache.http.pool.PoolStats;
49  import org.eclipse.aether.ConfigurationProperties;
50  import org.eclipse.aether.DefaultRepositoryCache;
51  import org.eclipse.aether.internal.test.util.TestFileUtils;
52  import org.eclipse.aether.internal.test.util.http.HttpTransporterTest;
53  import org.eclipse.aether.internal.test.util.http.RecordingTransportListener;
54  import org.eclipse.aether.repository.Proxy;
55  import org.eclipse.aether.spi.connector.transport.GetTask;
56  import org.eclipse.aether.spi.connector.transport.PeekTask;
57  import org.eclipse.aether.spi.connector.transport.PutTask;
58  import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
59  import org.eclipse.aether.spi.io.PathProcessorSupport;
60  import org.eclipse.aether.util.repository.AuthenticationBuilder;
61  import org.eclipse.jetty.server.Handler;
62  import org.eclipse.jetty.server.Request;
63  import org.eclipse.jetty.server.Response;
64  import org.eclipse.jetty.server.Server;
65  import org.eclipse.jetty.server.ServerConnector;
66  import org.eclipse.jetty.server.handler.ConnectHandler;
67  import org.eclipse.jetty.util.Callback;
68  import org.junit.jupiter.api.AfterEach;
69  import org.junit.jupiter.api.Test;
70  import org.junit.jupiter.api.parallel.ResourceLock;
71  import org.junit.jupiter.api.parallel.Resources;
72  import org.junit.jupiter.params.ParameterizedTest;
73  import org.junit.jupiter.params.provider.CsvSource;
74  import org.junit.jupiter.params.provider.ValueSource;
75  
76  import static org.junit.jupiter.api.Assertions.assertEquals;
77  import static org.junit.jupiter.api.Assertions.assertThrows;
78  import static org.junit.jupiter.api.Assertions.assertTrue;
79  
80  /**
81   * Apache Transporter UT.
82   * It does support WebDAV.
83   */
84  @ResourceLock(Resources.SYSTEM_PROPERTIES)
85  class ApacheTransporterTest extends HttpTransporterTest {
86  
87      public ApacheTransporterTest() {
88          super(() -> new ApacheTransporterFactory(standardChecksumExtractor(), new PathProcessorSupport()));
89      }
90  
91      @Override
92      protected Stream<String> supportedCompressionAlgorithms() {
93          return Stream.of("gzip", "deflate");
94      }
95  
96      protected boolean exposeContentCodingInTransportProperties() {
97          // see https://issues.apache.org/jira/browse/HTTPCORE-792
98          return false;
99      }
100 
101     @Override
102     protected boolean supportsHttp3() {
103         return false;
104     }
105 
106     @Override
107     protected boolean supportsHttp2() {
108         return false;
109     }
110 
111     @AfterEach
112     @Override
113     protected void tearDown() throws Exception {
114         // make sure to also release any connection in the global state (otherwise the check for connection leaks will
115         // fail)
116         GlobalState globalState = GlobalState.get(session);
117         if (globalState != null) {
118             globalState.close();
119         }
120         super.tearDown();
121     }
122 
123     @ParameterizedTest
124     @ValueSource(strings = {"http", "https"})
125     void testTransfers_SystemProxyAuthenticated(String protocol) throws Exception {
126         httpServer.setProxyAuthentication("testuser", "testpass");
127         try (SystemProperties properties = systemProxyProperties(protocol, httpServer.getHttpPort())) {
128             newTransporter("http://bad.localhost:1/");
129             assertTransfers();
130         }
131     }
132 
133     @Test
134     void testSystemProxyAuthenticationDisabled() throws Exception {
135         httpServer.setProxyAuthentication("testuser", "testpass");
136         try (SystemProperties properties = systemProxyProperties("http", httpServer.getHttpPort())) {
137             session.setConfigProperty(ApacheTransporterConfigurationKeys.CONFIG_PROP_USE_SYSTEM_PROPERTIES, false);
138             proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
139             newTransporter("http://bad.localhost:1/");
140             assertGetStatus(407);
141         }
142     }
143 
144     @ParameterizedTest
145     @ValueSource(strings = {"testpass", "wrong"})
146     void testExplicitProxyCredentialsTakePrecedence(String password) throws Exception {
147         httpServer.setProxyAuthentication("testuser", "testpass");
148         try (SystemProperties properties = systemProxyProperties("http", httpServer.getHttpPort())) {
149             proxy = new Proxy(
150                     Proxy.TYPE_HTTP,
151                     httpServer.getHost(),
152                     httpServer.getHttpPort(),
153                     new AuthenticationBuilder()
154                             .addUsername("testuser")
155                             .addPassword(password)
156                             .build());
157             newTransporter("http://bad.localhost:1/");
158             if ("testpass".equals(password)) {
159                 properties.set("http.proxyPassword", "wrong");
160                 assertTransfers();
161             } else {
162                 assertGetStatus(407);
163             }
164         }
165     }
166 
167     @ParameterizedTest
168     @CsvSource({"Host,other.invalid", "Port,1", "Port,invalid", "Port,", "User,", "Password,wrong"})
169     void testSystemProxyCredentialsMustMatch(String property, String value) throws Exception {
170         httpServer.setProxyAuthentication("testuser", "testpass");
171         try (SystemProperties properties = systemProxyProperties("http", httpServer.getHttpPort())) {
172             proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
173             properties.set("http.proxy" + property, value);
174             newTransporter("http://bad.localhost:1/");
175             assertGetStatus(407);
176         }
177     }
178 
179     @Test
180     void testSystemProxyPasswordDefaultsToEmpty() throws Exception {
181         httpServer.setProxyAuthentication("testuser", "");
182         try (SystemProperties properties = systemProxyProperties("http", httpServer.getHttpPort())) {
183             properties.set("http.proxyPassword", null);
184             newTransporter("http://bad.localhost:1/");
185             assertTransfers();
186         }
187     }
188 
189     @ParameterizedTest
190     @CsvSource({"http,80", "https,443"})
191     void testSystemProxyPortDefaults(String protocol, int port) throws Exception {
192         try (SystemProperties properties = new SystemProperties()) {
193             properties.set(protocol + ".proxyHost", "proxy.example");
194             properties.set(protocol + ".proxyPort", null);
195             properties.set(protocol + ".proxyUser", protocol + "-user");
196             assertEquals(protocol + "-user", selectSystemProxyUsername(protocol, port));
197         }
198     }
199 
200     @ParameterizedTest
201     @CsvSource({"http,http-user", "https,https-user"})
202     void testSystemProxyCredentialsPreferTargetProtocol(String protocol, String expectedUsername) throws Exception {
203         try (SystemProperties properties = distinctSystemProxyCredentials(8080)) {
204             assertEquals(expectedUsername, selectSystemProxyUsername(protocol, 8080));
205         }
206     }
207 
208     @Test
209     void testSystemProxyCredentialsFallBackToOppositeProtocol() throws Exception {
210         try (SystemProperties properties = new SystemProperties()) {
211             properties.set("https.proxyHost", "proxy.example");
212             properties.set("https.proxyPort", "8080");
213             properties.set("https.proxyUser", "https-user");
214             assertEquals("https-user", selectSystemProxyUsername("http", 8080));
215         }
216     }
217 
218     @ParameterizedTest
219     @ValueSource(ints = {401, 407})
220     void testSystemProxyCredentialsNotUsedOnDirectRoute(int status) throws Exception {
221         if (status == 401) {
222             httpServer.setAuthentication("testuser", "testpass");
223         } else {
224             httpServer.setProxyAuthentication("testuser", "testpass");
225         }
226         try (SystemProperties properties = systemProxyProperties("http", httpServer.getHttpPort())) {
227             properties.set("http.nonProxyHosts", "*");
228             newTransporter(httpServer.getHttpUrl());
229             assertGetStatus(status);
230             assertTrue(httpServer.getLogEntries().stream()
231                     .allMatch(entry -> entry.getRequestHeaders().get("Authorization") == null
232                             && entry.getRequestHeaders().get("Proxy-Authorization") == null));
233         }
234     }
235 
236     @ParameterizedTest
237     @ValueSource(booleans = {false, true})
238     void testSystemProxyCredentialsNotUsedForServer(boolean serverCredentials) throws Exception {
239         httpServer.setProxyAuthentication("testuser", "testpass");
240         httpServer.setAuthentication("testuser", "testpass");
241         try (SystemProperties properties = systemProxyProperties("http", httpServer.getHttpPort())) {
242             if (serverCredentials) {
243                 auth = new AuthenticationBuilder()
244                         .addUsername("testuser")
245                         .addPassword("testpass")
246                         .build();
247             }
248             newTransporter("http://bad.localhost:1/");
249             if (serverCredentials) {
250                 assertTransfers();
251             } else {
252                 assertGetStatus(401);
253                 assertTrue(httpServer.getLogEntries().stream()
254                         .allMatch(entry -> entry.getRequestHeaders().get("Authorization") == null));
255             }
256         }
257     }
258 
259     @ParameterizedTest
260     @ValueSource(booleans = {false, true})
261     void testSystemProxyAuthenticationThroughHttpsConnect(boolean authenticateServer) throws Exception {
262         httpServer.addHttp2ConnectorWithMutualTLS();
263         if (authenticateServer) {
264             httpServer.setAuthentication("testuser", "testpass");
265         }
266         Server proxyServer = new Server();
267         ServerConnector connector = new ServerConnector(proxyServer);
268         connector.setHost(httpServer.getHost());
269         proxyServer.addConnector(connector);
270         AtomicInteger authenticatedConnects = new AtomicInteger();
271         proxyServer.setHandler(new ConnectHandler() {
272             @Override
273             protected boolean handleAuthentication(Request request, Response response, String address) {
274                 String expected = "Basic "
275                         + Base64.getEncoder().encodeToString("testuser:testpass".getBytes(StandardCharsets.UTF_8));
276                 if (expected.equals(request.getHeaders().get("Proxy-Authorization"))) {
277                     authenticatedConnects.incrementAndGet();
278                     return true;
279                 }
280                 response.getHeaders().put("Proxy-Authenticate", "Basic realm=\"proxy\"");
281                 return false;
282             }
283         });
284         proxyServer.start();
285         try (SystemProperties properties = systemProxyProperties("https", connector.getLocalPort())) {
286             newTransporter(httpServer.getHttpsUrl());
287             if (authenticateServer) {
288                 assertGetStatus(401);
289             } else {
290                 assertTransfers();
291             }
292             assertTrue(authenticatedConnects.get() > 0);
293             assertTrue(httpServer.getLogEntries().stream()
294                     .allMatch(entry -> entry.getRequestHeaders().get("Authorization") == null
295                             && entry.getRequestHeaders().get("Proxy-Authorization") == null));
296         } finally {
297             if (transporter != null) {
298                 transporter.close();
299                 transporter = null;
300             }
301             proxyServer.stop();
302         }
303     }
304 
305     @Test
306     void testSystemProxyCredentialsNotForwardedAfterRedirectToDirectRoute() throws Exception {
307         Server proxyServer = new Server();
308         ServerConnector connector = new ServerConnector(proxyServer);
309         connector.setHost(httpServer.getHost());
310         proxyServer.addConnector(connector);
311         AtomicInteger authenticatedRequests = new AtomicInteger();
312         proxyServer.setHandler(new Handler.Abstract() {
313             @Override
314             public boolean handle(Request request, Response response, Callback callback) throws Exception {
315                 String expected = "Basic "
316                         + Base64.getEncoder().encodeToString("testuser:testpass".getBytes(StandardCharsets.UTF_8));
317                 if (expected.equals(request.getHeaders().get("Proxy-Authorization"))) {
318                     authenticatedRequests.incrementAndGet();
319                     response.setStatus(302);
320                     response.getHeaders().put("Location", httpServer.getHttpUrl() + "/repo/file.txt");
321                 } else {
322                     response.setStatus(407);
323                     response.getHeaders().put("Proxy-Authenticate", "Basic realm=\"proxy\"");
324                 }
325                 callback.succeeded();
326                 return true;
327             }
328         });
329         proxyServer.start();
330         try (SystemProperties properties = systemProxyProperties("http", connector.getLocalPort())) {
331             properties.set("http.nonProxyHosts", httpServer.getHost());
332             newTransporter("http://bad.localhost:1/");
333             GetTask task = new GetTask(URI.create("repo/file.txt"));
334             transporter.get(task);
335             assertEquals("test", task.getDataString());
336             assertTrue(authenticatedRequests.get() > 0);
337             assertTrue(httpServer.getLogEntries().stream()
338                     .allMatch(entry -> entry.getRequestHeaders().get("Authorization") == null
339                             && entry.getRequestHeaders().get("Proxy-Authorization") == null));
340         } finally {
341             if (transporter != null) {
342                 transporter.close();
343                 transporter = null;
344             }
345             proxyServer.stop();
346         }
347     }
348 
349     private void assertGetStatus(int status) {
350         HttpTransporterException failure = assertThrows(
351                 HttpTransporterException.class, () -> transporter.get(new GetTask(URI.create("repo/file.txt"))));
352         assertEquals(status, failure.getStatusCode());
353     }
354 
355     private void assertTransfers() throws Exception {
356         GetTask task = new GetTask(URI.create("repo/file.txt"));
357         transporter.get(task);
358         assertEquals("test", task.getDataString());
359         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
360         transporter.put(new PutTask(URI.create("repo/upload.txt")).setDataString("upload"));
361         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "upload.txt")));
362     }
363 
364     private SystemProperties systemProxyProperties(String protocol, int port) {
365         session.setConfigProperty(ApacheTransporterConfigurationKeys.CONFIG_PROP_USE_SYSTEM_PROPERTIES, true);
366         SystemProperties properties = new SystemProperties();
367         for (String scheme : new String[] {"http", "https"}) {
368             properties.set(scheme + ".proxyHost", httpServer.getHost());
369             properties.set(scheme + ".proxyPort", Integer.toString(port));
370             properties.set(scheme + ".proxyUser", null);
371             properties.set(scheme + ".proxyPassword", null);
372         }
373         properties.set(protocol + ".proxyUser", "testuser");
374         properties.set(protocol + ".proxyPassword", "testpass");
375         properties.set("http.nonProxyHosts", "");
376         return properties;
377     }
378 
379     private SystemProperties distinctSystemProxyCredentials(int port) {
380         SystemProperties properties = new SystemProperties();
381         for (String protocol : new String[] {"http", "https"}) {
382             properties.set(protocol + ".proxyHost", "proxy.example");
383             properties.set(protocol + ".proxyPort", Integer.toString(port));
384             properties.set(protocol + ".proxyUser", protocol + "-user");
385         }
386         return properties;
387     }
388 
389     private String selectSystemProxyUsername(String targetProtocol, int proxyPort) throws Exception {
390         HttpHost proxyHost = new HttpHost("proxy.example", proxyPort);
391         HttpClientContext context = HttpClientContext.create();
392         context.setAttribute(
393                 HttpClientContext.HTTP_ROUTE,
394                 new HttpRoute(new HttpHost("repository.example", -1, targetProtocol), proxyHost));
395         context.setCredentialsProvider(new BasicCredentialsProvider());
396         context.setRequestConfig(RequestConfig.DEFAULT);
397         context.setAuthSchemeRegistry(RegistryBuilder.<AuthSchemeProvider>create()
398                 .register(AuthSchemes.BASIC, new BasicSchemeFactory())
399                 .build());
400         Map<String, Header> challenges = Collections.singletonMap(
401                 AuthSchemes.BASIC.toLowerCase(), new BasicHeader("Proxy-Authenticate", "Basic realm=\"proxy\""));
402         Queue<AuthOption> options = new SystemProxyAuthenticationStrategy()
403                 .select(
404                         challenges,
405                         proxyHost,
406                         new BasicHttpResponse(HttpVersion.HTTP_1_1, 407, "Proxy Authentication Required"),
407                         context);
408         UsernamePasswordCredentials credentials =
409                 (UsernamePasswordCredentials) options.remove().getCredentials();
410         return credentials.getUserName();
411     }
412 
413     private static final class SystemProperties implements AutoCloseable {
414         private final Map<String, String> previous = new HashMap<>();
415 
416         void set(String key, String value) {
417             if (!previous.containsKey(key)) {
418                 previous.put(key, System.getProperty(key));
419             }
420             if (value == null) {
421                 System.clearProperty(key);
422             } else {
423                 System.setProperty(key, value);
424             }
425         }
426 
427         @Override
428         public void close() {
429             previous.forEach((key, value) -> {
430                 if (value == null) {
431                     System.clearProperty(key);
432                 } else {
433                     System.setProperty(key, value);
434                 }
435             });
436         }
437     }
438 
439     @Test
440     void testGet_WebDav() throws Exception {
441         httpServer.setWebDav(true);
442         RecordingTransportListener listener = new RecordingTransportListener();
443         GetTask task = new GetTask(URI.create("repo/dir/file.txt")).setListener(listener);
444         ((ApacheTransporter) transporter).getState().setWebDav(true);
445         transporter.get(task);
446         assertEquals("test", task.getDataString());
447         assertEquals(0L, listener.getDataOffset());
448         assertEquals(4L, listener.getDataLength());
449         assertEquals(1, listener.getStartedCount());
450         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
451         assertEquals(task.getDataString(), new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8));
452         assertEquals(
453                 1, httpServer.getLogEntries().size(), httpServer.getLogEntries().toString());
454     }
455 
456     @Test
457     void testPut_WebDav() throws Exception {
458         httpServer.setWebDav(true);
459         session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true);
460         newTransporter(httpServer.getHttpUrl());
461 
462         RecordingTransportListener listener = new RecordingTransportListener();
463         PutTask task = new PutTask(URI.create("repo/dir1/dir2/file.txt"))
464                 .setListener(listener)
465                 .setDataString("upload");
466         transporter.put(task);
467         assertEquals(0L, listener.getDataOffset());
468         assertEquals(6L, listener.getDataLength());
469         assertEquals(1, listener.getStartedCount());
470         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
471         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "dir1/dir2/file.txt")));
472 
473         assertEquals(
474                 5, httpServer.getLogEntries().size(), "Expected 5 requests but got: " + httpServer.getLogEntries());
475         assertEquals("OPTIONS", httpServer.getLogEntries().get(0).getMethod());
476         assertEquals("MKCOL", httpServer.getLogEntries().get(1).getMethod());
477         assertEquals("/repo/dir1/dir2/", httpServer.getLogEntries().get(1).getPath());
478         assertEquals("MKCOL", httpServer.getLogEntries().get(2).getMethod());
479         assertEquals("/repo/dir1/", httpServer.getLogEntries().get(2).getPath());
480         assertEquals("MKCOL", httpServer.getLogEntries().get(3).getMethod());
481         assertEquals("/repo/dir1/dir2/", httpServer.getLogEntries().get(3).getPath());
482         assertEquals("PUT", httpServer.getLogEntries().get(4).getMethod());
483     }
484 
485     @Test
486     void testConnectionReuse() throws Exception {
487         httpServer.addHttp2ConnectorWithMutualTLS();
488         session.setCache(new DefaultRepositoryCache());
489         for (int i = 0; i < 3; i++) {
490             newTransporter(httpServer.getHttpsUrl());
491             GetTask task = new GetTask(URI.create("repo/file.txt"));
492             transporter.get(task);
493             assertEquals("test", task.getDataString());
494         }
495         PoolStats stats = ((ConnPoolControl<?>)
496                         ((ApacheTransporter) transporter).getState().getConnectionManager())
497                 .getTotalStats();
498         assertEquals(1, stats.getAvailable(), stats.toString());
499     }
500 
501     @Test
502     void testConnectionNoReuse() throws Exception {
503         httpServer.addHttp2ConnectorWithMutualTLS();
504         session.setCache(new DefaultRepositoryCache());
505         session.setConfigProperty(ConfigurationProperties.HTTP_REUSE_CONNECTIONS, false);
506         for (int i = 0; i < 3; i++) {
507             newTransporter(httpServer.getHttpsUrl());
508             GetTask task = new GetTask(URI.create("repo/file.txt"));
509             transporter.get(task);
510             assertEquals("test", task.getDataString());
511         }
512         PoolStats stats = ((ConnPoolControl<?>)
513                         ((ApacheTransporter) transporter).getState().getConnectionManager())
514                 .getTotalStats();
515         assertEquals(0, stats.getAvailable(), stats.toString());
516     }
517 }