UNPKG

13 kBJavaScriptView Raw
1var http = require("http");
2var https = require("https");
3var path = require("path");
4
5var httpProxy = require("http-proxy");
6
7var auth = require("./auth");
8var util = require("./util");
9
10var oauth2 = require("./oauth2")();
11
12var urlTool = require("url");
13
14var exports = module.exports;
15
16var _LOCK = function(lockIdentifiers, workFunction)
17{
18 process.locks.lock(lockIdentifiers.join("_"), workFunction);
19};
20
21var NAMED_PROXY_HANDLERS_CACHE = require("lru-cache")({
22 max: 200,
23 maxAge: 1000 * 60 * 60 // 60 minutes
24});
25
26var 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
60var 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};