1 /** 2 HTTP (reverse) proxy implementation 3 4 Copyright: © 2012 RejectedSoftware e.K. 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 - implement a forward proxy 26 */ 27 28 /** 29 Transparently forwards all requests to the proxy to a destination_host. 30 31 You can use the hostName field in the 'settings' to combine multiple internal HTTP servers 32 into one public web server with multiple virtual hosts. 33 */ 34 void listenHTTPReverseProxy(HTTPServerSettings settings, HTTPReverseProxySettings proxy_settings) 35 { 36 // disable all advanced parsing in the server 37 settings.options = HTTPServerOption.None; 38 listenHTTP(settings, reverseProxyRequest(proxy_settings)); 39 } 40 /// ditto 41 void listenHTTPReverseProxy(HTTPServerSettings settings, string destination_host, ushort destination_port) 42 { 43 URL url; 44 url.schema = "http"; 45 url.host = destination_host; 46 url.port = destination_port; 47 auto proxy_settings = new HTTPReverseProxySettings; 48 proxy_settings.destination = url; 49 listenHTTPReverseProxy(settings, proxy_settings); 50 } 51 52 53 /** 54 Returns a HTTP request handler that forwards any request to the specified host/port. 55 */ 56 HTTPServerRequestDelegateS reverseProxyRequest(HTTPReverseProxySettings settings) 57 { 58 static immutable string[] non_forward_headers = ["Content-Length", "Transfer-Encoding", "Content-Encoding", "Connection"]; 59 static InetHeaderMap non_forward_headers_map; 60 if (non_forward_headers_map.length == 0) 61 foreach (n; non_forward_headers) 62 non_forward_headers_map[n] = ""; 63 64 auto url = settings.destination; 65 66 void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res) 67 @safe { 68 auto rurl = url; 69 rurl.localURI = req.requestURL; 70 71 //handle connect tunnels 72 if (req.method == HTTPMethod.CONNECT) { 73 if (!settings.handleConnectRequests) 74 { 75 throw new HTTPStatusException(HTTPStatus.methodNotAllowed); 76 } 77 78 TCPConnection ccon; 79 try ccon = connectTCP(url.getFilteredHost, url.port); 80 catch (Exception e) { 81 throw new HTTPStatusException(HTTPStatus.badGateway, "Connection to upstream server failed: "~e.msg); 82 } 83 auto scon = res.connectProxy(); 84 assert (scon); 85 86 import vibe.core.core : runTask; 87 runTask({ scon.pipe(ccon); }); 88 ccon.pipe(scon); 89 return; 90 } 91 92 //handle protocol upgrades 93 auto pUpgrade = "Upgrade" in req.headers; 94 auto pConnection = "Connection" in req.headers; 95 96 97 import std.algorithm : splitter, canFind; 98 import vibe.utils.string : icmp2; 99 bool isUpgrade = pConnection && (*pConnection).splitter(',').canFind!(a => a.icmp2("upgrade")); 100 101 void setupClientRequest(scope HTTPClientRequest creq) 102 { 103 creq.method = req.method; 104 creq.headers = req.headers.dup; 105 creq.headers["Host"] = url.getFilteredHost; 106 107 //handle protocol upgrades 108 if (!isUpgrade) { 109 creq.headers["Connection"] = "keep-alive"; 110 } 111 if (settings.avoidCompressedRequests && "Accept-Encoding" in creq.headers) 112 creq.headers.remove("Accept-Encoding"); 113 if (auto pfh = "X-Forwarded-Host" !in creq.headers) creq.headers["X-Forwarded-Host"] = req.headers["Host"]; 114 if (auto pfp = "X-Forwarded-Proto" !in creq.headers) creq.headers["X-Forwarded-Proto"] = req.tls ? "https" : "http"; 115 if (auto pff = "X-Forwarded-For" in req.headers) creq.headers["X-Forwarded-For"] = *pff ~ ", " ~ req.peer; 116 else creq.headers["X-Forwarded-For"] = req.peer; 117 req.bodyReader.pipe(creq.bodyWriter); 118 } 119 120 void handleClientResponse(scope HTTPClientResponse cres) 121 { 122 import vibe.utils.string; 123 124 // copy the response to the original requester 125 res.statusCode = cres.statusCode; 126 127 //handle protocol upgrades 128 if (cres.statusCode == HTTPStatus.switchingProtocols && isUpgrade) { 129 res.headers = cres.headers.dup; 130 131 auto scon = res.switchProtocol(""); 132 auto ccon = cres.switchProtocol(""); 133 134 import vibe.core.core : runTask; 135 runTask({ ccon.pipe(scon); }); 136 137 scon.pipe(ccon); 138 return; 139 } 140 141 // special case for empty response bodies 142 if ("Content-Length" !in cres.headers && "Transfer-Encoding" !in cres.headers || req.method == HTTPMethod.HEAD) { 143 foreach (key, ref value; cres.headers) 144 if (icmp2(key, "Connection") != 0) 145 res.headers[key] = value; 146 res.writeVoidBody(); 147 return; 148 } 149 150 // enforce compatibility with HTTP/1.0 clients that do not support chunked encoding 151 // (Squid and some other proxies) 152 if (res.httpVersion == HTTPVersion.HTTP_1_0 && ("Transfer-Encoding" in cres.headers || "Content-Length" !in cres.headers)) { 153 // copy all headers that may pass from upstream to client 154 foreach (n, ref v; cres.headers) 155 if (n !in non_forward_headers_map) 156 res.headers[n] = v; 157 158 if ("Transfer-Encoding" in res.headers) res.headers.remove("Transfer-Encoding"); 159 auto content = cres.bodyReader.readAll(1024*1024); 160 res.headers["Content-Length"] = to!string(content.length); 161 if (res.isHeadResponse) res.writeVoidBody(); 162 else res.bodyWriter.write(content); 163 return; 164 } 165 166 // to perform a verbatim copy of the client response 167 if ("Content-Length" in cres.headers) { 168 if ("Content-Encoding" in res.headers) res.headers.remove("Content-Encoding"); 169 foreach (key, ref value; cres.headers) 170 if (icmp2(key, "Connection") != 0) 171 res.headers[key] = value; 172 auto size = cres.headers["Content-Length"].to!size_t(); 173 if (res.isHeadResponse) res.writeVoidBody(); 174 else cres.readRawBody((scope InterfaceProxy!InputStream reader) { res.writeRawBody(reader, size); }); 175 assert(res.headerWritten); 176 return; 177 } 178 179 // fall back to a generic re-encoding of the response 180 // copy all headers that may pass from upstream to client 181 foreach (n, ref v; cres.headers) 182 if (n !in non_forward_headers_map) 183 res.headers[n] = v; 184 if (res.isHeadResponse) res.writeVoidBody(); 185 else cres.bodyReader.pipe(res.bodyWriter); 186 } 187 188 try requestHTTP(rurl, &setupClientRequest, &handleClientResponse); 189 catch (Exception e) { 190 throw new HTTPStatusException(HTTPStatus.badGateway, "Connection to upstream server failed: "~e.msg); 191 } 192 } 193 194 return &handleRequest; 195 } 196 /// ditto 197 HTTPServerRequestDelegateS reverseProxyRequest(string destination_host, ushort destination_port) 198 { 199 URL url; 200 url.schema = "http"; 201 url.host = destination_host; 202 url.port = destination_port; 203 auto settings = new HTTPReverseProxySettings; 204 settings.destination = url; 205 return reverseProxyRequest(settings); 206 } 207 208 /// ditto 209 HTTPServerRequestDelegateS reverseProxyRequest(URL destination) 210 { 211 auto settings = new HTTPReverseProxySettings; 212 settings.destination = destination; 213 return reverseProxyRequest(settings); 214 } 215 216 /** 217 Provides advanced configuration facilities for reverse proxy servers. 218 */ 219 final class HTTPReverseProxySettings { 220 /// Scheduled for deprecation - use `destination.host` instead. 221 @property string destinationHost() const { return destination.host; } 222 /// ditto 223 @property void destinationHost(string host) { destination.host = host; } 224 /// Scheduled for deprecation - use `destination.port` instead. 225 @property ushort destinationPort() const { return destination.port; } 226 /// ditto 227 @property void destinationPort(ushort port) { destination.port = port; } 228 229 /// The destination URL to forward requests to 230 URL destination = URL("http", InetPath("")); 231 /// Avoids compressed transfers between proxy and destination hosts 232 bool avoidCompressedRequests; 233 /// Handle CONNECT requests for creating a tunnel to the destination host 234 bool handleConnectRequests; 235 }