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}