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.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 }