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