import express = require("express"); import http = require("http"); import https = require('https'); import path = require("path"); import fs = require("fs"); import util = require("util"); import url = require("url"); import zlib = require("zlib"); var cookieParser = require('cookie-parser') var colors = require("colors"); var httpProxy = require("http-proxy"); import iwsc = require("./vorlon.IWebServerComponent"); import vorloncontext = require("../config/vorlon.servercontext"); export module VORLON { export class HttpProxy implements iwsc.VORLON.IWebServerComponent { private _proxy = null; private _fetchproxy = null; private _server = null; private _proxyCookieName = "vorlonProxyTarget"; private _proxySessionCookieName = "vorlonProxySession"; private _vorlonScript = "vorlon.max.js"; private baseURLConfig: vorloncontext.VORLON.IBaseURLConfig; private httpConfig: vorloncontext.VORLON.IHttpConfig; private _passport = require("passport"); private _startProxyOnly =false; private _log: vorloncontext.VORLON.ILogger; constructor(context : vorloncontext.VORLON.IVorlonServerContext, startProxyOnly:boolean=false) { this._startProxyOnly=startProxyOnly; this.baseURLConfig = context.baseURLConfig; this.httpConfig = context.httpConfig; this._log = context.logger; this._proxy = httpProxy.createProxyServer({}); this._fetchproxy = httpProxy.createProxyServer({}); } private insertVorlonScript(str: string, uri, _script: string, vorlonsessionid: string) { var position = str.indexOf(" 0) { var closing = str.indexOf(">", position) + 1; this._log.debug("PROXY Injert vorlon script in website with SESSIONID " + vorlonsessionid); var beforehead = str.substr(0, closing); var afterhead = str.substr(closing); str = beforehead + " " + _script + afterhead; } return str; } public start(): void { if(this._startProxyOnly){ this.addRoutes(express(),this._passport); } } public addRoutes(app: express.Express, passport: any): void { if(!this._startProxyOnly){ app.get(this.baseURLConfig.baseURL + "/httpproxy/fetch", this.fetchFile()); app.get(this.baseURLConfig.baseURL + "/browserspecificcontent", this.browserSpecificContent()); if (!this.httpConfig.enableWebproxy) { //the proxy is disabled, look at config.json to enable webproxy return; } app.get(this.baseURLConfig.baseURL + "/httpproxy", this.home()); app.get(this.baseURLConfig.baseURL + "/httpproxy/inject", this.inject()); } this.startProxyServer(); } public startProxyServer() { this._server = express(); this._server.set('host', this.httpConfig.proxyHost); if(this.httpConfig.proxyEnvPort) this._server.set('port', process.env.PORT); else this._server.set('port', this.httpConfig.proxyPort); this._server.use(cookieParser()); this._server.use(this.baseURLConfig.baseProxyURL + "/vorlonproxy/root.html", this.proxyForTarget()); this._server.use(this.baseURLConfig.baseProxyURL + "/vorlonproxy/*", this.proxyForRelativePath()); this._server.use(this.baseURLConfig.baseProxyURL + "/", this.proxyForRootDomain()); // http.createServer(this._server).listen(this.httpConfig.proxyPort, () => { // console.log("Vorlon.js proxy started on port " + this.httpConfig.proxyPort); // }); if (this.httpConfig.useSSL) { https.createServer(this.httpConfig.options, this._server).listen( this._server.get('port'), this._server.get('host'), undefined, () => { this._log.info('Vorlon.js PROXY with SSL listening at ' + this._server.get('host') + ':' + this._server.get('port')); }); } else { http.createServer(this._server).listen( this._server.get('port'), this._server.get('host'), undefined, () => { this._log.info('Vorlon.js PROXY listening at ' + this._server.get('host') + ':' + this._server.get('port')); }); } this._proxy.on("error", this.proxyError.bind(this)); this._proxy.on("proxyRes", this.proxyResult.bind(this)); this._proxy.on("proxyReq", this.proxyRequest.bind(this)); this._fetchproxy.on("proxyReq", this.proxyFetchRequest.bind(this)); this._fetchproxy.on("proxyRes", this.proxyFetchResult.bind(this)); } private vorlonClientFileUrl() { var scriptUrl = "http://localhost:" + this.httpConfig.port + "/" + this._vorlonScript; if (this.httpConfig.vorlonServerURL) { scriptUrl = this.httpConfig.vorlonServerURL + "/" + this._vorlonScript; } return scriptUrl; } private browserSpecificContent() { return (req: express.Request, res: express.Response) => { var userAgent = req.headers["user-agent"]; var reply = (browsername) => { res.write('Vorlon.js - Test page
This is content for ' + browsername + '
' + userAgent + '
'); res.end(); } if (userAgent == "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36") { reply("Google Chrome") } else if (userAgent == "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240") { reply("Microsoft Edge") } else if (userAgent == "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko") { reply("IE 11") } else { reply("Others") } } } private fetchFile() { return (req: express.Request, res: express.Response) => { var targetProxyUrl = req.query.fetchurl; this._log.debug("FETCH DOCUMENT " + targetProxyUrl); var opt = { target: targetProxyUrl, changeOrigin: true }; res.setHeader("Content-Type", "text/plain"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range'); this._fetchproxy.web(req, res, opt); }; } private proxyFetchRequest(proxyReq, req: express.Request, res: express.Response, opt) { var e = proxyReq; proxyReq.path = req.query.fetchurl; if (req.query.fetchuseragent) { proxyReq._headers["user-agent"] = req.query.fetchuseragent; this._log.debug("FETCH ISSUING UA REQUEST TO " + proxyReq.path); } else { this._log.debug("FETCH ISSUING REQUEST TO " + proxyReq.path); } } private proxyFetchResult(proxyRes, req: express.Request, res: express.Response) { var writeHead = res.writeHead; var encoding = proxyRes.headers["content-encoding"] || "none"; res.writeHead = function() { res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); res.header('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range, X-VorlonProxyEncoding'); res.header('Expires', '-1'); res.header('Pragma', 'no-cache'); var encoding = proxyRes.headers["content-encoding"] || "none"; res.header('X-VorlonProxyEncoding', encoding); writeHead.apply(res, arguments); }; } //Routes private proxyForRelativePath() { return (req: express.Request, res: express.Response) => { //disable accept-encoding //req.headers["accept-encoding"] = ""; res.setHeader("Content-Type", "text/plain"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range'); var cookieUrl = req.cookies[this._proxyCookieName]; var targetProxyUrl = (req).baseUrl.substr("/vorlonproxy/".length); // var idx = targetProxyUrl.lastIndexOf('/'); // if (idx > -1){ // targetProxyUrl = targetProxyUrl.substr(0, idx+1); // }else{ // targetProxyUrl = ""; // } if (cookieUrl) { var uri = url.parse(cookieUrl); var target = uri.href; if (targetProxyUrl) { if (target[target.length - 1] != '/') target = target + '/'; target = target + targetProxyUrl; } this._log.debug("PROXY RELATIVE REQUEST from target " + target + " for " + (req).baseUrl); var opt = { target: target, changeOrigin: true }; if (target.indexOf("https:") === 0) { opt.secure = true; } this._proxy.web(req, res, opt); } else { this._log.warn("PROXY RELATIVE REQUEST but no target for " + (req).baseUrl); } }; } private proxyForTarget() { return (req: express.Request, res: express.Response) => { //disable accept-encoding //req.headers["accept-encoding"] = ""; res.setHeader("Content-Type", "text/plain"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range'); var targetProxyUrl = req.query.vorlonproxytarget; if (!targetProxyUrl) { targetProxyUrl = req.cookies[this._proxyCookieName]; } if (targetProxyUrl) { this._log.info("PROXY REQUEST from target " + targetProxyUrl + " for " + (req).baseUrl); var opt = { target: targetProxyUrl, changeOrigin: true }; if (targetProxyUrl.indexOf("https:") === 0) { opt.secure = true; } this._proxy.web(req, res, opt); } else { this._log.warn("PROXY REQUEST but no target" + " for " + (req).baseUrl); } }; } private proxyForRootDomain() { return (req: express.Request, res: express.Response) => { //disable accept-encoding //req.headers["accept-encoding"] = ""; res.setHeader("Content-Type", "text/plain"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range'); var cookieUrl = req.cookies[this._proxyCookieName]; if (cookieUrl) { var uri = url.parse(cookieUrl); var target = uri.protocol + "//" + uri.hostname; this._log.debug("PROXY REQUEST for root http domain " + target) var opt = { target: target, changeOrigin: true }; if (target.indexOf("https:") === 0) { opt.secure = true; } this._proxy.web(req, res, opt); } else { this._log.warn("PROXY REQUEST from root but no cookie..."); } }; } private home() { return (req: express.Request, res: express.Response) => { res.render('httpproxy', { baseURL: this.baseURLConfig.baseURL }); }; } private inject() { return (req: express.Request, res: express.Response) => { var uri = url.parse(req.query.url); //res.cookie(this._proxyCookieName, uri.protocol + "//" + uri.hostname); this._log.debug("PROXY request for " + uri.hostname + " to port " + this.httpConfig.proxyPort); var rootUrl = "http://localhost:" + this.httpConfig.proxyPort; if (this.httpConfig.vorlonProxyURL) { rootUrl = this.httpConfig.vorlonProxyURL; } var sessionid = this.vorlonSessionIdFor(uri.protocol + "//" + uri.hostname); res.end(JSON.stringify({ url : rootUrl + "/vorlonproxy/root.html?vorlonproxytarget=" + encodeURIComponent(req.query.url)+"&vorlonsessionid=" + sessionid, session : sessionid })); }; } private vorlonSessionIdFor(targeturl, req?) { if (req && req.query.vorlonsessionid) { return req.query.vorlonsessionid; }else if (req && req.cookies[this._proxySessionCookieName]){ return req.cookies[this._proxySessionCookieName]; } var uri = url.parse(targeturl); var pat = /^(https?:\/\/)?(?:www\.)?([^\/]+)/; var match = uri.href.match(pat); var vorlonsessionid = match[2].replace(".",""); return vorlonsessionid; } //Events HttpProxy private proxyError(error, req: express.Request, res: express.Response) { var json; this._log.debug("proxy error", error); if (!res.headersSent) { res.writeHead(500, { "content-type": "application/json" }); } json = { error: "proxy_error", reason: error.message }; res.end(JSON.stringify(json)); } private proxyRequest(proxyReq, req: express.Request, res: express.Response, opt) { var e = proxyReq; if (proxyReq.path[proxyReq.path.length - 1] == "/") { proxyReq.path = proxyReq.path.substr(0, proxyReq.path.length - 1); } this._log.debug("PROXY ISSUING REQUEST TO " + proxyReq.path); } private proxyResult(proxyRes, req: express.Request, res: express.Response) { var _proxy = this; var chunks, end = res.end, writeHead = res.writeHead, write = res.write; var targetProxyUrl = req.query.vorlonproxytarget; if (!targetProxyUrl) { targetProxyUrl = req.cookies[this._proxyCookieName]; } var cspHeader = proxyRes.headers["content-security-policy"]; //TODO : manage content-security-policy header for script, ... if (proxyRes.statusCode >= 300) { this._log.warn("PROXY received status " + proxyRes.statusCode + " " + proxyRes.statusMessage); this._log.warn(proxyRes.req._header); } if (req.query.vorlonproxytarget && proxyRes.statusCode >= 300 && proxyRes.statusCode < 400) { return this.proxyResultForRedirection(targetProxyUrl, proxyRes, req, res); } this._log.debug("PROXY content type " + proxyRes.headers["content-type"]); if (targetProxyUrl && proxyRes.headers && proxyRes.headers["content-type"] && proxyRes.headers["content-type"].match("text/html")) { return this.proxyResultForPageContent(targetProxyUrl, proxyRes, req, res); } else { var encoding = proxyRes.headers["content-encoding"]; res.writeHead = function() { res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); res.header('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range'); res.header('Expires', '-1'); res.header('Pragma', 'no-cache'); res.header("content-security-policy", ""); res.header("content-security-policy", ""); res.header('X-VorlonProxyEncoding', encoding || "none"); writeHead.apply(res, arguments); }; } } private proxyResultForRedirection(targetProxyUrl: string, proxyRes, req: express.Request, res: express.Response) { var _proxy = this; var chunks, end = res.end, writeHead = res.writeHead, write = res.write; res.writeHead = function() { res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); res.header('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range'); res.header('Expires', '-1'); res.header('Pragma', 'no-cache'); res.header("content-security-policy", ""); var location = (res)._headers["location"]; location = location.substr(0, location.indexOf("?vorlonproxytarget=")); var vorlonsessionid = _proxy.vorlonSessionIdFor(targetProxyUrl, req); res.header("location", "?vorlonproxytarget=" + encodeURIComponent(location) + "&vorlonsessionid=" + encodeURIComponent(vorlonsessionid)); writeHead.apply(res, arguments); }; return; } private proxyResultForPageContent(targetProxyUrl: string, proxyRes, req: express.Request, res: express.Response) { var _proxy = this; var chunks, end = res.end, writeHead = res.writeHead, write = res.write; var encoding = proxyRes.headers["content-encoding"]; var uri = url.parse(targetProxyUrl); var vorlonsessionid = _proxy.vorlonSessionIdFor(targetProxyUrl, req); //var scriptUrl = "http://localhost:" + this.httpConfig.port + "/" + this._vorlonScript + "/" + vorlonsessionid; var _script = "" if (encoding == "gzip" || encoding == "deflate") { this._log.debug("PROXY content is encoded to " + encoding); var uncompress = (zlib).Gunzip(); if (encoding == "deflate") uncompress = (zlib).Inflate(); res.writeHead = function() { if (proxyRes.headers && proxyRes.headers["content-length"]) { res.setHeader("content-length", parseInt(proxyRes.headers["content-length"], 10) + _script.length); } res.setHeader("transfer-encoding", ""); res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); res.header('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range'); res.header('Expires', '-1'); res.header("content-security-policy", ""); res.header('Pragma', 'no-cache'); res.header('Content-Encoding', ''); //res.header('Content-Type', 'text/html; charset=utf-8'); res.removeHeader('Content-Encoding'); res.removeHeader('Content-Length'); res.header('X-VorlonProxyEncoding', encoding || "none"); //we must set cookie only if url was requested through Vorlon if (req.query.vorlonproxytarget) { _proxy._log.debug("set cookie " + req.query.vorlonproxytarget); res.cookie(_proxy._proxyCookieName, req.query.vorlonproxytarget); res.cookie(_proxy._proxySessionCookieName, vorlonsessionid); } writeHead.apply(res, arguments); }; (res).write = (data) => { uncompress.write(data); }; uncompress.on('data', (data) => { if (chunks) { chunks += data; } else { chunks = data; } return chunks; }); uncompress.on('end', (data) => { if (chunks && chunks.toString) { var contentstring = chunks.toString(); var tmp = _proxy.insertVorlonScript(contentstring, uri, _script, vorlonsessionid); write.apply(res, [tmp]); } end.apply(res); }); res.end = function(): void { uncompress.end(arguments[0]); }; } else { res.writeHead = function() { if (proxyRes.headers && proxyRes.headers["content-length"]) { res.setHeader("content-length", parseInt(proxyRes.headers["content-length"], 10) + _script.length); } res.setHeader("transfer-encoding", ""); res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); res.header('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range'); res.header('Expires', '-1'); res.header('Pragma', 'no-cache'); res.header("content-security-policy", ""); res.header('X-VorlonProxyEncoding', encoding || "none"); //we must set cookie only if url was requested through Vorlon if (req.query.vorlonproxytarget) { _proxy._log.debug("set cookie " + req.query.vorlonproxytarget); res.cookie(_proxy._proxyCookieName, req.query.vorlonproxytarget); } writeHead.apply(res, arguments); }; res.write = (data) => { if (chunks) { chunks += data; } else { chunks = data; } return chunks; }; res.end = function() { if (chunks && chunks.toString) { var tmp = _proxy.insertVorlonScript(chunks.toString(), uri, _script, vorlonsessionid); write.apply(res, [tmp]); } end.apply(res, arguments); }; } } } }