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