270 lines
7.7 KiB
JavaScript
270 lines
7.7 KiB
JavaScript
const express = require('express')
|
|
const router = express.Router()
|
|
|
|
const jsonwebtoken = require("jsonwebtoken");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const cookie = require("cookie");
|
|
const cookieParser = require("cookie-parser");
|
|
const axios = require("axios");
|
|
const { v4: uuidv4 } = require("uuid");
|
|
const ws = require("ws");
|
|
const WebSocketServer = ws.WebSocketServer;
|
|
const WebSocket = ws.WebSocket;
|
|
const passport = require('../passport-okta');
|
|
|
|
|
|
// This is the frontend application uri used for responding to requests.
|
|
//const frontendUri = "https://outstanding-desert-gatsby.glitch.me";
|
|
|
|
|
|
const privKey = fs.readFileSync(path.resolve(__dirname, '..', 'certs', 'privateKey.pem'));
|
|
const TENANT_DOMAIN = process.env["TENANT_DOMAIN"] || "innovation.us.qlikcloud.com";
|
|
const JWT_KEYID = process.env["JWT_KEYID"] || "8889be0d-6cf0-44eb-9fef-24bbc83712c5";
|
|
const JWT_ISSUER = process.env["JWT_ISSUER"] || TENANT_DOMAIN;
|
|
const JWT_USER_GROUPS = ["AnonJWTGroup"];
|
|
|
|
console.log("QSProxy# - TENANT_DOMAIN", TENANT_DOMAIN);
|
|
|
|
const auth = {
|
|
generateToken: function (user) {
|
|
// kid and issuer have to match with the IDP config and the audience has to be qlik.api/jwt-login-session
|
|
|
|
const signingOptions = {
|
|
keyid: JWT_KEYID,
|
|
algorithm: "RS256",
|
|
issuer: JWT_ISSUER,
|
|
expiresIn: "60m",
|
|
notBefore: "0s",
|
|
audience: "qlik.api/login/jwt-session",
|
|
};
|
|
|
|
// These are the claims that will be accepted and mapped anything else will be ignored. sub, subtype and name are mandatory.
|
|
const uuid = uuidv4();
|
|
userEmail = user.mail;
|
|
const payload = {
|
|
jti: uuid,
|
|
sub: `az/${user.sub}`,
|
|
subType: "user",
|
|
name: user.displayName,
|
|
email: userEmail,
|
|
email_verified: true,
|
|
groups: JWT_USER_GROUPS,
|
|
};
|
|
|
|
const token = jsonwebtoken.sign(payload, privKey, signingOptions);
|
|
return token;
|
|
},
|
|
getQlikSessionCookie: async function (token) {
|
|
|
|
try {
|
|
const resp = await axios(`https://${TENANT_DOMAIN}/login/jwt-session`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (resp.status === 200) {
|
|
console.log("QSProxy# - login/jwt-session", resp.headers['set-cookie'])
|
|
return resp.headers['set-cookie']
|
|
.map((e) => {
|
|
return e.split(";")[0];
|
|
})
|
|
.join(";");
|
|
}
|
|
return "";
|
|
} catch (e) {
|
|
console.log("QSProxy# - ERRRR", e);
|
|
return "";
|
|
}
|
|
|
|
},
|
|
};
|
|
|
|
const getStoreData = function (sidParsed) {
|
|
const store = passport.sessionStore;
|
|
return new Promise((resolve) => {
|
|
store.get(sidParsed, (err, session) => {
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
return resolve(session);
|
|
});
|
|
});
|
|
};
|
|
|
|
router.get("/*.js", async (req, res) => {
|
|
setCors(res);
|
|
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
|
|
res.end();
|
|
});
|
|
|
|
// fetch resource from qlik using a redirect instead of proxy
|
|
// This endpoint is necessary when your web application uses the capability API.
|
|
router.get("/resources/*", async (req, res) => {
|
|
setCors(res);
|
|
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
|
|
res.end();
|
|
});
|
|
|
|
router.get('/assets/*', async (req, res) => {
|
|
setCors(res);
|
|
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
|
|
res.end();
|
|
});
|
|
|
|
// Issues the necessary pre-flight request to make sure the browser
|
|
// knows how to work with the web application.
|
|
router.options("/*", async (req, res) => {
|
|
setCors(res);
|
|
res.status(200).end();
|
|
});
|
|
|
|
function setCors(res) {
|
|
//res.set("Access-Control-Allow-Origin", "http://localhost:3000");
|
|
//res.set("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
//res.set("Access-Control-Allow-Headers", "Content-Type, x-proxy-session-id");
|
|
//res.set("Access-Control-Allow-Credentials", "true");
|
|
}
|
|
|
|
async function newSession(req){
|
|
console.log("QSProxy# - New cookie request for user", req.user.mail);
|
|
const jwtToken = auth.generateToken(req.user);
|
|
const qlikSession = await auth.getQlikSessionCookie(jwtToken);
|
|
return encodeURIComponent(qlikSession);
|
|
}
|
|
|
|
// Intercepts a request to one of Qlik's REST APIs and proxies the request to
|
|
// Qlik Cloud.
|
|
router.get("/api/v1/*", passport.ensureAuthenticated, async (req, res) => {
|
|
|
|
setCors(res);
|
|
|
|
var session = req.session;
|
|
|
|
var reqHeaders = {};
|
|
try {
|
|
if (session && session.id && session.qlikSession) {
|
|
console.log("QSProxy# - COOKIE FROM session", session.qlikSession);
|
|
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
|
|
} else {
|
|
|
|
const newS = await newSession(req);
|
|
session.qlikSession = newS;
|
|
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
|
|
}
|
|
|
|
try {
|
|
console.log("QSProxy# - qlikSession", reqHeaders.cookie);
|
|
const { status, data } = await axios(`https://${TENANT_DOMAIN}${req.path}`, {
|
|
headers: reqHeaders,
|
|
});
|
|
res.status(status).end(data);
|
|
|
|
} catch (e2) {
|
|
console.log("QSProxy# Error: QlikSession expired, requesting a new one.");
|
|
let newS = await newSession(req);
|
|
session.qlikSession = newS;
|
|
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
|
|
let { status, data } = await axios(`https://${TENANT_DOMAIN}${req.path}`, {
|
|
headers: reqHeaders,
|
|
});
|
|
res.status(status).end(data);
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
res.status(500).end("Error obtaining qlik session");
|
|
}
|
|
|
|
});
|
|
|
|
router.get('/qlik-embed-iframe/*', async (req, res) => {
|
|
setCors(res);
|
|
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
|
|
res.end();
|
|
});
|
|
|
|
function init (server) {
|
|
|
|
// Websocket section for intercepting websocket requests from the
|
|
// frontend application. When the front end application communicates
|
|
// communicates with the backend using websockets, this set of
|
|
// functions will be invoked.
|
|
const wss = new WebSocketServer({ server });
|
|
|
|
wss.on("connection", async function connection(ws, req) {
|
|
let isOpened = false;
|
|
// WebSockets do not have access to session information.
|
|
// To get the session you need to parse the 1st-party cookie.
|
|
// This will give you access to the Qlik Cloud cookie in order
|
|
// to proxy requests.
|
|
const cookieString = req.headers.cookie;
|
|
|
|
let qlikCookie = "";
|
|
if (cookieString) {
|
|
const cookieParsed = cookie.parse(cookieString);
|
|
const appCookie = cookieParsed["connect.sid"];
|
|
if (appCookie) {
|
|
const sidParsed = cookieParser.signedCookie(appCookie, "secret");
|
|
var session = await getStoreData(sidParsed);
|
|
|
|
qlikCookie = decodeURIComponent(session.qlikSession);
|
|
}
|
|
}
|
|
|
|
const appId = req.url.match("/app/(.*)\\?")[1];
|
|
|
|
if (!qlikCookie){
|
|
console.log("QSProxy# - Error in Websocket: NO qlikCookie!");
|
|
return;
|
|
}
|
|
|
|
const matchCookie = qlikCookie.match("_csrfToken=(.*);");
|
|
|
|
if (!matchCookie) {
|
|
console.log("QSProxy# - Error in Websocket: cant find _csrfToken= in qlikCookie");
|
|
return;
|
|
}
|
|
|
|
const csrfToken = matchCookie[1];
|
|
|
|
var wsConnUrl = `wss://${TENANT_DOMAIN}/app/${appId}?qlik-csrf-token=${csrfToken}`;
|
|
const qlikWebSocket = new WebSocket(
|
|
wsConnUrl,
|
|
{
|
|
headers: {
|
|
cookie: qlikCookie,
|
|
},
|
|
}
|
|
);
|
|
|
|
qlikWebSocket.on("error", console.error);
|
|
const openPromise = new Promise((resolve) => {
|
|
qlikWebSocket.on("open", function open() {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
ws.on("message", async function message(data) {
|
|
if (!isOpened) {
|
|
await openPromise;
|
|
isOpened = true;
|
|
}
|
|
|
|
qlikWebSocket.send(data.toString());
|
|
});
|
|
|
|
qlikWebSocket.on("message", function message(data) {
|
|
ws.send(data.toString());
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
module.exports.router = router;
|
|
module.exports.init = init;
|
|
|