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
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;
|
|
|