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