001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.internal.test.util.http; 020 021import java.io.File; 022import java.io.IOException; 023import java.net.URI; 024import java.nio.channels.SeekableByteChannel; 025import java.nio.charset.StandardCharsets; 026import java.nio.file.Files; 027import java.nio.file.StandardOpenOption; 028import java.util.ArrayList; 029import java.util.Base64; 030import java.util.Collections; 031import java.util.List; 032import java.util.Map; 033import java.util.TreeMap; 034import java.util.concurrent.CountDownLatch; 035import java.util.concurrent.atomic.AtomicInteger; 036import java.util.regex.Matcher; 037import java.util.regex.Pattern; 038import java.util.stream.Collectors; 039 040import com.google.gson.Gson; 041import jakarta.servlet.http.HttpServletResponse; 042import org.eclipse.aether.internal.impl.checksum.Sha1ChecksumAlgorithmFactory; 043import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper; 044import org.eclipse.aether.spi.connector.transport.http.RFC9457.RFC9457Payload; 045import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; 046import org.eclipse.jetty.compression.server.CompressionConfig; 047import org.eclipse.jetty.compression.server.CompressionHandler; 048import org.eclipse.jetty.http.DateGenerator; 049import org.eclipse.jetty.http.HttpField; 050import org.eclipse.jetty.http.HttpFields; 051import org.eclipse.jetty.http.HttpHeader; 052import org.eclipse.jetty.http.HttpMethod; 053import org.eclipse.jetty.http.HttpURI; 054import org.eclipse.jetty.http.pathmap.MatchedResource; 055import org.eclipse.jetty.http.pathmap.PathMappings; 056import org.eclipse.jetty.http.pathmap.PathSpec; 057import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; 058import org.eclipse.jetty.io.ByteBufferPool; 059import org.eclipse.jetty.io.Content; 060import org.eclipse.jetty.server.Handler; 061import org.eclipse.jetty.server.HttpConfiguration; 062import org.eclipse.jetty.server.HttpConnectionFactory; 063import org.eclipse.jetty.server.Request; 064import org.eclipse.jetty.server.Response; 065import org.eclipse.jetty.server.SecureRequestCustomizer; 066import org.eclipse.jetty.server.Server; 067import org.eclipse.jetty.server.ServerConnector; 068import org.eclipse.jetty.server.SslConnectionFactory; 069import org.eclipse.jetty.util.Blocker; 070import org.eclipse.jetty.util.Callback; 071import org.eclipse.jetty.util.ssl.SslContextFactory; 072import org.slf4j.Logger; 073import org.slf4j.LoggerFactory; 074 075public class HttpServer { 076 077 public static class LogEntry { 078 079 private final String method; 080 081 private final String path; 082 083 private final Map<String, String> requestHeaders; 084 085 private Map<String, String> responseHeaders; 086 087 CountDownLatch responseHeadersAvailableSignal = new CountDownLatch(1); 088 089 public LogEntry(String method, String path, Map<String, String> requestHeaders) { 090 this.method = method; 091 this.path = path; 092 this.requestHeaders = requestHeaders; 093 } 094 095 public String getMethod() { 096 return method; 097 } 098 099 public String getPath() { 100 return path; 101 } 102 103 public Map<String, String> getRequestHeaders() { 104 return requestHeaders; 105 } 106 107 /** 108 * This method blocks until the response headers are available. 109 * @return the response headers 110 */ 111 public Map<String, String> getResponseHeaders() { 112 try { 113 if (!responseHeadersAvailableSignal.await(30, java.util.concurrent.TimeUnit.SECONDS)) { 114 throw new IllegalStateException("Timeout waiting for response headers to be available"); 115 } 116 } catch (InterruptedException e) { 117 Thread.currentThread().interrupt(); 118 throw new IllegalStateException("Interrupted while waiting for response headers to be available", e); 119 } 120 return responseHeaders; 121 } 122 123 public void setResponseHeaders(Map<String, String> responseHeaders) { 124 this.responseHeaders = responseHeaders; 125 responseHeadersAvailableSignal.countDown(); 126 } 127 128 @Override 129 public String toString() { 130 return method + " " + path; 131 } 132 } 133 134 public enum ExpectContinue { 135 FAIL, 136 PROPER, 137 BROKEN 138 } 139 140 public enum ChecksumHeader { 141 NEXUS, 142 XCHECKSUM 143 } 144 145 private static final Logger LOGGER = LoggerFactory.getLogger(HttpServer.class); 146 147 private File repoDir; 148 149 private boolean rangeSupport = true; 150 151 private boolean webDav; 152 153 private ExpectContinue expectContinue = ExpectContinue.PROPER; 154 155 private ChecksumHeader checksumHeader; 156 157 private Server server; 158 159 private ServerConnector httpConnector; 160 161 private ServerConnector httpsConnector; 162 163 private String username; 164 165 private String password; 166 167 private String proxyUsername; 168 169 private String proxyPassword; 170 171 private final AtomicInteger connectionsToClose = new AtomicInteger(0); 172 173 private final AtomicInteger serverErrorsBeforeWorks = new AtomicInteger(0); 174 175 private int serverErrorStatusCode; 176 177 private final List<LogEntry> logEntries = Collections.synchronizedList(new ArrayList<>()); 178 179 public String getHost() { 180 return "localhost"; 181 } 182 183 public int getHttpPort() { 184 return httpConnector != null ? httpConnector.getLocalPort() : -1; 185 } 186 187 public int getHttpsPort() { 188 return httpsConnector != null ? httpsConnector.getLocalPort() : -1; 189 } 190 191 public String getHttpUrl() { 192 return "http://" + getHost() + ":" + getHttpPort(); 193 } 194 195 public String getHttpsUrl() { 196 return "https://" + getHost() + ":" + getHttpsPort(); 197 } 198 199 public HttpServer addSslConnector() { 200 return addSslConnector(true, true); 201 } 202 203 public HttpServer addSelfSignedSslConnector() { 204 return addSslConnector(false, true); 205 } 206 207 public HttpServer addSelfSignedSslConnectorHttp2Only() { 208 return addSslConnector(false, false); 209 } 210 211 private HttpServer addSslConnector(boolean needClientAuth, boolean needHttp11) { 212 if (httpsConnector == null) { 213 SslContextFactory.Server ssl = new SslContextFactory.Server(); 214 ssl.setNeedClientAuth(needClientAuth); 215 if (!needClientAuth) { 216 ssl.setKeyStorePath(HttpTransporterTest.KEY_STORE_SELF_SIGNED_PATH 217 .toAbsolutePath() 218 .toString()); 219 ssl.setKeyStorePassword("server-pwd"); 220 ssl.setSniRequired(false); 221 } else { 222 ssl.setKeyStorePath( 223 HttpTransporterTest.KEY_STORE_PATH.toAbsolutePath().toString()); 224 ssl.setKeyStorePassword("server-pwd"); 225 ssl.setTrustStorePath( 226 HttpTransporterTest.TRUST_STORE_PATH.toAbsolutePath().toString()); 227 ssl.setTrustStorePassword("client-pwd"); 228 ssl.setSniRequired(false); 229 } 230 231 HttpConfiguration httpsConfig = new HttpConfiguration(); 232 SecureRequestCustomizer customizer = new SecureRequestCustomizer(); 233 customizer.setSniHostCheck(false); 234 httpsConfig.addCustomizer(customizer); 235 236 HttpConnectionFactory http1 = null; 237 if (needHttp11) { 238 http1 = new HttpConnectionFactory(httpsConfig); 239 } 240 241 HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpsConfig); 242 243 ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); 244 alpn.setDefaultProtocol(http1 != null ? http1.getProtocol() : http2.getProtocol()); 245 246 SslConnectionFactory tls = new SslConnectionFactory(ssl, alpn.getProtocol()); 247 if (http1 != null) { 248 httpsConnector = new ServerConnector(server, tls, alpn, http2, http1); 249 } else { 250 httpsConnector = new ServerConnector(server, tls, alpn, http2); 251 } 252 server.addConnector(httpsConnector); 253 try { 254 httpsConnector.start(); 255 } catch (Exception e) { 256 throw new IllegalStateException(e); 257 } 258 } 259 return this; 260 } 261 262 public List<LogEntry> getLogEntries() { 263 return logEntries; 264 } 265 266 public HttpServer setRepoDir(File repoDir) { 267 this.repoDir = repoDir; 268 return this; 269 } 270 271 public HttpServer setRangeSupport(boolean rangeSupport) { 272 this.rangeSupport = rangeSupport; 273 return this; 274 } 275 276 public HttpServer setWebDav(boolean webDav) { 277 this.webDav = webDav; 278 return this; 279 } 280 281 public HttpServer setExpectSupport(ExpectContinue expectContinue) { 282 this.expectContinue = expectContinue; 283 return this; 284 } 285 286 public HttpServer setChecksumHeader(ChecksumHeader checksumHeader) { 287 this.checksumHeader = checksumHeader; 288 return this; 289 } 290 291 public HttpServer setAuthentication(String username, String password) { 292 this.username = username; 293 this.password = password; 294 return this; 295 } 296 297 public HttpServer setProxyAuthentication(String username, String password) { 298 proxyUsername = username; 299 proxyPassword = password; 300 return this; 301 } 302 303 public HttpServer setConnectionsToClose(int connectionsToClose) { 304 this.connectionsToClose.set(connectionsToClose); 305 return this; 306 } 307 308 public HttpServer setServerErrorsBeforeWorks(int serverErrorsBeforeWorks) { 309 return setServerErrorsBeforeWorks(serverErrorsBeforeWorks, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 310 } 311 312 public HttpServer setServerErrorsBeforeWorks(int serverErrorsBeforeWorks, int errorStatusCode) { 313 this.serverErrorsBeforeWorks.set(serverErrorsBeforeWorks); 314 this.serverErrorStatusCode = errorStatusCode; 315 return this; 316 } 317 318 public HttpServer start() throws Exception { 319 if (server != null) { 320 return this; 321 } 322 323 server = new Server(); 324 httpConnector = new ServerConnector(server); 325 server.addConnector(httpConnector); 326 327 server.setHandler(new LogHandler(new CompressionEnforcingHandler(new Handler.Sequence( 328 new ConnectionClosingHandler(), 329 new ServerErrorHandler(), 330 new ProxyAuthHandler(), 331 new AuthHandler(), 332 new RedirectHandler(), 333 new RepoHandler(), 334 new RFC9457Handler())))); 335 server.start(); 336 337 return this; 338 } 339 340 public void stop() throws Exception { 341 if (server != null) { 342 server.stop(); 343 server = null; 344 httpConnector = null; 345 httpsConnector = null; 346 } 347 } 348 349 private class CompressionEnforcingHandler extends CompressionHandler { 350 // duplicate of CompressionHandler.pathConfigs which is private 351 private final PathMappings<CompressionConfig> pathConfigs = new PathMappings<>(); 352 353 CompressionEnforcingHandler(Handler handler) { 354 super(handler); 355 this.putConfiguration( 356 "/br/*", 357 CompressionConfig.builder().compressIncludeEncoding("br").build()); 358 this.putConfiguration( 359 "/zstd/*", 360 CompressionConfig.builder().compressIncludeEncoding("zstd").build()); 361 this.putConfiguration( 362 "/gzip/*", 363 CompressionConfig.builder().compressIncludeEncoding("gzip").build()); 364 this.putConfiguration( 365 "/deflate/*", 366 CompressionConfig.builder() 367 .compressIncludeEncoding("deflate") 368 .build()); 369 } 370 371 @Override 372 public CompressionConfig putConfiguration(PathSpec pathSpec, CompressionConfig config) { 373 // deliberately not set it in the super class yet 374 return pathConfigs.put(pathSpec, config); 375 } 376 377 @Override 378 public boolean handle(Request request, Response response, Callback callback) throws Exception { 379 Handler next = getHandler(); 380 if (next == null) { 381 return false; 382 } 383 String pathInContext = Request.getPathInContext(request); 384 MatchedResource<CompressionConfig> matchedConfig = this.pathConfigs.getMatched(pathInContext); 385 if (matchedConfig == null) { 386 if (LOGGER.isDebugEnabled()) { 387 LOGGER.debug("skipping compression: path {} has no matching compression config", pathInContext); 388 } 389 // No configuration, skip 390 return next.handle(request, response, callback); 391 } 392 393 // set the matched config in the super class for further processing, but for all paths 394 // no need to reset it later as this handler is not used among multiple requests 395 super.putConfiguration(PathSpec.from("/*"), matchedConfig.getResource()); 396 // first path segment determines the encoding, remove it from the request path for further processing 397 return super.handle(new StripLeadingPathSegmentsRequestWrapper(request, 1), response, callback); 398 } 399 } 400 401 private static class StripLeadingPathSegmentsRequestWrapper extends Request.Wrapper { 402 private final HttpURI modifiedURI; 403 404 StripLeadingPathSegmentsRequestWrapper(Request wrapped, int segmentsToStrip) { 405 super(wrapped); 406 this.modifiedURI = stripPathSegments(wrapped.getHttpURI(), segmentsToStrip); 407 } 408 409 private static HttpURI stripPathSegments(HttpURI originalURI, int segmentsToStrip) { 410 if (segmentsToStrip <= 0) { 411 return originalURI; 412 } 413 414 String originalPath = originalURI.getPath(); 415 if (originalPath == null || originalPath.isEmpty()) { 416 return originalURI; 417 } 418 419 // Split path into segments 420 String[] segments = originalPath.split("/"); 421 StringBuilder newPath = new StringBuilder(); 422 423 // Skip empty first segment (from leading /) and the specified number of segments 424 int skipCount = 0; 425 for (int i = 0; i < segments.length; i++) { 426 if (segments[i].isEmpty() && i == 0) { 427 // Skip leading empty segment from leading / 428 continue; 429 } 430 if (skipCount < segmentsToStrip) { 431 skipCount++; 432 continue; 433 } 434 newPath.append("/").append(segments[i]); 435 } 436 437 // If we stripped everything, return root path 438 if (newPath.isEmpty()) { 439 newPath.append("/"); 440 } 441 442 // Build new URI with modified path 443 return org.eclipse.jetty.http.HttpURI.build(originalURI) 444 .path(newPath.toString()) 445 .asImmutable(); 446 } 447 448 @Override 449 public HttpURI getHttpURI() { 450 return modifiedURI; 451 } 452 } 453 454 private class ConnectionClosingHandler extends Handler.Abstract { 455 456 @Override 457 public boolean handle(Request request, Response response, Callback callback) throws Exception { 458 if (connectionsToClose.getAndDecrement() > 0) { 459 request.getConnectionMetaData().getConnection().close(); 460 } 461 return false; 462 } 463 } 464 465 private class ServerErrorHandler extends Handler.Abstract { 466 @Override 467 public boolean handle(Request request, Response response, Callback callback) throws IOException { 468 if (serverErrorsBeforeWorks.getAndDecrement() > 0) { 469 response.setStatus(serverErrorStatusCode); 470 writeResponseBodyMessage(request, response, "Oops, come back later!"); 471 return true; 472 } 473 return false; 474 } 475 } 476 477 private class LogHandler extends Handler.Wrapper { 478 479 LogHandler(Handler handler) { 480 super(handler); 481 } 482 483 @Override 484 public boolean handle(Request req, Response response, Callback callback) throws Exception { 485 486 LOGGER.info( 487 "{} {}{}", 488 req.getMethod(), 489 req.getHttpURI().getDecodedPath(), 490 req.getHttpURI().getQuery() != null ? "?" + req.getHttpURI().getQuery() : ""); 491 492 Map<String, String> requestHeaders = 493 toUnmodifiableMap(req.getHeaders()); // capture request headers before other handlers modify them 494 LogEntry logEntry = new LogEntry(req.getMethod(), req.getHttpURI().getPathQuery(), requestHeaders); 495 logEntries.add(logEntry); 496 // prevent closing the response before logging (assume all writes are synchronous for simplicity) 497 boolean result = super.handle(req, response, callback); 498 // capture response headers after other handlers modified them 499 // at this point in time the connection may have been already closed (i.e. last chunk already sent) 500 logEntry.setResponseHeaders(toUnmodifiableMap(response.getHeaders())); 501 if (result) { 502 callback.succeeded(); 503 } 504 return result; 505 } 506 507 Map<String, String> toUnmodifiableMap(HttpFields headers) { 508 Map<String, String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 509 for (HttpField header : headers) { 510 map.put(header.getName(), header.getValueList().stream().collect(Collectors.joining(", "))); 511 } 512 return Collections.unmodifiableMap(map); 513 } 514 } 515 516 private static final Pattern SIMPLE_RANGE = Pattern.compile("bytes=([0-9])+-"); 517 518 private class RepoHandler extends Handler.Abstract { 519 @Override 520 public boolean handle(Request req, Response response, Callback callback) throws Exception { 521 String path = req.getHttpURI().getDecodedPath().substring(1); 522 523 if (!path.startsWith("repo/")) { 524 return false; 525 } 526 527 if (ExpectContinue.FAIL.equals(expectContinue) && req.getHeaders().get(HttpHeader.EXPECT) != null) { 528 response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); 529 writeResponseBodyMessage(req, response, "Expectation was set to fail"); 530 return true; 531 } 532 533 File file = new File(repoDir, path.substring(5)); 534 if (HttpMethod.GET.is(req.getMethod()) || HttpMethod.HEAD.is(req.getMethod())) { 535 if (!file.isFile() || path.endsWith("/")) { 536 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 537 writeResponseBodyMessage(req, response, "Not found"); 538 return true; 539 } 540 long ifUnmodifiedSince = req.getHeaders().getDateField(HttpHeader.IF_UNMODIFIED_SINCE); 541 if (ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince) { 542 response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); 543 writeResponseBodyMessage(req, response, "Precondition failed"); 544 return true; 545 } 546 long offset = 0L; 547 String range = req.getHeaders().get(HttpHeader.RANGE); 548 if (range != null && rangeSupport) { 549 Matcher m = SIMPLE_RANGE.matcher(range); 550 if (m.matches()) { 551 offset = Long.parseLong(m.group(1)); 552 if (offset >= file.length()) { 553 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); 554 writeResponseBodyMessage(req, response, "Range not satisfiable"); 555 return true; 556 } 557 } 558 String encoding = req.getHeaders().get(HttpHeader.ACCEPT_ENCODING); 559 if ((encoding != null && !"identity".equals(encoding)) || ifUnmodifiedSince == -1L) { 560 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 561 return true; 562 } 563 } 564 response.setStatus((offset > 0L) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK); 565 response.getHeaders().add(HttpHeader.LAST_MODIFIED, DateGenerator.formatDate(file.lastModified())); 566 response.getHeaders().add(HttpHeader.CONTENT_LENGTH, Long.toString(file.length() - offset)); 567 if (offset > 0L) { 568 response.getHeaders() 569 .add( 570 HttpHeader.CONTENT_RANGE, 571 "bytes " + offset + "-" + (file.length() - 1L) + "/" + file.length()); 572 } 573 if (checksumHeader != null) { 574 Map<String, String> checksums = ChecksumAlgorithmHelper.calculate( 575 file, Collections.singletonList(new Sha1ChecksumAlgorithmFactory())); 576 if (checksumHeader == ChecksumHeader.NEXUS) { 577 response.getHeaders().add(HttpHeader.ETAG.asString(), "{SHA1{" + checksums.get("SHA-1") + "}}"); 578 } else if (checksumHeader == ChecksumHeader.XCHECKSUM) { 579 response.getHeaders().add("x-checksum-sha1", checksums.get(Sha1ChecksumAlgorithmFactory.NAME)); 580 } 581 } 582 if (HttpMethod.HEAD.is(req.getMethod())) { 583 return true; 584 } 585 Content.Source contentSource = 586 Content.Source.from(new ByteBufferPool.Sized(null), file.toPath(), offset, -1); 587 try (Blocker.Callback fileReadCallback = Blocker.callback()) { 588 Content.copy(contentSource, response, fileReadCallback); 589 fileReadCallback.block(); 590 } 591 } else if (HttpMethod.PUT.is(req.getMethod())) { 592 if (!webDav) { 593 file.getParentFile().mkdirs(); 594 } 595 if (file.getParentFile().exists()) { 596 try (SeekableByteChannel channel = Files.newByteChannel( 597 file.toPath(), 598 StandardOpenOption.CREATE, 599 StandardOpenOption.WRITE, 600 StandardOpenOption.TRUNCATE_EXISTING); 601 Blocker.Callback fileWriteCallback = Blocker.callback()) { 602 Content.copy(req, Content.Sink.from(channel), fileWriteCallback); 603 fileWriteCallback.block(); 604 } catch (IOException e) { 605 file.delete(); 606 throw e; 607 } 608 response.setStatus(HttpServletResponse.SC_NO_CONTENT); 609 } else { 610 response.setStatus(HttpServletResponse.SC_FORBIDDEN); 611 } 612 } else if (HttpMethod.OPTIONS.is(req.getMethod())) { 613 if (webDav) { 614 response.getHeaders().add("DAV", "1,2"); 615 } 616 response.getHeaders().add(HttpHeader.ALLOW, "GET, PUT, HEAD, OPTIONS"); 617 response.setStatus(HttpServletResponse.SC_OK); 618 } else if (webDav && "MKCOL".equals(req.getMethod())) { 619 if (file.exists()) { 620 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); 621 } else if (file.mkdir()) { 622 response.setStatus(HttpServletResponse.SC_CREATED); 623 } else { 624 response.setStatus(HttpServletResponse.SC_CONFLICT); 625 } 626 } else { 627 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); 628 } 629 return true; 630 } 631 } 632 633 private void writeResponseBodyMessage(Request request, Response response, String message) throws IOException { 634 // write synchronously to avoid closing the response too early 635 try (Blocker.Callback callback = Blocker.callback()) { 636 Content.Sink.write(response, false, message, callback); 637 callback.block(); 638 } 639 } 640 641 private class RFC9457Handler extends Handler.Abstract { 642 @Override 643 public boolean handle(Request req, Response response, Callback callback) throws Exception { 644 String path = req.getHttpURI().getPath().substring(1); 645 646 if (!path.startsWith("rfc9457/")) { 647 return false; 648 } 649 650 if (HttpMethod.GET.is(req.getMethod())) { 651 response.setStatus(HttpServletResponse.SC_FORBIDDEN); 652 response.getHeaders().add(HttpHeader.CONTENT_TYPE.asString(), "application/problem+json"); 653 RFC9457Payload rfc9457Payload; 654 if (path.endsWith("missing_fields.txt")) { 655 rfc9457Payload = new RFC9457Payload(null, null, null, null, null); 656 } else { 657 rfc9457Payload = new RFC9457Payload( 658 URI.create("https://example.com/probs/out-of-credit"), 659 HttpServletResponse.SC_FORBIDDEN, 660 "You do not have enough credit.", 661 "Your current balance is 30, but that costs 50.", 662 URI.create("/account/12345/msgs/abc")); 663 } 664 writeResponseBodyMessage(req, response, buildRFC9457Message(rfc9457Payload)); 665 } 666 return true; 667 } 668 } 669 670 private String buildRFC9457Message(RFC9457Payload payload) { 671 return new Gson().toJson(payload, RFC9457Payload.class); 672 } 673 674 private class RedirectHandler extends Handler.Abstract { 675 @Override 676 public boolean handle(Request req, Response response, Callback callback) throws Exception { 677 String path = req.getHttpURI().getPath(); 678 if (!path.startsWith("/redirect/")) { 679 return false; 680 } 681 StringBuilder location = new StringBuilder(128); 682 String scheme = Request.getParameters(req).getValue("scheme"); 683 location.append(scheme != null ? scheme : req.getHttpURI().getScheme()); 684 location.append("://"); 685 location.append(Request.getServerName(req)); 686 location.append(":"); 687 if ("http".equalsIgnoreCase(scheme)) { 688 location.append(getHttpPort()); 689 } else if ("https".equalsIgnoreCase(scheme)) { 690 location.append(getHttpsPort()); 691 } else { 692 location.append(Request.getServerPort(req)); 693 } 694 location.append("/repo").append(path.substring(9)); 695 Response.sendRedirect( 696 req, response, callback, HttpServletResponse.SC_MOVED_PERMANENTLY, location.toString(), false); 697 return true; 698 } 699 } 700 701 private class AuthHandler extends Handler.Abstract { 702 @Override 703 public boolean handle(Request request, Response response, Callback callback) throws Exception { 704 if (ExpectContinue.BROKEN.equals(expectContinue) 705 && "100-continue".equalsIgnoreCase(request.getHeaders().get(HttpHeader.EXPECT))) { 706 // TODO: what is this for? 707 Request.asInputStream(request); 708 } 709 710 if (username != null && password != null) { 711 if (checkBasicAuth(request.getHeaders().get(HttpHeader.AUTHORIZATION), username, password)) { 712 return false; 713 } 714 response.getHeaders().add(HttpHeader.WWW_AUTHENTICATE, "Basic realm=\"Test-Realm\""); 715 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 716 return true; 717 } 718 return false; 719 } 720 } 721 722 private class ProxyAuthHandler extends Handler.Abstract { 723 @Override 724 public boolean handle(Request req, Response response, Callback callback) throws Exception { 725 if (proxyUsername != null && proxyPassword != null) { 726 if (checkBasicAuth( 727 req.getHeaders().get(HttpHeader.PROXY_AUTHORIZATION), proxyUsername, proxyPassword)) { 728 return false; 729 } 730 response.getHeaders().add(HttpHeader.PROXY_AUTHENTICATE, "basic realm=\"Test-Realm\""); 731 response.setStatus(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED); 732 return true; 733 } else { 734 return false; 735 } 736 } 737 } 738 739 static boolean checkBasicAuth(String credentials, String username, String password) { 740 if (credentials != null) { 741 int space = credentials.indexOf(' '); 742 if (space > 0) { 743 String method = credentials.substring(0, space); 744 if ("basic".equalsIgnoreCase(method)) { 745 credentials = credentials.substring(space + 1); 746 credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1); 747 int i = credentials.indexOf(':'); 748 if (i > 0) { 749 String user = credentials.substring(0, i); 750 String pass = credentials.substring(i + 1); 751 if (username.equals(user) && password.equals(pass)) { 752 return true; 753 } 754 } 755 } 756 } 757 } 758 return false; 759 } 760}