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