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;