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