1 | var http = require("http");
|
2 | var https = require("https");
|
3 | var path = require("path");
|
4 |
|
5 | var httpProxy = require("http-proxy");
|
6 |
|
7 | var auth = require("./auth");
|
8 | var util = require("./util");
|
9 |
|
10 | var oauth2 = require("./oauth2")();
|
11 |
|
12 | var urlTool = require("url");
|
13 |
|
14 | var exports = module.exports;
|
15 |
|
16 | var _LOCK = function(lockIdentifiers, workFunction)
|
17 | {
|
18 | process.locks.lock(lockIdentifiers.join("_"), workFunction);
|
19 | };
|
20 |
|
21 | var NAMED_PROXY_HANDLERS_CACHE = require("lru-cache")({
|
22 | max: 200,
|
23 | maxAge: 1000 * 60 * 60 // 60 minutes
|
24 | });
|
25 |
|
26 | var acquireProxyHandler = exports.acquireProxyHandler = function(proxyTarget, pathPrefix, callback)
|
27 | {
|
28 | var name = path.join(proxyTarget, (pathPrefix || "/"));
|
29 |
|
30 | // is it already in LRU cache?
|
31 | // if so hand it back
|
32 | var _cachedHandler = NAMED_PROXY_HANDLERS_CACHE[name];
|
33 | if (_cachedHandler)
|
34 | {
|
35 | return callback(null, _cachedHandler);
|
36 | }
|
37 |
|
38 | // take out a thread lock
|
39 | _LOCK(["acquireProxyHandler", name], function(releaseLockFn) {
|
40 |
|
41 | // second check to make sure another thread didn't create the handler in the meantime
|
42 | _cachedHandler = NAMED_PROXY_HANDLERS_CACHE[name];
|
43 | if (_cachedHandler)
|
44 | {
|
45 | releaseLockFn();
|
46 | return callback(null, _cachedHandler);
|
47 | }
|
48 |
|
49 | // create the proxy handler and cache it into LRU cache
|
50 | _cachedHandler = createProxyHandler(proxyTarget, pathPrefix);
|
51 |
|
52 | // store back into LRU cache
|
53 | NAMED_PROXY_HANDLERS_CACHE[name] = _cachedHandler;
|
54 |
|
55 | releaseLockFn();
|
56 | callback(null, _cachedHandler);
|
57 | });
|
58 | };
|
59 |
|
60 | var createProxyHandler = function(proxyTarget, pathPrefix)
|
61 | {
|
62 | ////////////////////////////////////////////////////////////////////////////
|
63 | //
|
64 | // HTTP/HTTPS Proxy Server to Cloud CMS
|
65 | // Facilitates Cross-Domain communication between Browser and Cloud Server
|
66 | // This must appear at the top of the app.js file (ahead of config) for things to work
|
67 | //
|
68 | ////////////////////////////////////////////////////////////////////////////
|
69 |
|
70 | // NOTE: changeOrigin must be true because of the way that we set host to host:port
|
71 | // in http-proxy's common.js line 102, the host is only properly set up if changeOrigin is set to true
|
72 | // this sets the "host" header and it has to match what is set at the network/transport level in a way
|
73 | // (inner workings of Node http request)
|
74 | //
|
75 | var proxyConfig = {
|
76 | "target": proxyTarget,
|
77 | "agent": http.globalAgent,
|
78 | "xfwd": false,
|
79 | "proxyTimeout": process.defaultHttpTimeoutMs,
|
80 | "changeOrigin": true
|
81 | };
|
82 |
|
83 | // use https?
|
84 | if (util.isHttps(proxyTarget))
|
85 | {
|
86 | // parse the target to get host
|
87 | var proxyHost = urlTool.parse(proxyTarget).host;
|
88 |
|
89 | proxyConfig = {
|
90 | "target": proxyTarget,
|
91 | "agent": https.globalAgent,
|
92 | "headers": {
|
93 | "host": proxyHost
|
94 | }
|
95 | };
|
96 | }
|
97 |
|
98 | // create proxy server instance
|
99 | var proxyServer = new httpProxy.createProxyServer(proxyConfig);
|
100 |
|
101 | // error handling
|
102 | proxyServer.on("error", function(err, req, res) {
|
103 | console.log(err);
|
104 | res.writeHead(500, {
|
105 | 'Content-Type': 'text/plain'
|
106 | });
|
107 |
|
108 | res.end('Something went wrong while proxying the request.');
|
109 | });
|
110 |
|
111 | // if we're using auth credentials that are picked up in SSO chain, then we listen for a 401
|
112 | // and if we hear it, we automatically invalidate the SSO chain so that the next request
|
113 | // will continue to work
|
114 | proxyServer.on("proxyRes", function (proxyRes, req, res) {
|
115 |
|
116 | if (req.gitana_user)
|
117 | {
|
118 | var chunks = [];
|
119 | // triggers on data receive
|
120 | proxyRes.on('data', function(chunk) {
|
121 | // add received chunk to chunks array
|
122 | chunks.push(chunk);
|
123 | });
|
124 |
|
125 | proxyRes.on("end", function () {
|
126 |
|
127 | if (proxyRes.statusCode === 401)
|
128 | {
|
129 | var text = "" + Buffer.concat(chunks);
|
130 | if (text && (text.indexOf("invalid_token") > -1) || (text.indexOf("invalid_grant") > -1))
|
131 | {
|
132 | var identifier = req.identity_properties.provider_id + "/" + req.identity_properties.user_identifier;
|
133 |
|
134 | _LOCK([identifier], function(releaseLockFn) {
|
135 |
|
136 | var cleanup = function (full)
|
137 | {
|
138 | delete Gitana.APPS[req.identity_properties.token];
|
139 | delete Gitana.PLATFORM_CACHE[req.identity_properties.token];
|
140 |
|
141 | if (full) {
|
142 | auth.removeUserCacheEntry(identifier);
|
143 | }
|
144 | };
|
145 |
|
146 | // null out the access token
|
147 | // this will force the refresh token to be used to get a new one on the next request
|
148 | req.gitana_user.getDriver().http.refresh(function (err) {
|
149 |
|
150 | if (err) {
|
151 | cleanup(true);
|
152 | req.log("Invalidated auth state for gitana user: " + req.identity_properties.token);
|
153 | releaseLockFn();
|
154 | return;
|
155 | }
|
156 |
|
157 | req.gitana_user.getDriver().reloadAuthInfo(function () {
|
158 | cleanup(true);
|
159 | req.log("Refreshed token for gitana user: " + req.identity_properties.token);
|
160 | releaseLockFn();
|
161 | });
|
162 | });
|
163 | });
|
164 | }
|
165 |
|
166 | }
|
167 | });
|
168 | }
|
169 | });
|
170 |
|
171 | var proxyHandlerServer = http.createServer(function(req, res) {
|
172 |
|
173 | // used to auto-assign the client header for /oauth/token requests
|
174 | oauth2.autoProxy(req);
|
175 |
|
176 | // copy domain host into "x-cloudcms-domainhost"
|
177 | if (req.domainHost)
|
178 | {
|
179 | req.headers["x-cloudcms-domainhost"] = req.domainHost; // this could be "localhost"
|
180 | }
|
181 |
|
182 | // copy virtual host into "x-cloudcms-virtualhost"
|
183 | if (req.virtualHost)
|
184 | {
|
185 | req.headers["x-cloudcms-virtualhost"] = req.virtualHost; // this could be "root.cloudcms.net" or "abc.cloudcms.net"
|
186 | }
|
187 |
|
188 | //console.log("req.domainHost = " + req.domainHost);
|
189 | //console.log("req.virtualHost = " + req.virtualHost);
|
190 |
|
191 | // copy deployment descriptor info
|
192 | if (req.descriptor)
|
193 | {
|
194 | if (req.descriptor.tenant)
|
195 | {
|
196 | if (req.descriptor.tenant.id)
|
197 | {
|
198 | req.headers["x-cloudcms-tenant-id"] = req.descriptor.tenant.id;
|
199 | }
|
200 |
|
201 | if (req.descriptor.tenant.title)
|
202 | {
|
203 | req.headers["x-cloudcms-tenant-title"] = req.descriptor.tenant.title;
|
204 | }
|
205 | }
|
206 |
|
207 | if (req.descriptor.application)
|
208 | {
|
209 | if (req.descriptor.application.id)
|
210 | {
|
211 | req.headers["x-cloudcms-application-id"] = req.descriptor.application.id;
|
212 | }
|
213 |
|
214 | if (req.descriptor.application.title)
|
215 | {
|
216 | req.headers["x-cloudcms-application-title"] = req.descriptor.application.title;
|
217 | }
|
218 | }
|
219 | }
|
220 |
|
221 | // set optional "x-cloudcms-origin" header
|
222 | var cloudcmsOrigin = null;
|
223 | if (req.virtualHost)
|
224 | {
|
225 | cloudcmsOrigin = req.virtualHost;
|
226 | }
|
227 | if (cloudcmsOrigin)
|
228 | {
|
229 | req.headers["x-cloudcms-origin"] = cloudcmsOrigin;
|
230 | }
|
231 |
|
232 | // set x-cloudcms-server-version header
|
233 | req.headers["x-cloudcms-server-version"] = process.env.CLOUDCMS_APPSERVER_PACKAGE_VERSION;
|
234 |
|
235 | // determine the domain to set the "host" header on the proxied call
|
236 | // this is what we pass to the API server
|
237 | var cookieDomain = req.domainHost;
|
238 |
|
239 | // if the incoming request is coming off of a CNAME entry that is maintained elsewhere (and they're just
|
240 | // forwarding the CNAME request to our machine), then we try to detect this...
|
241 | //
|
242 | // our algorithm here is pretty weak but suffices for the moment.
|
243 | // if the req.headers["x-forwarded-host"] first entry is in the req.headers["referer"] then we consider
|
244 | // things to have been CNAME forwarded
|
245 | // and so we write cookies back to the req.headers["x-forwarded-host"] first entry domain
|
246 | /*
|
247 | var xForwardedHost = req.headers["x-forwarded-host"];
|
248 | if (xForwardedHost)
|
249 | {
|
250 | xForwardedHost = xForwardedHost.split(",");
|
251 | if (xForwardedHost.length > 0)
|
252 | {
|
253 | var cnameCandidate = xForwardedHost[0];
|
254 |
|
255 | var referer = req.headers["referer"];
|
256 | if (referer && referer.indexOf("://" + cnameCandidate) > -1)
|
257 | {
|
258 | req.log("Detected CNAME: " + cnameCandidate);
|
259 |
|
260 | proxyHostHeader = cnameCandidate;
|
261 | }
|
262 | }
|
263 | }
|
264 | */
|
265 |
|
266 | // we fall back to using http-node-proxy's xfwd support
|
267 | // thus, spoof header here on request so that "x-forwarded-host" is set properly
|
268 | //req.headers["host"] = proxyHostHeader;
|
269 |
|
270 | // keep alive
|
271 | req.headers["connection"] = "keep-alive";
|
272 |
|
273 | // allow forced cookie domains
|
274 | var forcedCookieDomain = req.headers["cloudcmscookiedomain"];
|
275 | if (!forcedCookieDomain)
|
276 | {
|
277 | if (process.env.CLOUDCMS_FORCE_COOKIE_DOMAIN)
|
278 | {
|
279 | forcedCookieDomain = process.env.CLOUDCMS_FORCE_COOKIE_DOMAIN;
|
280 | }
|
281 | }
|
282 | if (forcedCookieDomain)
|
283 | {
|
284 | cookieDomain = forcedCookieDomain;
|
285 | }
|
286 |
|
287 | var updateSetCookieValue = function(value)
|
288 | {
|
289 | // replace the domain with the host
|
290 | var i = value.toLowerCase().indexOf("domain=");
|
291 | if (i > -1)
|
292 | {
|
293 | var j = value.indexOf(";", i);
|
294 | if (j === -1)
|
295 | {
|
296 | value = value.substring(0, i);
|
297 | }
|
298 | else
|
299 | {
|
300 | value = value.substring(0, i) + value.substring(j);
|
301 | }
|
302 | }
|
303 |
|
304 | // if the originating request isn't secure, strip out "secure" from cookie
|
305 | if (!util.isSecure(req))
|
306 | {
|
307 | var i = value.toLowerCase().indexOf("; secure");
|
308 | if (i > -1)
|
309 | {
|
310 | value = value.substring(0, i);
|
311 | }
|
312 | }
|
313 |
|
314 | // if the original request is secure, ensure cookies have "secure" set
|
315 | if (util.isSecure(req))
|
316 | {
|
317 | var i = value.toLowerCase().indexOf("; secure");
|
318 | var j = value.toLowerCase().indexOf(";secure");
|
319 | if (i === -1 && j === -1)
|
320 | {
|
321 | value += ";secure";
|
322 | }
|
323 | }
|
324 |
|
325 | return value;
|
326 | };
|
327 |
|
328 | // handles the setting of response headers
|
329 | // we filter off stuff we do not care about
|
330 | // we ensure proper domain on set-cookie (TODO: is this needed anymore?)
|
331 | var _setHeader = res.setHeader;
|
332 | res.setHeader = function(key, value)
|
333 | {
|
334 | var _key = key.toLowerCase();
|
335 |
|
336 | if (_key.indexOf("access-control-") === 0)
|
337 | {
|
338 | // skip any access control headers
|
339 | }
|
340 | else
|
341 | {
|
342 | if (_key === "set-cookie")
|
343 | {
|
344 | for (var x in value)
|
345 | {
|
346 | value[x] = updateSetCookieValue(value[x]);
|
347 | }
|
348 | }
|
349 |
|
350 | var existing = this.getHeader(key);
|
351 | if (!existing)
|
352 | {
|
353 | _setHeader.call(this, key, value);
|
354 | }
|
355 | }
|
356 | };
|
357 |
|
358 | // if the incoming request didn't have an "Authorization" header
|
359 | // and we have a logged in Gitana User via Auth, then set authorization header to Bearer Access Token
|
360 | if (!req.headers["authorization"])
|
361 | {
|
362 | if (req.gitana_user)
|
363 | {
|
364 | req.headers["authorization"] = "Bearer " + req.gitana_user.getDriver().http.accessToken();
|
365 | }
|
366 | else if (req.gitana_proxy_access_token)
|
367 | {
|
368 | req.headers["authorization"] = "Bearer " + req.gitana_proxy_access_token;
|
369 | }
|
370 | }
|
371 |
|
372 | if (pathPrefix) {
|
373 | req.url = path.join(pathPrefix, req.url);
|
374 | }
|
375 |
|
376 | proxyServer.web(req, res);
|
377 | });
|
378 |
|
379 | return proxyHandlerServer.listeners('request')[0];
|
380 | };
|