bidiServerRunner.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.BidiServerRunner = exports.debugInfo = void 0;
  7. /**
  8. * Copyright 2021 Google LLC.
  9. * Copyright (c) Microsoft Corporation.
  10. *
  11. * Licensed under the Apache License, Version 2.0 (the "License");
  12. * you may not use this file except in compliance with the License.
  13. * You may obtain a copy of the License at
  14. *
  15. * http://www.apache.org/licenses/LICENSE-2.0
  16. *
  17. * Unless required by applicable law or agreed to in writing, software
  18. * distributed under the License is distributed on an "AS IS" BASIS,
  19. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  20. * See the License for the specific language governing permissions and
  21. * limitations under the License.
  22. */
  23. const http_1 = __importDefault(require("http"));
  24. const debug_1 = __importDefault(require("debug"));
  25. const websocket_1 = __importDefault(require("websocket"));
  26. exports.debugInfo = (0, debug_1.default)('bidi:server:info');
  27. const debugInternal = (0, debug_1.default)('bidi:server:internal');
  28. const debugSend = (0, debug_1.default)('bidi:server:SEND ▸');
  29. const debugRecv = (0, debug_1.default)('bidi:server:RECV ◂');
  30. function getHttpRequestPayload(request) {
  31. return new Promise((resolve, reject) => {
  32. let data = '';
  33. request.on('data', (chunk) => {
  34. data += chunk;
  35. });
  36. request.on('end', () => {
  37. resolve(data);
  38. });
  39. request.on('error', (error) => {
  40. reject(error);
  41. });
  42. });
  43. }
  44. class BidiServerRunner {
  45. /**
  46. *
  47. * @param bidiPort port to start ws server on
  48. * @param onNewBidiConnectionOpen delegate to be called for each new
  49. * connection. `onNewBidiConnectionOpen` delegate should return another
  50. * `onConnectionClose` delegate, which will be called after the connection is
  51. * closed.
  52. */
  53. run(bidiPort, onNewBidiConnectionOpen) {
  54. const server = http_1.default.createServer(async (request, response) => {
  55. debugInternal(`${new Date().toString()} Received ${request.method ?? 'UNKNOWN METHOD'} request for ${request.url ?? 'UNKNOWN URL'}`);
  56. if (!request.url)
  57. return response.end(404);
  58. // https://w3c.github.io/webdriver-bidi/#transport, step 2.
  59. if (request.url === '/session') {
  60. response.writeHead(200, {
  61. 'Content-Type': 'application/json;charset=utf-8',
  62. 'Cache-Control': 'no-cache',
  63. });
  64. response.write(JSON.stringify({
  65. value: {
  66. sessionId: '1',
  67. capabilities: {
  68. webSocketUrl: `ws://localhost:${bidiPort}`,
  69. },
  70. },
  71. }));
  72. }
  73. else if (request.url.startsWith('/session')) {
  74. debugInternal(`Unknown session command ${request.method ?? 'UNKNOWN METHOD'} request for ${request.url} with payload ${await getHttpRequestPayload(request)}. 200 returned.`);
  75. response.writeHead(200, {
  76. 'Content-Type': 'application/json;charset=utf-8',
  77. 'Cache-Control': 'no-cache',
  78. });
  79. response.write(JSON.stringify({
  80. value: {},
  81. }));
  82. }
  83. else {
  84. debugInternal(`Unknown ${JSON.stringify(request.method)} request for ${JSON.stringify(request.url)} with payload ${JSON.stringify(await getHttpRequestPayload(request))}. 404 returned.`);
  85. response.writeHead(404);
  86. }
  87. return response.end();
  88. });
  89. server.listen(bidiPort, () => {
  90. (0, exports.debugInfo)('Server is listening on port', bidiPort);
  91. });
  92. const wsServer = new websocket_1.default.server({
  93. httpServer: server,
  94. autoAcceptConnections: false,
  95. });
  96. wsServer.on('request', async (request) => {
  97. debugInternal('new WS request received:', request.resourceURL.path);
  98. const bidiServer = new BidiServer();
  99. const onBidiConnectionClosed = await onNewBidiConnectionOpen(bidiServer);
  100. const connection = request.accept();
  101. connection.on('message', (message) => {
  102. // 1. If |type| is not text, return.
  103. if (message.type !== 'utf8') {
  104. this.#respondWithError(connection, {}, 'invalid argument', `not supported type (${message.type})`);
  105. return;
  106. }
  107. const plainCommandData = message.utf8Data;
  108. debugRecv(plainCommandData);
  109. bidiServer.onMessage(plainCommandData);
  110. });
  111. connection.on('close', () => {
  112. debugInternal(`${new Date().toString()} Peer ${connection.remoteAddress} disconnected.`);
  113. onBidiConnectionClosed();
  114. });
  115. bidiServer.initialize((messageStr) => {
  116. return this.#sendClientMessageStr(messageStr, connection);
  117. });
  118. });
  119. }
  120. #sendClientMessageStr(messageStr, connection) {
  121. debugSend(messageStr);
  122. connection.sendUTF(messageStr);
  123. return Promise.resolve();
  124. }
  125. #sendClientMessage(messageObj, connection) {
  126. const messageStr = JSON.stringify(messageObj);
  127. return this.#sendClientMessageStr(messageStr, connection);
  128. }
  129. #respondWithError(connection, plainCommandData, errorCode, errorMessage) {
  130. const errorResponse = this.#getErrorResponse(plainCommandData, errorCode, errorMessage);
  131. void this.#sendClientMessage(errorResponse, connection);
  132. }
  133. #getErrorResponse(plainCommandData, errorCode, errorMessage) {
  134. // XXX: this is bizarre per spec. We reparse the payload and
  135. // extract the ID, regardless of what kind of value it was.
  136. let commandId;
  137. try {
  138. const commandData = JSON.parse(plainCommandData);
  139. if ('id' in commandData) {
  140. commandId = commandData.id;
  141. }
  142. }
  143. catch { }
  144. return {
  145. id: commandId,
  146. error: errorCode,
  147. message: errorMessage,
  148. // XXX: optional stacktrace field.
  149. };
  150. }
  151. }
  152. exports.BidiServerRunner = BidiServerRunner;
  153. class BidiServer {
  154. #handlers = [];
  155. #sendBidiMessage = null;
  156. setOnMessage(handler) {
  157. this.#handlers.push(handler);
  158. }
  159. sendMessage(message) {
  160. if (!this.#sendBidiMessage)
  161. throw new Error('BiDi connection is not initialised yet');
  162. return this.#sendBidiMessage(message);
  163. }
  164. close() {
  165. // Intentionally empty.
  166. }
  167. initialize(sendBidiMessage) {
  168. this.#sendBidiMessage = sendBidiMessage;
  169. }
  170. onMessage(messageStr) {
  171. for (const handler of this.#handlers)
  172. handler(messageStr);
  173. }
  174. }
  175. //# sourceMappingURL=bidiServerRunner.js.map