1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
82
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
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
115
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 }