1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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 }