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.
 
 

265 lines
9.1 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 http = require('http');
const httpProxy = require('http-proxy');
const ChromeDriver = require('./chromedriver.js');
const ChromePool = require('./chrome_pool.js');
const debug = require('debug')('chromedriver_proxy:proxy');
class HttpServer {
constructor(config) {
const c = config || {};
this.port = c.port || 4444;
this.baseUrl = c.baseUrl || null;
if (this.baseUrl !== null && !this.baseUrl.startsWith('/')) {
this.baseUrl = `/${this.baseUrl}`;
}
if (this.baseUrl !== null && this.baseUrl.endsWith('/')) {
this.baseUrl = this.baseUrl.substring(0, this.baseUrl.length - 1);
}
if (this.baseUrl !== null) {
this.endBaseUrl = this.baseUrl.length;
}
}
start(opts, fn) {
const options = opts || {};
const self = this;
const timeout = options.timeout || -1;
const keepAliveTimeout = options.keepAliveTimeout || 0;
options.chromedriver = options.chromedriver || {};
options.chromePool = options.chromePool || {};
options.screenRecorder = options.chromePool.screenRecorder || {};
const tmpDir = options.tmpDir || '/tmp';
options.chromedriver.tmpDir = options.chromedriver.tmpDir || tmpDir;
options.chromePool.tmpDir = options.chromePool.tmpDir || tmpDir;
options.screenRecorder.tmpDir = options.screenRecorder.tmpDir || tmpDir;
self.chromedriver = new ChromeDriver(options.chromedriver);
const chromedriverStart = self.chromedriver.start();
self.chromepool = new ChromePool(options.chromePool);
const bypassChromePool = !self.chromepool.enable;
self.activeSessions = {};
self.screenRecorders = {};
self.httpAgent = http.globalAgent;
self.httpAgent.maxSockets = 10000;
self.httpAgent.keepAlive = true;
self.httpAgent.keepAliveMsecs = 30000;
//
// Create a proxy server with custom application logic
//
const proxyConfig = {
agent: self.httpAgent,
};
if (timeout !== -1) {
proxyConfig.proxyTimeout = timeout;
}
const proxy = httpProxy.createProxyServer(proxyConfig);
proxy.on('error', (err, req, res) => {
res.writeHead(500, {});
const blob = JSON.stringify({
message: err.message,
stack: err.stack,
});
res.end(blob);
debug(`proxy error: ${blob}`);
});
if (!bypassChromePool) {
proxy.on('proxyRes', (proxyRes, req) => {
if (req.method === 'DELETE' && req.url.length === 41) {
const sessionId = req.url.substring(9);
const port = self.activeSessions[sessionId];
self.chromepool.put(port);
delete self.activeSessions[sessionId];
debug(`Delete session: ${sessionId} at port: ${port}`);
}
});
}
const chromedriverEndpoint = `http://127.0.0.1:${self.chromedriver.port}`;
//
// Create your custom server and just call `proxy.web()` to proxy
// a web request to the target passed in the options
// also you can use `proxy.ws()` to proxy a websockets request
//
self.server = http.createServer((req, res) => {
if (timeout !== -1) {
res.setTimeout(timeout, () => {
res.write(JSON.stringify({ value: { error: 'TIMEOUT', message: 'proxy timeout', stacktrace: '' } }));
res.end();
});
}
if (self.baseUrl !== null) {
req.url = req.url.substring(self.endBaseUrl);
}
if (bypassChromePool) {
proxy.web(req, res, { target: chromedriverEndpoint });
return;
}
const path = req.url.slice(42);
if (req.url === '/session' && req.method === 'POST') {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
const caps = JSON.parse(body);
const chromeOptions = caps.desiredCapabilities.chromeOptions || caps.desiredCapabilities['goog:chromeOptions'] || {};
self.chromepool.get({ args: chromeOptions.args })
.then(port => self.chromepool.getAgent({ port }).then(() => port)).then((port) => {
chromeOptions.debuggerAddress = `localhost:${port}`;
if (caps.capabilities !== undefined && caps.capabilities.alwaysMatch !== undefined) {
caps.capabilities.alwaysMatch['goog:chromeOptions'].debuggerAddress = chromeOptions.debuggerAddress;
}
const nbody = JSON.stringify(caps);
debug(`capabilities: ${nbody}`);
req.headers['Content-Length'] = Buffer.byteLength(nbody);
const proxiedReq = http.request({
port: 4445,
method: req.method,
path: req.url,
headers: req.headers,
agent: self.httpAgent,
timeout: 2000,
hostname: 'localhost',
}, (resp) => {
res.writeHead(resp.statusCode, resp.headers);
let sessionBlob = '';
resp.on('data', (chunk) => {
sessionBlob += chunk;
res.write(chunk);
});
resp.on('end', () => {
const sessionInfo = JSON.parse(sessionBlob);
if (sessionInfo.status === 0) {
const { sessionId } = sessionInfo;
self.activeSessions[sessionId] = port;
debug(`Started session: ${sessionId} at port: ${port}`);
} else if (sessionInfo.value) {
const { sessionId } = sessionInfo.value;
self.activeSessions[sessionId] = port;
debug(`Started session: ${sessionId} at port: ${port}`);
} else {
debug(`Error: ${sessionBlob}`);
// ahhhhhhhhhhhh something broke!!! ...
}
res.end();
});
});
proxiedReq.on('error', (err) => {
debug(err);
res.writeHead(500);
res.end();
});
proxiedReq.write(nbody);
proxiedReq.end();
}).catch((err) => {
debug(err);
res.writeHead(500);
res.end(err);
});
});
} else if (path.slice(0, 18) === 'chromedriver-proxy') {
const sessionId = req.url.substring(9, 41);
const body = [];
const port = self.activeSessions[sessionId];
req.on('data', (chunk) => { body.push(chunk); });
req.on('end', () => {
debug(`start proxy request to chrome agent port: ${port} path: ${req.url}`);
self.chromepool.sendToAgent({
action: 'req',
port,
value: {
path: path.slice(19),
sessionId,
httpMethod: req.method,
body: Buffer.concat(body).toString(),
},
}).then((result) => {
res.write(JSON.stringify({
status: 0,
value: result,
}));
res.end();
debug(`end proxy request to chrome agent port: ${port} path: ${req.url}`);
}).catch((err) => {
res.writeHead(500);
res.end();
debug(`error proxy request to chrome agent port: ${port} path: ${req.url} error: ${err}`);
});
});
} else {
// You can define here your custom logic to handle the request
// and then proxy the request.
proxy.web(req, res, { target: chromedriverEndpoint });
}
});
self.server.keepAliveTimeout = keepAliveTimeout;
self.server.listen(self.port, () => {
chromedriverStart.then(() => {
if (fn) {
fn();
}
debug(`started proxy server at port: ${self.port}`);
}).catch((err) => {
console.error(`FATAL UNABLE TO START CHROMEDRIVER: ${err}`);
process.exit(1);
});
});
}
stop(fn) {
const self = this;
self.httpAgent.destroy();
self.server.close(() => {
self.chromedriver.stop().then(() => {
self.chromepool.killAll();
if (fn) {
fn();
}
});
});
}
}
module.exports = HttpServer;