1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.proxy;
20
21 import java.io.IOException;
22 import java.net.InetAddress;
23 import java.net.URI;
24 import java.net.UnknownHostException;
25 import java.nio.ByteBuffer;
26 import java.util.Enumeration;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.Locale;
30 import java.util.Set;
31 import java.util.concurrent.Executor;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.TimeoutException;
34 import javax.servlet.AsyncContext;
35 import javax.servlet.ServletConfig;
36 import javax.servlet.ServletContext;
37 import javax.servlet.ServletException;
38 import javax.servlet.UnavailableException;
39 import javax.servlet.http.HttpServlet;
40 import javax.servlet.http.HttpServletRequest;
41 import javax.servlet.http.HttpServletResponse;
42
43 import org.eclipse.jetty.client.HttpClient;
44 import org.eclipse.jetty.client.api.Request;
45 import org.eclipse.jetty.client.api.Response;
46 import org.eclipse.jetty.client.api.Result;
47 import org.eclipse.jetty.client.util.InputStreamContentProvider;
48 import org.eclipse.jetty.http.HttpField;
49 import org.eclipse.jetty.http.HttpHeader;
50 import org.eclipse.jetty.http.HttpMethod;
51 import org.eclipse.jetty.http.HttpVersion;
52 import org.eclipse.jetty.util.HttpCookieStore;
53 import org.eclipse.jetty.util.log.Log;
54 import org.eclipse.jetty.util.log.Logger;
55 import org.eclipse.jetty.util.thread.QueuedThreadPool;
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80 public class ProxyServlet extends HttpServlet
81 {
82 protected static final String ASYNC_CONTEXT = ProxyServlet.class.getName() + ".asyncContext";
83 private static final Set<String> HOP_HEADERS = new HashSet<>();
84 static
85 {
86 HOP_HEADERS.add("proxy-connection");
87 HOP_HEADERS.add("connection");
88 HOP_HEADERS.add("keep-alive");
89 HOP_HEADERS.add("transfer-encoding");
90 HOP_HEADERS.add("te");
91 HOP_HEADERS.add("trailer");
92 HOP_HEADERS.add("proxy-authorization");
93 HOP_HEADERS.add("proxy-authenticate");
94 HOP_HEADERS.add("upgrade");
95 }
96
97 private final Set<String> _whiteList = new HashSet<>();
98 private final Set<String> _blackList = new HashSet<>();
99
100 protected Logger _log;
101 private String _hostHeader;
102 private String _viaHost;
103 private HttpClient _client;
104 private long _timeout;
105
106 @Override
107 public void init() throws ServletException
108 {
109 _log = createLogger();
110
111 ServletConfig config = getServletConfig();
112
113 _hostHeader = config.getInitParameter("hostHeader");
114
115 _viaHost = config.getInitParameter("viaHost");
116 if (_viaHost == null)
117 _viaHost = viaHost();
118
119 try
120 {
121 _client = createHttpClient();
122
123
124 getServletContext().setAttribute(config.getServletName() + ".HttpClient", _client);
125
126 String whiteList = config.getInitParameter("whiteList");
127 if (whiteList != null)
128 getWhiteListHosts().addAll(parseList(whiteList));
129
130 String blackList = config.getInitParameter("blackList");
131 if (blackList != null)
132 getBlackListHosts().addAll(parseList(blackList));
133 }
134 catch (Exception e)
135 {
136 throw new ServletException(e);
137 }
138 }
139
140 public String getViaHost()
141 {
142 return _viaHost;
143 }
144
145 public long getTimeout()
146 {
147 return _timeout;
148 }
149
150 public void setTimeout(long timeout)
151 {
152 this._timeout = timeout;
153 }
154
155 public Set<String> getWhiteListHosts()
156 {
157 return _whiteList;
158 }
159
160 public Set<String> getBlackListHosts()
161 {
162 return _blackList;
163 }
164
165 protected static String viaHost()
166 {
167 try
168 {
169 return InetAddress.getLocalHost().getHostName();
170 }
171 catch (UnknownHostException x)
172 {
173 return "localhost";
174 }
175 }
176
177
178
179
180 protected Logger createLogger()
181 {
182 String name = getServletConfig().getServletName();
183 name = name.replace('-', '.');
184 return Log.getLogger(name);
185 }
186
187 public void destroy()
188 {
189 try
190 {
191 _client.stop();
192 }
193 catch (Exception x)
194 {
195 _log.debug(x);
196 }
197 }
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249 protected HttpClient createHttpClient() throws ServletException
250 {
251 ServletConfig config = getServletConfig();
252
253 HttpClient client = newHttpClient();
254
255
256 client.setFollowRedirects(false);
257
258
259 client.setCookieStore(new HttpCookieStore.Empty());
260
261 Executor executor;
262 String value = config.getInitParameter("maxThreads");
263 if (value == null || "-".equals(value))
264 {
265 executor = (Executor)getServletContext().getAttribute("org.eclipse.jetty.server.Executor");
266 if (executor==null)
267 throw new IllegalStateException("No server executor for proxy");
268 }
269 else
270 {
271 QueuedThreadPool qtp= new QueuedThreadPool(Integer.parseInt(value));
272 String servletName = config.getServletName();
273 int dot = servletName.lastIndexOf('.');
274 if (dot >= 0)
275 servletName = servletName.substring(dot + 1);
276 qtp.setName(servletName);
277 executor=qtp;
278 }
279
280 client.setExecutor(executor);
281
282 value = config.getInitParameter("maxConnections");
283 if (value == null)
284 value = "256";
285 client.setMaxConnectionsPerDestination(Integer.parseInt(value));
286
287 value = config.getInitParameter("idleTimeout");
288 if (value == null)
289 value = "30000";
290 client.setIdleTimeout(Long.parseLong(value));
291
292 value = config.getInitParameter("timeout");
293 if (value == null)
294 value = "60000";
295 _timeout = Long.parseLong(value);
296
297 value = config.getInitParameter("requestBufferSize");
298 if (value != null)
299 client.setRequestBufferSize(Integer.parseInt(value));
300
301 value = config.getInitParameter("responseBufferSize");
302 if (value != null)
303 client.setResponseBufferSize(Integer.parseInt(value));
304
305 try
306 {
307 client.start();
308
309
310 client.getContentDecoderFactories().clear();
311
312 return client;
313 }
314 catch (Exception x)
315 {
316 throw new ServletException(x);
317 }
318 }
319
320
321
322
323 protected HttpClient newHttpClient()
324 {
325 return new HttpClient();
326 }
327
328 private Set<String> parseList(String list)
329 {
330 Set<String> result = new HashSet<>();
331 String[] hosts = list.split(",");
332 for (String host : hosts)
333 {
334 host = host.trim();
335 if (host.length() == 0)
336 continue;
337 result.add(host);
338 }
339 return result;
340 }
341
342
343
344
345
346
347
348
349 public boolean validateDestination(String host, int port)
350 {
351 String hostPort = host + ":" + port;
352 if (!_whiteList.isEmpty())
353 {
354 if (!_whiteList.contains(hostPort))
355 {
356 _log.debug("Host {}:{} not whitelisted", host, port);
357 return false;
358 }
359 }
360 if (!_blackList.isEmpty())
361 {
362 if (_blackList.contains(hostPort))
363 {
364 _log.debug("Host {}:{} blacklisted", host, port);
365 return false;
366 }
367 }
368 return true;
369 }
370
371 @Override
372 protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
373 {
374 final int requestId = getRequestId(request);
375
376 URI rewrittenURI = rewriteURI(request);
377
378 if (_log.isDebugEnabled())
379 {
380 StringBuffer uri = request.getRequestURL();
381 if (request.getQueryString() != null)
382 uri.append("?").append(request.getQueryString());
383 _log.debug("{} rewriting: {} -> {}", requestId, uri, rewrittenURI);
384 }
385
386 if (rewrittenURI == null)
387 {
388 response.sendError(HttpServletResponse.SC_FORBIDDEN);
389 return;
390 }
391
392 final Request proxyRequest = _client.newRequest(rewrittenURI)
393 .method(HttpMethod.fromString(request.getMethod()))
394 .version(HttpVersion.fromString(request.getProtocol()));
395
396
397 boolean hasContent = false;
398 for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements();)
399 {
400 String headerName = headerNames.nextElement();
401 String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH);
402
403
404 if (HOP_HEADERS.contains(lowerHeaderName))
405 continue;
406
407 if (_hostHeader != null && HttpHeader.HOST.is(headerName))
408 continue;
409
410 if (request.getContentLength() > 0 || request.getContentType() != null ||
411 HttpHeader.TRANSFER_ENCODING.is(headerName))
412 hasContent = true;
413
414 for (Enumeration<String> headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();)
415 {
416 String headerValue = headerValues.nextElement();
417 if (headerValue != null)
418 proxyRequest.header(headerName, headerValue);
419 }
420 }
421
422
423 if (_hostHeader != null)
424 proxyRequest.header(HttpHeader.HOST, _hostHeader);
425
426
427 addViaHeader(proxyRequest);
428 addXForwardedHeaders(proxyRequest, request);
429
430 if (hasContent)
431 {
432 proxyRequest.content(new InputStreamContentProvider(request.getInputStream())
433 {
434 @Override
435 public long getLength()
436 {
437 return request.getContentLength();
438 }
439
440 @Override
441 protected ByteBuffer onRead(byte[] buffer, int offset, int length)
442 {
443 _log.debug("{} proxying content to upstream: {} bytes", requestId, length);
444 return super.onRead(buffer, offset, length);
445 }
446 });
447 }
448
449 final AsyncContext asyncContext = request.startAsync();
450
451 asyncContext.setTimeout(0);
452 request.setAttribute(ASYNC_CONTEXT, asyncContext);
453
454 customizeProxyRequest(proxyRequest, request);
455
456 if (_log.isDebugEnabled())
457 {
458 StringBuilder builder = new StringBuilder(request.getMethod());
459 builder.append(" ").append(request.getRequestURI());
460 String query = request.getQueryString();
461 if (query != null)
462 builder.append("?").append(query);
463 builder.append(" ").append(request.getProtocol()).append("\r\n");
464 for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements();)
465 {
466 String headerName = headerNames.nextElement();
467 builder.append(headerName).append(": ");
468 for (Enumeration<String> headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();)
469 {
470 String headerValue = headerValues.nextElement();
471 if (headerValue != null)
472 builder.append(headerValue);
473 if (headerValues.hasMoreElements())
474 builder.append(",");
475 }
476 builder.append("\r\n");
477 }
478 builder.append("\r\n");
479
480 _log.debug("{} proxying to upstream:{}{}{}{}",
481 requestId,
482 System.lineSeparator(),
483 builder,
484 proxyRequest,
485 System.lineSeparator(),
486 proxyRequest.getHeaders().toString().trim());
487 }
488
489 proxyRequest.timeout(getTimeout(), TimeUnit.MILLISECONDS);
490 proxyRequest.send(new ProxyResponseListener(request, response));
491 }
492
493 protected Request addViaHeader(Request proxyRequest)
494 {
495 return proxyRequest.header(HttpHeader.VIA, "http/1.1 " + getViaHost());
496 }
497
498 protected void addXForwardedHeaders(Request proxyRequest, HttpServletRequest request)
499 {
500 proxyRequest.header(HttpHeader.X_FORWARDED_FOR, request.getRemoteAddr());
501 proxyRequest.header(HttpHeader.X_FORWARDED_PROTO, request.getScheme());
502 proxyRequest.header(HttpHeader.X_FORWARDED_HOST, request.getHeader(HttpHeader.HOST.asString()));
503 proxyRequest.header(HttpHeader.X_FORWARDED_SERVER, request.getLocalName());
504 }
505
506 protected void onResponseHeaders(HttpServletRequest request, HttpServletResponse response, Response proxyResponse)
507 {
508 for (HttpField field : proxyResponse.getHeaders())
509 {
510 String headerName = field.getName();
511 String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH);
512 if (HOP_HEADERS.contains(lowerHeaderName))
513 continue;
514
515 String newHeaderValue = filterResponseHeader(request, headerName, field.getValue());
516 if (newHeaderValue == null || newHeaderValue.trim().length() == 0)
517 continue;
518
519 response.addHeader(headerName, newHeaderValue);
520 }
521 }
522
523 protected void onResponseContent(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, byte[] buffer, int offset, int length) throws IOException
524 {
525 response.getOutputStream().write(buffer, offset, length);
526 _log.debug("{} proxying content to downstream: {} bytes", getRequestId(request), length);
527 }
528
529 protected void onResponseSuccess(HttpServletRequest request, HttpServletResponse response, Response proxyResponse)
530 {
531 AsyncContext asyncContext = (AsyncContext)request.getAttribute(ASYNC_CONTEXT);
532 asyncContext.complete();
533 _log.debug("{} proxying successful", getRequestId(request));
534 }
535
536 protected void onResponseFailure(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, Throwable failure)
537 {
538 _log.debug(getRequestId(request) + " proxying failed", failure);
539 if (!response.isCommitted())
540 {
541 if (failure instanceof TimeoutException)
542 response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT);
543 else
544 response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
545 }
546 AsyncContext asyncContext = (AsyncContext)request.getAttribute(ASYNC_CONTEXT);
547 asyncContext.complete();
548 }
549
550 protected int getRequestId(HttpServletRequest request)
551 {
552 return System.identityHashCode(request);
553 }
554
555 protected URI rewriteURI(HttpServletRequest request)
556 {
557 if (!validateDestination(request.getServerName(), request.getServerPort()))
558 return null;
559
560 StringBuffer uri = request.getRequestURL();
561 String query = request.getQueryString();
562 if (query != null)
563 uri.append("?").append(query);
564
565 return URI.create(uri.toString());
566 }
567
568
569
570
571
572
573
574
575 protected void customizeProxyRequest(Request proxyRequest, HttpServletRequest request)
576 {
577 }
578
579
580
581
582
583
584
585
586
587
588
589 protected String filterResponseHeader(HttpServletRequest request, String headerName, String headerValue)
590 {
591 return headerValue;
592 }
593
594
595
596
597
598
599
600
601
602
603
604
605
606 public static class Transparent extends ProxyServlet
607 {
608 private String _proxyTo;
609 private String _prefix;
610
611 public Transparent()
612 {
613 }
614
615 public Transparent(String proxyTo, String prefix)
616 {
617 _proxyTo = URI.create(proxyTo).normalize().toString();
618 _prefix = URI.create(prefix).normalize().toString();
619 }
620
621 @Override
622 public void init() throws ServletException
623 {
624 super.init();
625
626 ServletConfig config = getServletConfig();
627
628 String prefix = config.getInitParameter("prefix");
629 _prefix = prefix == null ? _prefix : prefix;
630
631
632 String contextPath = getServletContext().getContextPath();
633 _prefix = _prefix == null ? contextPath : (contextPath + _prefix);
634
635 String proxyTo = config.getInitParameter("proxyTo");
636 _proxyTo = proxyTo == null ? _proxyTo : proxyTo;
637
638 if (_proxyTo == null)
639 throw new UnavailableException("Init parameter 'proxyTo' is required.");
640
641 if (!_prefix.startsWith("/"))
642 throw new UnavailableException("Init parameter 'prefix' parameter must start with a '/'.");
643
644 _log.debug(config.getServletName() + " @ " + _prefix + " to " + _proxyTo);
645 }
646
647 @Override
648 protected URI rewriteURI(HttpServletRequest request)
649 {
650 String path = request.getRequestURI();
651 if (!path.startsWith(_prefix))
652 return null;
653
654 StringBuilder uri = new StringBuilder(_proxyTo);
655 if (_proxyTo.endsWith("/"))
656 uri.setLength(uri.length() - 1);
657 String rest = path.substring(_prefix.length());
658 if (!rest.startsWith("/"))
659 uri.append("/");
660 uri.append(rest);
661 String query = request.getQueryString();
662 if (query != null)
663 uri.append("?").append(query);
664 URI rewrittenURI = URI.create(uri.toString()).normalize();
665
666 if (!validateDestination(rewrittenURI.getHost(), rewrittenURI.getPort()))
667 return null;
668
669 return rewrittenURI;
670 }
671 }
672
673 private class ProxyResponseListener extends Response.Listener.Adapter
674 {
675 private final HttpServletRequest request;
676 private final HttpServletResponse response;
677
678 public ProxyResponseListener(HttpServletRequest request, HttpServletResponse response)
679 {
680 this.request = request;
681 this.response = response;
682 }
683
684 @Override
685 public void onBegin(Response proxyResponse)
686 {
687 response.setStatus(proxyResponse.getStatus());
688 }
689
690 @Override
691 public void onHeaders(Response proxyResponse)
692 {
693 onResponseHeaders(request, response, proxyResponse);
694
695 if (_log.isDebugEnabled())
696 {
697 StringBuilder builder = new StringBuilder("\r\n");
698 builder.append(request.getProtocol()).append(" ").append(response.getStatus()).append(" ").append(proxyResponse.getReason()).append("\r\n");
699 for (String headerName : response.getHeaderNames())
700 {
701 builder.append(headerName).append(": ");
702 for (Iterator<String> headerValues = response.getHeaders(headerName).iterator(); headerValues.hasNext();)
703 {
704 String headerValue = headerValues.next();
705 if (headerValue != null)
706 builder.append(headerValue);
707 if (headerValues.hasNext())
708 builder.append(",");
709 }
710 builder.append("\r\n");
711 }
712 _log.debug("{} proxying to downstream:{}{}{}{}{}",
713 getRequestId(request),
714 System.lineSeparator(),
715 proxyResponse,
716 System.lineSeparator(),
717 proxyResponse.getHeaders().toString().trim(),
718 System.lineSeparator(),
719 builder);
720 }
721 }
722
723 @Override
724 public void onContent(Response proxyResponse, ByteBuffer content)
725 {
726 byte[] buffer;
727 int offset;
728 int length = content.remaining();
729 if (content.hasArray())
730 {
731 buffer = content.array();
732 offset = content.arrayOffset();
733 }
734 else
735 {
736 buffer = new byte[length];
737 content.get(buffer);
738 offset = 0;
739 }
740
741 try
742 {
743 onResponseContent(request, response, proxyResponse, buffer, offset, length);
744 }
745 catch (IOException x)
746 {
747 proxyResponse.abort(x);
748 }
749 }
750
751 @Override
752 public void onSuccess(Response proxyResponse)
753 {
754 onResponseSuccess(request, response, proxyResponse);
755 }
756
757 @Override
758 public void onFailure(Response proxyResponse, Throwable failure)
759 {
760 onResponseFailure(request, response, proxyResponse, failure);
761 }
762
763 @Override
764 public void onComplete(Result result)
765 {
766 _log.debug("{} proxying complete", getRequestId(request));
767 }
768 }
769 }