blob: b59cd74b95a0b035295f5f785aeb127615892ba2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.eclipse.aether.transport.http;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.aether.util.ChecksumUtils;
import org.eclipse.jetty.http.HttpHeaders;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HttpServer
{
public static class LogEntry
{
public final String method;
public final String path;
public final Map<String, String> headers;
public LogEntry( String method, String path, Map<String, String> headers )
{
this.method = method;
this.path = path;
this.headers = headers;
}
@Override
public String toString()
{
return method + " " + path;
}
}
public enum WebDav
{
/** DAV header advertised, MKCOL required for missing parent directories */
REQUIRED,
/** DAV header advertised, MKCOL supported but not required */
OPTIONAL
}
public enum ExpectContinue
{
/** reject request with "Expectation Failed" */
FAIL,
/** send "Continue" only if request made it past authentication */
PROPER,
/** send "Continue" before authentication has been checked */
BROKEN
}
public enum ChecksumHeader
{
NEXUS
}
private static final Logger log = LoggerFactory.getLogger( HttpServer.class );
private String serverHeader = "Dummy";
private File repoDir;
private boolean rangeSupport = true;
private WebDav webDav;
private ExpectContinue expectContinue = ExpectContinue.PROPER;
private ChecksumHeader checksumHeader;
private Server server;
private Connector httpConnector;
private Connector httpsConnector;
private String credentialEncoding = StringUtil.__ISO_8859_1;
private String username;
private String password;
private String proxyUsername;
private String proxyPassword;
private List<LogEntry> logEntries = Collections.synchronizedList( new ArrayList<LogEntry>() );
public String getHost()
{
return "localhost";
}
public int getHttpPort()
{
return httpConnector != null ? httpConnector.getLocalPort() : -1;
}
public int getHttpsPort()
{
return httpsConnector != null ? httpsConnector.getLocalPort() : -1;
}
public String getHttpUrl()
{
return "http://" + getHost() + ":" + getHttpPort();
}
public String getHttpsUrl()
{
return "https://" + getHost() + ":" + getHttpsPort();
}
public HttpServer addSslConnector()
{
if ( httpsConnector == null )
{
SslContextFactory ssl = new SslContextFactory();
ssl.setKeyStorePath( new File( "src/test/resources/ssl/server-store" ).getAbsolutePath() );
ssl.setKeyStorePassword( "server-pwd" );
ssl.setTrustStore( new File( "src/test/resources/ssl/client-store" ).getAbsolutePath() );
ssl.setTrustStorePassword( "client-pwd" );
ssl.setNeedClientAuth( true );
httpsConnector = new SslSelectChannelConnector( ssl );
if ( server != null )
{
server.addConnector( httpsConnector );
try
{
httpsConnector.start();
}
catch ( Exception e )
{
throw new IllegalStateException( e );
}
}
}
return this;
}
public List<LogEntry> getLogEntries()
{
return logEntries;
}
public HttpServer setServer( String server )
{
this.serverHeader = server;
return this;
}
public HttpServer setRepoDir( File repoDir )
{
this.repoDir = repoDir;
return this;
}
public HttpServer setRangeSupport( boolean rangeSupport )
{
this.rangeSupport = rangeSupport;
return this;
}
public HttpServer setWebDav( WebDav webDav )
{
this.webDav = webDav;
return this;
}
public HttpServer setExpectSupport( ExpectContinue expectContinue )
{
this.expectContinue = expectContinue;
return this;
}
public HttpServer setChecksumHeader( ChecksumHeader checksumHeader )
{
this.checksumHeader = checksumHeader;
return this;
}
public HttpServer setCredentialEncoding( String credentialEncoding )
{
this.credentialEncoding = ( credentialEncoding != null ) ? credentialEncoding : StringUtil.__ISO_8859_1;
return this;
}
public HttpServer setAuthentication( String username, String password )
{
this.username = username;
this.password = password;
return this;
}
public HttpServer setProxyAuthentication( String username, String password )
{
proxyUsername = username;
proxyPassword = password;
return this;
}
public HttpServer start()
throws Exception
{
if ( server != null )
{
return this;
}
httpConnector = new SelectChannelConnector();
HandlerList handlers = new HandlerList();
handlers.addHandler( new CommonHandler() );
handlers.addHandler( new LogHandler() );
handlers.addHandler( new ProxyAuthHandler() );
handlers.addHandler( new AuthHandler() );
handlers.addHandler( new RedirectHandler() );
handlers.addHandler( new RepoHandler() );
server = new Server();
server.addConnector( httpConnector );
if ( httpsConnector != null )
{
server.addConnector( httpsConnector );
}
server.setHandler( handlers );
server.start();
return this;
}
public void stop()
throws Exception
{
if ( server != null )
{
server.stop();
server = null;
httpConnector = null;
httpsConnector = null;
}
}
private class CommonHandler
extends AbstractHandler
{
public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
throws IOException
{
response.setHeader( HttpHeaders.SERVER, serverHeader );
}
}
private class LogHandler
extends AbstractHandler
{
@SuppressWarnings( "unchecked" )
public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
throws IOException
{
log.info( "{} {}{}", new Object[] { req.getMethod(), req.getRequestURL(),
req.getQueryString() != null ? "?" + req.getQueryString() : "" } );
Map<String, String> headers = new TreeMap<String, String>( String.CASE_INSENSITIVE_ORDER );
for ( Enumeration<String> en = req.getHeaderNames(); en.hasMoreElements(); )
{
String name = en.nextElement();
StringBuilder buffer = new StringBuilder( 128 );
for ( Enumeration<String> ien = req.getHeaders( name ); ien.hasMoreElements(); )
{
if ( buffer.length() > 0 )
{
buffer.append( ", " );
}
buffer.append( ien.nextElement() );
}
headers.put( name, buffer.toString() );
}
logEntries.add( new LogEntry( req.getMethod(), req.getPathInfo(), Collections.unmodifiableMap( headers ) ) );
}
}
private class RepoHandler
extends AbstractHandler
{
private final Pattern SIMPLE_RANGE = Pattern.compile( "bytes=([0-9])+-" );
public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
throws IOException
{
String path = req.getPathInfo().substring( 1 );
if ( !path.startsWith( "repo/" ) )
{
return;
}
req.setHandled( true );
if ( ExpectContinue.FAIL.equals( expectContinue ) && request.getHeader( HttpHeaders.EXPECT ) != null )
{
response.setStatus( HttpServletResponse.SC_EXPECTATION_FAILED );
return;
}
File file = new File( repoDir, path.substring( 5 ) );
if ( HttpMethods.GET.equals( req.getMethod() ) || HttpMethods.HEAD.equals( req.getMethod() ) )
{
if ( !file.isFile() || path.endsWith( URIUtil.SLASH ) )
{
response.setStatus( HttpServletResponse.SC_NOT_FOUND );
return;
}
long ifUnmodifiedSince = request.getDateHeader( HttpHeaders.IF_UNMODIFIED_SINCE );
if ( ifUnmodifiedSince != -1 && file.lastModified() > ifUnmodifiedSince )
{
response.setStatus( HttpServletResponse.SC_PRECONDITION_FAILED );
return;
}
long offset = 0;
String range = request.getHeader( HttpHeaders.RANGE );
if ( range != null && rangeSupport )
{
Matcher m = SIMPLE_RANGE.matcher( range );
if ( m.matches() )
{
offset = Long.parseLong( m.group( 1 ) );
if ( offset >= file.length() )
{
response.setStatus( HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE );
return;
}
}
String encoding = request.getHeader( HttpHeaders.ACCEPT_ENCODING );
if ( ( encoding != null && !"identity".equals( encoding ) ) || ifUnmodifiedSince == -1 )
{
response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
return;
}
}
response.setStatus( ( offset > 0 ) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK );
response.setDateHeader( HttpHeaders.LAST_MODIFIED, file.lastModified() );
response.setHeader( HttpHeaders.CONTENT_LENGTH, Long.toString( file.length() - offset ) );
if ( offset > 0 )
{
response.setHeader( HttpHeaders.CONTENT_RANGE, "bytes " + offset + "-" + ( file.length() - 1 )
+ "/" + file.length() );
}
if ( checksumHeader != null )
{
Map<String, Object> checksums = ChecksumUtils.calc( file, Collections.singleton( "SHA-1" ) );
switch ( checksumHeader )
{
case NEXUS:
response.setHeader( HttpHeaders.ETAG, "{SHA1{" + checksums.get( "SHA-1" ) + "}}" );
break;
}
}
if ( HttpMethods.HEAD.equals( req.getMethod() ) )
{
return;
}
FileInputStream is = new FileInputStream( file );
try
{
if ( offset > 0 )
{
long skipped = is.skip( offset );
while ( skipped < offset && is.read() >= 0 )
{
skipped++;
}
}
IO.copy( is, response.getOutputStream() );
}
finally
{
IO.close( is );
}
}
else if ( HttpMethods.PUT.equals( req.getMethod() ) )
{
if ( !WebDav.REQUIRED.equals( webDav ) )
{
file.getParentFile().mkdirs();
}
if ( file.getParentFile().exists() )
{
try
{
FileOutputStream os = new FileOutputStream( file );
try
{
IO.copy( request.getInputStream(), os );
}
finally
{
os.close();
}
}
catch ( IOException e )
{
file.delete();
throw e;
}
response.setStatus( HttpServletResponse.SC_NO_CONTENT );
}
else
{
response.setStatus( HttpServletResponse.SC_FORBIDDEN );
}
}
else if ( HttpMethods.OPTIONS.equals( req.getMethod() ) )
{
if ( webDav != null )
{
response.setHeader( "DAV", "1,2" );
}
response.setHeader( HttpHeaders.ALLOW, "GET, PUT, HEAD, OPTIONS" );
response.setStatus( HttpServletResponse.SC_OK );
}
else if ( webDav != null && "MKCOL".equals( req.getMethod() ) )
{
if ( file.exists() )
{
response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
}
else if ( file.mkdir() )
{
response.setStatus( HttpServletResponse.SC_CREATED );
}
else
{
response.setStatus( HttpServletResponse.SC_CONFLICT );
}
}
else
{
response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
}
}
}
private class RedirectHandler
extends AbstractHandler
{
public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
throws IOException
{
String path = req.getPathInfo();
if ( !path.startsWith( "/redirect/" ) )
{
return;
}
req.setHandled( true );
StringBuilder location = new StringBuilder( 128 );
String scheme = req.getParameter( "scheme" );
String host = req.getParameter( "host" );
String port = req.getParameter( "port" );
location.append( scheme != null ? scheme : req.getScheme() );
location.append( "://" );
location.append( host != null ? host : req.getServerName() );
location.append( ":" );
if ( port != null )
{
location.append( port );
}
else if ( "http".equalsIgnoreCase( scheme ) )
{
location.append( getHttpPort() );
}
else if ( "https".equalsIgnoreCase( scheme ) )
{
location.append( getHttpsPort() );
}
else
{
location.append( req.getServerPort() );
}
location.append( "/repo" ).append( path.substring( 9 ) );
response.setStatus( HttpServletResponse.SC_MOVED_PERMANENTLY );
response.setHeader( HttpHeaders.LOCATION, location.toString() );
}
}
private class AuthHandler
extends AbstractHandler
{
public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
throws IOException
{
if ( ExpectContinue.BROKEN.equals( expectContinue )
&& "100-continue".equalsIgnoreCase( request.getHeader( HttpHeaders.EXPECT ) ) )
{
request.getInputStream();
}
if ( username != null && password != null )
{
if ( checkBasicAuth( request.getHeader( HttpHeaders.AUTHORIZATION ), username, password ) )
{
return;
}
req.setHandled( true );
response.setHeader( HttpHeaders.WWW_AUTHENTICATE, "basic realm=\"Test-Realm\"" );
response.setStatus( HttpServletResponse.SC_UNAUTHORIZED );
}
}
}
private class ProxyAuthHandler
extends AbstractHandler
{
public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
throws IOException
{
if ( proxyUsername != null && proxyPassword != null )
{
if ( checkBasicAuth( request.getHeader( HttpHeaders.PROXY_AUTHORIZATION ), proxyUsername, proxyPassword ) )
{
return;
}
req.setHandled( true );
response.setHeader( HttpHeaders.PROXY_AUTHENTICATE, "basic realm=\"Test-Realm\"" );
response.setStatus( HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED );
}
}
}
boolean checkBasicAuth( String credentials, String username, String password )
{
if ( credentials != null )
{
int space = credentials.indexOf( ' ' );
if ( space > 0 )
{
String method = credentials.substring( 0, space );
if ( "basic".equalsIgnoreCase( method ) )
{
credentials = credentials.substring( space + 1 );
try
{
credentials = B64Code.decode( credentials, credentialEncoding );
}
catch ( UnsupportedEncodingException e )
{
throw new IllegalStateException( e );
}
int i = credentials.indexOf( ':' );
if ( i > 0 )
{
String user = credentials.substring( 0, i );
String pass = credentials.substring( i + 1 );
if ( username.equals( user ) && password.equals( pass ) )
{
return true;
}
}
}
}
}
return false;
}
}