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;