WebSocketServer.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. "use strict";
  2. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
  3. if (k2 === undefined) k2 = k;
  4. var desc = Object.getOwnPropertyDescriptor(m, k);
  5. if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
  6. desc = { enumerable: true, get: function() { return m[k]; } };
  7. }
  8. Object.defineProperty(o, k2, desc);
  9. }) : (function(o, m, k, k2) {
  10. if (k2 === undefined) k2 = k;
  11. o[k2] = m[k];
  12. }));
  13. var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
  14. Object.defineProperty(o, "default", { enumerable: true, value: v });
  15. }) : function(o, v) {
  16. o["default"] = v;
  17. });
  18. var __importStar = (this && this.__importStar) || function (mod) {
  19. if (mod && mod.__esModule) return mod;
  20. var result = {};
  21. if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
  22. __setModuleDefault(result, mod);
  23. return result;
  24. };
  25. var __importDefault = (this && this.__importDefault) || function (mod) {
  26. return (mod && mod.__esModule) ? mod : { "default": mod };
  27. };
  28. Object.defineProperty(exports, "__esModule", { value: true });
  29. exports.WebSocketServer = exports.debugInfo = void 0;
  30. /**
  31. * Copyright 2021 Google LLC.
  32. * Copyright (c) Microsoft Corporation.
  33. *
  34. * Licensed under the Apache License, Version 2.0 (the "License");
  35. * you may not use this file except in compliance with the License.
  36. * You may obtain a copy of the License at
  37. *
  38. * http://www.apache.org/licenses/LICENSE-2.0
  39. *
  40. * Unless required by applicable law or agreed to in writing, software
  41. * distributed under the License is distributed on an "AS IS" BASIS,
  42. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  43. * See the License for the specific language governing permissions and
  44. * limitations under the License.
  45. */
  46. const http_1 = __importDefault(require("http"));
  47. const debug_1 = __importDefault(require("debug"));
  48. const websocket = __importStar(require("websocket"));
  49. const uuid_js_1 = require("../utils/uuid.js");
  50. const BrowserInstance_js_1 = require("./BrowserInstance.js");
  51. exports.debugInfo = (0, debug_1.default)('bidi:server:info');
  52. const debugInternal = (0, debug_1.default)('bidi:server:internal');
  53. const debugSend = (0, debug_1.default)('bidi:server:SEND ▸');
  54. const debugRecv = (0, debug_1.default)('bidi:server:RECV ◂');
  55. class WebSocketServer {
  56. #sessions = new Map();
  57. #port;
  58. #verbose;
  59. #server;
  60. #wsServer;
  61. constructor(port, verbose) {
  62. this.#port = port;
  63. this.#verbose = verbose;
  64. this.#server = http_1.default.createServer(this.#onRequest.bind(this));
  65. this.#wsServer = new websocket.server({
  66. httpServer: this.#server,
  67. autoAcceptConnections: false,
  68. });
  69. this.#wsServer.on('request', this.#onWsRequest.bind(this));
  70. void this.#listen();
  71. }
  72. #logServerStarted() {
  73. (0, exports.debugInfo)('BiDi server is listening on port', this.#port);
  74. (0, exports.debugInfo)('BiDi server was started successfully.');
  75. }
  76. async #listen() {
  77. try {
  78. this.#server.listen(this.#port, () => {
  79. this.#logServerStarted();
  80. });
  81. }
  82. catch (error) {
  83. if (error &&
  84. typeof error === 'object' &&
  85. 'code' in error &&
  86. error.code === 'EADDRINUSE') {
  87. await new Promise((resolve) => {
  88. setTimeout(resolve, 500);
  89. });
  90. (0, exports.debugInfo)('Retrying to run BiDi server');
  91. this.#server.listen(this.#port, () => {
  92. this.#logServerStarted();
  93. });
  94. }
  95. throw error;
  96. }
  97. }
  98. async #onRequest(request, response) {
  99. debugInternal(`Received HTTP ${JSON.stringify(request.method)} request for ${JSON.stringify(request.url)}`);
  100. if (!request.url) {
  101. return response.end(404);
  102. }
  103. // https://w3c.github.io/webdriver-bidi/#transport, step 2.
  104. if (request.url === '/session') {
  105. const body = await new Promise((resolve, reject) => {
  106. const bodyArray = [];
  107. request.on('data', (chunk) => {
  108. bodyArray.push(chunk);
  109. });
  110. request.on('error', reject);
  111. request.on('end', () => {
  112. resolve(Buffer.concat(bodyArray));
  113. });
  114. });
  115. debugInternal(`Creating session by HTTP request ${body.toString()}`);
  116. // https://w3c.github.io/webdriver-bidi/#transport, step 3.
  117. const jsonBody = JSON.parse(body.toString());
  118. response.writeHead(200, {
  119. 'Content-Type': 'application/json;charset=utf-8',
  120. 'Cache-Control': 'no-cache',
  121. });
  122. const sessionId = (0, uuid_js_1.uuidv4)();
  123. const session = {
  124. sessionId,
  125. // TODO: launch browser instance and set it to the session after WPT
  126. // tests clean up is switched to pure BiDi.
  127. browserInstancePromise: undefined,
  128. sessionOptions: {
  129. chromeOptions: this.#getChromeOptions(jsonBody.capabilities),
  130. mapperOptions: this.#getMapperOptions(jsonBody.capabilities),
  131. verbose: this.#verbose,
  132. },
  133. };
  134. this.#sessions.set(sessionId, session);
  135. const webSocketUrl = `ws://localhost:${this.#port}/session/${sessionId}`;
  136. debugInternal(`Session created. WebSocket URL: ${JSON.stringify(webSocketUrl)}.`);
  137. response.write(JSON.stringify({
  138. value: {
  139. sessionId,
  140. capabilities: {
  141. webSocketUrl,
  142. },
  143. },
  144. }));
  145. return response.end();
  146. }
  147. else if (request.url.startsWith('/session')) {
  148. debugInternal(`Unknown session command ${request.method ?? 'UNKNOWN METHOD'} request for ${request.url} with payload ${await this.#getHttpRequestPayload(request)}. 200 returned.`);
  149. response.writeHead(200, {
  150. 'Content-Type': 'application/json;charset=utf-8',
  151. 'Cache-Control': 'no-cache',
  152. });
  153. response.write(JSON.stringify({
  154. value: {},
  155. }));
  156. return response.end();
  157. }
  158. debugInternal(`Unknown ${request.method} request for ${JSON.stringify(request.url)} with payload ${await this.#getHttpRequestPayload(request)}. 404 returned.`);
  159. return response.end(404);
  160. }
  161. #onWsRequest(request) {
  162. // Session is set either by Classic or BiDi commands.
  163. let session;
  164. // Request to `/session` should be treated as a new session request.
  165. let requestSessionId = '';
  166. if ((request.resource ?? '').startsWith(`/session/`)) {
  167. requestSessionId = (request.resource ?? '').split('/').pop() ?? '';
  168. }
  169. debugInternal(`new WS request received. Path: ${JSON.stringify(request.resourceURL.path)}, sessionId: ${JSON.stringify(requestSessionId)}`);
  170. if (requestSessionId !== '' &&
  171. requestSessionId !== undefined &&
  172. !this.#sessions.has(requestSessionId)) {
  173. debugInternal('Unknown session id:', requestSessionId);
  174. request.reject();
  175. return;
  176. }
  177. const connection = request.accept();
  178. session = this.#sessions.get(requestSessionId ?? '');
  179. if (session !== undefined) {
  180. // BrowserInstance is created for each new WS connection, even for the
  181. // same SessionId. This is because WPT uses a single session for all the
  182. // tests, but cleans up tests using WebDriver Classic commands, which is
  183. // not implemented in this Mapper runner.
  184. // TODO: connect to an existing BrowserInstance instead.
  185. const sessionOptions = session.sessionOptions;
  186. session.browserInstancePromise = this.#closeBrowserInstanceIfLaunched(session)
  187. .then(async () => await this.#launchBrowserInstance(connection, sessionOptions))
  188. .catch((e) => {
  189. (0, exports.debugInfo)('Error while creating session', e);
  190. connection.close(500, 'cannot create browser instance');
  191. throw e;
  192. });
  193. }
  194. connection.on('message', async (message) => {
  195. // If type is not text, return error.
  196. if (message.type !== 'utf8') {
  197. this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `not supported type (${message.type})`);
  198. return;
  199. }
  200. const plainCommandData = message.utf8Data;
  201. if (debugRecv.enabled) {
  202. try {
  203. debugRecv(JSON.parse(plainCommandData));
  204. }
  205. catch {
  206. debugRecv(plainCommandData);
  207. }
  208. }
  209. // Try to parse the message to handle some of BiDi commands.
  210. let parsedCommandData;
  211. try {
  212. parsedCommandData = JSON.parse(plainCommandData);
  213. }
  214. catch (e) {
  215. this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `Cannot parse data as JSON`);
  216. return;
  217. }
  218. // Handle creating new session.
  219. if (parsedCommandData.method === 'session.new') {
  220. if (session !== undefined) {
  221. (0, exports.debugInfo)('WS connection already have an associated session.');
  222. this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, 'WS connection already have an associated session.');
  223. return;
  224. }
  225. try {
  226. const sessionOptions = {
  227. chromeOptions: this.#getChromeOptions(parsedCommandData.params?.capabilities),
  228. mapperOptions: this.#getMapperOptions(parsedCommandData.params?.capabilities),
  229. verbose: this.#verbose,
  230. };
  231. const browserInstance = await this.#launchBrowserInstance(connection, sessionOptions);
  232. const sessionId = (0, uuid_js_1.uuidv4)();
  233. session = {
  234. sessionId,
  235. browserInstancePromise: Promise.resolve(browserInstance),
  236. sessionOptions,
  237. };
  238. this.#sessions.set(sessionId, session);
  239. }
  240. catch (e) {
  241. (0, exports.debugInfo)('Error while creating session', e);
  242. this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, e?.message ?? 'Unknown error');
  243. return;
  244. }
  245. // TODO: extend with capabilities.
  246. this.#sendClientMessage({
  247. id: parsedCommandData.id,
  248. type: 'success',
  249. result: {
  250. sessionId: session.sessionId,
  251. capabilities: {},
  252. },
  253. }, connection);
  254. return;
  255. }
  256. // Handle ending session. Close browser if open, remove session.
  257. if (parsedCommandData.method === 'session.end') {
  258. if (session === undefined) {
  259. (0, exports.debugInfo)('WS connection does not have an associated session.');
  260. this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, 'WS connection does not have an associated session.');
  261. return;
  262. }
  263. try {
  264. await this.#closeBrowserInstanceIfLaunched(session);
  265. this.#sessions.delete(session.sessionId);
  266. }
  267. catch (e) {
  268. (0, exports.debugInfo)('Error while closing session', e);
  269. this.#respondWithError(connection, plainCommandData, "unknown error" /* ErrorCode.UnknownError */, `Session cannot be closed. Error: ${e?.message}`);
  270. return;
  271. }
  272. this.#sendClientMessage({
  273. id: parsedCommandData.id,
  274. type: 'success',
  275. result: {},
  276. }, connection);
  277. return;
  278. }
  279. if (session === undefined) {
  280. (0, exports.debugInfo)('Session is not yet initialized.');
  281. this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Session is not yet initialized.');
  282. return;
  283. }
  284. if (session.browserInstancePromise === undefined) {
  285. (0, exports.debugInfo)('Browser instance is not launched.');
  286. this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Browser instance is not launched.');
  287. return;
  288. }
  289. const browserInstance = await session.browserInstancePromise;
  290. // Handle `browser.close` command.
  291. if (parsedCommandData.method === 'browser.close') {
  292. await browserInstance.close();
  293. this.#sendClientMessage({
  294. id: parsedCommandData.id,
  295. type: 'success',
  296. result: {},
  297. }, connection);
  298. return;
  299. }
  300. // Forward all other commands to BiDi Mapper.
  301. await browserInstance.bidiSession().sendCommand(plainCommandData);
  302. });
  303. connection.on('close', async () => {
  304. debugInternal(`Peer ${connection.remoteAddress} disconnected.`);
  305. // TODO: don't close Browser instance to allow re-connecting to the session.
  306. await this.#closeBrowserInstanceIfLaunched(session);
  307. });
  308. }
  309. async #closeBrowserInstanceIfLaunched(session) {
  310. if (session === undefined || session.browserInstancePromise === undefined) {
  311. return;
  312. }
  313. const browserInstance = await session.browserInstancePromise;
  314. session.browserInstancePromise = undefined;
  315. void browserInstance.close();
  316. }
  317. #getMapperOptions(capabilities) {
  318. const acceptInsecureCerts = capabilities?.alwaysMatch?.acceptInsecureCerts ?? false;
  319. return { acceptInsecureCerts };
  320. }
  321. #getChromeOptions(capabilities) {
  322. const chromeCapabilities = capabilities?.alwaysMatch?.['goog:chromeOptions'];
  323. return {
  324. chromeArgs: chromeCapabilities?.args ?? [],
  325. chromeBinary: chromeCapabilities?.binary ?? undefined,
  326. };
  327. }
  328. async #launchBrowserInstance(connection, sessionOptions) {
  329. (0, exports.debugInfo)('Scheduling browser launch...');
  330. const browserInstance = await BrowserInstance_js_1.BrowserInstance.run(sessionOptions.chromeOptions, sessionOptions.mapperOptions, sessionOptions.verbose);
  331. // Forward messages from BiDi Mapper to the client unconditionally.
  332. browserInstance.bidiSession().on('message', (message) => {
  333. this.#sendClientMessageString(message, connection);
  334. });
  335. (0, exports.debugInfo)('Browser is launched!');
  336. return browserInstance;
  337. }
  338. #sendClientMessageString(message, connection) {
  339. if (debugSend.enabled) {
  340. try {
  341. debugSend(JSON.parse(message));
  342. }
  343. catch {
  344. debugSend(message);
  345. }
  346. }
  347. connection.sendUTF(message);
  348. }
  349. #sendClientMessage(object, connection) {
  350. const json = JSON.stringify(object);
  351. return this.#sendClientMessageString(json, connection);
  352. }
  353. #respondWithError(connection, plainCommandData, errorCode, errorMessage) {
  354. const errorResponse = this.#getErrorResponse(plainCommandData, errorCode, errorMessage);
  355. void this.#sendClientMessage(errorResponse, connection);
  356. }
  357. #getErrorResponse(plainCommandData, errorCode, errorMessage) {
  358. // XXX: this is bizarre per spec. We reparse the payload and
  359. // extract the ID, regardless of what kind of value it was.
  360. let commandId;
  361. try {
  362. const commandData = JSON.parse(plainCommandData);
  363. if ('id' in commandData) {
  364. commandId = commandData.id;
  365. }
  366. }
  367. catch { }
  368. return {
  369. type: 'error',
  370. id: commandId,
  371. error: errorCode,
  372. message: errorMessage,
  373. // XXX: optional stacktrace field.
  374. };
  375. }
  376. #getHttpRequestPayload(request) {
  377. return new Promise((resolve, reject) => {
  378. let data = '';
  379. request.on('data', (chunk) => {
  380. data += chunk;
  381. });
  382. request.on('end', () => {
  383. resolve(data);
  384. });
  385. request.on('error', (error) => {
  386. reject(error);
  387. });
  388. });
  389. }
  390. }
  391. exports.WebSocketServer = WebSocketServer;
  392. //# sourceMappingURL=WebSocketServer.js.map