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 }