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