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