1 package org.eclipse.aether.transport.http;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InterruptedIOException;
25 import java.io.OutputStream;
26 import java.net.URI;
27 import java.net.URISyntaxException;
28 import java.util.Collections;
29 import java.util.Date;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
34
35 import org.apache.http.Header;
36 import org.apache.http.HttpEntity;
37 import org.apache.http.HttpEntityEnclosingRequest;
38 import org.apache.http.HttpHeaders;
39 import org.apache.http.HttpHost;
40 import org.apache.http.HttpResponse;
41 import org.apache.http.HttpStatus;
42 import org.apache.http.auth.AuthScope;
43 import org.apache.http.auth.params.AuthParams;
44 import org.apache.http.client.CredentialsProvider;
45 import org.apache.http.client.HttpClient;
46 import org.apache.http.client.HttpResponseException;
47 import org.apache.http.client.methods.HttpGet;
48 import org.apache.http.client.methods.HttpHead;
49 import org.apache.http.client.methods.HttpOptions;
50 import org.apache.http.client.methods.HttpPut;
51 import org.apache.http.client.methods.HttpUriRequest;
52 import org.apache.http.client.utils.DateUtils;
53 import org.apache.http.client.utils.URIUtils;
54 import org.apache.http.conn.params.ConnRouteParams;
55 import org.apache.http.entity.AbstractHttpEntity;
56 import org.apache.http.entity.ByteArrayEntity;
57 import org.apache.http.impl.client.DecompressingHttpClient;
58 import org.apache.http.impl.client.DefaultHttpClient;
59 import org.apache.http.params.HttpConnectionParams;
60 import org.apache.http.params.HttpParams;
61 import org.apache.http.params.HttpProtocolParams;
62 import org.apache.http.util.EntityUtils;
63 import org.eclipse.aether.ConfigurationProperties;
64 import org.eclipse.aether.RepositorySystemSession;
65 import org.eclipse.aether.repository.AuthenticationContext;
66 import org.eclipse.aether.repository.Proxy;
67 import org.eclipse.aether.repository.RemoteRepository;
68 import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
69 import org.eclipse.aether.spi.connector.transport.GetTask;
70 import org.eclipse.aether.spi.connector.transport.PeekTask;
71 import org.eclipse.aether.spi.connector.transport.PutTask;
72 import org.eclipse.aether.spi.connector.transport.TransportTask;
73 import org.eclipse.aether.spi.log.Logger;
74 import org.eclipse.aether.transfer.NoTransporterException;
75 import org.eclipse.aether.transfer.TransferCancelledException;
76 import org.eclipse.aether.util.ConfigUtils;
77
78
79
80
81 final class HttpTransporter
82 extends AbstractTransporter
83 {
84
85 private static final Pattern CONTENT_RANGE_PATTERN =
86 Pattern.compile( "\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*" );
87
88 private final Logger logger;
89
90 private final AuthenticationContext repoAuthContext;
91
92 private final AuthenticationContext proxyAuthContext;
93
94 private final URI baseUri;
95
96 private final HttpHost server;
97
98 private final HttpHost proxy;
99
100 private final HttpClient client;
101
102 private final Map<?, ?> headers;
103
104 private final LocalState state;
105
106 HttpTransporter( RemoteRepository repository, RepositorySystemSession session, Logger logger )
107 throws NoTransporterException
108 {
109 if ( !"http".equalsIgnoreCase( repository.getProtocol() )
110 && !"https".equalsIgnoreCase( repository.getProtocol() ) )
111 {
112 throw new NoTransporterException( repository );
113 }
114 this.logger = logger;
115 try
116 {
117 baseUri = new URI( repository.getUrl() ).parseServerAuthority();
118 if ( baseUri.isOpaque() )
119 {
120 throw new URISyntaxException( repository.getUrl(), "URL must not be opaque" );
121 }
122 server = URIUtils.extractHost( baseUri );
123 if ( server == null )
124 {
125 throw new URISyntaxException( repository.getUrl(), "URL lacks host name" );
126 }
127 }
128 catch ( URISyntaxException e )
129 {
130 throw new NoTransporterException( repository, e.getMessage(), e );
131 }
132 proxy = toHost( repository.getProxy() );
133
134 repoAuthContext = AuthenticationContext.forRepository( session, repository );
135 proxyAuthContext = AuthenticationContext.forProxy( session, repository );
136
137 state = new LocalState( session, repository, new SslConfig( session, repoAuthContext ) );
138
139 headers =
140 ConfigUtils.getMap( session, Collections.emptyMap(), ConfigurationProperties.HTTP_HEADERS + "."
141 + repository.getId(), ConfigurationProperties.HTTP_HEADERS );
142
143 DefaultHttpClient client = new DefaultHttpClient( state.getConnectionManager() );
144
145 configureClient( client.getParams(), session, repository, proxy );
146
147 client.setCredentialsProvider( toCredentialsProvider( server, repoAuthContext, proxy, proxyAuthContext ) );
148
149 this.client = new DecompressingHttpClient( client );
150 }
151
152 private static HttpHost toHost( Proxy proxy )
153 {
154 HttpHost host = null;
155 if ( proxy != null )
156 {
157 host = new HttpHost( proxy.getHost(), proxy.getPort() );
158 }
159 return host;
160 }
161
162 private static void configureClient( HttpParams params, RepositorySystemSession session,
163 RemoteRepository repository, HttpHost proxy )
164 {
165 AuthParams.setCredentialCharset( params,
166 ConfigUtils.getString( session,
167 ConfigurationProperties.DEFAULT_HTTP_CREDENTIAL_ENCODING,
168 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING + "."
169 + repository.getId(),
170 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING ) );
171 ConnRouteParams.setDefaultProxy( params, proxy );
172 HttpConnectionParams.setConnectionTimeout( params,
173 ConfigUtils.getInteger( session,
174 ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
175 ConfigurationProperties.CONNECT_TIMEOUT
176 + "." + repository.getId(),
177 ConfigurationProperties.CONNECT_TIMEOUT ) );
178 HttpConnectionParams.setSoTimeout( params,
179 ConfigUtils.getInteger( session,
180 ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
181 ConfigurationProperties.REQUEST_TIMEOUT + "."
182 + repository.getId(),
183 ConfigurationProperties.REQUEST_TIMEOUT ) );
184 HttpProtocolParams.setUserAgent( params, ConfigUtils.getString( session,
185 ConfigurationProperties.DEFAULT_USER_AGENT,
186 ConfigurationProperties.USER_AGENT ) );
187 }
188
189 private static CredentialsProvider toCredentialsProvider( HttpHost server, AuthenticationContext serverAuthCtx,
190 HttpHost proxy, AuthenticationContext proxyAuthCtx )
191 {
192 CredentialsProvider provider = toCredentialsProvider( server.getHostName(), AuthScope.ANY_PORT, serverAuthCtx );
193 if ( proxy != null )
194 {
195 CredentialsProvider p = toCredentialsProvider( proxy.getHostName(), proxy.getPort(), proxyAuthCtx );
196 provider = new DemuxCredentialsProvider( provider, p, proxy );
197 }
198 return provider;
199 }
200
201 private static CredentialsProvider toCredentialsProvider( String host, int port, AuthenticationContext ctx )
202 {
203 DeferredCredentialsProvider provider = new DeferredCredentialsProvider();
204 if ( ctx != null )
205 {
206 AuthScope basicScope = new AuthScope( host, port );
207 provider.setCredentials( basicScope, new DeferredCredentialsProvider.BasicFactory( ctx ) );
208
209 AuthScope ntlmScope = new AuthScope( host, port, AuthScope.ANY_REALM, "ntlm" );
210 provider.setCredentials( ntlmScope, new DeferredCredentialsProvider.NtlmFactory( ctx ) );
211 }
212 return provider;
213 }
214
215 LocalState getState()
216 {
217 return state;
218 }
219
220 private URI resolve( TransportTask task )
221 {
222 return UriUtils.resolve( baseUri, task.getLocation() );
223 }
224
225 public int classify( Throwable error )
226 {
227 if ( error instanceof HttpResponseException
228 && ( (HttpResponseException) error ).getStatusCode() == HttpStatus.SC_NOT_FOUND )
229 {
230 return ERROR_NOT_FOUND;
231 }
232 return ERROR_OTHER;
233 }
234
235 @Override
236 protected void implPeek( PeekTask task )
237 throws Exception
238 {
239 HttpHead request = commonHeaders( new HttpHead( resolve( task ) ) );
240 execute( request, null );
241 }
242
243 @Override
244 protected void implGet( GetTask task )
245 throws Exception
246 {
247 EntityGetter getter = new EntityGetter( task );
248 HttpGet request = commonHeaders( new HttpGet( resolve( task ) ) );
249 resume( request, task );
250 try
251 {
252 execute( request, getter );
253 }
254 catch ( HttpResponseException e )
255 {
256 if ( e.getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED && request.containsHeader( HttpHeaders.RANGE ) )
257 {
258 request = commonHeaders( new HttpGet( request.getURI() ) );
259 execute( request, getter );
260 return;
261 }
262 throw e;
263 }
264 }
265
266 @Override
267 protected void implPut( PutTask task )
268 throws Exception
269 {
270 PutTaskEntity entity = new PutTaskEntity( task );
271 HttpPut request = commonHeaders( entity( new HttpPut( resolve( task ) ), entity ) );
272 try
273 {
274 execute( request, null );
275 }
276 catch ( HttpResponseException e )
277 {
278 if ( e.getStatusCode() == HttpStatus.SC_EXPECTATION_FAILED && request.containsHeader( HttpHeaders.EXPECT ) )
279 {
280 state.setExpectContinue( false );
281 request = commonHeaders( entity( new HttpPut( request.getURI() ), entity ) );
282 execute( request, null );
283 return;
284 }
285 throw e;
286 }
287 }
288
289 private void execute( HttpUriRequest request, EntityGetter getter )
290 throws Exception
291 {
292 try
293 {
294 SharingHttpContext context = new SharingHttpContext( state );
295 prepare( request, context );
296 HttpResponse response = client.execute( server, request, context );
297 try
298 {
299 context.close();
300 handleStatus( response );
301 if ( getter != null )
302 {
303 getter.handle( response );
304 }
305 }
306 finally
307 {
308 EntityUtils.consumeQuietly( response.getEntity() );
309 }
310 }
311 catch ( IOException e )
312 {
313 if ( e.getCause() instanceof TransferCancelledException )
314 {
315 throw (Exception) e.getCause();
316 }
317 throw e;
318 }
319 }
320
321 private void prepare( HttpUriRequest request, SharingHttpContext context )
322 {
323 boolean put = HttpPut.METHOD_NAME.equalsIgnoreCase( request.getMethod() );
324 if ( state.getWebDav() == null && ( put || isPayloadPresent( request ) ) )
325 {
326 try
327 {
328 HttpOptions req = commonHeaders( new HttpOptions( request.getURI() ) );
329 HttpResponse response = client.execute( server, req, context );
330 state.setWebDav( isWebDav( response ) );
331 EntityUtils.consumeQuietly( response.getEntity() );
332 }
333 catch ( IOException e )
334 {
335 logger.debug( "Failed to prepare HTTP context", e );
336 }
337 }
338 if ( put && Boolean.TRUE.equals( state.getWebDav() ) )
339 {
340 mkdirs( request.getURI(), context );
341 }
342 }
343
344 private boolean isWebDav( HttpResponse response )
345 {
346 return response.containsHeader( HttpHeaders.DAV );
347 }
348
349 private void mkdirs( URI uri, SharingHttpContext context )
350 {
351 List<URI> dirs = UriUtils.getDirectories( baseUri, uri );
352 int index = 0;
353 for ( ; index < dirs.size(); index++ )
354 {
355 try
356 {
357 HttpResponse response =
358 client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
359 try
360 {
361 int status = response.getStatusLine().getStatusCode();
362 if ( status < 300 || status == HttpStatus.SC_METHOD_NOT_ALLOWED )
363 {
364 break;
365 }
366 else if ( status == HttpStatus.SC_CONFLICT )
367 {
368 continue;
369 }
370 handleStatus( response );
371 }
372 finally
373 {
374 EntityUtils.consumeQuietly( response.getEntity() );
375 }
376 }
377 catch ( IOException e )
378 {
379 logger.debug( "Failed to create parent directory " + dirs.get( index ), e );
380 return;
381 }
382 }
383 for ( index--; index >= 0; index-- )
384 {
385 try
386 {
387 HttpResponse response =
388 client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
389 try
390 {
391 handleStatus( response );
392 }
393 finally
394 {
395 EntityUtils.consumeQuietly( response.getEntity() );
396 }
397 }
398 catch ( IOException e )
399 {
400 logger.debug( "Failed to create parent directory " + dirs.get( index ), e );
401 return;
402 }
403 }
404 }
405
406 private <T extends HttpEntityEnclosingRequest> T entity( T request, HttpEntity entity )
407 {
408 request.setEntity( entity );
409 return request;
410 }
411
412 private boolean isPayloadPresent( HttpUriRequest request )
413 {
414 if ( request instanceof HttpEntityEnclosingRequest )
415 {
416 HttpEntity entity = ( (HttpEntityEnclosingRequest) request ).getEntity();
417 return entity != null && entity.getContentLength() != 0;
418 }
419 return false;
420 }
421
422 private <T extends HttpUriRequest> T commonHeaders( T request )
423 {
424 request.setHeader( HttpHeaders.CACHE_CONTROL, "no-cache, no-store" );
425 request.setHeader( HttpHeaders.PRAGMA, "no-cache" );
426
427 if ( state.isExpectContinue() && isPayloadPresent( request ) )
428 {
429 request.setHeader( HttpHeaders.EXPECT, "100-continue" );
430 }
431
432 for ( Map.Entry<?, ?> entry : headers.entrySet() )
433 {
434 if ( !( entry.getKey() instanceof String ) )
435 {
436 continue;
437 }
438 if ( entry.getValue() instanceof String )
439 {
440 request.setHeader( entry.getKey().toString(), entry.getValue().toString() );
441 }
442 else
443 {
444 request.removeHeaders( entry.getKey().toString() );
445 }
446 }
447
448 if ( !state.isExpectContinue() )
449 {
450 request.removeHeaders( HttpHeaders.EXPECT );
451 }
452
453 return request;
454 }
455
456 private <T extends HttpUriRequest> T resume( T request, GetTask task )
457 {
458 long resumeOffset = task.getResumeOffset();
459 if ( resumeOffset > 0L && task.getDataFile() != null )
460 {
461 request.setHeader( HttpHeaders.RANGE, "bytes=" + Long.toString( resumeOffset ) + '-' );
462 request.setHeader( HttpHeaders.IF_UNMODIFIED_SINCE,
463 DateUtils.formatDate( new Date( task.getDataFile().lastModified() - 60L * 1000L ) ) );
464 request.setHeader( HttpHeaders.ACCEPT_ENCODING, "identity" );
465 }
466 return request;
467 }
468
469 private void handleStatus( HttpResponse response )
470 throws HttpResponseException
471 {
472 int status = response.getStatusLine().getStatusCode();
473 if ( status >= 300 )
474 {
475 throw new HttpResponseException( status, response.getStatusLine().getReasonPhrase() + " (" + status + ")" );
476 }
477 }
478
479 @Override
480 protected void implClose()
481 {
482 AuthenticationContext.close( repoAuthContext );
483 AuthenticationContext.close( proxyAuthContext );
484 state.close();
485 }
486
487 private class EntityGetter
488 {
489
490 private final GetTask task;
491
492 EntityGetter( GetTask task )
493 {
494 this.task = task;
495 }
496
497 public void handle( HttpResponse response )
498 throws IOException, TransferCancelledException
499 {
500 HttpEntity entity = response.getEntity();
501 if ( entity == null )
502 {
503 entity = new ByteArrayEntity( new byte[0] );
504 }
505
506 long offset = 0L, length = entity.getContentLength();
507 String range = getHeader( response, HttpHeaders.CONTENT_RANGE );
508 if ( range != null )
509 {
510 Matcher m = CONTENT_RANGE_PATTERN.matcher( range );
511 if ( !m.matches() )
512 {
513 throw new IOException( "Invalid Content-Range header for partial download: " + range );
514 }
515 offset = Long.parseLong( m.group( 1 ) );
516 length = Long.parseLong( m.group( 2 ) ) + 1L;
517 if ( offset < 0L || offset >= length || ( offset > 0L && offset != task.getResumeOffset() ) )
518 {
519 throw new IOException( "Invalid Content-Range header for partial download from offset "
520 + task.getResumeOffset() + ": " + range );
521 }
522 }
523
524 InputStream is = entity.getContent();
525 utilGet( task, is, true, length, offset > 0L );
526 extractChecksums( response );
527 }
528
529 private void extractChecksums( HttpResponse response )
530 {
531
532 String etag = getHeader( response, HttpHeaders.ETAG );
533 if ( etag != null )
534 {
535 int start = etag.indexOf( "SHA1{" ), end = etag.indexOf( "}", start + 5 );
536 if ( start >= 0 && end > start )
537 {
538 task.setChecksum( "SHA-1", etag.substring( start + 5, end ) );
539 }
540 }
541 }
542
543 private String getHeader( HttpResponse response, String name )
544 {
545 Header header = response.getFirstHeader( name );
546 return ( header != null ) ? header.getValue() : null;
547 }
548
549 }
550
551 private class PutTaskEntity
552 extends AbstractHttpEntity
553 {
554
555 private final PutTask task;
556
557 PutTaskEntity( PutTask task )
558 {
559 this.task = task;
560 }
561
562 public boolean isRepeatable()
563 {
564 return true;
565 }
566
567 public boolean isStreaming()
568 {
569 return false;
570 }
571
572 public long getContentLength()
573 {
574 return task.getDataLength();
575 }
576
577 public InputStream getContent()
578 throws IOException
579 {
580 return task.newInputStream();
581 }
582
583 public void writeTo( OutputStream os )
584 throws IOException
585 {
586 try
587 {
588 utilPut( task, os, false );
589 }
590 catch ( TransferCancelledException e )
591 {
592 throw (IOException) new InterruptedIOException().initCause( e );
593 }
594 }
595
596 }
597
598 }