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