Tons of Solutions Engineering work done today for the rest of the CS team! Headway, Howard Hanna, Engels, Brighton, etc. Also completed Datasnippers auth flow and worked on Anthology's script. Cloned Anthology's courses (900..) and will clone Full Story on Monday.

This commit is contained in:
Norm Rasmussen
2024-01-05 17:07:59 -05:00
parent ce261975ca
commit a5fe4bd2c8
3157 changed files with 554269 additions and 16 deletions

View File

@ -0,0 +1,339 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebSocketServer = exports.debugInfo = void 0;
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const http_1 = __importDefault(require("http"));
const debug_1 = __importDefault(require("debug"));
const websocket = __importStar(require("websocket"));
const uuid_js_1 = require("../utils/uuid.js");
const BrowserInstance_js_1 = require("./BrowserInstance.js");
exports.debugInfo = (0, debug_1.default)('bidi:server:info');
const debugInternal = (0, debug_1.default)('bidi:server:internal');
const debugSend = (0, debug_1.default)('bidi:server:SEND ▸');
const debugRecv = (0, debug_1.default)('bidi:server:RECV ◂');
class WebSocketServer {
static #sessions = new Map();
/**
* @param bidiPort Port to start ws server on.
* @param channel
* @param headless
* @param verbose
*/
static run(bidiPort, channel, headless, verbose) {
const server = http_1.default.createServer(async (request, response) => {
debugInternal(`${new Date().toString()} Received HTTP ${JSON.stringify(request.method)} request for ${JSON.stringify(request.url)}`);
if (!request.url) {
return response.end(404);
}
// https://w3c.github.io/webdriver-bidi/#transport, step 2.
if (request.url === '/session') {
const body = [];
request
.on('data', (chunk) => {
body.push(chunk);
})
.on('end', () => {
const jsonBody = JSON.parse(Buffer.concat(body).toString());
response.writeHead(200, {
'Content-Type': 'application/json;charset=utf-8',
'Cache-Control': 'no-cache',
});
const sessionId = (0, uuid_js_1.uuidv4)();
const session = {
sessionId,
// TODO: launch browser instance and set it to the session after WPT
// tests clean up is switched to pure BiDi.
browserInstancePromise: undefined,
sessionOptions: {
chromeOptions: this.#getChromeOptions(jsonBody.capabilities, channel, headless),
mapperOptions: this.#getMapperOptions(jsonBody.capabilities),
verbose,
},
};
this.#sessions.set(sessionId, session);
const webSocketUrl = `ws://localhost:${bidiPort}/session/${sessionId}`;
debugInternal(`Session created. WebSocket URL: ${JSON.stringify(webSocketUrl)}.`);
response.write(JSON.stringify({
value: {
sessionId,
capabilities: {
webSocketUrl,
},
},
}));
return response.end();
});
return;
}
else if (request.url.startsWith('/session')) {
debugInternal(`Unknown session command ${request.method ?? 'UNKNOWN METHOD'} request for ${request.url} with payload ${await WebSocketServer.#getHttpRequestPayload(request)}. 200 returned.`);
response.writeHead(200, {
'Content-Type': 'application/json;charset=utf-8',
'Cache-Control': 'no-cache',
});
response.write(JSON.stringify({
value: {},
}));
}
else {
debugInternal(`Unknown ${JSON.stringify(request.method)} request for ${JSON.stringify(request.url)} with payload ${JSON.stringify(await WebSocketServer.#getHttpRequestPayload(request))}. 404 returned.`);
response.writeHead(404);
}
return response.end();
});
server.listen(bidiPort, () => {
(0, exports.debugInfo)('BiDi server is listening on port', bidiPort);
});
const wsServer = new websocket.server({
httpServer: server,
autoAcceptConnections: false,
});
wsServer.on('request', (request) => {
// Session is set either by Classic or BiDi commands.
let session;
const requestSessionId = (request.resource ?? '').split('/').pop();
debugInternal(`new WS request received. Path: ${JSON.stringify(request.resourceURL.path)}, sessionId: ${JSON.stringify(requestSessionId)}`);
if (requestSessionId !== '' &&
requestSessionId !== undefined &&
!this.#sessions.has(requestSessionId)) {
debugInternal('Unknown session id:', requestSessionId);
request.reject();
return;
}
const connection = request.accept();
session = this.#sessions.get(requestSessionId ?? '');
if (session !== undefined) {
// BrowserInstance is created for each new WS connection, even for the
// same SessionId. This is because WPT uses a single session for all the
// tests, but cleans up tests using WebDriver Classic commands, which is
// not implemented in this Mapper runner.
// TODO: connect to an existing BrowserInstance instead.
const sessionOptions = session.sessionOptions;
session.browserInstancePromise = this.#closeBrowserInstanceIfLaunched(session)
.then(async () => await this.#launchBrowserInstance(connection, sessionOptions))
.catch((e) => {
(0, exports.debugInfo)('Error while creating session', e);
connection.close(500, 'cannot create browser instance');
throw e;
});
}
connection.on('message', async (message) => {
// If type is not text, return error.
if (message.type !== 'utf8') {
this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `not supported type (${message.type})`);
return;
}
const plainCommandData = message.utf8Data;
if (debugRecv.enabled) {
try {
debugRecv(JSON.parse(plainCommandData));
}
catch {
debugRecv(plainCommandData);
}
}
// Try to parse the message to handle some of BiDi commands.
let parsedCommandData;
try {
parsedCommandData = JSON.parse(plainCommandData);
}
catch (e) {
this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `Cannot parse data as JSON`);
return;
}
// Handle creating new session.
if (parsedCommandData.method === 'session.new') {
if (session !== undefined) {
(0, exports.debugInfo)('WS connection already have an associated session.');
this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, 'WS connection already have an associated session.');
return;
}
try {
const sessionOptions = {
chromeOptions: this.#getChromeOptions(parsedCommandData.params?.capabilities, channel, headless),
mapperOptions: this.#getMapperOptions(parsedCommandData.params?.capabilities),
verbose,
};
const browserInstance = await this.#launchBrowserInstance(connection, sessionOptions);
const sessionId = (0, uuid_js_1.uuidv4)();
session = {
sessionId,
browserInstancePromise: Promise.resolve(browserInstance),
sessionOptions,
};
this.#sessions.set(sessionId, session);
}
catch (e) {
(0, exports.debugInfo)('Error while creating session', e);
this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, e?.message ?? 'Unknown error');
return;
}
// TODO: extend with capabilities.
this.#sendClientMessage({
id: parsedCommandData.id,
type: 'success',
result: {
sessionId: session.sessionId,
capabilities: {},
},
}, connection);
return;
}
if (session === undefined) {
(0, exports.debugInfo)('Session is not yet initialized.');
this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Session is not yet initialized.');
return;
}
if (session.browserInstancePromise === undefined) {
(0, exports.debugInfo)('Browser instance is not launched.');
this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Browser instance is not launched.');
return;
}
const browserInstance = await session.browserInstancePromise;
// Handle `browser.close` command.
if (parsedCommandData.method === 'browser.close') {
await browserInstance.close();
this.#sendClientMessage({
id: parsedCommandData.id,
type: 'success',
result: {},
}, connection);
return;
}
// Forward all other commands to BiDi Mapper.
await browserInstance.bidiSession().sendCommand(plainCommandData);
});
connection.on('close', async () => {
debugInternal(`${new Date().toString()} Peer ${connection.remoteAddress} disconnected.`);
// TODO: don't close Browser instance to allow re-connecting to the session.
await this.#closeBrowserInstanceIfLaunched(session);
});
});
}
static async #closeBrowserInstanceIfLaunched(session) {
if (session === undefined || session.browserInstancePromise === undefined) {
return;
}
const browserInstance = await session.browserInstancePromise;
session.browserInstancePromise = undefined;
void browserInstance.close();
}
static #getMapperOptions(capabilities) {
const acceptInsecureCerts = capabilities?.alwaysMatch?.acceptInsecureCerts ?? false;
return { acceptInsecureCerts };
}
static #getChromeOptions(capabilities, channel, headless) {
const chromeCapabilities = capabilities?.alwaysMatch?.['goog:chromeOptions'];
return {
chromeArgs: chromeCapabilities?.args ?? [],
channel,
headless,
chromeBinary: chromeCapabilities?.binary ?? undefined,
};
}
static async #launchBrowserInstance(connection, sessionOptions) {
(0, exports.debugInfo)('Scheduling browser launch...');
const browserInstance = await BrowserInstance_js_1.BrowserInstance.run(sessionOptions.chromeOptions, sessionOptions.mapperOptions, sessionOptions.verbose);
// Forward messages from BiDi Mapper to the client unconditionally.
browserInstance.bidiSession().on('message', (message) => {
this.#sendClientMessageString(message, connection);
});
(0, exports.debugInfo)('Browser is launched!');
return browserInstance;
}
static #sendClientMessageString(message, connection) {
if (debugSend.enabled) {
try {
debugSend(JSON.parse(message));
}
catch {
debugSend(message);
}
}
connection.sendUTF(message);
}
static #sendClientMessage(object, connection) {
const json = JSON.stringify(object);
return this.#sendClientMessageString(json, connection);
}
static #respondWithError(connection, plainCommandData, errorCode, errorMessage) {
const errorResponse = this.#getErrorResponse(plainCommandData, errorCode, errorMessage);
void this.#sendClientMessage(errorResponse, connection);
}
static #getErrorResponse(plainCommandData, errorCode, errorMessage) {
// XXX: this is bizarre per spec. We reparse the payload and
// extract the ID, regardless of what kind of value it was.
let commandId;
try {
const commandData = JSON.parse(plainCommandData);
if ('id' in commandData) {
commandId = commandData.id;
}
}
catch { }
return {
type: 'error',
id: commandId,
error: errorCode,
message: errorMessage,
// XXX: optional stacktrace field.
};
}
static #getHttpRequestPayload(request) {
return new Promise((resolve, reject) => {
let data = '';
request.on('data', (chunk) => {
data += chunk;
});
request.on('end', () => {
resolve(data);
});
request.on('error', (error) => {
reject(error);
});
});
}
}
exports.WebSocketServer = WebSocketServer;
//# sourceMappingURL=WebSocketServer.js.map