001package org.eclipse.aether.transport.http; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.File; 023import java.io.FileInputStream; 024import java.io.FileOutputStream; 025import java.io.IOException; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.Enumeration; 029import java.util.List; 030import java.util.Map; 031import java.util.TreeMap; 032import java.util.regex.Matcher; 033import java.util.regex.Pattern; 034 035import javax.servlet.http.HttpServletRequest; 036import javax.servlet.http.HttpServletResponse; 037 038import org.eclipse.aether.util.ChecksumUtils; 039import org.eclipse.jetty.http.HttpHeader; 040import org.eclipse.jetty.http.HttpMethod; 041import org.eclipse.jetty.server.Request; 042import org.eclipse.jetty.server.Server; 043import org.eclipse.jetty.server.ServerConnector; 044import org.eclipse.jetty.server.handler.AbstractHandler; 045import org.eclipse.jetty.server.handler.HandlerList; 046import org.eclipse.jetty.util.B64Code; 047import org.eclipse.jetty.util.IO; 048import org.eclipse.jetty.util.StringUtil; 049import org.eclipse.jetty.util.URIUtil; 050import org.eclipse.jetty.util.ssl.SslContextFactory; 051import org.slf4j.Logger; 052import org.slf4j.LoggerFactory; 053 054public class HttpServer 055{ 056 057 public static class LogEntry 058 { 059 060 public final String method; 061 062 public final String path; 063 064 public final Map<String, String> headers; 065 066 public LogEntry( String method, String path, Map<String, String> headers ) 067 { 068 this.method = method; 069 this.path = path; 070 this.headers = headers; 071 } 072 073 @Override 074 public String toString() 075 { 076 return method + " " + path; 077 } 078 079 } 080 081 public enum ExpectContinue 082 { 083 FAIL, PROPER, BROKEN 084 } 085 086 public enum ChecksumHeader 087 { 088 NEXUS 089 } 090 091 private static final Logger log = LoggerFactory.getLogger( HttpServer.class ); 092 093 private File repoDir; 094 095 private boolean rangeSupport = true; 096 097 private boolean webDav; 098 099 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 // Suppressed due to an exception already thrown in the try block. 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 // Suppressed due to an exception already thrown in the try block. 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}