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}