Notebooks >> Scripts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

277 lines
8.4 KiB

/**
* Copyright (c) 2017 ZipRecruiter
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* */
const process = require('process');
const CDP = require('chrome-remote-interface');
const debug = require('debug')('chromedriver_proxy:chrome_agent');
const gpath = require('path');
const ScreenRecorder = require(gpath.join(__dirname, 'screen_recorder'));
const RealTimeScreenRecorder = require(gpath.join(__dirname, 'realtime_screen_recorder'));
class ChromeAgent {
constructor(options) {
Object.keys(options).forEach((i) => {
this[i] = options[i];
});
this.screenRecorderOptions = this.screenRecorder || {};
delete this.screenRecorder;
this.scriptIds = new Set();
this.routes = [
['POST', 'screencast/start', 'startScreencast'],
['POST', 'screencast/subtitle/add', 'addSubtitle'],
['POST', 'screencast/stop', 'stopScreencast'],
['GET', 'screencast/path', 'getScreencastPath'],
['GET', 'screencast/s3', 'getScreencastS3'],
['POST', 'headers', 'setHeaders'],
['POST', 'useragent', 'setUserAgent'],
['POST', 'script', 'addScript'],
['DELETE', 'scripts', 'removeAllScripts'],
['POST', 'storage', 'setClearStorage'],
['POST', 'navigate', 'navigate'],
];
}
async setClearStorage(options) {
const self = this;
self.chromeStorage = options.values;
return {};
}
async getConn() {
const self = this;
if (self.client) {
return self.client;
}
return new Promise((resolve, reject) => {
CDP({ host: '127.0.0.1', port: self.port, target: self.target }, (client) => {
self.client = client;
resolve(client);
}).on('error', (err) => {
debug('unable to connect to debugger');
reject(err);
});
});
}
async navigate(options) {
const self = this;
const { Page } = await self.getConn();
await Page.enable();
debug(`navigate ${JSON.stringify(options.values)}`);
const result = await Page.navigate(options.values);
if (typeof result.errorText !== 'undefined') {
debug(`navigation error ${result.errorText}`);
return Promise.reject(new Error(result.errorText));
}
return result;
}
async addScript(options) {
const self = this;
const opts = options;
const { Page } = await self.getConn();
await Page.enable();
// for backwards compatibility
if ('scriptSource' in opts) {
opts.source = opts.scriptSource;
delete opts.scriptSource;
}
debug(`add script ${JSON.stringify(opts)}`);
// BUG returns an empty string for an identifier
const result = await Page.addScriptToEvaluateOnNewDocument(opts);
debug(`added script: ${JSON.stringify(result)}`);
self.scriptIds.add(result.identifier);
return result;
}
async removeAllScripts() {
const self = this;
const { Page } = await self.getConn();
await Page.enable();
debug('remove all scripts start');
const p = [];
self.scriptIds.forEach((id) => {
p.push(Page.removeScriptToEvaluateOnLoad({ identifier: id }));
});
await Promise.all(p);
self.scriptIds = new Set();
debug('remove all scripts done');
}
async setHeaders(options) {
const self = this;
const { Network } = await self.getConn();
debug(`set extra headers ${JSON.stringify(options)}`);
return Network.setExtraHTTPHeaders({ headers: options.headers });
}
async setUserAgent(options) {
const self = this;
const { Network } = await self.getConn();
debug(`set user agent ${JSON.stringify(options)}`);
return Network.setUserAgentOverride({ userAgent: options.userAgent });
}
async getScreencastPath() {
const self = this;
const result = await self.screenRecorderVideo;
debug(`local screencast path: ${result}`);
return { path: result };
}
async getScreencastS3() {
const self = this;
const result = await self.screenRecorderVideo.then(() => self.sr.s3UploadResult);
debug(`s3 upload: ${JSON.stringify(result)}`);
return result;
}
async startScreencast(options) {
const self = this;
debug('start screencast');
const client = await self.getConn();
const defaultOptions = self.screenRecorderOptions;
defaultOptions.client = client;
if (options.format === 'm3u8') {
self.sr = new RealTimeScreenRecorder(defaultOptions);
} else {
self.sr = new ScreenRecorder(defaultOptions);
}
const { Page } = client;
await Page.enable();
debug(`screen recorder options ${JSON.stringify(options)}`);
await self.sr.start(options);
return self.sr.expectedResult();
}
async stopScreencast(options) {
const self = this;
debug('stop screencast');
if (!self.sr) {
throw new Error('must call startScreencast before calling stopScreencast');
}
self.screenRecorderVideo = self.sr.stop(options);
return self.sr.expectedResult();
}
async addSubtitle(options) {
const self = this;
if (!self.sr) {
throw new Error('must call startScreencast before calling addSubtitle');
}
self.sr.addSubtitle(options);
return {};
}
async cleanBrowser() {
const self = this;
debug('start browser cleanup');
try {
const {
Network, Target, Page, Storage,
} = await self.getConn();
const closeTarget = function closeTarget(target) {
return Target.activateTarget(target).then(() => Target.closeTarget(target));
};
const clearData = [Network.clearBrowserCookies()];
self.chromeStorage.forEach((e) => {
clearData.push(Storage.clearDataForOrigin(e));
});
await Promise.all(clearData);
await Page.navigate({ url: 'about:blank' });
const result = await Target.getTargets();
let p = Promise.resolve();
result.targetInfos.forEach((target) => {
// close everything but the main target
if (target.targetId !== self.target) {
p = p.then(() => closeTarget(target));
}
});
await p;
} catch (err) {
debug(`Browser cleanup error: ${err}`);
throw err;
}
}
async stop() {
const self = this;
debug('stop agent');
if (typeof self.screenRecorderVideo !== 'undefined') {
await self.screenRecorderVideo.then(() => self.sr.s3UploadResult);
}
if (self.client) {
await self.removeAllScripts();
await self.cleanBrowser();
self.client.close();
}
}
handle(blob) {
const self = this;
try {
if (blob.action === 'stop') {
return self.stop().then(() => {
debug('stop agent success');
process.exit(0);
}).catch((err) => {
debug(`error when stopping agent: ${err}`);
process.exit(1);
});
}
const { value } = blob;
const { path, sessionId, httpMethod } = value;
const body = value.body ? JSON.parse(blob.value.body) : {};
body.sessionId = sessionId;
let action = null;
const route = Object.values(self.routes).find((r) => {
if (httpMethod === r[0] && path === r[1]) {
return true;
}
return false;
});
action = route && route[2];
if (action === null) {
return Promise.reject(new Error(`unknown path: ${httpMethod} ${path}`));
}
if (typeof (self[action]) !== 'function') {
return Promise.reject(new Error(`undefined method: ${action}`));
}
return self[action](body);
} catch (err) {
return Promise.reject(new Error(`unknown error: ${err}`));
}
}
}
module.exports = ChromeAgent;