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