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