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 if (port != -1) {
278 httpsConnector.setPort(port);
279 }
280 server.addConnector(httpsConnector);
281 try {
282 httpsConnector.start();
283 } catch (Exception e) {
284 throw new IllegalStateException(e);
285 }
286 }
287 return this;
288 }
289
290 private SslContextFactory.Server createServerSslContextFactory(boolean needClientAuth) {
291 SslContextFactory.Server ssl = new SslContextFactory.Server();
292 ssl.setSniRequired(false);
293 ssl.setKeyStorePath(
294 HttpTransporterTest.SERVER_STORE_PATH.toAbsolutePath().toString());
295 ssl.setKeyStorePassword("server-pwd");
296 ssl.setNeedClientAuth(needClientAuth);
297 if (needClientAuth) {
298 ssl.setTrustStorePath(
299 HttpTransporterTest.CLIENT_STORE_PATH.toAbsolutePath().toString());
300 ssl.setTrustStorePassword("client-pwd");
301 }
302 return ssl;
303 }
304
305 public HttpServer addHttp3Connector(boolean needClientAuth) {
306 return addHttp3Connector(needClientAuth, -1);
307 }
308
309 public HttpServer addHttp3Connector(boolean needClientAuth, int port) {
310 if (http3Connector == null) {
311 QuicheServerQuicConfiguration serverQuicConfig = HTTP3ServerQuicConfiguration.configure(
312 new QuicheServerQuicConfiguration(HttpTransporterTest.PEM_QUICHE_SERVER_PATH));
313 http3Connector = new QuicheServerConnector(
314 server,
315 createServerSslContextFactory(needClientAuth),
316 serverQuicConfig,
317 new HTTP3ServerConnectionFactory());
318 if (port != -1) {
319 http3Connector.setPort(port);
320 }
321 server.addConnector(http3Connector);
322 try {
323 http3Connector.start();
324 } catch (Exception e) {
325 throw new IllegalStateException(e);
326 }
327 }
328 return this;
329 }
330
331
332
333
334
335
336
337
338 int findFreeTcpAndUdpPort() {
339 for (int i = 0; i < 20; i++) {
340 int port;
341 try (ServerSocket serverSocket = new ServerSocket(0)) {
342 port = serverSocket.getLocalPort();
343 } catch (IOException e) {
344 throw new IllegalStateException("Failed to find a free TCP port", e);
345 }
346 if (isUdpPortFree(port)) {
347 return port;
348 }
349 }
350 throw new IllegalStateException("Failed to find a port free for both TCP and UDP");
351 }
352
353 private static boolean isUdpPortFree(int port) {
354 try (DatagramSocket socket = new DatagramSocket(null)) {
355 socket.bind(new InetSocketAddress(port));
356 return true;
357 } catch (IOException e) {
358 return false;
359 }
360 }
361
362 public List<LogEntry> getLogEntries() {
363 return logEntries;
364 }
365
366 public HttpServer setRepoDir(File repoDir) {
367 this.repoDir = repoDir;
368 return this;
369 }
370
371 public HttpServer setRangeSupport(boolean rangeSupport) {
372 this.rangeSupport = rangeSupport;
373 return this;
374 }
375
376 public HttpServer setResponseBodyForPut(String body) {
377 this.responseBodyForPut = body;
378 return this;
379 }
380
381 public HttpServer setWebDav(boolean webDav) {
382 this.webDav = webDav;
383 return this;
384 }
385
386 public HttpServer setExpectSupport(ExpectContinue expectContinue) {
387 this.expectContinue = expectContinue;
388 return this;
389 }
390
391 public HttpServer setChecksumHeader(ChecksumHeader checksumHeader) {
392 this.checksumHeader = checksumHeader;
393 return this;
394 }
395
396 public HttpServer setAuthentication(String username, String password) {
397 this.username = username;
398 this.password = password;
399 return this;
400 }
401
402 public HttpServer setProxyAuthentication(String username, String password) {
403 proxyUsername = username;
404 proxyPassword = password;
405 return this;
406 }
407
408 public HttpServer setConnectionsToClose(int connectionsToClose) {
409 this.connectionsToClose.set(connectionsToClose);
410 return this;
411 }
412
413 public HttpServer setServerErrorsBeforeWorks(int serverErrorsBeforeWorks) {
414 return setServerErrorsBeforeWorks(serverErrorsBeforeWorks, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
415 }
416
417 public HttpServer setServerErrorsBeforeWorks(int serverErrorsBeforeWorks, int errorStatusCode) {
418 return setServerErrorsBeforeWorks(serverErrorsBeforeWorks, errorStatusCode, Collections.emptyMap());
419 }
420
421 public HttpServer setServerErrorsBeforeWorks(
422 int serverErrorsBeforeWorks, int errorStatusCode, Map<String, String> headers) {
423 this.serverErrorsBeforeWorks.set(serverErrorsBeforeWorks);
424 this.serverErrorStatusCode = errorStatusCode;
425 this.serverErrorHeaders = headers;
426 return this;
427 }
428
429 public HttpServer start() throws Exception {
430 if (server != null) {
431 return this;
432 }
433
434 server = new Server();
435 httpConnector = new ServerConnector(server);
436
437 server.addConnector(httpConnector);
438
439 server.setHandler(new LogHandler(new CompressionEnforcingHandler(new Handler.Sequence(
440 new ConnectionClosingHandler(),
441 new ServerErrorHandler(),
442 new ProxyAuthHandler(),
443 new AuthHandler(),
444 new RedirectHandler(),
445 new RepoHandler(),
446 new RFC9457Handler()))));
447 server.start();
448
449 return this;
450 }
451
452 public void stop() throws Exception {
453 if (server != null) {
454 server.stop();
455 server = null;
456 httpConnector = null;
457 httpsConnector = null;
458 }
459 }
460
461 public int getNumConnectedEndPoints() {
462 if (server.isStopped()) {
463 throw new IllegalStateException("Server is stopped");
464 }
465 Collection<EndPoint> connectedEndPoints = new ArrayList<>();
466 if (httpConnector != null) {
467 connectedEndPoints.addAll(httpConnector.getConnectedEndPoints());
468 }
469 if (httpsConnector != null) {
470 connectedEndPoints.addAll(httpsConnector.getConnectedEndPoints());
471 }
472 if (http3Connector != null) {
473
474
475 http3Connector.getConnectedEndPoints().stream()
476 .filter(endPoint -> !(endPoint instanceof DatagramChannelEndPoint))
477 .forEach(connectedEndPoints::add);
478 }
479 return connectedEndPoints.size();
480 }
481
482 private class CompressionEnforcingHandler extends CompressionHandler {
483
484 private final PathMappings<CompressionConfig> pathConfigs = new PathMappings<>();
485
486 CompressionEnforcingHandler(Handler handler) {
487 super(handler);
488 this.putConfiguration(
489 "/br/*",
490 CompressionConfig.builder().compressIncludeEncoding("br").build());
491 this.putConfiguration(
492 "/zstd/*",
493 CompressionConfig.builder().compressIncludeEncoding("zstd").build());
494 this.putConfiguration(
495 "/gzip/*",
496 CompressionConfig.builder().compressIncludeEncoding("gzip").build());
497 this.putConfiguration(
498 "/deflate/*",
499 CompressionConfig.builder()
500 .compressIncludeEncoding("deflate")
501 .build());
502 }
503
504 @Override
505 public CompressionConfig putConfiguration(PathSpec pathSpec, CompressionConfig config) {
506
507 return pathConfigs.put(pathSpec, config);
508 }
509
510 @Override
511 public boolean handle(Request request, Response response, Callback callback) throws Exception {
512 Handler next = getHandler();
513 if (next == null) {
514 return false;
515 }
516 String pathInContext = Request.getPathInContext(request);
517 MatchedResource<CompressionConfig> matchedConfig = this.pathConfigs.getMatched(pathInContext);
518 if (matchedConfig == null) {
519 if (LOGGER.isDebugEnabled()) {
520 LOGGER.debug("skipping compression: path {} has no matching compression config", pathInContext);
521 }
522
523 return next.handle(request, response, callback);
524 }
525
526
527
528 super.putConfiguration(PathSpec.from("/*"), matchedConfig.getResource());
529
530 return super.handle(new StripLeadingPathSegmentsRequestWrapper(request, 1), response, callback);
531 }
532 }
533
534 private static class StripLeadingPathSegmentsRequestWrapper extends Request.Wrapper {
535 private final HttpURI modifiedURI;
536
537 StripLeadingPathSegmentsRequestWrapper(Request wrapped, int segmentsToStrip) {
538 super(wrapped);
539 this.modifiedURI = stripPathSegments(wrapped.getHttpURI(), segmentsToStrip);
540 }
541
542 private static HttpURI stripPathSegments(HttpURI originalURI, int segmentsToStrip) {
543 if (segmentsToStrip <= 0) {
544 return originalURI;
545 }
546
547 String originalPath = originalURI.getPath();
548 if (originalPath == null || originalPath.isEmpty()) {
549 return originalURI;
550 }
551
552
553 String[] segments = originalPath.split("/");
554 StringBuilder newPath = new StringBuilder();
555
556
557 int skipCount = 0;
558 for (int i = 0; i < segments.length; i++) {
559 if (segments[i].isEmpty() && i == 0) {
560
561 continue;
562 }
563 if (skipCount < segmentsToStrip) {
564 skipCount++;
565 continue;
566 }
567 newPath.append("/").append(segments[i]);
568 }
569
570
571 if (newPath.isEmpty()) {
572 newPath.append("/");
573 }
574
575
576 return org.eclipse.jetty.http.HttpURI.build(originalURI)
577 .path(newPath.toString())
578 .asImmutable();
579 }
580
581 @Override
582 public HttpURI getHttpURI() {
583 return modifiedURI;
584 }
585 }
586
587 private class ConnectionClosingHandler extends Handler.Abstract {
588
589 @Override
590 public boolean handle(Request request, Response response, Callback callback) throws Exception {
591 if (connectionsToClose.getAndDecrement() > 0) {
592 request.getConnectionMetaData().getConnection().close();
593 }
594 return false;
595 }
596 }
597
598 private class ServerErrorHandler extends Handler.Abstract {
599 @Override
600 public boolean handle(Request request, Response response, Callback callback) throws IOException {
601 if (serverErrorsBeforeWorks.getAndDecrement() > 0) {
602 response.setStatus(serverErrorStatusCode);
603 for (Map.Entry<String, String> header : serverErrorHeaders.entrySet()) {
604 response.getHeaders().add(header.getKey(), header.getValue());
605 }
606 writeResponseBodyMessage(request, response, "Oops, come back later!");
607 return true;
608 }
609 return false;
610 }
611 }
612
613 private class LogHandler extends Handler.Wrapper {
614
615 LogHandler(Handler handler) {
616 super(handler);
617 }
618
619 @Override
620 public boolean handle(Request req, Response response, Callback callback) throws Exception {
621
622 LOGGER.info(
623 "{} {} {}{}",
624 req.getConnectionMetaData().getHttpVersion(),
625 req.getMethod(),
626 req.getHttpURI().getDecodedPath(),
627 req.getHttpURI().getQuery() != null ? "?" + req.getHttpURI().getQuery() : "");
628
629 Map<String, String> requestHeaders =
630 toUnmodifiableMap(req.getHeaders());
631 LogEntry logEntry = new LogEntry(
632 req.getConnectionMetaData().getHttpVersion(),
633 req.getMethod(),
634 req.getHttpURI().getPathQuery(),
635 requestHeaders);
636 logEntries.add(logEntry);
637
638 boolean result = super.handle(req, response, callback);
639
640
641 logEntry.setResponseHeaders(toUnmodifiableMap(response.getHeaders()));
642 if (result) {
643 callback.succeeded();
644 }
645 return result;
646 }
647
648 Map<String, String> toUnmodifiableMap(HttpFields headers) {
649 Map<String, String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
650 for (HttpField header : headers) {
651 map.put(header.getName(), header.getValueList().stream().collect(Collectors.joining(", ")));
652 }
653 return Collections.unmodifiableMap(map);
654 }
655 }
656
657 private static final Pattern SIMPLE_RANGE = Pattern.compile("bytes=([0-9])+-");
658
659 private class RepoHandler extends Handler.Abstract {
660 @Override
661 public boolean handle(Request req, Response response, Callback callback) throws Exception {
662 String path = req.getHttpURI().getDecodedPath().substring(1);
663
664 if (!path.startsWith("repo/")) {
665 return false;
666 }
667
668 if (ExpectContinue.FAIL.equals(expectContinue) && req.getHeaders().get(HttpHeader.EXPECT) != null) {
669 response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED);
670 writeResponseBodyMessage(req, response, "Expectation was set to fail");
671 return true;
672 }
673
674 File file = new File(repoDir, path.substring(5));
675 if (HttpMethod.GET.is(req.getMethod()) || HttpMethod.HEAD.is(req.getMethod())) {
676 if (!file.isFile() || path.endsWith("/")) {
677 response.setStatus(HttpServletResponse.SC_NOT_FOUND);
678 writeResponseBodyMessage(req, response, "Not found");
679 return true;
680 }
681 long ifUnmodifiedSince = req.getHeaders().getDateField(HttpHeader.IF_UNMODIFIED_SINCE);
682 if (ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince) {
683 response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
684 writeResponseBodyMessage(req, response, "Precondition failed");
685 return true;
686 }
687 long offset = 0L;
688 String range = req.getHeaders().get(HttpHeader.RANGE);
689 if (range != null && rangeSupport) {
690 Matcher m = SIMPLE_RANGE.matcher(range);
691 if (m.matches()) {
692 offset = Long.parseLong(m.group(1));
693 if (offset >= file.length()) {
694 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
695 writeResponseBodyMessage(req, response, "Range not satisfiable");
696 return true;
697 }
698 }
699 String encoding = req.getHeaders().get(HttpHeader.ACCEPT_ENCODING);
700 if ((encoding != null && !"identity".equals(encoding)) || ifUnmodifiedSince == -1L) {
701 response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
702 return true;
703 }
704 }
705 response.setStatus((offset > 0L) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK);
706 response.getHeaders().add(HttpHeader.LAST_MODIFIED, DateGenerator.formatDate(file.lastModified()));
707 response.getHeaders().add(HttpHeader.CONTENT_LENGTH, Long.toString(file.length() - offset));
708 if (offset > 0L) {
709 response.getHeaders()
710 .add(
711 HttpHeader.CONTENT_RANGE,
712 "bytes " + offset + "-" + (file.length() - 1L) + "/" + file.length());
713 }
714 if (checksumHeader != null) {
715 Map<String, String> checksums = ChecksumAlgorithmHelper.calculate(
716 file, Collections.singletonList(new Sha1ChecksumAlgorithmFactory()));
717 if (checksumHeader == ChecksumHeader.NEXUS) {
718 response.getHeaders().add(HttpHeader.ETAG.asString(), "{SHA1{" + checksums.get("SHA-1") + "}}");
719 } else if (checksumHeader == ChecksumHeader.XCHECKSUM) {
720 response.getHeaders().add("x-checksum-sha1", checksums.get(Sha1ChecksumAlgorithmFactory.NAME));
721 } else if (checksumHeader == ChecksumHeader.XCHECKSUM_GOOGLE) {
722 response.getHeaders()
723 .add("x-goog-meta-checksum-sha1", checksums.get(Sha1ChecksumAlgorithmFactory.NAME));
724 } else if (checksumHeader == ChecksumHeader.XCHECKSUM_AMAZON) {
725 response.getHeaders()
726 .add("x-amz-meta-checksum-sha1", checksums.get(Sha1ChecksumAlgorithmFactory.NAME));
727 }
728 }
729 if (HttpMethod.HEAD.is(req.getMethod())) {
730 return true;
731 }
732 Content.Source contentSource =
733 Content.Source.from(new ByteBufferPool.Sized(null), file.toPath(), offset, -1);
734 try (Blocker.Callback fileReadCallback = Blocker.callback()) {
735 Content.copy(contentSource, response, fileReadCallback);
736 fileReadCallback.block();
737 }
738 } else if (HttpMethod.PUT.is(req.getMethod())) {
739 if (!webDav) {
740 file.getParentFile().mkdirs();
741 }
742 if (file.getParentFile().exists()) {
743 try (SeekableByteChannel channel = Files.newByteChannel(
744 file.toPath(),
745 StandardOpenOption.CREATE,
746 StandardOpenOption.WRITE,
747 StandardOpenOption.TRUNCATE_EXISTING);
748 Blocker.Callback fileWriteCallback = Blocker.callback()) {
749 Content.copy(req, Content.Sink.from(channel), fileWriteCallback);
750 fileWriteCallback.block();
751 } catch (IOException e) {
752 LOGGER.warn("Failed to write file {}", file.getAbsolutePath(), e);
753 file.delete();
754 throw e;
755 }
756
757
758 if (responseBodyForPut != null) {
759 writeResponseBodyMessage(req, response, responseBodyForPut);
760 response.setStatus(HttpServletResponse.SC_CREATED);
761 } else {
762 response.setStatus(HttpServletResponse.SC_NO_CONTENT);
763 }
764 } else {
765 response.setStatus(HttpServletResponse.SC_FORBIDDEN);
766 }
767 } else if (HttpMethod.OPTIONS.is(req.getMethod())) {
768 if (webDav) {
769 response.getHeaders().add("DAV", "1,2");
770 }
771 response.getHeaders().add(HttpHeader.ALLOW, "GET, PUT, HEAD, OPTIONS");
772 response.setStatus(HttpServletResponse.SC_OK);
773 } else if (webDav && "MKCOL".equals(req.getMethod())) {
774 if (file.exists()) {
775 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
776 } else if (file.mkdir()) {
777 response.setStatus(HttpServletResponse.SC_CREATED);
778 } else {
779 response.setStatus(HttpServletResponse.SC_CONFLICT);
780 }
781 } else {
782 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
783 }
784 return true;
785 }
786 }
787
788 private void writeResponseBodyMessage(Request request, Response response, String message) throws IOException {
789
790 try (Blocker.Callback callback = Blocker.callback()) {
791 Content.Sink.write(response, false, message, callback);
792 callback.block();
793 }
794 }
795
796 private class RFC9457Handler extends Handler.Abstract {
797 @Override
798 public boolean handle(Request req, Response response, Callback callback) throws Exception {
799 String path = req.getHttpURI().getPath().substring(1);
800
801 if (!path.startsWith("rfc9457/")) {
802 return false;
803 }
804
805 if (HttpMethod.GET.is(req.getMethod())) {
806 response.setStatus(HttpServletResponse.SC_FORBIDDEN);
807 response.getHeaders().add(HttpHeader.CONTENT_TYPE.asString(), "application/problem+json");
808 RFC9457Payload rfc9457Payload;
809 if (path.endsWith("missing_fields.txt")) {
810 rfc9457Payload = new RFC9457Payload(null, null, null, null, null);
811 } else {
812 rfc9457Payload = new RFC9457Payload(
813 URI.create("https://example.com/probs/out-of-credit"),
814 HttpServletResponse.SC_FORBIDDEN,
815 "You do not have enough credit.",
816 "Your current balance is 30, but that costs 50.",
817 URI.create("/account/12345/msgs/abc"));
818 }
819 writeResponseBodyMessage(req, response, buildRFC9457Message(rfc9457Payload));
820 }
821 return true;
822 }
823 }
824
825 private String buildRFC9457Message(RFC9457Payload payload) {
826 return new Gson().toJson(payload, RFC9457Payload.class);
827 }
828
829 private class RedirectHandler extends Handler.Abstract {
830 @Override
831 public boolean handle(Request req, Response response, Callback callback) throws Exception {
832 String path = req.getHttpURI().getPath();
833 if (!path.startsWith("/redirect/")) {
834 return false;
835 }
836 StringBuilder location = new StringBuilder(128);
837 String scheme = Request.getParameters(req).getValue("scheme");
838 location.append(scheme != null ? scheme : req.getHttpURI().getScheme());
839 location.append("://");
840 location.append(Request.getServerName(req));
841 location.append(":");
842 if ("http".equalsIgnoreCase(scheme)) {
843 location.append(getHttpPort());
844 } else if ("https".equalsIgnoreCase(scheme)) {
845 location.append(getHttpsPort());
846 } else {
847 location.append(Request.getServerPort(req));
848 }
849 location.append("/repo").append(path.substring(9));
850 Response.sendRedirect(
851 req, response, callback, HttpServletResponse.SC_MOVED_PERMANENTLY, location.toString(), false);
852 return true;
853 }
854 }
855
856 private class AuthHandler extends Handler.Abstract {
857 @Override
858 public boolean handle(Request request, Response response, Callback callback) throws Exception {
859 if (ExpectContinue.BROKEN.equals(expectContinue)
860 && "100-continue".equalsIgnoreCase(request.getHeaders().get(HttpHeader.EXPECT))) {
861
862 Request.asInputStream(request);
863 }
864
865 if (username != null && password != null) {
866 if (checkBasicAuth(request.getHeaders().get(HttpHeader.AUTHORIZATION), username, password)) {
867 return false;
868 }
869 response.getHeaders().add(HttpHeader.WWW_AUTHENTICATE, "Basic realm=\"Test-Realm\"");
870 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
871 return true;
872 }
873 return false;
874 }
875 }
876
877 private class ProxyAuthHandler extends Handler.Abstract {
878 @Override
879 public boolean handle(Request req, Response response, Callback callback) throws Exception {
880 if (proxyUsername != null && proxyPassword != null) {
881 if (checkBasicAuth(
882 req.getHeaders().get(HttpHeader.PROXY_AUTHORIZATION), proxyUsername, proxyPassword)) {
883 return false;
884 }
885 response.getHeaders().add(HttpHeader.PROXY_AUTHENTICATE, "basic realm=\"Test-Realm\"");
886 response.setStatus(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED);
887 return true;
888 } else {
889 return false;
890 }
891 }
892 }
893
894 static boolean checkBasicAuth(String credentials, String username, String password) {
895 if (credentials != null) {
896 int space = credentials.indexOf(' ');
897 if (space > 0) {
898 String method = credentials.substring(0, space);
899 if ("basic".equalsIgnoreCase(method)) {
900 credentials = credentials.substring(space + 1);
901 credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1);
902 int i = credentials.indexOf(':');
903 if (i > 0) {
904 String user = credentials.substring(0, i);
905 String pass = credentials.substring(i + 1);
906 if (username.equals(user) && password.equals(pass)) {
907 return true;
908 }
909 }
910 }
911 }
912 }
913 return false;
914 }
915 }