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 java.io.File;
22 import java.io.IOException;
23 import java.net.DatagramSocket;
24 import java.net.InetSocketAddress;
25 import java.net.ServerSocket;
26 import java.net.URI;
27 import java.nio.channels.SeekableByteChannel;
28 import java.nio.charset.StandardCharsets;
29 import java.nio.file.Files;
30 import java.nio.file.StandardOpenOption;
31 import java.util.ArrayList;
32 import java.util.Base64;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.TreeMap;
38 import java.util.concurrent.CountDownLatch;
39 import java.util.concurrent.atomic.AtomicInteger;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42 import java.util.stream.Collectors;
43
44 import com.google.gson.Gson;
45 import jakarta.servlet.http.HttpServletResponse;
46 import org.eclipse.aether.internal.impl.checksum.Sha1ChecksumAlgorithmFactory;
47 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper;
48 import org.eclipse.aether.spi.connector.transport.http.RFC9457.RFC9457Payload;
49 import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
50 import org.eclipse.jetty.compression.server.CompressionConfig;
51 import org.eclipse.jetty.compression.server.CompressionHandler;
52 import org.eclipse.jetty.http.DateGenerator;
53 import org.eclipse.jetty.http.HttpField;
54 import org.eclipse.jetty.http.HttpFields;
55 import org.eclipse.jetty.http.HttpHeader;
56 import org.eclipse.jetty.http.HttpMethod;
57 import org.eclipse.jetty.http.HttpURI;
58 import org.eclipse.jetty.http.HttpVersion;
59 import org.eclipse.jetty.http.pathmap.MatchedResource;
60 import org.eclipse.jetty.http.pathmap.PathMappings;
61 import org.eclipse.jetty.http.pathmap.PathSpec;
62 import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
63 import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory;
64 import org.eclipse.jetty.http3.server.HTTP3ServerQuicConfiguration;
65 import org.eclipse.jetty.io.ByteBufferPool;
66 import org.eclipse.jetty.io.Content;
67 import org.eclipse.jetty.io.DatagramChannelEndPoint;
68 import org.eclipse.jetty.io.EndPoint;
69 import org.eclipse.jetty.quic.quiche.server.QuicheServerConnector;
70 import org.eclipse.jetty.quic.quiche.server.QuicheServerQuicConfiguration;
71 import org.eclipse.jetty.server.Handler;
72 import org.eclipse.jetty.server.HttpConfiguration;
73 import org.eclipse.jetty.server.HttpConnectionFactory;
74 import org.eclipse.jetty.server.Request;
75 import org.eclipse.jetty.server.Response;
76 import org.eclipse.jetty.server.SecureRequestCustomizer;
77 import org.eclipse.jetty.server.Server;
78 import org.eclipse.jetty.server.ServerConnector;
79 import org.eclipse.jetty.server.SslConnectionFactory;
80 import org.eclipse.jetty.util.Blocker;
81 import org.eclipse.jetty.util.Callback;
82 import org.eclipse.jetty.util.ssl.SslContextFactory;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
85
86 public class HttpServer {
87
88 public static class LogEntry {
89 private final HttpVersion version;
90
91 private final String method;
92
93 private final String path;
94
95 private final Map<String, String> requestHeaders;
96
97 private Map<String, String> responseHeaders;
98
99 CountDownLatch responseHeadersAvailableSignal = new CountDownLatch(1);
100
101 public LogEntry(HttpVersion version, String method, String path, Map<String, String> requestHeaders) {
102 this.version = version;
103 this.method = method;
104 this.path = path;
105 this.requestHeaders = requestHeaders;
106 }
107
108 public HttpVersion getVersion() {
109 return version;
110 }
111
112 public String getMethod() {
113 return method;
114 }
115
116 public String getPath() {
117 return path;
118 }
119
120 public Map<String, String> getRequestHeaders() {
121 return requestHeaders;
122 }
123
124
125
126
127
128 public Map<String, String> getResponseHeaders() {
129 try {
130 if (!responseHeadersAvailableSignal.await(30, java.util.concurrent.TimeUnit.SECONDS)) {
131 throw new IllegalStateException("Timeout waiting for response headers to be available");
132 }
133 } catch (InterruptedException e) {
134 Thread.currentThread().interrupt();
135 throw new IllegalStateException("Interrupted while waiting for response headers to be available", e);
136 }
137 return responseHeaders;
138 }
139
140 public void setResponseHeaders(Map<String, String> responseHeaders) {
141 this.responseHeaders = responseHeaders;
142 responseHeadersAvailableSignal.countDown();
143 }
144
145 @Override
146 public String toString() {
147 return version + " " + method + " " + path;
148 }
149 }
150
151 public enum ExpectContinue {
152 FAIL,
153 PROPER,
154 BROKEN
155 }
156
157 public enum ChecksumHeader {
158 NEXUS,
159 XCHECKSUM,
160 XCHECKSUM_GOOGLE,
161 XCHECKSUM_AMAZON
162 }
163
164 private static final Logger LOGGER = LoggerFactory.getLogger(HttpServer.class);
165
166 private File repoDir;
167
168 private boolean rangeSupport = true;
169
170 private boolean webDav;
171
172 private ExpectContinue expectContinue = ExpectContinue.PROPER;
173
174 private ChecksumHeader checksumHeader;
175
176 private Server server;
177
178 private ServerConnector httpConnector;
179
180 private ServerConnector httpsConnector;
181
182 private QuicheServerConnector http3Connector;
183
184 private String username;
185
186 private String password;
187
188 private String proxyUsername;
189
190 private String proxyPassword;
191
192 private final AtomicInteger connectionsToClose = new AtomicInteger(0);
193
194 private final AtomicInteger serverErrorsBeforeWorks = new AtomicInteger(0);
195
196 private int serverErrorStatusCode;
197
198 private final List<LogEntry> logEntries = Collections.synchronizedList(new ArrayList<>());
199
200 private String responseBodyForPut;
201
202 private Map<String, String> serverErrorHeaders = Collections.emptyMap();
203
204 public String getHost() {
205 return "localhost";
206 }
207
208 public int getHttpPort() {
209 return httpConnector != null ? httpConnector.getLocalPort() : -1;
210 }
211
212 public int getHttpsPort() {
213 return httpsConnector != null ? httpsConnector.getLocalPort() : -1;
214 }
215
216 public int getHttp3Port() {
217 return http3Connector != null ? http3Connector.getLocalPort() : -1;
218 }
219
220 public String getHttpUrl() {
221 return "http://" + getHost() + ":" + getHttpPort();
222 }
223
224 public String getHttpsUrl() {
225 return "https://" + getHost() + ":" + getHttpsPort();
226 }
227
228 public String getHttp3Url() {
229 return "https://" + getHost() + ":" + getHttp3Port();
230 }
231
232 public HttpServer addHttp2ConnectorWithMutualTLS() {
233 return addHttp2Connector(true, true, -1);
234 }
235
236 public HttpServer addHttp2Connector() {
237 return addHttp2Connector(false, true, -1);
238 }
239
240 public HttpServer addHttp2OnlyConnector() {
241 return addHttp2Connector(false, false, -1);
242 }
243
244 public HttpServer addHttp2OnlyConnectorWithMutualTLS() {
245 return addHttp2Connector(true, false, -1);
246 }
247
248 public HttpServer addHttp2OnlyConnectorWithMutualTLS(int port) {
249 return addHttp2Connector(true, false, port);
250 }
251
252 private HttpServer addHttp2Connector(boolean needClientAuth, boolean needHttp11, int port) {
253 if (httpsConnector == null) {
254 SslContextFactory.Server ssl = createServerSslContextFactory(needClientAuth);
255
256 HttpConfiguration httpsConfig = new HttpConfiguration();
257 SecureRequestCustomizer customizer = new SecureRequestCustomizer();
258 customizer.setSniHostCheck(false);
259 httpsConfig.addCustomizer(customizer);
260
261 HttpConnectionFactory http1 = null;
262 if (needHttp11) {
263 http1 = new HttpConnectionFactory(httpsConfig);
264 }
265
266 HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpsConfig);
267
268 ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
269 alpn.setDefaultProtocol(http1 != null ? http1.getProtocol() : http2.getProtocol());
270
271 SslConnectionFactory tls = new SslConnectionFactory(ssl, alpn.getProtocol());
272 if (http1 != null) {
273 httpsConnector = new ServerConnector(server, tls, alpn, http2, http1);
274 } else {
275 httpsConnector = new ServerConnector(server, tls, alpn, http2);
276 }
277
278 if (port != -1) {
279 httpsConnector.setPort(port);
280 }
281
282 server.addConnector(httpsConnector);
283 try {
284 httpsConnector.start();
285 } catch (Exception e) {
286 throw new IllegalStateException(e);
287 }
288 }
289 return this;
290 }
291
292 private SslContextFactory.Server createServerSslContextFactory(boolean needClientAuth) {
293 SslContextFactory.Server ssl = new SslContextFactory.Server();
294 ssl.setSniRequired(false);
295 ssl.setKeyStorePath(
296 HttpTransporterTest.SERVER_STORE_PATH.toAbsolutePath().toString());
297 ssl.setKeyStorePassword("server-pwd");
298 ssl.setNeedClientAuth(needClientAuth);
299 if (needClientAuth) {
300 ssl.setTrustStorePath(
301 HttpTransporterTest.CLIENT_STORE_PATH.toAbsolutePath().toString());
302 ssl.setTrustStorePassword("client-pwd");
303 }
304 return ssl;
305 }
306
307 public HttpServer addHttp3Connector(boolean needClientAuth) {
308 return addHttp3Connector(needClientAuth, -1);
309 }
310
311 public HttpServer addHttp3Connector(boolean needClientAuth, int port) {
312 if (http3Connector == null) {
313 QuicheServerQuicConfiguration serverQuicConfig = HTTP3ServerQuicConfiguration.configure(
314 new QuicheServerQuicConfiguration(HttpTransporterTest.PEM_QUICHE_SERVER_PATH));
315 http3Connector = new QuicheServerConnector(
316 server,
317 createServerSslContextFactory(needClientAuth),
318 serverQuicConfig,
319 new HTTP3ServerConnectionFactory());
320 if (port != -1) {
321 http3Connector.setPort(port);
322 }
323 server.addConnector(http3Connector);
324 try {
325 http3Connector.start();
326 } catch (Exception e) {
327 throw new IllegalStateException(e);
328 }
329 }
330 return this;
331 }
332
333
334
335
336
337
338
339
340 int findFreeTcpAndUdpPort() {
341
342 for (int i = 0; i < 100; i++) {
343 int port;
344 try (ServerSocket serverSocket = new ServerSocket(0)) {
345 port = serverSocket.getLocalPort();
346 } catch (IOException e) {
347 throw new IllegalStateException("Failed to find a free TCP port", e);
348 }
349 if (isUdpPortFree(port)) {
350 return port;
351 }
352 }
353 throw new IllegalStateException("Failed to find a port free for both TCP and UDP");
354 }
355
356 private static boolean isUdpPortFree(int port) {
357 try (DatagramSocket socket = new DatagramSocket(null)) {
358 socket.bind(new InetSocketAddress(port));
359 return true;
360 } catch (IOException e) {
361 return false;
362 }
363 }
364
365 public List<LogEntry> getLogEntries() {
366 return logEntries;
367 }
368
369 public HttpServer setRepoDir(File repoDir) {
370 this.repoDir = repoDir;
371 return this;
372 }
373
374 public HttpServer setRangeSupport(boolean rangeSupport) {
375 this.rangeSupport = rangeSupport;
376 return this;
377 }
378
379 public HttpServer setResponseBodyForPut(String body) {
380 this.responseBodyForPut = body;
381 return this;
382 }
383
384 public HttpServer setWebDav(boolean webDav) {
385 this.webDav = webDav;
386 return this;
387 }
388
389 public HttpServer setExpectSupport(ExpectContinue expectContinue) {
390 this.expectContinue = expectContinue;
391 return this;
392 }
393
394 public HttpServer setChecksumHeader(ChecksumHeader checksumHeader) {
395 this.checksumHeader = checksumHeader;
396 return this;
397 }
398
399 public HttpServer setAuthentication(String username, String password) {
400 this.username = username;
401 this.password = password;
402 return this;
403 }
404
405 public HttpServer setProxyAuthentication(String username, String password) {
406 proxyUsername = username;
407 proxyPassword = password;
408 return this;
409 }
410
411 public HttpServer setConnectionsToClose(int connectionsToClose) {
412 this.connectionsToClose.set(connectionsToClose);
413 return this;
414 }
415
416 public HttpServer setServerErrorsBeforeWorks(int serverErrorsBeforeWorks) {
417 return setServerErrorsBeforeWorks(serverErrorsBeforeWorks, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
418 }
419
420 public HttpServer setServerErrorsBeforeWorks(int serverErrorsBeforeWorks, int errorStatusCode) {
421 return setServerErrorsBeforeWorks(serverErrorsBeforeWorks, errorStatusCode, Collections.emptyMap());
422 }
423
424 public HttpServer setServerErrorsBeforeWorks(
425 int serverErrorsBeforeWorks, int errorStatusCode, Map<String, String> headers) {
426 this.serverErrorsBeforeWorks.set(serverErrorsBeforeWorks);
427 this.serverErrorStatusCode = errorStatusCode;
428 this.serverErrorHeaders = headers;
429 return this;
430 }
431
432 public HttpServer start() throws Exception {
433 if (server != null) {
434 return this;
435 }
436
437 server = new Server();
438 httpConnector = new ServerConnector(server);
439
440 server.addConnector(httpConnector);
441
442 server.setHandler(new LogHandler(new CompressionEnforcingHandler(new Handler.Sequence(
443 new ConnectionClosingHandler(),
444 new ServerErrorHandler(),
445 new ProxyAuthHandler(),
446 new AuthHandler(),
447 new RedirectHandler(),
448 new RepoHandler(),
449 new RFC9457Handler()))));
450 server.start();
451
452 return this;
453 }
454
455 public void stop() throws Exception {
456 if (server != null) {
457 server.stop();
458 server = null;
459 httpConnector = null;
460 httpsConnector = null;
461 }
462 }
463
464 public int getNumConnectedEndPoints() {
465 if (server.isStopped()) {
466 throw new IllegalStateException("Server is stopped");
467 }
468 Collection<EndPoint> connectedEndPoints = new ArrayList<>();
469 if (httpConnector != null) {
470 connectedEndPoints.addAll(httpConnector.getConnectedEndPoints());
471 }
472 if (httpsConnector != null) {
473 connectedEndPoints.addAll(httpsConnector.getConnectedEndPoints());
474 }
475 if (http3Connector != null) {
476
477
478 http3Connector.getConnectedEndPoints().stream()
479 .filter(endPoint -> !(endPoint instanceof DatagramChannelEndPoint))
480 .forEach(connectedEndPoints::add);
481 }
482 return connectedEndPoints.size();
483 }
484
485 private class CompressionEnforcingHandler extends CompressionHandler {
486
487 private final PathMappings<CompressionConfig> pathConfigs = new PathMappings<>();
488
489 CompressionEnforcingHandler(Handler handler) {
490 super(handler);
491 this.putConfiguration(
492 "/br/*",
493 CompressionConfig.builder().compressIncludeEncoding("br").build());
494 this.putConfiguration(
495 "/zstd/*",
496 CompressionConfig.builder().compressIncludeEncoding("zstd").build());
497 this.putConfiguration(
498 "/gzip/*",
499 CompressionConfig.builder().compressIncludeEncoding("gzip").build());
500 this.putConfiguration(
501 "/deflate/*",
502 CompressionConfig.builder()
503 .compressIncludeEncoding("deflate")
504 .build());
505 }
506
507 @Override
508 public CompressionConfig putConfiguration(PathSpec pathSpec, CompressionConfig config) {
509
510 return pathConfigs.put(pathSpec, config);
511 }
512
513 @Override
514 public boolean handle(Request request, Response response, Callback callback) throws Exception {
515 Handler next = getHandler();
516 if (next == null) {
517 return false;
518 }
519 String pathInContext = Request.getPathInContext(request);
520 MatchedResource<CompressionConfig> matchedConfig = this.pathConfigs.getMatched(pathInContext);
521 if (matchedConfig == null) {
522 if (LOGGER.isDebugEnabled()) {
523 LOGGER.debug("skipping compression: path {} has no matching compression config", pathInContext);
524 }
525
526 return next.handle(request, response, callback);
527 }
528
529
530
531 super.putConfiguration(PathSpec.from("/*"), matchedConfig.getResource());
532
533 return super.handle(new StripLeadingPathSegmentsRequestWrapper(request, 1), response, callback);
534 }
535 }
536
537 private static class StripLeadingPathSegmentsRequestWrapper extends Request.Wrapper {
538 private final HttpURI modifiedURI;
539
540 StripLeadingPathSegmentsRequestWrapper(Request wrapped, int segmentsToStrip) {
541 super(wrapped);
542 this.modifiedURI = stripPathSegments(wrapped.getHttpURI(), segmentsToStrip);
543 }
544
545 private static HttpURI stripPathSegments(HttpURI originalURI, int segmentsToStrip) {
546 if (segmentsToStrip <= 0) {
547 return originalURI;
548 }
549
550 String originalPath = originalURI.getPath();
551 if (originalPath == null || originalPath.isEmpty()) {
552 return originalURI;
553 }
554
555
556 String[] segments = originalPath.split("/");
557 StringBuilder newPath = new StringBuilder();
558
559
560 int skipCount = 0;
561 for (int i = 0; i < segments.length; i++) {
562 if (segments[i].isEmpty() && i == 0) {
563
564 continue;
565 }
566 if (skipCount < segmentsToStrip) {
567 skipCount++;
568 continue;
569 }
570 newPath.append("/").append(segments[i]);
571 }
572
573
574 if (newPath.isEmpty()) {
575 newPath.append("/");
576 }
577
578
579 return org.eclipse.jetty.http.HttpURI.build(originalURI)
580 .path(newPath.toString())
581 .asImmutable();
582 }
583
584 @Override
585 public HttpURI getHttpURI() {
586 return modifiedURI;
587 }
588 }
589
590 private class ConnectionClosingHandler extends Handler.Abstract {
591
592 @Override
593 public boolean handle(Request request, Response response, Callback callback) throws Exception {
594 if (connectionsToClose.getAndDecrement() > 0) {
595 request.getConnectionMetaData().getConnection().close();
596 }
597 return false;
598 }
599 }
600
601 private class ServerErrorHandler extends Handler.Abstract {
602 @Override
603 public boolean handle(Request request, Response response, Callback callback) throws IOException {
604 if (serverErrorsBeforeWorks.getAndDecrement() > 0) {
605 response.setStatus(serverErrorStatusCode);
606 for (Map.Entry<String, String> header : serverErrorHeaders.entrySet()) {
607 response.getHeaders().add(header.getKey(), header.getValue());
608 }
609 writeResponseBodyMessage(request, response, "Oops, come back later!");
610 return true;
611 }
612 return false;
613 }
614 }
615
616 private class LogHandler extends Handler.Wrapper {
617
618 LogHandler(Handler handler) {
619 super(handler);
620 }
621
622 @Override
623 public boolean handle(Request req, Response response, Callback callback) throws Exception {
624
625 LOGGER.info(
626 "{} {} {}{}",
627 req.getConnectionMetaData().getHttpVersion(),
628 req.getMethod(),
629 req.getHttpURI().getDecodedPath(),
630 req.getHttpURI().getQuery() != null ? "?" + req.getHttpURI().getQuery() : "");
631
632 Map<String, String> requestHeaders =
633 toUnmodifiableMap(req.getHeaders());
634 LogEntry logEntry = new LogEntry(
635 req.getConnectionMetaData().getHttpVersion(),
636 req.getMethod(),
637 req.getHttpURI().getPathQuery(),
638 requestHeaders);
639 logEntries.add(logEntry);
640
641 boolean result = super.handle(req, response, callback);
642
643
644 logEntry.setResponseHeaders(toUnmodifiableMap(response.getHeaders()));
645 if (result) {
646 callback.succeeded();
647 }
648 return result;
649 }
650
651 Map<String, String> toUnmodifiableMap(HttpFields headers) {
652 Map<String, String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
653 for (HttpField header : headers) {
654 map.put(header.getName(), header.getValueList().stream().collect(Collectors.joining(", ")));
655 }
656 return Collections.unmodifiableMap(map);
657 }
658 }
659
660 private static final Pattern SIMPLE_RANGE = Pattern.compile("bytes=([0-9])+-");
661
662 private class RepoHandler extends Handler.Abstract {
663 @Override
664 public boolean handle(Request req, Response response, Callback callback) throws Exception {
665 String path = req.getHttpURI().getDecodedPath().substring(1);
666
667 if (!path.startsWith("repo/")) {
668 return false;
669 }
670
671 if (ExpectContinue.FAIL.equals(expectContinue) && req.getHeaders().get(HttpHeader.EXPECT) != null) {
672 response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED);
673 writeResponseBodyMessage(req, response, "Expectation was set to fail");
674 return true;
675 }
676
677 File file = new File(repoDir, path.substring(5));
678 if (HttpMethod.GET.is(req.getMethod()) || HttpMethod.HEAD.is(req.getMethod())) {
679 if (!file.isFile() || path.endsWith("/")) {
680 response.setStatus(HttpServletResponse.SC_NOT_FOUND);
681 writeResponseBodyMessage(req, response, "Not found");
682 return true;
683 }
684
685 long ifUnmodifiedSince = req.getHeaders().getDateField(HttpHeader.IF_UNMODIFIED_SINCE);
686 if (ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince) {
687 response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
688 writeResponseBodyMessage(req, response, "Precondition failed");
689 return true;
690 }
691
692 long offset = 0L;
693 String range = req.getHeaders().get(HttpHeader.RANGE);
694 if (range != null && rangeSupport) {
695 Matcher m = SIMPLE_RANGE.matcher(range);
696 if (m.matches()) {
697 offset = Long.parseLong(m.group(1));
698 if (offset >= file.length()) {
699 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
700 writeResponseBodyMessage(req, response, "Range not satisfiable");
701 return true;
702 }
703 }
704 String encoding = req.getHeaders().get(HttpHeader.ACCEPT_ENCODING);
705 if ((encoding != null && !"identity".equals(encoding)) || ifUnmodifiedSince == -1L) {
706 response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
707 return true;
708 }
709 }
710
711 response.setStatus((offset > 0L) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK);
712 response.getHeaders().add(HttpHeader.LAST_MODIFIED, DateGenerator.formatDate(file.lastModified()));
713 response.getHeaders().add(HttpHeader.CONTENT_LENGTH, Long.toString(file.length() - offset));
714 if (offset > 0L) {
715 response.getHeaders()
716 .add(
717 HttpHeader.CONTENT_RANGE,
718 "bytes " + offset + "-" + (file.length() - 1L) + "/" + file.length());
719 }
720 if (checksumHeader != null) {
721 Map<String, String> checksums = ChecksumAlgorithmHelper.calculate(
722 file, Collections.singletonList(new Sha1ChecksumAlgorithmFactory()));
723 if (checksumHeader == ChecksumHeader.NEXUS) {
724 response.getHeaders().add(HttpHeader.ETAG.asString(), "{SHA1{" + checksums.get("SHA-1") + "}}");
725 } else if (checksumHeader == ChecksumHeader.XCHECKSUM) {
726 response.getHeaders().add("x-checksum-sha1", checksums.get(Sha1ChecksumAlgorithmFactory.NAME));
727 } else if (checksumHeader == ChecksumHeader.XCHECKSUM_GOOGLE) {
728 response.getHeaders()
729 .add("x-goog-meta-checksum-sha1", checksums.get(Sha1ChecksumAlgorithmFactory.NAME));
730 } else if (checksumHeader == ChecksumHeader.XCHECKSUM_AMAZON) {
731 response.getHeaders()
732 .add("x-amz-meta-checksum-sha1", checksums.get(Sha1ChecksumAlgorithmFactory.NAME));
733 }
734 }
735 if (HttpMethod.HEAD.is(req.getMethod())) {
736 return true;
737 }
738 Content.Source contentSource =
739 Content.Source.from(new ByteBufferPool.Sized(null), file.toPath(), offset, -1);
740 try (Blocker.Callback fileReadCallback = Blocker.callback()) {
741 Content.copy(contentSource, response, fileReadCallback);
742 fileReadCallback.block();
743 }
744 } else if (HttpMethod.PUT.is(req.getMethod())) {
745 if (!webDav) {
746 file.getParentFile().mkdirs();
747 }
748 if (file.getParentFile().exists()) {
749 try (SeekableByteChannel channel = Files.newByteChannel(
750 file.toPath(),
751 StandardOpenOption.CREATE,
752 StandardOpenOption.WRITE,
753 StandardOpenOption.TRUNCATE_EXISTING);
754 Blocker.Callback fileWriteCallback = Blocker.callback()) {
755 Content.copy(req, Content.Sink.from(channel), fileWriteCallback);
756 fileWriteCallback.block();
757 } catch (IOException e) {
758 LOGGER.warn("Failed to write file {}", file.getAbsolutePath(), e);
759 file.delete();
760 throw e;
761 }
762
763
764 if (responseBodyForPut != null) {
765 writeResponseBodyMessage(req, response, responseBodyForPut);
766 response.setStatus(HttpServletResponse.SC_CREATED);
767 } else {
768 response.setStatus(HttpServletResponse.SC_NO_CONTENT);
769 }
770 } else {
771 response.setStatus(HttpServletResponse.SC_FORBIDDEN);
772 }
773 } else if (HttpMethod.OPTIONS.is(req.getMethod())) {
774 if (webDav) {
775 response.getHeaders().add("DAV", "1,2");
776 }
777 response.getHeaders().add(HttpHeader.ALLOW, "GET, PUT, HEAD, OPTIONS");
778 response.setStatus(HttpServletResponse.SC_OK);
779 } else if (webDav && "MKCOL".equals(req.getMethod())) {
780 if (file.exists()) {
781 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
782 } else if (file.mkdir()) {
783 response.setStatus(HttpServletResponse.SC_CREATED);
784 } else {
785 response.setStatus(HttpServletResponse.SC_CONFLICT);
786 }
787 } else {
788 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
789 }
790 return true;
791 }
792 }
793
794 private void writeResponseBodyMessage(Request request, Response response, String message) throws IOException {
795
796 try (Blocker.Callback callback = Blocker.callback()) {
797 Content.Sink.write(response, false, message, callback);
798 callback.block();
799 }
800 }
801
802 private class RFC9457Handler extends Handler.Abstract {
803 @Override
804 public boolean handle(Request req, Response response, Callback callback) throws Exception {
805 String path = req.getHttpURI().getPath().substring(1);
806
807 if (!path.startsWith("rfc9457/")) {
808 return false;
809 }
810
811 if (HttpMethod.GET.is(req.getMethod())) {
812 response.setStatus(HttpServletResponse.SC_FORBIDDEN);
813 response.getHeaders().add(HttpHeader.CONTENT_TYPE.asString(), "application/problem+json");
814 RFC9457Payload rfc9457Payload;
815 if (path.endsWith("missing_fields.txt")) {
816 rfc9457Payload = new RFC9457Payload(null, null, null, null, null);
817 } else {
818 rfc9457Payload = new RFC9457Payload(
819 URI.create("https://example.com/probs/out-of-credit"),
820 HttpServletResponse.SC_FORBIDDEN,
821 "You do not have enough credit.",
822 "Your current balance is 30, but that costs 50.",
823 URI.create("/account/12345/msgs/abc"));
824 }
825 writeResponseBodyMessage(req, response, buildRFC9457Message(rfc9457Payload));
826 }
827 return true;
828 }
829 }
830
831 private String buildRFC9457Message(RFC9457Payload payload) {
832 return new Gson().toJson(payload, RFC9457Payload.class);
833 }
834
835 private class RedirectHandler extends Handler.Abstract {
836 @Override
837 public boolean handle(Request req, Response response, Callback callback) throws Exception {
838 String path = req.getHttpURI().getPath();
839 if (!path.startsWith("/redirect/")) {
840 return false;
841 }
842 StringBuilder location = new StringBuilder(128);
843 String scheme = Request.getParameters(req).getValue("scheme");
844 location.append(scheme != null ? scheme : req.getHttpURI().getScheme());
845 location.append("://");
846 location.append(Request.getServerName(req));
847 location.append(":");
848 if ("http".equalsIgnoreCase(scheme)) {
849 location.append(getHttpPort());
850 } else if ("https".equalsIgnoreCase(scheme)) {
851 location.append(getHttpsPort());
852 } else {
853 location.append(Request.getServerPort(req));
854 }
855 location.append("/repo").append(path.substring(9));
856 Response.sendRedirect(
857 req, response, callback, HttpServletResponse.SC_MOVED_PERMANENTLY, location.toString(), false);
858 return true;
859 }
860 }
861
862 private class AuthHandler extends Handler.Abstract {
863 @Override
864 public boolean handle(Request request, Response response, Callback callback) throws Exception {
865 if (ExpectContinue.BROKEN.equals(expectContinue)
866 && "100-continue".equalsIgnoreCase(request.getHeaders().get(HttpHeader.EXPECT))) {
867
868 Request.asInputStream(request);
869 }
870
871 if (username != null && password != null) {
872 if (checkBasicAuth(request.getHeaders().get(HttpHeader.AUTHORIZATION), username, password)) {
873 return false;
874 }
875 response.getHeaders().add(HttpHeader.WWW_AUTHENTICATE, "Basic realm=\"Test-Realm\"");
876 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
877 return true;
878 }
879 return false;
880 }
881 }
882
883 private class ProxyAuthHandler extends Handler.Abstract {
884 @Override
885 public boolean handle(Request req, Response response, Callback callback) throws Exception {
886 if (proxyUsername != null && proxyPassword != null) {
887 if (checkBasicAuth(
888 req.getHeaders().get(HttpHeader.PROXY_AUTHORIZATION), proxyUsername, proxyPassword)) {
889 return false;
890 }
891 response.getHeaders().add(HttpHeader.PROXY_AUTHENTICATE, "basic realm=\"Test-Realm\"");
892 response.setStatus(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED);
893 return true;
894 } else {
895 return false;
896 }
897 }
898 }
899
900 static boolean checkBasicAuth(String credentials, String username, String password) {
901 if (credentials != null) {
902 int space = credentials.indexOf(' ');
903 if (space > 0) {
904 String method = credentials.substring(0, space);
905 if ("basic".equalsIgnoreCase(method)) {
906 credentials = credentials.substring(space + 1);
907 credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1);
908 int i = credentials.indexOf(':');
909 if (i > 0) {
910 String user = credentials.substring(0, i);
911 String pass = credentials.substring(i + 1);
912 if (username.equals(user) && password.equals(pass)) {
913 return true;
914 }
915 }
916 }
917 }
918 }
919 return false;
920 }
921 }