proxy.d 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. /**
  2. HTTP (reverse) proxy implementation
  3. Copyright: © 2012 Sönke Ludwig
  4. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
  5. Authors: Sönke Ludwig
  6. */
  7. module vibe.http.proxy;
  8. import vibe.core.core : runTask;
  9. import vibe.core.log;
  10. import vibe.http.client;
  11. import vibe.http.server;
  12. import vibe.inet.message;
  13. import vibe.stream.operations;
  14. import vibe.internal.interfaceproxy : InterfaceProxy;
  15. import std.conv;
  16. import std.exception;
  17. /*
  18. TODO:
  19. - use a client pool
  20. - implement a path based reverse proxy
  21. */
  22. /**
  23. Transparently forwards all requests to the proxy to another host.
  24. The configurations set in 'settings' and 'proxy_settings' determines the exact
  25. behavior.
  26. */
  27. void listenHTTPProxy(HTTPServerSettings settings, HTTPProxySettings proxy_settings)
  28. {
  29. // disable all advanced parsing in the server
  30. settings.options = HTTPServerOption.none;
  31. listenHTTP(settings, proxyRequest(proxy_settings));
  32. }
  33. /**
  34. Transparently forwards all requests to the proxy to a destination_host.
  35. You can use the hostName field in the 'settings' to combine multiple internal HTTP servers
  36. into one public web server with multiple virtual hosts.
  37. */
  38. void listenHTTPReverseProxy(HTTPServerSettings settings, string destination_host, ushort destination_port)
  39. {
  40. URL url;
  41. url.schema = "http";
  42. url.host = destination_host;
  43. url.port = destination_port;
  44. auto proxy_settings = new HTTPProxySettings(ProxyMode.reverse);
  45. proxy_settings.destination = url;
  46. listenHTTPProxy(settings, proxy_settings);
  47. }
  48. /**
  49. Transparently forwards all requests to the proxy to the requestURL of the request.
  50. */
  51. void listenHTTPForwardProxy(HTTPServerSettings settings) {
  52. auto proxy_settings = new HTTPProxySettings(ProxyMode.forward);
  53. proxy_settings.handleConnectRequests = true;
  54. listenHTTPProxy(settings, proxy_settings);
  55. }
  56. /**
  57. Returns a HTTP request handler that forwards any request to the specified or requested host/port.
  58. */
  59. HTTPServerRequestDelegateS proxyRequest(HTTPProxySettings settings)
  60. {
  61. static immutable string[] non_forward_headers = ["Content-Length", "Transfer-Encoding", "Content-Encoding", "Connection"];
  62. static InetHeaderMap non_forward_headers_map;
  63. if (non_forward_headers_map.length == 0)
  64. foreach (n; non_forward_headers)
  65. non_forward_headers_map[n] = "";
  66. void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res)
  67. @safe {
  68. auto url = settings.destination;
  69. if (settings.proxyMode == ProxyMode.reverse) {
  70. url.localURI = req.requestURL;
  71. }
  72. else {
  73. url = URL(req.requestURL);
  74. }
  75. //handle connect tunnels
  76. if (req.method == HTTPMethod.CONNECT) {
  77. if (!settings.handleConnectRequests)
  78. {
  79. throw new HTTPStatusException(HTTPStatus.methodNotAllowed);
  80. }
  81. // CONNECT resources are of the form server:port and not
  82. // schema://server:port, so they need some adjustment
  83. // TODO: use a more efficient means to parse this
  84. url = URL.parse("http://"~req.requestURL);
  85. TCPConnection ccon;
  86. try ccon = connectTCP(url.getFilteredHost, url.port);
  87. catch (Exception e) {
  88. throw new HTTPStatusException(HTTPStatus.badGateway, "Connection to upstream server failed: "~e.msg);
  89. }
  90. res.writeVoidBody();
  91. auto scon = res.connectProxy();
  92. assert (scon);
  93. runTask(() nothrow {
  94. try scon.pipe(ccon);
  95. catch (Exception e) {
  96. logException(e, "Failed to forward proxy data from server to client");
  97. try scon.close();
  98. catch (Exception e) logException(e, "Failed to close server connection after error");
  99. try ccon.close();
  100. catch (Exception e) logException(e, "Failed to close client connection after error");
  101. }
  102. });
  103. ccon.pipe(scon);
  104. return;
  105. }
  106. //handle protocol upgrades
  107. auto pUpgrade = "Upgrade" in req.headers;
  108. auto pConnection = "Connection" in req.headers;
  109. import std.algorithm : splitter, canFind;
  110. import vibe.internal.string : icmp2;
  111. bool isUpgrade = pConnection && (*pConnection).splitter(',').canFind!(a => a.icmp2("upgrade"));
  112. void setupClientRequest(scope HTTPClientRequest creq)
  113. {
  114. creq.method = req.method;
  115. creq.headers = req.headers.dup;
  116. creq.headers["Host"] = url.getFilteredHost;
  117. //handle protocol upgrades
  118. if (!isUpgrade) {
  119. creq.headers["Connection"] = "keep-alive";
  120. }
  121. if (settings.avoidCompressedRequests && "Accept-Encoding" in creq.headers)
  122. creq.headers.remove("Accept-Encoding");
  123. if (auto pfh = "X-Forwarded-Host" !in creq.headers) creq.headers["X-Forwarded-Host"] = req.headers["Host"];
  124. if (auto pfp = "X-Forwarded-Proto" !in creq.headers) creq.headers["X-Forwarded-Proto"] = req.tls ? "https" : "http";
  125. if (auto pff = "X-Forwarded-For" in req.headers) creq.headers["X-Forwarded-For"] = *pff ~ ", " ~ req.peer;
  126. else creq.headers["X-Forwarded-For"] = req.peer;
  127. req.bodyReader.pipe(creq.bodyWriter);
  128. }
  129. void handleClientResponse(scope HTTPClientResponse cres)
  130. {
  131. // copy the response to the original requester
  132. res.statusCode = cres.statusCode;
  133. //handle protocol upgrades
  134. if (cres.statusCode == HTTPStatus.switchingProtocols && isUpgrade) {
  135. res.headers = cres.headers.dup;
  136. auto scon = res.switchProtocol("");
  137. auto ccon = cres.switchProtocol("");
  138. runTask(() nothrow {
  139. try ccon.pipe(scon);
  140. catch (Exception e) {
  141. logException(e, "Failed to forward proxy data from client to server");
  142. try scon.close();
  143. catch (Exception e) logException(e, "Failed to close server connection after error");
  144. try ccon.close();
  145. catch (Exception e) logException(e, "Failed to close client connection after error");
  146. }
  147. });
  148. scon.pipe(ccon);
  149. return;
  150. }
  151. // special case for empty response bodies
  152. if ("Content-Length" !in cres.headers && "Transfer-Encoding" !in cres.headers || req.method == HTTPMethod.HEAD) {
  153. foreach (key, ref value; cres.headers.byKeyValue)
  154. if (icmp2(key, "Connection") != 0)
  155. res.headers.addField(key,value);
  156. res.writeVoidBody();
  157. return;
  158. }
  159. // enforce compatibility with HTTP/1.0 clients that do not support chunked encoding
  160. // (Squid and some other proxies)
  161. if (res.httpVersion == HTTPVersion.HTTP_1_0 && ("Transfer-Encoding" in cres.headers || "Content-Length" !in cres.headers)) {
  162. // copy all headers that may pass from upstream to client
  163. foreach (n, ref v; cres.headers.byKeyValue)
  164. if (n !in non_forward_headers_map)
  165. res.headers.addField(n,v);
  166. if ("Transfer-Encoding" in res.headers) res.headers.remove("Transfer-Encoding");
  167. auto content = cres.bodyReader.readAll(1024*1024);
  168. res.headers["Content-Length"] = to!string(content.length);
  169. if (res.isHeadResponse) res.writeVoidBody();
  170. else res.bodyWriter.write(content);
  171. return;
  172. }
  173. // to perform a verbatim copy of the client response
  174. if ("Content-Length" in cres.headers) {
  175. if ("Content-Encoding" in res.headers) res.headers.remove("Content-Encoding");
  176. foreach (key, ref value; cres.headers.byKeyValue)
  177. if (icmp2(key, "Connection") != 0)
  178. res.headers.addField(key,value);
  179. auto size = cres.headers["Content-Length"].to!size_t();
  180. if (res.isHeadResponse) res.writeVoidBody();
  181. else cres.readRawBody((scope InterfaceProxy!InputStream reader) { res.writeRawBody(reader, size); });
  182. assert(res.headerWritten);
  183. return;
  184. }
  185. // fall back to a generic re-encoding of the response
  186. // copy all headers that may pass from upstream to client
  187. foreach (n, ref v; cres.headers.byKeyValue)
  188. if (n !in non_forward_headers_map)
  189. res.headers.addField(n,v);
  190. if (res.isHeadResponse) res.writeVoidBody();
  191. else cres.bodyReader.pipe(res.bodyWriter);
  192. }
  193. try requestHTTP(url, &setupClientRequest, &handleClientResponse);
  194. catch (Exception e) {
  195. throw new HTTPStatusException(HTTPStatus.badGateway, "Connection to upstream server failed: "~e.msg);
  196. }
  197. }
  198. return &handleRequest;
  199. }
  200. /**
  201. Returns a HTTP request handler that forwards any request to the specified host/port.
  202. */
  203. HTTPServerRequestDelegateS reverseProxyRequest(string destination_host, ushort destination_port)
  204. {
  205. URL url;
  206. url.schema = "http";
  207. url.host = destination_host;
  208. url.port = destination_port;
  209. auto settings = new HTTPProxySettings(ProxyMode.reverse);
  210. settings.destination = url;
  211. return proxyRequest(settings);
  212. }
  213. /// ditto
  214. HTTPServerRequestDelegateS reverseProxyRequest(URL destination)
  215. {
  216. auto settings = new HTTPProxySettings(ProxyMode.reverse);
  217. settings.destination = destination;
  218. return proxyRequest(settings);
  219. }
  220. /**
  221. Returns a HTTP request handler that forwards any request to the requested host/port.
  222. */
  223. HTTPServerRequestDelegateS forwardProxyRequest() {
  224. return proxyRequest(new HTTPProxySettings(ProxyMode.forward));
  225. }
  226. /**
  227. Enum to represent the two modes a proxy can operate as.
  228. */
  229. enum ProxyMode {forward, reverse}
  230. /**
  231. Provides advanced configuration facilities for reverse proxy servers.
  232. */
  233. final class HTTPProxySettings {
  234. /// Scheduled for deprecation - use `destination.host` instead.
  235. @property string destinationHost() const { return destination.host; }
  236. /// ditto
  237. @property void destinationHost(string host) { destination.host = host; }
  238. /// Scheduled for deprecation - use `destination.port` instead.
  239. @property ushort destinationPort() const { return destination.port; }
  240. /// ditto
  241. @property void destinationPort(ushort port) { destination.port = port; }
  242. /// The destination URL to forward requests to
  243. URL destination = URL("http", InetPath(""));
  244. /// The mode of the proxy i.e forward, reverse
  245. ProxyMode proxyMode;
  246. /// Avoids compressed transfers between proxy and destination hosts
  247. bool avoidCompressedRequests;
  248. /// Handle CONNECT requests for creating a tunnel to the destination host
  249. bool handleConnectRequests;
  250. /// Empty default constructor for backwards compatibility - will be deprecated soon.
  251. deprecated("Pass an explicit `ProxyMode` argument")
  252. this() { proxyMode = ProxyMode.reverse; }
  253. /// Explicitly sets the proxy mode.
  254. this(ProxyMode mode) { proxyMode = mode; }
  255. }
  256. /// Compatibility alias
  257. deprecated("Use `HTTPProxySettings(ProxyMode.reverse)` instead.")
  258. alias HTTPReverseProxySettings = HTTPProxySettings;