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 LOGGER = 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.Server ssl = new SslContextFactory.Server();
149             ssl.setNeedClientAuth( true );
150             ssl.setKeyStorePath( new File( "src/test/resources/ssl/server-store" ).getAbsolutePath() );
151             ssl.setKeyStorePassword( "server-pwd" );
152             ssl.setTrustStorePath( new File( "src/test/resources/ssl/client-store" ).getAbsolutePath() );
153             ssl.setTrustStorePassword( "client-pwd" );
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         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
258         {
259             LOGGER.info( "{} {}{}", req.getMethod(), req.getRequestURL(),
260                     req.getQueryString() != null ? "?" + req.getQueryString() : "");
261 
262             Map<String, String> headers = new TreeMap<>( String.CASE_INSENSITIVE_ORDER );
263             for ( Enumeration<String> en = req.getHeaderNames(); en.hasMoreElements(); )
264             {
265                 String name = en.nextElement();
266                 StringBuilder buffer = new StringBuilder( 128 );
267                 for ( Enumeration<String> ien = req.getHeaders( name ); ien.hasMoreElements(); )
268                 {
269                     if ( buffer.length() > 0 )
270                     {
271                         buffer.append( ", " );
272                     }
273                     buffer.append( ien.nextElement() );
274                 }
275                 headers.put( name, buffer.toString() );
276             }
277             logEntries.add( new LogEntry( req.getMethod(), req.getPathInfo(), Collections.unmodifiableMap( headers ) ) );
278         }
279 
280     }
281 
282     private class RepoHandler
283         extends AbstractHandler
284     {
285 
286         private final Pattern SIMPLE_RANGE = Pattern.compile( "bytes=([0-9])+-" );
287 
288         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
289             throws IOException
290         {
291             String path = req.getPathInfo().substring( 1 );
292 
293             if ( !path.startsWith( "repo/" ) )
294             {
295                 return;
296             }
297             req.setHandled( true );
298 
299             if ( ExpectContinue.FAIL.equals( expectContinue ) && request.getHeader( HttpHeader.EXPECT.asString() ) != null )
300             {
301                 response.setStatus( HttpServletResponse.SC_EXPECTATION_FAILED );
302                 return;
303             }
304 
305             File file = new File( repoDir, path.substring( 5 ) );
306             if ( HttpMethod.GET.is( req.getMethod() ) || HttpMethod.HEAD.is( req.getMethod() ) )
307             {
308                 if ( !file.isFile() || path.endsWith( URIUtil.SLASH ) )
309                 {
310                     response.setStatus( HttpServletResponse.SC_NOT_FOUND );
311                     return;
312                 }
313                 long ifUnmodifiedSince = request.getDateHeader( HttpHeader.IF_UNMODIFIED_SINCE.asString() );
314                 if ( ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince )
315                 {
316                     response.setStatus( HttpServletResponse.SC_PRECONDITION_FAILED );
317                     return;
318                 }
319                 long offset = 0L;
320                 String range = request.getHeader( HttpHeader.RANGE.asString() );
321                 if ( range != null && rangeSupport )
322                 {
323                     Matcher m = SIMPLE_RANGE.matcher( range );
324                     if ( m.matches() )
325                     {
326                         offset = Long.parseLong( m.group( 1 ) );
327                         if ( offset >= file.length() )
328                         {
329                             response.setStatus( HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE );
330                             return;
331                         }
332                     }
333                     String encoding = request.getHeader( HttpHeader.ACCEPT_ENCODING.asString() );
334                     if ( ( encoding != null && !"identity".equals( encoding ) ) || ifUnmodifiedSince == -1L )
335                     {
336                         response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
337                         return;
338                     }
339                 }
340                 response.setStatus( ( offset > 0L ) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK );
341                 response.setDateHeader( HttpHeader.LAST_MODIFIED.asString(), file.lastModified() );
342                 response.setHeader( HttpHeader.CONTENT_LENGTH.asString(), Long.toString( file.length() - offset ) );
343                 if ( offset > 0L )
344                 {
345                     response.setHeader( HttpHeader.CONTENT_RANGE.asString(), "bytes " + offset + "-" + ( file.length() - 1L )
346                         + "/" + file.length() );
347                 }
348                 if ( checksumHeader != null )
349                 {
350                     Map<String, Object> checksums = ChecksumUtils.calc( file, Collections.singleton( "SHA-1" ) );
351                     if ( checksumHeader == ChecksumHeader.NEXUS )
352                     {
353                         response.setHeader( HttpHeader.ETAG.asString(), "{SHA1{" + checksums.get( "SHA-1" ) + "}}" );
354                     }
355                 }
356                 if ( HttpMethod.HEAD.is( req.getMethod() ) )
357                 {
358                     return;
359                 }
360                 FileInputStream is = null;
361                 try
362                 {
363                     is = new FileInputStream( file );
364                     if ( offset > 0L )
365                     {
366                         long skipped = is.skip( offset );
367                         while ( skipped < offset && is.read() >= 0 )
368                         {
369                             skipped++;
370                         }
371                     }
372                     IO.copy( is, response.getOutputStream() );
373                     is.close();
374                     is = null;
375                 }
376                 finally
377                 {
378                     try
379                     {
380                         if ( is != null )
381                         {
382                             is.close();
383                         }
384                     }
385                     catch ( final IOException e )
386                     {
387                         // Suppressed due to an exception already thrown in the try block.
388                     }
389                 }
390             }
391             else if ( HttpMethod.PUT.is( req.getMethod() ) )
392             {
393                 if ( !webDav )
394                 {
395                     file.getParentFile().mkdirs();
396                 }
397                 if ( file.getParentFile().exists() )
398                 {
399                     try
400                     {
401                         FileOutputStream os = null;
402                         try
403                         {
404                             os = new FileOutputStream( file );
405                             IO.copy( request.getInputStream(), os );
406                             os.close();
407                             os = null;
408                         }
409                         finally
410                         {
411                             try
412                             {
413                                 if ( os != null )
414                                 {
415                                     os.close();
416                                 }
417                             }
418                             catch ( final IOException e )
419                             {
420                                 // Suppressed due to an exception already thrown in the try block.
421                             }
422                         }
423                     }
424                     catch ( IOException e )
425                     {
426                         file.delete();
427                         throw e;
428                     }
429                     response.setStatus( HttpServletResponse.SC_NO_CONTENT );
430                 }
431                 else
432                 {
433                     response.setStatus( HttpServletResponse.SC_FORBIDDEN );
434                 }
435             }
436             else if ( HttpMethod.OPTIONS.is( req.getMethod() ) )
437             {
438                 if ( webDav )
439                 {
440                     response.setHeader( "DAV", "1,2" );
441                 }
442                 response.setHeader( HttpHeader.ALLOW.asString(), "GET, PUT, HEAD, OPTIONS" );
443                 response.setStatus( HttpServletResponse.SC_OK );
444             }
445             else if ( webDav && "MKCOL".equals( req.getMethod() ) )
446             {
447                 if ( file.exists() )
448                 {
449                     response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
450                 }
451                 else if ( file.mkdir() )
452                 {
453                     response.setStatus( HttpServletResponse.SC_CREATED );
454                 }
455                 else
456                 {
457                     response.setStatus( HttpServletResponse.SC_CONFLICT );
458                 }
459             }
460             else
461             {
462                 response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
463             }
464         }
465 
466     }
467 
468     private class RedirectHandler
469         extends AbstractHandler
470     {
471 
472         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
473         {
474             String path = req.getPathInfo();
475             if ( !path.startsWith( "/redirect/" ) )
476             {
477                 return;
478             }
479             req.setHandled( true );
480             StringBuilder location = new StringBuilder( 128 );
481             String scheme = req.getParameter( "scheme" );
482             location.append( scheme != null ? scheme : req.getScheme() );
483             location.append( "://" );
484             location.append( req.getServerName() );
485             location.append( ":" );
486             if ( "http".equalsIgnoreCase( scheme ) )
487             {
488                 location.append( getHttpPort() );
489             }
490             else if ( "https".equalsIgnoreCase( scheme ) )
491             {
492                 location.append( getHttpsPort() );
493             }
494             else
495             {
496                 location.append( req.getServerPort() );
497             }
498             location.append( "/repo" ).append( path.substring( 9 ) );
499             response.setStatus( HttpServletResponse.SC_MOVED_PERMANENTLY );
500             response.setHeader( HttpHeader.LOCATION.asString(), location.toString() );
501         }
502 
503     }
504 
505     private class AuthHandler
506         extends AbstractHandler
507     {
508 
509         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
510             throws IOException
511         {
512             if ( ExpectContinue.BROKEN.equals( expectContinue )
513                 && "100-continue".equalsIgnoreCase( request.getHeader( HttpHeader.EXPECT.asString() ) ) )
514             {
515                 request.getInputStream();
516             }
517 
518             if ( username != null && password != null )
519             {
520                 if ( checkBasicAuth( request.getHeader( HttpHeader.AUTHORIZATION.asString() ), username, password ) )
521                 {
522                     return;
523                 }
524                 req.setHandled( true );
525                 response.setHeader( HttpHeader.WWW_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\"" );
526                 response.setStatus( HttpServletResponse.SC_UNAUTHORIZED );
527             }
528         }
529 
530     }
531 
532     private class ProxyAuthHandler
533         extends AbstractHandler
534     {
535 
536         public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
537         {
538             if ( proxyUsername != null && proxyPassword != null )
539             {
540                 if ( checkBasicAuth( request.getHeader( HttpHeader.PROXY_AUTHORIZATION.asString() ), proxyUsername, proxyPassword ) )
541                 {
542                     return;
543                 }
544                 req.setHandled( true );
545                 response.setHeader( HttpHeader.PROXY_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\"" );
546                 response.setStatus( HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED );
547             }
548         }
549 
550     }
551 
552     static boolean checkBasicAuth( String credentials, String username, String password )
553     {
554         if ( credentials != null )
555         {
556             int space = credentials.indexOf( ' ' );
557             if ( space > 0 )
558             {
559                 String method = credentials.substring( 0, space );
560                 if ( "basic".equalsIgnoreCase( method ) )
561                 {
562                     credentials = credentials.substring( space + 1 );
563                     credentials = B64Code.decode( credentials, StringUtil.__ISO_8859_1 );
564                     int i = credentials.indexOf( ':' );
565                     if ( i > 0 )
566                     {
567                         String user = credentials.substring( 0, i );
568                         String pass = credentials.substring( i + 1 );
569                         if ( username.equals( user ) && password.equals( pass ) )
570                         {
571                             return true;
572                         }
573                     }
574                 }
575             }
576         }
577         return false;
578     }
579 
580 }