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.io.UnsupportedEncodingException; 027import java.nio.charset.StandardCharsets; 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.Enumeration; 031import java.util.List; 032import java.util.Map; 033import java.util.TreeMap; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036 037import javax.servlet.http.HttpServletRequest; 038import javax.servlet.http.HttpServletResponse; 039 040import org.eclipse.aether.util.ChecksumUtils; 041import org.eclipse.jetty.http.HttpHeaders; 042import org.eclipse.jetty.http.HttpMethods; 043import org.eclipse.jetty.server.Connector; 044import org.eclipse.jetty.server.Request; 045import org.eclipse.jetty.server.Server; 046import org.eclipse.jetty.server.handler.AbstractHandler; 047import org.eclipse.jetty.server.handler.HandlerList; 048import org.eclipse.jetty.server.nio.SelectChannelConnector; 049import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; 050import org.eclipse.jetty.util.B64Code; 051import org.eclipse.jetty.util.IO; 052import org.eclipse.jetty.util.StringUtil; 053import org.eclipse.jetty.util.URIUtil; 054import org.eclipse.jetty.util.ssl.SslContextFactory; 055import org.slf4j.Logger; 056import org.slf4j.LoggerFactory; 057 058public class HttpServer 059{ 060 061 public static class LogEntry 062 { 063 064 public final String method; 065 066 public final String path; 067 068 public final Map<String, String> headers; 069 070 public LogEntry( String method, String path, Map<String, String> headers ) 071 { 072 this.method = method; 073 this.path = path; 074 this.headers = headers; 075 } 076 077 @Override 078 public String toString() 079 { 080 return method + " " + path; 081 } 082 083 } 084 085 public enum ExpectContinue 086 { 087 FAIL, PROPER, BROKEN 088 } 089 090 public enum ChecksumHeader 091 { 092 NEXUS 093 } 094 095 private static final Logger log = LoggerFactory.getLogger( HttpServer.class ); 096 097 private File repoDir; 098 099 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 // Suppressed due to an exception already thrown in the try block. 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 // Suppressed due to an exception already thrown in the try block. 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}