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 LOGGER = 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.Server ssl = new SslContextFactory.Server();
149            ssl.setNeedClientAuth( true );
150            ssl.setKeyStorePath( new File( "src/test/resources/ssl/server-store" ).getAbsolutePath() );
151            ssl.setKeyStorePassword( "server-pwd" );
152            ssl.setTrustStorePath( new File( "src/test/resources/ssl/client-store" ).getAbsolutePath() );
153            ssl.setTrustStorePassword( "client-pwd" );
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        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
258        {
259            LOGGER.info( "{} {}{}", req.getMethod(), req.getRequestURL(),
260                    req.getQueryString() != null ? "?" + req.getQueryString() : "");
261
262            Map<String, String> headers = new TreeMap<>( String.CASE_INSENSITIVE_ORDER );
263            for ( Enumeration<String> en = req.getHeaderNames(); en.hasMoreElements(); )
264            {
265                String name = en.nextElement();
266                StringBuilder buffer = new StringBuilder( 128 );
267                for ( Enumeration<String> ien = req.getHeaders( name ); ien.hasMoreElements(); )
268                {
269                    if ( buffer.length() > 0 )
270                    {
271                        buffer.append( ", " );
272                    }
273                    buffer.append( ien.nextElement() );
274                }
275                headers.put( name, buffer.toString() );
276            }
277            logEntries.add( new LogEntry( req.getMethod(), req.getPathInfo(), Collections.unmodifiableMap( headers ) ) );
278        }
279
280    }
281
282    private class RepoHandler
283        extends AbstractHandler
284    {
285
286        private final Pattern SIMPLE_RANGE = Pattern.compile( "bytes=([0-9])+-" );
287
288        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
289            throws IOException
290        {
291            String path = req.getPathInfo().substring( 1 );
292
293            if ( !path.startsWith( "repo/" ) )
294            {
295                return;
296            }
297            req.setHandled( true );
298
299            if ( ExpectContinue.FAIL.equals( expectContinue ) && request.getHeader( HttpHeader.EXPECT.asString() ) != null )
300            {
301                response.setStatus( HttpServletResponse.SC_EXPECTATION_FAILED );
302                return;
303            }
304
305            File file = new File( repoDir, path.substring( 5 ) );
306            if ( HttpMethod.GET.is( req.getMethod() ) || HttpMethod.HEAD.is( req.getMethod() ) )
307            {
308                if ( !file.isFile() || path.endsWith( URIUtil.SLASH ) )
309                {
310                    response.setStatus( HttpServletResponse.SC_NOT_FOUND );
311                    return;
312                }
313                long ifUnmodifiedSince = request.getDateHeader( HttpHeader.IF_UNMODIFIED_SINCE.asString() );
314                if ( ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince )
315                {
316                    response.setStatus( HttpServletResponse.SC_PRECONDITION_FAILED );
317                    return;
318                }
319                long offset = 0L;
320                String range = request.getHeader( HttpHeader.RANGE.asString() );
321                if ( range != null && rangeSupport )
322                {
323                    Matcher m = SIMPLE_RANGE.matcher( range );
324                    if ( m.matches() )
325                    {
326                        offset = Long.parseLong( m.group( 1 ) );
327                        if ( offset >= file.length() )
328                        {
329                            response.setStatus( HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE );
330                            return;
331                        }
332                    }
333                    String encoding = request.getHeader( HttpHeader.ACCEPT_ENCODING.asString() );
334                    if ( ( encoding != null && !"identity".equals( encoding ) ) || ifUnmodifiedSince == -1L )
335                    {
336                        response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
337                        return;
338                    }
339                }
340                response.setStatus( ( offset > 0L ) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK );
341                response.setDateHeader( HttpHeader.LAST_MODIFIED.asString(), file.lastModified() );
342                response.setHeader( HttpHeader.CONTENT_LENGTH.asString(), Long.toString( file.length() - offset ) );
343                if ( offset > 0L )
344                {
345                    response.setHeader( HttpHeader.CONTENT_RANGE.asString(), "bytes " + offset + "-" + ( file.length() - 1L )
346                        + "/" + file.length() );
347                }
348                if ( checksumHeader != null )
349                {
350                    Map<String, Object> checksums = ChecksumUtils.calc( file, Collections.singleton( "SHA-1" ) );
351                    if ( checksumHeader == ChecksumHeader.NEXUS )
352                    {
353                        response.setHeader( HttpHeader.ETAG.asString(), "{SHA1{" + checksums.get( "SHA-1" ) + "}}" );
354                    }
355                }
356                if ( HttpMethod.HEAD.is( req.getMethod() ) )
357                {
358                    return;
359                }
360                FileInputStream is = null;
361                try
362                {
363                    is = new FileInputStream( file );
364                    if ( offset > 0L )
365                    {
366                        long skipped = is.skip( offset );
367                        while ( skipped < offset && is.read() >= 0 )
368                        {
369                            skipped++;
370                        }
371                    }
372                    IO.copy( is, response.getOutputStream() );
373                    is.close();
374                    is = null;
375                }
376                finally
377                {
378                    try
379                    {
380                        if ( is != null )
381                        {
382                            is.close();
383                        }
384                    }
385                    catch ( final IOException e )
386                    {
387                        // Suppressed due to an exception already thrown in the try block.
388                    }
389                }
390            }
391            else if ( HttpMethod.PUT.is( req.getMethod() ) )
392            {
393                if ( !webDav )
394                {
395                    file.getParentFile().mkdirs();
396                }
397                if ( file.getParentFile().exists() )
398                {
399                    try
400                    {
401                        FileOutputStream os = null;
402                        try
403                        {
404                            os = new FileOutputStream( file );
405                            IO.copy( request.getInputStream(), os );
406                            os.close();
407                            os = null;
408                        }
409                        finally
410                        {
411                            try
412                            {
413                                if ( os != null )
414                                {
415                                    os.close();
416                                }
417                            }
418                            catch ( final IOException e )
419                            {
420                                // Suppressed due to an exception already thrown in the try block.
421                            }
422                        }
423                    }
424                    catch ( IOException e )
425                    {
426                        file.delete();
427                        throw e;
428                    }
429                    response.setStatus( HttpServletResponse.SC_NO_CONTENT );
430                }
431                else
432                {
433                    response.setStatus( HttpServletResponse.SC_FORBIDDEN );
434                }
435            }
436            else if ( HttpMethod.OPTIONS.is( req.getMethod() ) )
437            {
438                if ( webDav )
439                {
440                    response.setHeader( "DAV", "1,2" );
441                }
442                response.setHeader( HttpHeader.ALLOW.asString(), "GET, PUT, HEAD, OPTIONS" );
443                response.setStatus( HttpServletResponse.SC_OK );
444            }
445            else if ( webDav && "MKCOL".equals( req.getMethod() ) )
446            {
447                if ( file.exists() )
448                {
449                    response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
450                }
451                else if ( file.mkdir() )
452                {
453                    response.setStatus( HttpServletResponse.SC_CREATED );
454                }
455                else
456                {
457                    response.setStatus( HttpServletResponse.SC_CONFLICT );
458                }
459            }
460            else
461            {
462                response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
463            }
464        }
465
466    }
467
468    private class RedirectHandler
469        extends AbstractHandler
470    {
471
472        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
473        {
474            String path = req.getPathInfo();
475            if ( !path.startsWith( "/redirect/" ) )
476            {
477                return;
478            }
479            req.setHandled( true );
480            StringBuilder location = new StringBuilder( 128 );
481            String scheme = req.getParameter( "scheme" );
482            location.append( scheme != null ? scheme : req.getScheme() );
483            location.append( "://" );
484            location.append( req.getServerName() );
485            location.append( ":" );
486            if ( "http".equalsIgnoreCase( scheme ) )
487            {
488                location.append( getHttpPort() );
489            }
490            else if ( "https".equalsIgnoreCase( scheme ) )
491            {
492                location.append( getHttpsPort() );
493            }
494            else
495            {
496                location.append( req.getServerPort() );
497            }
498            location.append( "/repo" ).append( path.substring( 9 ) );
499            response.setStatus( HttpServletResponse.SC_MOVED_PERMANENTLY );
500            response.setHeader( HttpHeader.LOCATION.asString(), location.toString() );
501        }
502
503    }
504
505    private class AuthHandler
506        extends AbstractHandler
507    {
508
509        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
510            throws IOException
511        {
512            if ( ExpectContinue.BROKEN.equals( expectContinue )
513                && "100-continue".equalsIgnoreCase( request.getHeader( HttpHeader.EXPECT.asString() ) ) )
514            {
515                request.getInputStream();
516            }
517
518            if ( username != null && password != null )
519            {
520                if ( checkBasicAuth( request.getHeader( HttpHeader.AUTHORIZATION.asString() ), username, password ) )
521                {
522                    return;
523                }
524                req.setHandled( true );
525                response.setHeader( HttpHeader.WWW_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\"" );
526                response.setStatus( HttpServletResponse.SC_UNAUTHORIZED );
527            }
528        }
529
530    }
531
532    private class ProxyAuthHandler
533        extends AbstractHandler
534    {
535
536        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
537        {
538            if ( proxyUsername != null && proxyPassword != null )
539            {
540                if ( checkBasicAuth( request.getHeader( HttpHeader.PROXY_AUTHORIZATION.asString() ), proxyUsername, proxyPassword ) )
541                {
542                    return;
543                }
544                req.setHandled( true );
545                response.setHeader( HttpHeader.PROXY_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\"" );
546                response.setStatus( HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED );
547            }
548        }
549
550    }
551
552    static boolean checkBasicAuth( String credentials, String username, String password )
553    {
554        if ( credentials != null )
555        {
556            int space = credentials.indexOf( ' ' );
557            if ( space > 0 )
558            {
559                String method = credentials.substring( 0, space );
560                if ( "basic".equalsIgnoreCase( method ) )
561                {
562                    credentials = credentials.substring( space + 1 );
563                    credentials = B64Code.decode( credentials, StringUtil.__ISO_8859_1 );
564                    int i = credentials.indexOf( ':' );
565                    if ( i > 0 )
566                    {
567                        String user = credentials.substring( 0, i );
568                        String pass = credentials.substring( i + 1 );
569                        if ( username.equals( user ) && password.equals( pass ) )
570                        {
571                            return true;
572                        }
573                    }
574                }
575            }
576        }
577        return false;
578    }
579
580}