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