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