Qlik Community

Qlik Sense Integration, Extensions, & APIs

Discussion board where members can learn more about Integration, Extensions and API’s for Qlik Sense.

Announcements
BARC’s The BI Survey 19 makes it official. BI users love Qlik. GET REPORT
Highlighted
suzoodev
New Contributor II

Connect to same session as Puppeteer using enigma.js and ticket authentication

Hi all,

I've created a node server that on request opens a given app using a virtual proxy that uses the ticket method of authentication. The app opens fine on the server when I view without it being headless, but it disconnects the app I have open that is not via the ticketed virtual proxy. The error I get here says "Connection to the  Qlik Sense Engine failed for unspecified reasons. This is the first issue if anyone can help with this?

The second issue doesn't cause an error but I then go on to use the QPS API to get the session ID for the app that has opened in Puppeteer and create a WebSocket connection using enigma to the same session. I'm passing the local certificates for the server and also adding headers for the ticket information and session ID. This connects successfully, however when I make selections using enigma they return a promise saying true but the app opened by Puppeteer doesn't reflect the selection. No extra sessions are created against the proxy so I assume they are using the same session. Is there any reason that these calls to the engine via enigma wouldn't be reflected in a session opened in the browser?

Any help or pointers to information anyone could provide are much appreciated! Have read quite a lot of info but the ticketing authentication seems to be slim, however I'm not sure that's the issue, also can't find much on people using Puppeteer with Sense.

const util = require('util')
const express = require('express')
const router = express.Router()
const puppeteer = require('puppeteer')
const fs = require('fs')
const PDFDocument = require('pdfkit')
const enigma = require('enigma.js');
const WebSocket = require('ws');
const path = require('path');
const axios = require('axios');
const https = require('https');

const schema = require('enigma.js/schemas/12.20.0.json');
const SenseUtilities = require('enigma.js/sense-utilities');

const certificatesPath = 'C:/ProgramData/Qlik/Sense/Repository/Exported Certificates/.Local Certificates/';

const readCert = filename => fs.readFileSync(path.resolve(__dirname, certificatesPath, filename));
const ca = readCert('root.pem')
const key = readCert('client_key.pem')
const cert = readCert('client.pem')

https.globalAgent.options.ca = ca
https.globalAgent.options.key = key
https.globalAgent.options.cert = cert

function getXrfkey() {
   return Array.apply(0, Array(16)).map(function() {
    return (function(charset){
        return charset.charAt(Math.floor(Math.random() * charset.length))
    }('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'));
  }).join('')
}

async function getQlikTicket(userDirectory, userId, proxy) {
  
  let xrfKey = getXrfkey()

  let url = `https://servername.domain:4243/qps${proxy ? '/' + proxy : ''}/ticket?Xrfkey=${xrfKey}`
  let headers = {
    'Content-Type': 'application/json',
    'X-Qlik-Xrfkey': xrfKey
  }

  let ticket = await axios.post(url, {userId, userDirectory}, {headers})
  return ticket
}

async function getSessions(userDirectory, userId, proxy) {
  
  let xrfKey = getXrfkey()

  let url = `https://servername.domain:4243/qps${proxy ? '/' + proxy : ''}/user/${userDirectory}/${userId}?Xrfkey=${xrfKey}`
  let headers = {
    'X-Qlik-Xrfkey': xrfKey
  }

  let sessions = await axios.get(url, {headers})
  return sessions
}

async function deleteSession(sessionId, proxy) {
  
  let xrfKey = getXrfkey()

  let url = `https://servername.domain:4243/qps${proxy ? '/' + proxy : ''}/session/${sessionId}?Xrfkey=${xrfKey}`
  let headers = {
    'X-Qlik-Xrfkey': xrfKey
  }

  let deletedSession = await axios.delete(url, {headers})
  return deletedSession
}

router.get('/generate', (req, res, next) => {
  
  //full path to app that needs to be opened is given via reportURL parameter in the request
  const reportURL = decodeURI(req.query.reportURL)

  const reportInfo = reportURL.split('/')
  const reportApp = reportInfo[5]
  const reportSheet = reportInfo[7]

  const engineHost = 'servername.domain';
  const enginePort = 4747;
  const appId = 'engineData';
  const userDirectory = 'directory';
  const userId = 'name';
  const proxy = 'proxyname';

  puppeteer.launch({headless: false, args: ['--shm-size=1gb']}).then(async browser => {
    
    const page = await browser.newPage()
    await page.setViewport({width: 1684, height: 1190})

    const qlikTicket = await getQlikTicket(userDirectory, userId, proxy)

    await page.goto(reportURL.replace('/sense', `${proxy ? '/' + proxy : ''}/sense`) + "?&qlikTicket=" + qlikTicket.data.Ticket, {waitUntil: ['load']})
    
    // wait for specific element to know when the page has finished loading
    await page.waitFor('.lui-textarea.comment-text') 

    // use qps API to get the session details
    const sessions = await getSessions(userDirectory, userId, proxy)
    const sessionId = sessions.data[0].SessionId

    //create session connection using qlik ticket from puppeteer browser session and enigma.js
    const session = enigma.create({
      schema,
      url: `wss://${engineHost}:${enginePort}/hdr/app/${appId}`,
      createSocket: url => new WebSocket(url, {
        ca: [readCert('root.pem')],
        key: readCert('client_key.pem'),
        cert: readCert('client.pem'),
        headers: {
          'X-Qlik-User': `UserDirectory=${encodeURIComponent(userDirectory)}; UserId=${encodeURIComponent(userId)}`,
          'X-Qlik-Security': `SecureRequest=true;Context=AppAccess;TicketAttribute1=${qlikTicket.data.Ticket}`,
          'X-Qlik-ProxySession': sessionId
        },
      })
    });

    await session.open().then(global => {
      return global.openDoc(reportApp)
    }).then(doc => {
      return doc.getField('FieldName') //enter name of the field here
    }).then(field => {
      return field.select('FieldValue') //enter the value in the field to select here
    }).then(selection => {
      session.close()
    }).catch(err => console.log(err))

    //screenshot the page
    await page.screenshot({path: 'D:/QS_Applications/report-images/report.png', fullPage: true})

    //Close the browser
    await browser.close()

    //Delete the Qlik session for the proxy so only ever 1 session active in the proxy
    await deleteSession(sessionId, proxy)
    
  }).then(data => {

    var doc = new PDFDocument({
      size: "A4",
      layout: 'landscape'
    })
    
    doc.pipe(res)

    try {
      doc.image('D:/QS_Applications/report-images/report.png', 0, 0, {scale: 0.5})
    } catch(err) {
      console.log(err)
    }

    doc.end()

    res.setHeader('Content-Type', 'application/pdf')
    res.setHeader('Content-Disposition', 'inline; filename=report.pdf');
  }).catch(err => {
    console.log(err)
  })

})

module.exports = router;