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 LOGGER = 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 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 // Suppressed due to an exception already thrown in the try block. 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 // Suppressed due to an exception already thrown in the try block. 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}