2024-01-05 17:07:59 -05:00
"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" ) ) ;
2024-09-23 20:52:09 -04:00
const Deferred _js _1 = require ( "../utils/Deferred.js" ) ;
2024-01-05 17:07:59 -05:00
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 {
2024-02-28 17:13:10 -05:00
# sessions = new Map ( ) ;
# port ;
# verbose ;
# server ;
# wsServer ;
2024-09-23 20:52:09 -04:00
constructor ( port , verbose ) {
2024-02-28 17:13:10 -05:00
this . # port = port ;
this . # verbose = verbose ;
2024-09-23 20:52:09 -04:00
this . # server = http _1 . default . createServer ( ( request , response ) => {
return this . # onRequest ( request , response ) . catch ( ( e ) => {
( 0 , exports . debugInfo ) ( 'Error while processing request' , e ) ;
response . writeHead ( 500 , String ( e ) ) ;
} ) ;
} ) ;
2024-02-28 17:13:10 -05:00
this . # wsServer = new websocket . server ( {
httpServer : this . # server ,
autoAcceptConnections : false ,
} ) ;
this . # wsServer . on ( 'request' , this . # onWsRequest . bind ( this ) ) ;
2024-09-23 20:52:09 -04:00
void this . # listen ( ) ;
}
# logServerStarted ( ) {
( 0 , exports . debugInfo ) ( 'BiDi server is listening on port' , this . # port ) ;
( 0 , exports . debugInfo ) ( 'BiDi server was started successfully.' ) ;
}
async # listen ( ) {
try {
this . # server . listen ( this . # port , ( ) => {
this . # logServerStarted ( ) ;
} ) ;
}
catch ( error ) {
if ( error &&
typeof error === 'object' &&
'code' in error &&
error . code === 'EADDRINUSE' ) {
await new Promise ( ( resolve ) => {
setTimeout ( resolve , 500 ) ;
} ) ;
( 0 , exports . debugInfo ) ( 'Retrying to run BiDi server' ) ;
this . # server . listen ( this . # port , ( ) => {
this . # logServerStarted ( ) ;
} ) ;
}
throw error ;
}
2024-02-28 17:13:10 -05:00
}
async # onRequest ( request , response ) {
debugInternal ( ` Received HTTP ${ JSON . stringify ( request . method ) } request for ${ JSON . stringify ( request . url ) } ` ) ;
if ( ! request . url ) {
2024-09-23 20:52:09 -04:00
throw new Error ( 'Request URL is empty.' ) ;
2024-02-28 17:13:10 -05:00
}
// https://w3c.github.io/webdriver-bidi/#transport, step 2.
if ( request . url === '/session' ) {
const body = await new Promise ( ( resolve , reject ) => {
const bodyArray = [ ] ;
request . on ( 'data' , ( chunk ) => {
bodyArray . push ( chunk ) ;
2024-01-05 17:07:59 -05:00
} ) ;
2024-02-28 17:13:10 -05:00
request . on ( 'error' , reject ) ;
request . on ( 'end' , ( ) => {
resolve ( Buffer . concat ( bodyArray ) ) ;
} ) ;
} ) ;
2024-09-23 20:52:09 -04:00
debugInternal ( ` Creating session by HTTP request ${ body . toString ( ) } ` ) ;
2024-02-28 17:13:10 -05:00
// https://w3c.github.io/webdriver-bidi/#transport, step 3.
const jsonBody = JSON . parse ( 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 : {
2024-09-23 20:52:09 -04:00
chromeOptions : this . # getChromeOptions ( jsonBody . capabilities ) ,
2024-02-28 17:13:10 -05:00
verbose : this . # verbose ,
2024-09-23 20:52:09 -04:00
sessionNewBody : ` {"id":0,"method":"session.new","params": ${ body . toString ( ) } } ` ,
2024-02-28 17:13:10 -05:00
} ,
} ;
this . # sessions . set ( sessionId , session ) ;
const webSocketUrl = ` ws://localhost: ${ this . # port } /session/ ${ sessionId } ` ;
debugInternal ( ` Session created. WebSocket URL: ${ JSON . stringify ( webSocketUrl ) } . ` ) ;
response . write ( JSON . stringify ( {
value : {
sessionId ,
capabilities : {
webSocketUrl ,
} ,
} ,
} ) ) ;
return response . end ( ) ;
}
else if ( request . url . startsWith ( '/session' ) ) {
debugInternal ( ` Unknown session command ${ request . method ? ? 'UNKNOWN METHOD' } request for ${ request . url } with payload ${ await this . # getHttpRequestPayload ( request ) } . 200 returned. ` ) ;
response . writeHead ( 200 , {
'Content-Type' : 'application/json;charset=utf-8' ,
'Cache-Control' : 'no-cache' ,
} ) ;
response . write ( JSON . stringify ( {
value : { } ,
} ) ) ;
return response . end ( ) ;
}
2024-09-23 20:52:09 -04:00
throw new Error ( ` Unknown " ${ request . method } " request for " ${ JSON . stringify ( request . url ) } " with payload " ${ await this . # getHttpRequestPayload ( request ) } ". ` ) ;
2024-02-28 17:13:10 -05:00
}
# onWsRequest ( request ) {
// Session is set either by Classic or BiDi commands.
let session ;
2024-09-23 20:52:09 -04:00
// Request to `/session` should be treated as a new session request.
let requestSessionId = '' ;
if ( ( request . resource ? ? '' ) . startsWith ( ` /session/ ` ) ) {
requestSessionId = ( request . resource ? ? '' ) . split ( '/' ) . pop ( ) ? ? '' ;
}
2024-02-28 17:13:10 -05:00
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 } ) ` ) ;
2024-01-05 17:07:59 -05:00
return ;
}
2024-02-28 17:13:10 -05:00
const plainCommandData = message . utf8Data ;
if ( debugRecv . enabled ) {
try {
debugRecv ( JSON . parse ( plainCommandData ) ) ;
}
catch {
debugRecv ( plainCommandData ) ;
}
2024-01-05 17:07:59 -05:00
}
2024-02-28 17:13:10 -05:00
// Try to parse the message to handle some of BiDi commands.
let parsedCommandData ;
try {
parsedCommandData = JSON . parse ( plainCommandData ) ;
2024-01-05 17:07:59 -05:00
}
2024-09-23 20:52:09 -04:00
catch ( error ) {
this . # respondWithError ( connection , { } , "invalid argument" /* ErrorCode.InvalidArgument */ , ` Cannot parse data as JSON, ${ error } ` ) ;
2024-01-05 17:07:59 -05:00
return ;
}
2024-02-28 17:13:10 -05:00
// 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.' ) ;
2024-01-05 17:07:59 -05:00
return ;
}
try {
2024-02-28 17:13:10 -05:00
const sessionOptions = {
2024-09-23 20:52:09 -04:00
chromeOptions : this . # getChromeOptions ( parsedCommandData . params ? . capabilities ) ,
2024-02-28 17:13:10 -05:00
verbose : this . # verbose ,
2024-09-23 20:52:09 -04:00
sessionNewBody : plainCommandData ,
2024-02-28 17:13:10 -05:00
} ;
2024-09-23 20:52:09 -04:00
const browserInstance = await this . # launchBrowserInstance ( connection , sessionOptions , true ) ;
2024-02-28 17:13:10 -05:00
const sessionId = ( 0 , uuid _js _1 . uuidv4 ) ( ) ;
session = {
sessionId ,
browserInstancePromise : Promise . resolve ( browserInstance ) ,
sessionOptions ,
} ;
this . # sessions . set ( sessionId , session ) ;
2024-01-05 17:07:59 -05:00
}
catch ( e ) {
2024-02-28 17:13:10 -05:00
( 0 , exports . debugInfo ) ( 'Error while creating session' , e ) ;
this . # respondWithError ( connection , plainCommandData , "session not created" /* ErrorCode.SessionNotCreated */ , e ? . message ? ? 'Unknown error' ) ;
2024-01-05 17:07:59 -05:00
return ;
}
2024-09-23 20:52:09 -04:00
return ;
}
// Handle ending session. Close browser if open, remove session.
if ( parsedCommandData . method === 'session.end' ) {
if ( session === undefined ) {
( 0 , exports . debugInfo ) ( 'WS connection does not have an associated session.' ) ;
this . # respondWithError ( connection , plainCommandData , "session not created" /* ErrorCode.SessionNotCreated */ , 'WS connection does not have an associated session.' ) ;
return ;
}
try {
await this . # closeBrowserInstanceIfLaunched ( session ) ;
this . # sessions . delete ( session . sessionId ) ;
}
catch ( e ) {
( 0 , exports . debugInfo ) ( 'Error while closing session' , e ) ;
this . # respondWithError ( connection , plainCommandData , "unknown error" /* ErrorCode.UnknownError */ , ` Session cannot be closed. Error: ${ e ? . message } ` ) ;
return ;
}
2024-02-28 17:13:10 -05:00
this . # sendClientMessage ( {
id : parsedCommandData . id ,
type : 'success' ,
2024-09-23 20:52:09 -04:00
result : { } ,
2024-02-28 17:13:10 -05:00
} , 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 ( ` Peer ${ connection . remoteAddress } disconnected. ` ) ;
// TODO: don't close Browser instance to allow re-connecting to the session.
await this . # closeBrowserInstanceIfLaunched ( session ) ;
2024-01-05 17:07:59 -05:00
} ) ;
}
2024-02-28 17:13:10 -05:00
async # closeBrowserInstanceIfLaunched ( session ) {
2024-01-05 17:07:59 -05:00
if ( session === undefined || session . browserInstancePromise === undefined ) {
return ;
}
const browserInstance = await session . browserInstancePromise ;
session . browserInstancePromise = undefined ;
void browserInstance . close ( ) ;
}
2024-09-23 20:52:09 -04:00
# getChromeOptions ( capabilities ) {
2024-01-05 17:07:59 -05:00
const chromeCapabilities = capabilities ? . alwaysMatch ? . [ 'goog:chromeOptions' ] ;
return {
chromeArgs : chromeCapabilities ? . args ? ? [ ] ,
chromeBinary : chromeCapabilities ? . binary ? ? undefined ,
} ;
}
2024-09-23 20:52:09 -04:00
async # launchBrowserInstance ( connection , sessionOptions , passSessionNewThrough = false ) {
2024-01-05 17:07:59 -05:00
( 0 , exports . debugInfo ) ( 'Scheduling browser launch...' ) ;
2024-09-23 20:52:09 -04:00
const browserInstance = await BrowserInstance _js _1 . BrowserInstance . run ( sessionOptions . chromeOptions , sessionOptions . verbose ) ;
const body = JSON . parse ( sessionOptions . sessionNewBody ) ;
const id = body . id ;
const sessionCreated = new Deferred _js _1 . Deferred ( ) ;
const sessionResponseListener = ( message ) => {
const jsonMessage = JSON . parse ( message ) ;
if ( jsonMessage [ 'id' ] === id ) {
( 0 , exports . debugInfo ) ( 'Receiving session.new response from mapper' , message ) ;
sessionCreated . resolve ( ) ;
if ( passSessionNewThrough ) {
this . # sendClientMessageString ( message , connection ) ;
}
}
} ;
browserInstance . bidiSession ( ) . on ( 'message' , sessionResponseListener ) ;
( 0 , exports . debugInfo ) ( 'Sending session.new to mapper' , sessionOptions . sessionNewBody ) ;
await browserInstance
. bidiSession ( )
. sendCommand ( sessionOptions . sessionNewBody ) ;
await sessionCreated ;
browserInstance . bidiSession ( ) . off ( 'message' , sessionResponseListener ) ;
2024-01-05 17:07:59 -05:00
// 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 ;
}
2024-02-28 17:13:10 -05:00
# sendClientMessageString ( message , connection ) {
2024-01-05 17:07:59 -05:00
if ( debugSend . enabled ) {
try {
debugSend ( JSON . parse ( message ) ) ;
}
catch {
debugSend ( message ) ;
}
}
connection . sendUTF ( message ) ;
}
2024-02-28 17:13:10 -05:00
# sendClientMessage ( object , connection ) {
2024-01-05 17:07:59 -05:00
const json = JSON . stringify ( object ) ;
return this . # sendClientMessageString ( json , connection ) ;
}
2024-02-28 17:13:10 -05:00
# respondWithError ( connection , plainCommandData , errorCode , errorMessage ) {
2024-01-05 17:07:59 -05:00
const errorResponse = this . # getErrorResponse ( plainCommandData , errorCode , errorMessage ) ;
void this . # sendClientMessage ( errorResponse , connection ) ;
}
2024-02-28 17:13:10 -05:00
# getErrorResponse ( plainCommandData , errorCode , errorMessage ) {
2024-01-05 17:07:59 -05:00
// 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.
} ;
}
2024-02-28 17:13:10 -05:00
# getHttpRequestPayload ( request ) {
2024-01-05 17:07:59 -05:00
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