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