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