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