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;