Skip to content

Instantly share code, notes, and snippets.

@airhorns
Created January 8, 2020 20:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save airhorns/d78b5558fb149fa72a46c14d0ad2e837 to your computer and use it in GitHub Desktop.
Save airhorns/d78b5558fb149fa72a46c14d0ad2e837 to your computer and use it in GitHub Desktop.
Browserless-token authentication for lighthouse
import https from 'https';
import WebSocket from 'ws';
import log from 'lighthouse-logger';
import queryString from 'query-string';
import LighthouseError from 'lighthouse/lighthouse-core/lib/lh-error';
import ChromeProtocol from 'lighthouse/lighthouse-core/gather/connections/cri';
const CONNECT_TIMEOUT = 10000;
log.setLevel('info');
// Subclass of lighthouse chrome protocol that supports the ?token= auth setup that browserless uses to auth WS connections.
// Browserless uses this token parameter in the URL to authenticate between it's various users, but the lighthouse npm module
// expects a no-auth-required chrome instance that it can just connect right to using a host and protocol. This uses the 4th
// parameter to the lighthouse function to pass in a custom connection with the little bits overriden to faciliate an
// authenticated connection between a local lighthouse client and a remote browserless'd chrome.
export class BrowserlessFriendlyChromeProtocol extends ChromeProtocol {
constructor(readonly port: string, readonly hostname: string, readonly token: string) {
super(port, hostname);
}
// Override to add the ?token= to the REST API endpoint call
_runJsonCommand(command: string) {
return new Promise((resolve, reject) => {
const request = https.get(
{
hostname: this.hostname,
port: this.port,
path: '/json/' + command + `?token=${this.token}`,
},
response => {
let data = '';
response.on('data', chunk => {
data += chunk;
});
response.on('end', () => {
if (response.statusCode === 200) {
try {
resolve(JSON.parse(data));
return;
} catch (e) {
// In the case of 'close' & 'activate' Chromium returns a string rather than JSON: goo.gl/7v27xD
if (data === 'Target is closing' || data === 'Target activated') {
return resolve({ message: data });
}
return reject(e);
}
}
reject(new Error(`Protocol JSON API error (${command}), status: ${response.statusCode}`));
});
},
);
// This error handler is critical to ensuring Lighthouse exits cleanly even when Chrome crashes.
// See https://github.com/GoogleChrome/lighthouse/pull/8583.
request.on('error', reject);
request.setTimeout(CONNECT_TIMEOUT, () => {
// Reject on error with code specifically indicating timeout in connection setup.
const err = new LighthouseError(LighthouseError.errors.CRI_TIMEOUT);
log.error('CriConnection', err.friendlyMessage);
reject(err);
request.abort();
});
});
}
// Override to add wss:// hack for the returned URL
_connectToSocket(response: any) {
let url = response.webSocketDebuggerUrl;
if (url.startsWith('ws://')) {
url = 'wss://' + url.slice(5);
}
this._pageId = response.id;
return new Promise((resolve, reject) => {
const ws = new WebSocket(url, {
perMessageDeflate: false,
});
ws.on('open', () => {
this._ws = ws;
resolve();
});
ws.on('message', data => this.handleRawMessage(/** @type {string} */ data));
ws.on('close', this.dispose.bind(this));
ws.on('error', reject);
});
}
}
export const lighthouseConnectionFromWSEndpoint = (wsEndpoint: string) => {
const url = new URL(wsEndpoint);
const params = queryString.parse(url.search);
if (params.token) {
return new BrowserlessFriendlyChromeProtocol(url.port, url.hostname, params.token as string);
} else {
return new ChromeProtocol(url.port, url.hostname);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment