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