View Javadoc
1   package org.eclipse.aether.transport.http;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   * 
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   * 
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.UnsupportedEncodingException;
27  import java.nio.charset.StandardCharsets;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.Enumeration;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.TreeMap;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  
37  import javax.servlet.http.HttpServletRequest;
38  import javax.servlet.http.HttpServletResponse;
39  
40  import org.eclipse.aether.util.ChecksumUtils;
41  import org.eclipse.jetty.http.HttpHeaders;
42  import org.eclipse.jetty.http.HttpMethods;
43  import org.eclipse.jetty.server.Connector;
44  import org.eclipse.jetty.server.Request;
45  import org.eclipse.jetty.server.Server;
46  import org.eclipse.jetty.server.handler.AbstractHandler;
47  import org.eclipse.jetty.server.handler.HandlerList;
48  import org.eclipse.jetty.server.nio.SelectChannelConnector;
49  import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
50  import org.eclipse.jetty.util.B64Code;
51  import org.eclipse.jetty.util.IO;
52  import org.eclipse.jetty.util.StringUtil;
53  import org.eclipse.jetty.util.URIUtil;
54  import org.eclipse.jetty.util.ssl.SslContextFactory;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  public class HttpServer
59  {
60  
61      public static class LogEntry
62      {
63  
64          public final String method;
65  
66          public final String path;
67  
68          public final Map<String, String> headers;
69  
70          public LogEntry( String method, String path, Map<String, String> headers )
71          {
72              this.method = method;
73              this.path = path;
74              this.headers = headers;
75          }
76  
77          @Override
78          public String toString()
79          {
80              return method + " " + path;
81          }
82  
83      }
84  
85      public enum ExpectContinue
86      {
87          FAIL, PROPER, BROKEN
88      }
89  
90      public enum ChecksumHeader
91      {
92          NEXUS
93      }
94  
95      private static final Logger log = LoggerFactory.getLogger( HttpServer.class );
96  
97      private File repoDir;
98  
99      private boolean rangeSupport = true;
100 
101     private boolean webDav;
102 
103     private ExpectContinue expectContinue = ExpectContinue.PROPER;
104 
105     private ChecksumHeader checksumHeader;
106 
107     private Server server;
108 
109     private Connector httpConnector;
110 
111     private Connector httpsConnector;
112 
113     private String username;
114 
115     private String password;
116 
117     private String proxyUsername;
118 
119     private String proxyPassword;
120 
121     private List<LogEntry> logEntries = Collections.synchronizedList( new ArrayList<LogEntry>() );
122 
123     public String getHost()
124     {
125         return "localhost";
126     }
127 
128     public int getHttpPort()
129     {
130         return httpConnector != null ? httpConnector.getLocalPort() : -1;
131     }
132 
133     public int getHttpsPort()
134     {
135         return httpsConnector != null ? httpsConnector.getLocalPort() : -1;
136     }
137 
138     public String getHttpUrl()
139     {
140         return "http://" + getHost() + ":" + getHttpPort();
141     }
142 
143     public String getHttpsUrl()
144     {
145         return "https://" + getHost() + ":" + getHttpsPort();
146     }
147 
148     public HttpServer addSslConnector()
149     {
150         if ( httpsConnector == null )
151         {
152             SslContextFactory ssl = new SslContextFactory();
153             ssl.setKeyStorePath( new File( "src/test/resources/ssl/server-store" ).getAbsolutePath() );
154             ssl.setKeyStorePassword( "server-pwd" );
155             ssl.setTrustStore( new File( "src/test/resources/ssl/client-store" ).getAbsolutePath() );
156             ssl.setTrustStorePassword( "client-pwd" );
157             ssl.setNeedClientAuth( true );
158             httpsConnector = new SslSelectChannelConnector( ssl );
159             server.addConnector( httpsConnector );
160             try
161             {
162                 httpsConnector.start();
163             }
164             catch ( Exception e )
165             {
166                 throw new IllegalStateException( e );
167             }
168         }
169         return this;
170     }
171 
172     public List<LogEntry> getLogEntries()
173     {
174         return logEntries;
175     }
176 
177     public HttpServer setRepoDir( File repoDir )
178     {
179         this.repoDir = repoDir;
180         return this;
181     }
182 
183     public HttpServer setRangeSupport( boolean rangeSupport )
184     {
185         this.rangeSupport = rangeSupport;
186         return this;
187     }
188 
189     public HttpServer setWebDav( boolean webDav )
190     {
191         this.webDav = webDav;
192         return this;
193     }
194 
195     public HttpServer setExpectSupport( ExpectContinue expectContinue )
196     {
197         this.expectContinue = expectContinue;
198         return this;
199     }
200 
201     public HttpServer setChecksumHeader( ChecksumHeader checksumHeader )
202     {
203         this.checksumHeader = checksumHeader;
204         return this;
205     }
206 
207     public HttpServer setAuthentication( String username, String password )
208     {
209         this.username = username;
210         this.password = password;
211         return this;
212     }
213 
214     public HttpServer setProxyAuthentication( String username, String password )
215     {
216         proxyUsername = username;
217         proxyPassword = password;
218         return this;
219     }
220 
221     public HttpServer start()
222         throws Exception
223     {
224         if ( server != null )
225         {
226             return this;
227         }
228 
229         httpConnector = new SelectChannelConnector();
230 
231         HandlerList handlers = new HandlerList();
232         handlers.addHandler( new LogHandler() );
233         handlers.addHandler( new ProxyAuthHandler() );
234         handlers.addHandler( new AuthHandler() );
235         handlers.addHandler( new RedirectHandler() );
236         handlers.addHandler( new RepoHandler() );
237 
238         server = new Server();
239         server.addConnector( httpConnector );
240         server.setHandler( handlers );
241         server.start();
242 
243         return this;
244     }
245 
246     public void stop()
247         throws Exception
248     {
249         if ( server != null )
250         {
251             server.stop();
252             server = null;
253             httpConnector = null;
254             httpsConnector = null;
255         }
256     }
257 
258     private class LogHandler
259         extends AbstractHandler
260     {
261 
262         @SuppressWarnings( "unchecked" )
263         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
264             throws IOException
265         {
266             log.info( "{} {}{}", new Object[] { req.getMethod(), req.getRequestURL(),
267                 req.getQueryString() != null ? "?" + req.getQueryString() : "" } );
268 
269             Map<String, String> headers = new TreeMap<String, String>( String.CASE_INSENSITIVE_ORDER );
270             for ( Enumeration<String> en = req.getHeaderNames(); en.hasMoreElements(); )
271             {
272                 String name = en.nextElement();
273                 StringBuilder buffer = new StringBuilder( 128 );
274                 for ( Enumeration<String> ien = req.getHeaders( name ); ien.hasMoreElements(); )
275                 {
276                     if ( buffer.length() > 0 )
277                     {
278                         buffer.append( ", " );
279                     }
280                     buffer.append( ien.nextElement() );
281                 }
282                 headers.put( name, buffer.toString() );
283             }
284             logEntries.add( new LogEntry( req.getMethod(), req.getPathInfo(), Collections.unmodifiableMap( headers ) ) );
285         }
286 
287     }
288 
289     private class RepoHandler
290         extends AbstractHandler
291     {
292 
293         private final Pattern SIMPLE_RANGE = Pattern.compile( "bytes=([0-9])+-" );
294 
295         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
296             throws IOException
297         {
298             String path = req.getPathInfo().substring( 1 );
299 
300             if ( !path.startsWith( "repo/" ) )
301             {
302                 return;
303             }
304             req.setHandled( true );
305 
306             if ( ExpectContinue.FAIL.equals( expectContinue ) && request.getHeader( HttpHeaders.EXPECT ) != null )
307             {
308                 response.setStatus( HttpServletResponse.SC_EXPECTATION_FAILED );
309                 return;
310             }
311 
312             File file = new File( repoDir, path.substring( 5 ) );
313             if ( HttpMethods.GET.equals( req.getMethod() ) || HttpMethods.HEAD.equals( req.getMethod() ) )
314             {
315                 if ( !file.isFile() || path.endsWith( URIUtil.SLASH ) )
316                 {
317                     response.setStatus( HttpServletResponse.SC_NOT_FOUND );
318                     return;
319                 }
320                 long ifUnmodifiedSince = request.getDateHeader( HttpHeaders.IF_UNMODIFIED_SINCE );
321                 if ( ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince )
322                 {
323                     response.setStatus( HttpServletResponse.SC_PRECONDITION_FAILED );
324                     return;
325                 }
326                 long offset = 0L;
327                 String range = request.getHeader( HttpHeaders.RANGE );
328                 if ( range != null && rangeSupport )
329                 {
330                     Matcher m = SIMPLE_RANGE.matcher( range );
331                     if ( m.matches() )
332                     {
333                         offset = Long.parseLong( m.group( 1 ) );
334                         if ( offset >= file.length() )
335                         {
336                             response.setStatus( HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE );
337                             return;
338                         }
339                     }
340                     String encoding = request.getHeader( HttpHeaders.ACCEPT_ENCODING );
341                     if ( ( encoding != null && !"identity".equals( encoding ) ) || ifUnmodifiedSince == -1L )
342                     {
343                         response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
344                         return;
345                     }
346                 }
347                 response.setStatus( ( offset > 0L ) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK );
348                 response.setDateHeader( HttpHeaders.LAST_MODIFIED, file.lastModified() );
349                 response.setHeader( HttpHeaders.CONTENT_LENGTH, Long.toString( file.length() - offset ) );
350                 if ( offset > 0L )
351                 {
352                     response.setHeader( HttpHeaders.CONTENT_RANGE, "bytes " + offset + "-" + ( file.length() - 1L )
353                         + "/" + file.length() );
354                 }
355                 if ( checksumHeader != null )
356                 {
357                     Map<String, Object> checksums = ChecksumUtils.calc( file, Collections.singleton( "SHA-1" ) );
358                     switch ( checksumHeader )
359                     {
360                         case NEXUS:
361                             response.setHeader( HttpHeaders.ETAG, "{SHA1{" + checksums.get( "SHA-1" ) + "}}" );
362                             break;
363                     }
364                 }
365                 if ( HttpMethods.HEAD.equals( req.getMethod() ) )
366                 {
367                     return;
368                 }
369                 FileInputStream is = null;
370                 try
371                 {
372                     is = new FileInputStream( file );
373                     if ( offset > 0L )
374                     {
375                         long skipped = is.skip( offset );
376                         while ( skipped < offset && is.read() >= 0 )
377                         {
378                             skipped++;
379                         }
380                     }
381                     IO.copy( is, response.getOutputStream() );
382                     is.close();
383                     is = null;
384                 }
385                 finally
386                 {
387                     try
388                     {
389                         if ( is != null )
390                         {
391                             is.close();
392                         }
393                     }
394                     catch ( final IOException e )
395                     {
396                         // Suppressed due to an exception already thrown in the try block.
397                     }
398                 }
399             }
400             else if ( HttpMethods.PUT.equals( req.getMethod() ) )
401             {
402                 if ( !webDav )
403                 {
404                     file.getParentFile().mkdirs();
405                 }
406                 if ( file.getParentFile().exists() )
407                 {
408                     try
409                     {
410                         FileOutputStream os = null;
411                         try
412                         {
413                             os = new FileOutputStream( file );
414                             IO.copy( request.getInputStream(), os );
415                             os.close();
416                             os = null;
417                         }
418                         finally
419                         {
420                             try
421                             {
422                                 if ( os != null )
423                                 {
424                                     os.close();
425                                 }
426                             }
427                             catch ( final IOException e )
428                             {
429                                 // Suppressed due to an exception already thrown in the try block.
430                             }
431                         }
432                     }
433                     catch ( IOException e )
434                     {
435                         file.delete();
436                         throw e;
437                     }
438                     response.setStatus( HttpServletResponse.SC_NO_CONTENT );
439                 }
440                 else
441                 {
442                     response.setStatus( HttpServletResponse.SC_FORBIDDEN );
443                 }
444             }
445             else if ( HttpMethods.OPTIONS.equals( req.getMethod() ) )
446             {
447                 if ( webDav )
448                 {
449                     response.setHeader( "DAV", "1,2" );
450                 }
451                 response.setHeader( HttpHeaders.ALLOW, "GET, PUT, HEAD, OPTIONS" );
452                 response.setStatus( HttpServletResponse.SC_OK );
453             }
454             else if ( webDav && "MKCOL".equals( req.getMethod() ) )
455             {
456                 if ( file.exists() )
457                 {
458                     response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
459                 }
460                 else if ( file.mkdir() )
461                 {
462                     response.setStatus( HttpServletResponse.SC_CREATED );
463                 }
464                 else
465                 {
466                     response.setStatus( HttpServletResponse.SC_CONFLICT );
467                 }
468             }
469             else
470             {
471                 response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
472             }
473         }
474 
475     }
476 
477     private class RedirectHandler
478         extends AbstractHandler
479     {
480 
481         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
482             throws IOException
483         {
484             String path = req.getPathInfo();
485             if ( !path.startsWith( "/redirect/" ) )
486             {
487                 return;
488             }
489             req.setHandled( true );
490             StringBuilder location = new StringBuilder( 128 );
491             String scheme = req.getParameter( "scheme" );
492             location.append( scheme != null ? scheme : req.getScheme() );
493             location.append( "://" );
494             location.append( req.getServerName() );
495             location.append( ":" );
496             if ( "http".equalsIgnoreCase( scheme ) )
497             {
498                 location.append( getHttpPort() );
499             }
500             else if ( "https".equalsIgnoreCase( scheme ) )
501             {
502                 location.append( getHttpsPort() );
503             }
504             else
505             {
506                 location.append( req.getServerPort() );
507             }
508             location.append( "/repo" ).append( path.substring( 9 ) );
509             response.setStatus( HttpServletResponse.SC_MOVED_PERMANENTLY );
510             response.setHeader( HttpHeaders.LOCATION, location.toString() );
511         }
512 
513     }
514 
515     private class AuthHandler
516         extends AbstractHandler
517     {
518 
519         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
520             throws IOException
521         {
522             if ( ExpectContinue.BROKEN.equals( expectContinue )
523                 && "100-continue".equalsIgnoreCase( request.getHeader( HttpHeaders.EXPECT ) ) )
524             {
525                 request.getInputStream();
526             }
527 
528             if ( username != null && password != null )
529             {
530                 if ( checkBasicAuth( request.getHeader( HttpHeaders.AUTHORIZATION ), username, password ) )
531                 {
532                     return;
533                 }
534                 req.setHandled( true );
535                 response.setHeader( HttpHeaders.WWW_AUTHENTICATE, "basic realm=\"Test-Realm\"" );
536                 response.setStatus( HttpServletResponse.SC_UNAUTHORIZED );
537             }
538         }
539 
540     }
541 
542     private class ProxyAuthHandler
543         extends AbstractHandler
544     {
545 
546         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
547             throws IOException
548         {
549             if ( proxyUsername != null && proxyPassword != null )
550             {
551                 if ( checkBasicAuth( request.getHeader( HttpHeaders.PROXY_AUTHORIZATION ), proxyUsername, proxyPassword ) )
552                 {
553                     return;
554                 }
555                 req.setHandled( true );
556                 response.setHeader( HttpHeaders.PROXY_AUTHENTICATE, "basic realm=\"Test-Realm\"" );
557                 response.setStatus( HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED );
558             }
559         }
560 
561     }
562 
563     static boolean checkBasicAuth( String credentials, String username, String password )
564     {
565         if ( credentials != null )
566         {
567             int space = credentials.indexOf( ' ' );
568             if ( space > 0 )
569             {
570                 String method = credentials.substring( 0, space );
571                 if ( "basic".equalsIgnoreCase( method ) )
572                 {
573                     credentials = credentials.substring( space + 1 );
574                     try
575                     {
576                         credentials = B64Code.decode( credentials, StringUtil.__ISO_8859_1 );
577                     }
578                     catch ( UnsupportedEncodingException e )
579                     {
580                         throw new IllegalStateException( e );
581                     }
582                     int i = credentials.indexOf( ':' );
583                     if ( i > 0 )
584                     {
585                         String user = credentials.substring( 0, i );
586                         String pass = credentials.substring( i + 1 );
587                         if ( username.equals( user ) && password.equals( pass ) )
588                         {
589                             return true;
590                         }
591                     }
592                 }
593             }
594         }
595         return false;
596     }
597 
598 }