View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.internal.test.util.http;
20  
21  import javax.servlet.http.HttpServletRequest;
22  import javax.servlet.http.HttpServletResponse;
23  
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.OutputStream;
29  import java.nio.charset.StandardCharsets;
30  import java.util.ArrayList;
31  import java.util.Base64;
32  import java.util.Collections;
33  import java.util.Enumeration;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.TreeMap;
37  import java.util.concurrent.atomic.AtomicInteger;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  
41  import org.eclipse.aether.internal.impl.checksum.Sha1ChecksumAlgorithmFactory;
42  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper;
43  import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
44  import org.eclipse.jetty.http.HttpHeader;
45  import org.eclipse.jetty.http.HttpMethod;
46  import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
47  import org.eclipse.jetty.server.HttpConfiguration;
48  import org.eclipse.jetty.server.HttpConnectionFactory;
49  import org.eclipse.jetty.server.Request;
50  import org.eclipse.jetty.server.Response;
51  import org.eclipse.jetty.server.SecureRequestCustomizer;
52  import org.eclipse.jetty.server.Server;
53  import org.eclipse.jetty.server.ServerConnector;
54  import org.eclipse.jetty.server.SslConnectionFactory;
55  import org.eclipse.jetty.server.handler.AbstractHandler;
56  import org.eclipse.jetty.server.handler.HandlerList;
57  import org.eclipse.jetty.util.IO;
58  import org.eclipse.jetty.util.URIUtil;
59  import org.eclipse.jetty.util.ssl.SslContextFactory;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  public class HttpServer {
64  
65      public static class LogEntry {
66  
67          private final String method;
68  
69          private final String path;
70  
71          private final Map<String, String> headers;
72  
73          public LogEntry(String method, String path, Map<String, String> headers) {
74              this.method = method;
75              this.path = path;
76              this.headers = headers;
77          }
78  
79          public String getMethod() {
80              return method;
81          }
82  
83          public String getPath() {
84              return path;
85          }
86  
87          public Map<String, String> getHeaders() {
88              return headers;
89          }
90  
91          @Override
92          public String toString() {
93              return method + " " + path;
94          }
95      }
96  
97      public enum ExpectContinue {
98          FAIL,
99          PROPER,
100         BROKEN
101     }
102 
103     public enum ChecksumHeader {
104         NEXUS,
105         XCHECKSUM
106     }
107 
108     private static final Logger LOGGER = LoggerFactory.getLogger(HttpServer.class);
109 
110     private File repoDir;
111 
112     private boolean rangeSupport = true;
113 
114     private boolean webDav;
115 
116     private ExpectContinue expectContinue = ExpectContinue.PROPER;
117 
118     private ChecksumHeader checksumHeader;
119 
120     private Server server;
121 
122     private ServerConnector httpConnector;
123 
124     private ServerConnector httpsConnector;
125 
126     private String username;
127 
128     private String password;
129 
130     private String proxyUsername;
131 
132     private String proxyPassword;
133 
134     private final AtomicInteger connectionsToClose = new AtomicInteger(0);
135 
136     private final AtomicInteger serverErrorsBeforeWorks = new AtomicInteger(0);
137 
138     private final List<LogEntry> logEntries = Collections.synchronizedList(new ArrayList<>());
139 
140     public String getHost() {
141         return "localhost";
142     }
143 
144     public int getHttpPort() {
145         return httpConnector != null ? httpConnector.getLocalPort() : -1;
146     }
147 
148     public int getHttpsPort() {
149         return httpsConnector != null ? httpsConnector.getLocalPort() : -1;
150     }
151 
152     public String getHttpUrl() {
153         return "http://" + getHost() + ":" + getHttpPort();
154     }
155 
156     public String getHttpsUrl() {
157         return "https://" + getHost() + ":" + getHttpsPort();
158     }
159 
160     public HttpServer addSslConnector() {
161         return addSslConnector(true, true);
162     }
163 
164     public HttpServer addSelfSignedSslConnector() {
165         return addSslConnector(false, true);
166     }
167 
168     public HttpServer addSelfSignedSslConnectorHttp2Only() {
169         return addSslConnector(false, false);
170     }
171 
172     private HttpServer addSslConnector(boolean needClientAuth, boolean needHttp11) {
173         if (httpsConnector == null) {
174             SslContextFactory.Server ssl = new SslContextFactory.Server();
175             ssl.setNeedClientAuth(needClientAuth);
176             if (!needClientAuth) {
177                 ssl.setKeyStorePath(HttpTransporterTest.KEY_STORE_SELF_SIGNED_PATH
178                         .toAbsolutePath()
179                         .toString());
180                 ssl.setKeyStorePassword("server-pwd");
181                 ssl.setSniRequired(false);
182             } else {
183                 ssl.setKeyStorePath(
184                         HttpTransporterTest.KEY_STORE_PATH.toAbsolutePath().toString());
185                 ssl.setKeyStorePassword("server-pwd");
186                 ssl.setTrustStorePath(
187                         HttpTransporterTest.TRUST_STORE_PATH.toAbsolutePath().toString());
188                 ssl.setTrustStorePassword("client-pwd");
189                 ssl.setSniRequired(false);
190             }
191 
192             HttpConfiguration httpsConfig = new HttpConfiguration();
193             SecureRequestCustomizer customizer = new SecureRequestCustomizer();
194             customizer.setSniHostCheck(false);
195             httpsConfig.addCustomizer(customizer);
196 
197             HttpConnectionFactory http1 = null;
198             if (needHttp11) {
199                 http1 = new HttpConnectionFactory(httpsConfig);
200             }
201 
202             HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpsConfig);
203 
204             ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
205             alpn.setDefaultProtocol(http1 != null ? http1.getProtocol() : http2.getProtocol());
206 
207             SslConnectionFactory tls = new SslConnectionFactory(ssl, alpn.getProtocol());
208             if (http1 != null) {
209                 httpsConnector = new ServerConnector(server, tls, alpn, http2, http1);
210             } else {
211                 httpsConnector = new ServerConnector(server, tls, alpn, http2);
212             }
213             server.addConnector(httpsConnector);
214             try {
215                 httpsConnector.start();
216             } catch (Exception e) {
217                 throw new IllegalStateException(e);
218             }
219         }
220         return this;
221     }
222 
223     public List<LogEntry> getLogEntries() {
224         return logEntries;
225     }
226 
227     public HttpServer setRepoDir(File repoDir) {
228         this.repoDir = repoDir;
229         return this;
230     }
231 
232     public HttpServer setRangeSupport(boolean rangeSupport) {
233         this.rangeSupport = rangeSupport;
234         return this;
235     }
236 
237     public HttpServer setWebDav(boolean webDav) {
238         this.webDav = webDav;
239         return this;
240     }
241 
242     public HttpServer setExpectSupport(ExpectContinue expectContinue) {
243         this.expectContinue = expectContinue;
244         return this;
245     }
246 
247     public HttpServer setChecksumHeader(ChecksumHeader checksumHeader) {
248         this.checksumHeader = checksumHeader;
249         return this;
250     }
251 
252     public HttpServer setAuthentication(String username, String password) {
253         this.username = username;
254         this.password = password;
255         return this;
256     }
257 
258     public HttpServer setProxyAuthentication(String username, String password) {
259         proxyUsername = username;
260         proxyPassword = password;
261         return this;
262     }
263 
264     public HttpServer setConnectionsToClose(int connectionsToClose) {
265         this.connectionsToClose.set(connectionsToClose);
266         return this;
267     }
268 
269     public HttpServer setServerErrorsBeforeWorks(int serverErrorsBeforeWorks) {
270         this.serverErrorsBeforeWorks.set(serverErrorsBeforeWorks);
271         return this;
272     }
273 
274     public HttpServer start() throws Exception {
275         if (server != null) {
276             return this;
277         }
278 
279         HandlerList handlers = new HandlerList();
280         handlers.addHandler(new ConnectionClosingHandler());
281         handlers.addHandler(new ServerErrorHandler());
282         handlers.addHandler(new LogHandler());
283         handlers.addHandler(new ProxyAuthHandler());
284         handlers.addHandler(new AuthHandler());
285         handlers.addHandler(new RedirectHandler());
286         handlers.addHandler(new RepoHandler());
287 
288         server = new Server();
289         httpConnector = new ServerConnector(server);
290         server.addConnector(httpConnector);
291         server.setHandler(handlers);
292         server.start();
293 
294         return this;
295     }
296 
297     public void stop() throws Exception {
298         if (server != null) {
299             server.stop();
300             server = null;
301             httpConnector = null;
302             httpsConnector = null;
303         }
304     }
305 
306     private class ConnectionClosingHandler extends AbstractHandler {
307         @Override
308         public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) {
309             if (connectionsToClose.getAndDecrement() > 0) {
310                 Response jettyResponse = (Response) response;
311                 jettyResponse.getHttpChannel().getConnection().close();
312             }
313         }
314     }
315 
316     private class ServerErrorHandler extends AbstractHandler {
317         @Override
318         public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response)
319                 throws IOException {
320             if (serverErrorsBeforeWorks.getAndDecrement() > 0) {
321                 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
322                 writeResponseBodyMessage(response, "Oops, come back later!");
323             }
324         }
325     }
326 
327     private class LogHandler extends AbstractHandler {
328         @Override
329         public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) {
330             LOGGER.info(
331                     "{} {}{}",
332                     req.getMethod(),
333                     req.getRequestURL(),
334                     req.getQueryString() != null ? "?" + req.getQueryString() : "");
335 
336             Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
337             for (Enumeration<String> en = req.getHeaderNames(); en.hasMoreElements(); ) {
338                 String name = en.nextElement();
339                 StringBuilder buffer = new StringBuilder(128);
340                 for (Enumeration<String> ien = req.getHeaders(name); ien.hasMoreElements(); ) {
341                     if (buffer.length() > 0) {
342                         buffer.append(", ");
343                     }
344                     buffer.append(ien.nextElement());
345                 }
346                 headers.put(name, buffer.toString());
347             }
348             logEntries.add(new LogEntry(req.getMethod(), req.getPathInfo(), Collections.unmodifiableMap(headers)));
349         }
350     }
351 
352     private static final Pattern SIMPLE_RANGE = Pattern.compile("bytes=([0-9])+-");
353 
354     private class RepoHandler extends AbstractHandler {
355         @Override
356         public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response)
357                 throws IOException {
358             String path = req.getPathInfo().substring(1);
359 
360             if (!path.startsWith("repo/")) {
361                 return;
362             }
363             req.setHandled(true);
364 
365             if (ExpectContinue.FAIL.equals(expectContinue) && request.getHeader(HttpHeader.EXPECT.asString()) != null) {
366                 response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED);
367                 writeResponseBodyMessage(response, "Expectation was set to fail");
368                 return;
369             }
370 
371             File file = new File(repoDir, path.substring(5));
372             if (HttpMethod.GET.is(req.getMethod()) || HttpMethod.HEAD.is(req.getMethod())) {
373                 if (!file.isFile() || path.endsWith(URIUtil.SLASH)) {
374                     response.setStatus(HttpServletResponse.SC_NOT_FOUND);
375                     writeResponseBodyMessage(response, "Not found");
376                     return;
377                 }
378                 long ifUnmodifiedSince = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
379                 if (ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince) {
380                     response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
381                     writeResponseBodyMessage(response, "Precondition failed");
382                     return;
383                 }
384                 long offset = 0L;
385                 String range = request.getHeader(HttpHeader.RANGE.asString());
386                 if (range != null && rangeSupport) {
387                     Matcher m = SIMPLE_RANGE.matcher(range);
388                     if (m.matches()) {
389                         offset = Long.parseLong(m.group(1));
390                         if (offset >= file.length()) {
391                             response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
392                             writeResponseBodyMessage(response, "Range not satisfiable");
393                             return;
394                         }
395                     }
396                     String encoding = request.getHeader(HttpHeader.ACCEPT_ENCODING.asString());
397                     if ((encoding != null && !"identity".equals(encoding)) || ifUnmodifiedSince == -1L) {
398                         response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
399                         return;
400                     }
401                 }
402                 response.setStatus((offset > 0L) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK);
403                 response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(), file.lastModified());
404                 response.setHeader(HttpHeader.CONTENT_LENGTH.asString(), Long.toString(file.length() - offset));
405                 if (offset > 0L) {
406                     response.setHeader(
407                             HttpHeader.CONTENT_RANGE.asString(),
408                             "bytes " + offset + "-" + (file.length() - 1L) + "/" + file.length());
409                 }
410                 if (checksumHeader != null) {
411                     Map<String, String> checksums = ChecksumAlgorithmHelper.calculate(
412                             file, Collections.singletonList(new Sha1ChecksumAlgorithmFactory()));
413                     if (checksumHeader == ChecksumHeader.NEXUS) {
414                         response.setHeader(HttpHeader.ETAG.asString(), "{SHA1{" + checksums.get("SHA-1") + "}}");
415                     } else if (checksumHeader == ChecksumHeader.XCHECKSUM) {
416                         response.setHeader("x-checksum-sha1", checksums.get(Sha1ChecksumAlgorithmFactory.NAME));
417                     }
418                 }
419                 if (HttpMethod.HEAD.is(req.getMethod())) {
420                     return;
421                 }
422                 FileInputStream is = null;
423                 try {
424                     is = new FileInputStream(file);
425                     if (offset > 0L) {
426                         long skipped = is.skip(offset);
427                         while (skipped < offset && is.read() >= 0) {
428                             skipped++;
429                         }
430                     }
431                     IO.copy(is, response.getOutputStream());
432                     is.close();
433                     is = null;
434                 } finally {
435                     try {
436                         if (is != null) {
437                             is.close();
438                         }
439                     } catch (final IOException e) {
440                         // Suppressed due to an exception already thrown in the try block.
441                     }
442                 }
443             } else if (HttpMethod.PUT.is(req.getMethod())) {
444                 if (!webDav) {
445                     file.getParentFile().mkdirs();
446                 }
447                 if (file.getParentFile().exists()) {
448                     try {
449                         FileOutputStream os = null;
450                         try {
451                             os = new FileOutputStream(file);
452                             IO.copy(request.getInputStream(), os);
453                             os.close();
454                             os = null;
455                         } finally {
456                             try {
457                                 if (os != null) {
458                                     os.close();
459                                 }
460                             } catch (final IOException e) {
461                                 // Suppressed due to an exception already thrown in the try block.
462                             }
463                         }
464                     } catch (IOException e) {
465                         file.delete();
466                         throw e;
467                     }
468                     response.setStatus(HttpServletResponse.SC_NO_CONTENT);
469                 } else {
470                     response.setStatus(HttpServletResponse.SC_FORBIDDEN);
471                 }
472             } else if (HttpMethod.OPTIONS.is(req.getMethod())) {
473                 if (webDav) {
474                     response.setHeader("DAV", "1,2");
475                 }
476                 response.setHeader(HttpHeader.ALLOW.asString(), "GET, PUT, HEAD, OPTIONS");
477                 response.setStatus(HttpServletResponse.SC_OK);
478             } else if (webDav && "MKCOL".equals(req.getMethod())) {
479                 if (file.exists()) {
480                     response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
481                 } else if (file.mkdir()) {
482                     response.setStatus(HttpServletResponse.SC_CREATED);
483                 } else {
484                     response.setStatus(HttpServletResponse.SC_CONFLICT);
485                 }
486             } else {
487                 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
488             }
489         }
490     }
491 
492     private void writeResponseBodyMessage(HttpServletResponse response, String message) throws IOException {
493         try (OutputStream outputStream = response.getOutputStream()) {
494             outputStream.write(message.getBytes(StandardCharsets.UTF_8));
495         }
496     }
497 
498     private class RedirectHandler extends AbstractHandler {
499         @Override
500         public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) {
501             String path = req.getPathInfo();
502             if (!path.startsWith("/redirect/")) {
503                 return;
504             }
505             req.setHandled(true);
506             StringBuilder location = new StringBuilder(128);
507             String scheme = req.getParameter("scheme");
508             location.append(scheme != null ? scheme : req.getScheme());
509             location.append("://");
510             location.append(req.getServerName());
511             location.append(":");
512             if ("http".equalsIgnoreCase(scheme)) {
513                 location.append(getHttpPort());
514             } else if ("https".equalsIgnoreCase(scheme)) {
515                 location.append(getHttpsPort());
516             } else {
517                 location.append(req.getServerPort());
518             }
519             location.append("/repo").append(path.substring(9));
520             response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
521             response.setHeader(HttpHeader.LOCATION.asString(), location.toString());
522         }
523     }
524 
525     private class AuthHandler extends AbstractHandler {
526         @Override
527         public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response)
528                 throws IOException {
529             if (ExpectContinue.BROKEN.equals(expectContinue)
530                     && "100-continue".equalsIgnoreCase(request.getHeader(HttpHeader.EXPECT.asString()))) {
531                 request.getInputStream();
532             }
533 
534             if (username != null && password != null) {
535                 if (checkBasicAuth(request.getHeader(HttpHeader.AUTHORIZATION.asString()), username, password)) {
536                     return;
537                 }
538                 req.setHandled(true);
539                 response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\"");
540                 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
541             }
542         }
543     }
544 
545     private class ProxyAuthHandler extends AbstractHandler {
546         @Override
547         public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) {
548             if (proxyUsername != null && proxyPassword != null) {
549                 if (checkBasicAuth(
550                         request.getHeader(HttpHeader.PROXY_AUTHORIZATION.asString()), proxyUsername, proxyPassword)) {
551                     return;
552                 }
553                 req.setHandled(true);
554                 response.setHeader(HttpHeader.PROXY_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\"");
555                 response.setStatus(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED);
556             }
557         }
558     }
559 
560     static boolean checkBasicAuth(String credentials, String username, String password) {
561         if (credentials != null) {
562             int space = credentials.indexOf(' ');
563             if (space > 0) {
564                 String method = credentials.substring(0, space);
565                 if ("basic".equalsIgnoreCase(method)) {
566                     credentials = credentials.substring(space + 1);
567                     credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1);
568                     int i = credentials.indexOf(':');
569                     if (i > 0) {
570                         String user = credentials.substring(0, i);
571                         String pass = credentials.substring(i + 1);
572                         if (username.equals(user) && password.equals(pass)) {
573                             return true;
574                         }
575                     }
576                 }
577             }
578         }
579         return false;
580     }
581 }