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.internal.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 			// copy the response to the original requester
156 			res.statusCode = cres.statusCode;
157 
158 			//handle protocol upgrades
159 			if (cres.statusCode == HTTPStatus.switchingProtocols && isUpgrade) {
160 				res.headers = cres.headers.dup;
161 
162 				auto scon = res.switchProtocol("");
163 				auto ccon = cres.switchProtocol("");
164 
165 				runTask(() nothrow {
166 					try ccon.pipe(scon);
167 					catch (Exception e) {
168 						logException(e, "Failed to forward proxy data from client to server");
169 						try scon.close();
170 						catch (Exception e) logException(e, "Failed to close server connection after error");
171 						try ccon.close();
172 						catch (Exception e) logException(e, "Failed to close client connection after error");
173 					}
174 				});
175 
176 				scon.pipe(ccon);
177 				return;
178 			}
179 
180 			// special case for empty response bodies
181 			if ("Content-Length" !in cres.headers && "Transfer-Encoding" !in cres.headers || req.method == HTTPMethod.HEAD) {
182 				foreach (key, ref value; cres.headers.byKeyValue)
183 					if (icmp2(key, "Connection") != 0)
184 						res.headers[key] = value;
185 				res.writeVoidBody();
186 				return;
187 			}
188 
189 			// enforce compatibility with HTTP/1.0 clients that do not support chunked encoding
190 			// (Squid and some other proxies)
191 			if (res.httpVersion == HTTPVersion.HTTP_1_0 && ("Transfer-Encoding" in cres.headers || "Content-Length" !in cres.headers)) {
192 				// copy all headers that may pass from upstream to client
193 				foreach (n, ref v; cres.headers.byKeyValue)
194 					if (n !in non_forward_headers_map)
195 						res.headers[n] = v;
196 
197 				if ("Transfer-Encoding" in res.headers) res.headers.remove("Transfer-Encoding");
198 				auto content = cres.bodyReader.readAll(1024*1024);
199 				res.headers["Content-Length"] = to!string(content.length);
200 				if (res.isHeadResponse) res.writeVoidBody();
201 				else res.bodyWriter.write(content);
202 				return;
203 			}
204 
205 			// to perform a verbatim copy of the client response
206 			if ("Content-Length" in cres.headers) {
207 				if ("Content-Encoding" in res.headers) res.headers.remove("Content-Encoding");
208 				foreach (key, ref value; cres.headers.byKeyValue)
209 					if (icmp2(key, "Connection") != 0)
210 						res.headers[key] = value;
211 				auto size = cres.headers["Content-Length"].to!size_t();
212 				if (res.isHeadResponse) res.writeVoidBody();
213 				else cres.readRawBody((scope InterfaceProxy!InputStream reader) { res.writeRawBody(reader, size); });
214 				assert(res.headerWritten);
215 				return;
216 			}
217 
218 			// fall back to a generic re-encoding of the response
219 			// copy all headers that may pass from upstream to client
220 			foreach (n, ref v; cres.headers.byKeyValue)
221 				if (n !in non_forward_headers_map)
222 					res.headers[n] = v;
223 			if (res.isHeadResponse) res.writeVoidBody();
224 			else cres.bodyReader.pipe(res.bodyWriter);
225 		}
226 
227 		try requestHTTP(url, &setupClientRequest, &handleClientResponse);
228 		catch (Exception e) {
229 			throw new HTTPStatusException(HTTPStatus.badGateway, "Connection to upstream server failed: "~e.msg);
230 		}
231 	}
232 
233 	return &handleRequest;
234 }
235 
236 /**
237 	Returns a HTTP request handler that forwards any request to the specified host/port.
238 */
239 HTTPServerRequestDelegateS reverseProxyRequest(string destination_host, ushort destination_port)
240 {
241 	URL url;
242 	url.schema = "http";
243 	url.host = destination_host;
244 	url.port = destination_port;
245 	auto settings = new HTTPProxySettings(ProxyMode.reverse);
246 	settings.destination = url;
247 	return proxyRequest(settings);
248 }
249 
250 /// ditto
251 HTTPServerRequestDelegateS reverseProxyRequest(URL destination)
252 {
253 	auto settings = new HTTPProxySettings(ProxyMode.reverse);
254 	settings.destination = destination;
255 	return proxyRequest(settings);
256 }
257 
258 /**
259 	Returns a HTTP request handler that forwards any request to the requested host/port.
260 */
261 HTTPServerRequestDelegateS forwardProxyRequest() {
262     return proxyRequest(new HTTPProxySettings(ProxyMode.forward));
263 }
264 
265 /**
266 	Enum to represent the two modes a proxy can operate as.
267 */
268 enum ProxyMode {forward, reverse}
269 
270 /**
271 	Provides advanced configuration facilities for reverse proxy servers.
272 */
273 final class HTTPProxySettings {
274 	/// Scheduled for deprecation - use `destination.host` instead.
275 	@property string destinationHost() const { return destination.host; }
276 	/// ditto
277 	@property void destinationHost(string host) { destination.host = host; }
278 	/// Scheduled for deprecation - use `destination.port` instead.
279 	@property ushort destinationPort() const { return destination.port; }
280 	/// ditto
281 	@property void destinationPort(ushort port) { destination.port = port; }
282 
283 	/// The destination URL to forward requests to
284 	URL destination = URL("http", InetPath(""));
285 	/// The mode of the proxy i.e forward, reverse
286 	ProxyMode proxyMode;
287 	/// Avoids compressed transfers between proxy and destination hosts
288 	bool avoidCompressedRequests;
289 	/// Handle CONNECT requests for creating a tunnel to the destination host
290 	bool handleConnectRequests;
291 
292 	/// Empty default constructor for backwards compatibility - will be deprecated soon.
293 	deprecated("Pass an explicit `ProxyMode` argument")
294 	this() { proxyMode = ProxyMode.reverse; }
295 	/// Explicitly sets the proxy mode.
296 	this(ProxyMode mode) { proxyMode = mode; }
297 }
298 /// Compatibility alias
299 deprecated("Use `HTTPProxySettings(ProxyMode.reverse)` instead.")
300 alias HTTPReverseProxySettings = HTTPProxySettings;