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