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 javax.servlet.http.HttpServletRequest; 022import javax.servlet.http.HttpServletResponse; 023 024import java.io.*; 025import java.nio.charset.StandardCharsets; 026import java.util.ArrayList; 027import java.util.Base64; 028import java.util.Collections; 029import java.util.Enumeration; 030import java.util.List; 031import java.util.Map; 032import java.util.TreeMap; 033import java.util.concurrent.atomic.AtomicInteger; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036 037import org.eclipse.aether.internal.impl.checksum.Sha1ChecksumAlgorithmFactory; 038import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper; 039import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; 040import org.eclipse.jetty.http.HttpHeader; 041import org.eclipse.jetty.http.HttpMethod; 042import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; 043import org.eclipse.jetty.server.*; 044import org.eclipse.jetty.server.handler.AbstractHandler; 045import org.eclipse.jetty.server.handler.HandlerList; 046import org.eclipse.jetty.util.IO; 047import org.eclipse.jetty.util.URIUtil; 048import org.eclipse.jetty.util.ssl.SslContextFactory; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052public class HttpServer { 053 054 public static class LogEntry { 055 056 private final String method; 057 058 private final String path; 059 060 private final Map<String, String> headers; 061 062 public LogEntry(String method, String path, Map<String, String> headers) { 063 this.method = method; 064 this.path = path; 065 this.headers = headers; 066 } 067 068 public String getMethod() { 069 return method; 070 } 071 072 public String getPath() { 073 return path; 074 } 075 076 public Map<String, String> getHeaders() { 077 return headers; 078 } 079 080 @Override 081 public String toString() { 082 return method + " " + path; 083 } 084 } 085 086 public enum ExpectContinue { 087 FAIL, 088 PROPER, 089 BROKEN 090 } 091 092 public enum ChecksumHeader { 093 NEXUS, 094 XCHECKSUM 095 } 096 097 private static final Logger LOGGER = LoggerFactory.getLogger(HttpServer.class); 098 099 private File repoDir; 100 101 private boolean rangeSupport = true; 102 103 private boolean webDav; 104 105 private ExpectContinue expectContinue = ExpectContinue.PROPER; 106 107 private ChecksumHeader checksumHeader; 108 109 private Server server; 110 111 private ServerConnector httpConnector; 112 113 private ServerConnector httpsConnector; 114 115 private String username; 116 117 private String password; 118 119 private String proxyUsername; 120 121 private String proxyPassword; 122 123 private final AtomicInteger connectionsToClose = new AtomicInteger(0); 124 125 private final AtomicInteger serverErrorsBeforeWorks = new AtomicInteger(0); 126 127 private final List<LogEntry> logEntries = Collections.synchronizedList(new ArrayList<>()); 128 129 public String getHost() { 130 return "localhost"; 131 } 132 133 public int getHttpPort() { 134 return httpConnector != null ? httpConnector.getLocalPort() : -1; 135 } 136 137 public int getHttpsPort() { 138 return httpsConnector != null ? httpsConnector.getLocalPort() : -1; 139 } 140 141 public String getHttpUrl() { 142 return "http://" + getHost() + ":" + getHttpPort(); 143 } 144 145 public String getHttpsUrl() { 146 return "https://" + getHost() + ":" + getHttpsPort(); 147 } 148 149 public HttpServer addSslConnector() { 150 return addSslConnector(true); 151 } 152 153 public HttpServer addSelfSignedSslConnector() { 154 return addSslConnector(false); 155 } 156 157 private HttpServer addSslConnector(boolean needClientAuth) { 158 if (httpsConnector == null) { 159 SslContextFactory.Server ssl = new SslContextFactory.Server(); 160 ssl.setNeedClientAuth(needClientAuth); 161 if (!needClientAuth) { 162 ssl.setKeyStorePath(HttpTransporterTest.KEY_STORE_SELF_SIGNED_PATH 163 .toAbsolutePath() 164 .toString()); 165 ssl.setKeyStorePassword("server-pwd"); 166 ssl.setSniRequired(false); 167 } else { 168 ssl.setKeyStorePath( 169 HttpTransporterTest.KEY_STORE_PATH.toAbsolutePath().toString()); 170 ssl.setKeyStorePassword("server-pwd"); 171 ssl.setTrustStorePath( 172 HttpTransporterTest.TRUST_STORE_PATH.toAbsolutePath().toString()); 173 ssl.setTrustStorePassword("client-pwd"); 174 ssl.setSniRequired(false); 175 } 176 177 HttpConfiguration httpsConfig = new HttpConfiguration(); 178 SecureRequestCustomizer customizer = new SecureRequestCustomizer(); 179 customizer.setSniHostCheck(false); 180 httpsConfig.addCustomizer(customizer); 181 182 HttpConnectionFactory http1 = new HttpConnectionFactory(httpsConfig); 183 184 HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpsConfig); 185 186 ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); 187 alpn.setDefaultProtocol(http1.getProtocol()); 188 189 SslConnectionFactory tls = new SslConnectionFactory(ssl, alpn.getProtocol()); 190 httpsConnector = new ServerConnector(server, tls, alpn, http2, http1); 191 server.addConnector(httpsConnector); 192 try { 193 httpsConnector.start(); 194 } catch (Exception e) { 195 throw new IllegalStateException(e); 196 } 197 } 198 return this; 199 } 200 201 public List<LogEntry> getLogEntries() { 202 return logEntries; 203 } 204 205 public HttpServer setRepoDir(File repoDir) { 206 this.repoDir = repoDir; 207 return this; 208 } 209 210 public HttpServer setRangeSupport(boolean rangeSupport) { 211 this.rangeSupport = rangeSupport; 212 return this; 213 } 214 215 public HttpServer setWebDav(boolean webDav) { 216 this.webDav = webDav; 217 return this; 218 } 219 220 public HttpServer setExpectSupport(ExpectContinue expectContinue) { 221 this.expectContinue = expectContinue; 222 return this; 223 } 224 225 public HttpServer setChecksumHeader(ChecksumHeader checksumHeader) { 226 this.checksumHeader = checksumHeader; 227 return this; 228 } 229 230 public HttpServer setAuthentication(String username, String password) { 231 this.username = username; 232 this.password = password; 233 return this; 234 } 235 236 public HttpServer setProxyAuthentication(String username, String password) { 237 proxyUsername = username; 238 proxyPassword = password; 239 return this; 240 } 241 242 public HttpServer setConnectionsToClose(int connectionsToClose) { 243 this.connectionsToClose.set(connectionsToClose); 244 return this; 245 } 246 247 public HttpServer setServerErrorsBeforeWorks(int serverErrorsBeforeWorks) { 248 this.serverErrorsBeforeWorks.set(serverErrorsBeforeWorks); 249 return this; 250 } 251 252 public HttpServer start() throws Exception { 253 if (server != null) { 254 return this; 255 } 256 257 HandlerList handlers = new HandlerList(); 258 handlers.addHandler(new ConnectionClosingHandler()); 259 handlers.addHandler(new ServerErrorHandler()); 260 handlers.addHandler(new LogHandler()); 261 handlers.addHandler(new ProxyAuthHandler()); 262 handlers.addHandler(new AuthHandler()); 263 handlers.addHandler(new RedirectHandler()); 264 handlers.addHandler(new RepoHandler()); 265 266 server = new Server(); 267 httpConnector = new ServerConnector(server); 268 server.addConnector(httpConnector); 269 server.setHandler(handlers); 270 server.start(); 271 272 return this; 273 } 274 275 public void stop() throws Exception { 276 if (server != null) { 277 server.stop(); 278 server = null; 279 httpConnector = null; 280 httpsConnector = null; 281 } 282 } 283 284 private class ConnectionClosingHandler extends AbstractHandler { 285 @Override 286 public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { 287 if (connectionsToClose.getAndDecrement() > 0) { 288 Response jettyResponse = (Response) response; 289 jettyResponse.getHttpChannel().getConnection().close(); 290 } 291 } 292 } 293 294 private class ServerErrorHandler extends AbstractHandler { 295 @Override 296 public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) 297 throws IOException { 298 if (serverErrorsBeforeWorks.getAndDecrement() > 0) { 299 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 300 writeResponseBodyMessage(response, "Oops, come back later!"); 301 } 302 } 303 } 304 305 private class LogHandler extends AbstractHandler { 306 @Override 307 public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { 308 LOGGER.info( 309 "{} {}{}", 310 req.getMethod(), 311 req.getRequestURL(), 312 req.getQueryString() != null ? "?" + req.getQueryString() : ""); 313 314 Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 315 for (Enumeration<String> en = req.getHeaderNames(); en.hasMoreElements(); ) { 316 String name = en.nextElement(); 317 StringBuilder buffer = new StringBuilder(128); 318 for (Enumeration<String> ien = req.getHeaders(name); ien.hasMoreElements(); ) { 319 if (buffer.length() > 0) { 320 buffer.append(", "); 321 } 322 buffer.append(ien.nextElement()); 323 } 324 headers.put(name, buffer.toString()); 325 } 326 logEntries.add(new LogEntry(req.getMethod(), req.getPathInfo(), Collections.unmodifiableMap(headers))); 327 } 328 } 329 330 private static final Pattern SIMPLE_RANGE = Pattern.compile("bytes=([0-9])+-"); 331 332 private class RepoHandler extends AbstractHandler { 333 @Override 334 public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) 335 throws IOException { 336 String path = req.getPathInfo().substring(1); 337 338 if (!path.startsWith("repo/")) { 339 return; 340 } 341 req.setHandled(true); 342 343 if (ExpectContinue.FAIL.equals(expectContinue) && request.getHeader(HttpHeader.EXPECT.asString()) != null) { 344 response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); 345 writeResponseBodyMessage(response, "Expectation was set to fail"); 346 return; 347 } 348 349 File file = new File(repoDir, path.substring(5)); 350 if (HttpMethod.GET.is(req.getMethod()) || HttpMethod.HEAD.is(req.getMethod())) { 351 if (!file.isFile() || path.endsWith(URIUtil.SLASH)) { 352 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 353 writeResponseBodyMessage(response, "Not found"); 354 return; 355 } 356 long ifUnmodifiedSince = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString()); 357 if (ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince) { 358 response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); 359 writeResponseBodyMessage(response, "Precondition failed"); 360 return; 361 } 362 long offset = 0L; 363 String range = request.getHeader(HttpHeader.RANGE.asString()); 364 if (range != null && rangeSupport) { 365 Matcher m = SIMPLE_RANGE.matcher(range); 366 if (m.matches()) { 367 offset = Long.parseLong(m.group(1)); 368 if (offset >= file.length()) { 369 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); 370 writeResponseBodyMessage(response, "Range not satisfiable"); 371 return; 372 } 373 } 374 String encoding = request.getHeader(HttpHeader.ACCEPT_ENCODING.asString()); 375 if ((encoding != null && !"identity".equals(encoding)) || ifUnmodifiedSince == -1L) { 376 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 377 return; 378 } 379 } 380 response.setStatus((offset > 0L) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK); 381 response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(), file.lastModified()); 382 response.setHeader(HttpHeader.CONTENT_LENGTH.asString(), Long.toString(file.length() - offset)); 383 if (offset > 0L) { 384 response.setHeader( 385 HttpHeader.CONTENT_RANGE.asString(), 386 "bytes " + offset + "-" + (file.length() - 1L) + "/" + file.length()); 387 } 388 if (checksumHeader != null) { 389 Map<String, String> checksums = ChecksumAlgorithmHelper.calculate( 390 file, Collections.singletonList(new Sha1ChecksumAlgorithmFactory())); 391 if (checksumHeader == ChecksumHeader.NEXUS) { 392 response.setHeader(HttpHeader.ETAG.asString(), "{SHA1{" + checksums.get("SHA-1") + "}}"); 393 } else if (checksumHeader == ChecksumHeader.XCHECKSUM) { 394 response.setHeader("x-checksum-sha1", checksums.get(Sha1ChecksumAlgorithmFactory.NAME)); 395 } 396 } 397 if (HttpMethod.HEAD.is(req.getMethod())) { 398 return; 399 } 400 FileInputStream is = null; 401 try { 402 is = new FileInputStream(file); 403 if (offset > 0L) { 404 long skipped = is.skip(offset); 405 while (skipped < offset && is.read() >= 0) { 406 skipped++; 407 } 408 } 409 IO.copy(is, response.getOutputStream()); 410 is.close(); 411 is = null; 412 } finally { 413 try { 414 if (is != null) { 415 is.close(); 416 } 417 } catch (final IOException e) { 418 // Suppressed due to an exception already thrown in the try block. 419 } 420 } 421 } else if (HttpMethod.PUT.is(req.getMethod())) { 422 if (!webDav) { 423 file.getParentFile().mkdirs(); 424 } 425 if (file.getParentFile().exists()) { 426 try { 427 FileOutputStream os = null; 428 try { 429 os = new FileOutputStream(file); 430 IO.copy(request.getInputStream(), os); 431 os.close(); 432 os = null; 433 } finally { 434 try { 435 if (os != null) { 436 os.close(); 437 } 438 } catch (final IOException e) { 439 // Suppressed due to an exception already thrown in the try block. 440 } 441 } 442 } catch (IOException e) { 443 file.delete(); 444 throw e; 445 } 446 response.setStatus(HttpServletResponse.SC_NO_CONTENT); 447 } else { 448 response.setStatus(HttpServletResponse.SC_FORBIDDEN); 449 } 450 } else if (HttpMethod.OPTIONS.is(req.getMethod())) { 451 if (webDav) { 452 response.setHeader("DAV", "1,2"); 453 } 454 response.setHeader(HttpHeader.ALLOW.asString(), "GET, PUT, HEAD, OPTIONS"); 455 response.setStatus(HttpServletResponse.SC_OK); 456 } else if (webDav && "MKCOL".equals(req.getMethod())) { 457 if (file.exists()) { 458 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); 459 } else if (file.mkdir()) { 460 response.setStatus(HttpServletResponse.SC_CREATED); 461 } else { 462 response.setStatus(HttpServletResponse.SC_CONFLICT); 463 } 464 } else { 465 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); 466 } 467 } 468 } 469 470 private void writeResponseBodyMessage(HttpServletResponse response, String message) throws IOException { 471 try (OutputStream outputStream = response.getOutputStream()) { 472 outputStream.write(message.getBytes(StandardCharsets.UTF_8)); 473 } 474 } 475 476 private class RedirectHandler extends AbstractHandler { 477 @Override 478 public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { 479 String path = req.getPathInfo(); 480 if (!path.startsWith("/redirect/")) { 481 return; 482 } 483 req.setHandled(true); 484 StringBuilder location = new StringBuilder(128); 485 String scheme = req.getParameter("scheme"); 486 location.append(scheme != null ? scheme : req.getScheme()); 487 location.append("://"); 488 location.append(req.getServerName()); 489 location.append(":"); 490 if ("http".equalsIgnoreCase(scheme)) { 491 location.append(getHttpPort()); 492 } else if ("https".equalsIgnoreCase(scheme)) { 493 location.append(getHttpsPort()); 494 } else { 495 location.append(req.getServerPort()); 496 } 497 location.append("/repo").append(path.substring(9)); 498 response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); 499 response.setHeader(HttpHeader.LOCATION.asString(), location.toString()); 500 } 501 } 502 503 private class AuthHandler extends AbstractHandler { 504 @Override 505 public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) 506 throws IOException { 507 if (ExpectContinue.BROKEN.equals(expectContinue) 508 && "100-continue".equalsIgnoreCase(request.getHeader(HttpHeader.EXPECT.asString()))) { 509 request.getInputStream(); 510 } 511 512 if (username != null && password != null) { 513 if (checkBasicAuth(request.getHeader(HttpHeader.AUTHORIZATION.asString()), username, password)) { 514 return; 515 } 516 req.setHandled(true); 517 response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\""); 518 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 519 } 520 } 521 } 522 523 private class ProxyAuthHandler extends AbstractHandler { 524 @Override 525 public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { 526 if (proxyUsername != null && proxyPassword != null) { 527 if (checkBasicAuth( 528 request.getHeader(HttpHeader.PROXY_AUTHORIZATION.asString()), proxyUsername, proxyPassword)) { 529 return; 530 } 531 req.setHandled(true); 532 response.setHeader(HttpHeader.PROXY_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\""); 533 response.setStatus(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED); 534 } 535 } 536 } 537 538 static boolean checkBasicAuth(String credentials, String username, String password) { 539 if (credentials != null) { 540 int space = credentials.indexOf(' '); 541 if (space > 0) { 542 String method = credentials.substring(0, space); 543 if ("basic".equalsIgnoreCase(method)) { 544 credentials = credentials.substring(space + 1); 545 credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1); 546 int i = credentials.indexOf(':'); 547 if (i > 0) { 548 String user = credentials.substring(0, i); 549 String pass = credentials.substring(i + 1); 550 if (username.equals(user) && password.equals(pass)) { 551 return true; 552 } 553 } 554 } 555 } 556 } 557 return false; 558 } 559}