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