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