mirror of
https://github.com/github/codeql-action.git
synced 2025-12-30 19:20:08 +08:00
245 lines
7.0 KiB
TypeScript
245 lines
7.0 KiB
TypeScript
import { ChildProcess } from 'child_process'
|
|
import { HttpRequestEventMap } from './glossary'
|
|
import { Interceptor } from './Interceptor'
|
|
import { BatchInterceptor } from './BatchInterceptor'
|
|
import { ClientRequestInterceptor } from './interceptors/ClientRequest'
|
|
import { XMLHttpRequestInterceptor } from './interceptors/XMLHttpRequest'
|
|
import { handleRequest } from './utils/handleRequest'
|
|
import { RequestController } from './RequestController'
|
|
import { FetchResponse } from './utils/fetchUtils'
|
|
|
|
export interface SerializedRequest {
|
|
id: string
|
|
url: string
|
|
method: string
|
|
headers: Array<[string, string]>
|
|
credentials: RequestCredentials
|
|
body: string
|
|
}
|
|
|
|
interface RevivedRequest extends Omit<SerializedRequest, 'url' | 'headers'> {
|
|
url: URL
|
|
headers: Headers
|
|
}
|
|
|
|
export interface SerializedResponse {
|
|
status: number
|
|
statusText: string
|
|
headers: Array<[string, string]>
|
|
body: string
|
|
}
|
|
|
|
export class RemoteHttpInterceptor extends BatchInterceptor<
|
|
[ClientRequestInterceptor, XMLHttpRequestInterceptor]
|
|
> {
|
|
constructor() {
|
|
super({
|
|
name: 'remote-interceptor',
|
|
interceptors: [
|
|
new ClientRequestInterceptor(),
|
|
new XMLHttpRequestInterceptor(),
|
|
],
|
|
})
|
|
}
|
|
|
|
protected setup() {
|
|
super.setup()
|
|
|
|
let handleParentMessage: NodeJS.MessageListener
|
|
|
|
this.on('request', async ({ request, requestId, controller }) => {
|
|
// Send the stringified intercepted request to
|
|
// the parent process where the remote resolver is established.
|
|
const serializedRequest = JSON.stringify({
|
|
id: requestId,
|
|
method: request.method,
|
|
url: request.url,
|
|
headers: Array.from(request.headers.entries()),
|
|
credentials: request.credentials,
|
|
body: ['GET', 'HEAD'].includes(request.method)
|
|
? null
|
|
: await request.text(),
|
|
} as SerializedRequest)
|
|
|
|
this.logger.info(
|
|
'sent serialized request to the child:',
|
|
serializedRequest
|
|
)
|
|
|
|
process.send?.(`request:${serializedRequest}`)
|
|
|
|
const responsePromise = new Promise<void>((resolve) => {
|
|
handleParentMessage = (message) => {
|
|
if (typeof message !== 'string') {
|
|
return resolve()
|
|
}
|
|
|
|
if (message.startsWith(`response:${requestId}`)) {
|
|
const [, serializedResponse] =
|
|
message.match(/^response:.+?:(.+)$/) || []
|
|
|
|
if (!serializedResponse) {
|
|
return resolve()
|
|
}
|
|
|
|
const responseInit = JSON.parse(
|
|
serializedResponse
|
|
) as SerializedResponse
|
|
|
|
const mockedResponse = new FetchResponse(responseInit.body, {
|
|
url: request.url,
|
|
status: responseInit.status,
|
|
statusText: responseInit.statusText,
|
|
headers: responseInit.headers,
|
|
})
|
|
|
|
/**
|
|
* @todo Support "errorWith" as well.
|
|
* This response handling from the child is incomplete.
|
|
*/
|
|
|
|
controller.respondWith(mockedResponse)
|
|
return resolve()
|
|
}
|
|
}
|
|
})
|
|
|
|
// Listen for the mocked response message from the parent.
|
|
this.logger.info(
|
|
'add "message" listener to the parent process',
|
|
handleParentMessage
|
|
)
|
|
process.addListener('message', handleParentMessage)
|
|
|
|
return responsePromise
|
|
})
|
|
|
|
this.subscriptions.push(() => {
|
|
process.removeListener('message', handleParentMessage)
|
|
})
|
|
}
|
|
}
|
|
|
|
export function requestReviver(key: string, value: any) {
|
|
switch (key) {
|
|
case 'url':
|
|
return new URL(value)
|
|
|
|
case 'headers':
|
|
return new Headers(value)
|
|
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
export interface RemoveResolverOptions {
|
|
process: ChildProcess
|
|
}
|
|
|
|
export class RemoteHttpResolver extends Interceptor<HttpRequestEventMap> {
|
|
static symbol = Symbol('remote-resolver')
|
|
private process: ChildProcess
|
|
|
|
constructor(options: RemoveResolverOptions) {
|
|
super(RemoteHttpResolver.symbol)
|
|
this.process = options.process
|
|
}
|
|
|
|
protected setup() {
|
|
const logger = this.logger.extend('setup')
|
|
|
|
const handleChildMessage: NodeJS.MessageListener = async (message) => {
|
|
logger.info('received message from child!', message)
|
|
|
|
if (typeof message !== 'string' || !message.startsWith('request:')) {
|
|
logger.info('unknown message, ignoring...')
|
|
return
|
|
}
|
|
|
|
const [, serializedRequest] = message.match(/^request:(.+)$/) || []
|
|
if (!serializedRequest) {
|
|
return
|
|
}
|
|
|
|
const requestJson = JSON.parse(
|
|
serializedRequest,
|
|
requestReviver
|
|
) as RevivedRequest
|
|
|
|
logger.info('parsed intercepted request', requestJson)
|
|
|
|
const request = new Request(requestJson.url, {
|
|
method: requestJson.method,
|
|
headers: new Headers(requestJson.headers),
|
|
credentials: requestJson.credentials,
|
|
body: requestJson.body,
|
|
})
|
|
|
|
const controller = new RequestController(request)
|
|
await handleRequest({
|
|
request,
|
|
requestId: requestJson.id,
|
|
controller,
|
|
emitter: this.emitter,
|
|
onResponse: async (response) => {
|
|
this.logger.info('received mocked response!', { response })
|
|
|
|
const responseClone = response.clone()
|
|
const responseText = await responseClone.text()
|
|
|
|
// // Send the mocked response to the child process.
|
|
const serializedResponse = JSON.stringify({
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: Array.from(response.headers.entries()),
|
|
body: responseText,
|
|
} as SerializedResponse)
|
|
|
|
this.process.send(
|
|
`response:${requestJson.id}:${serializedResponse}`,
|
|
(error) => {
|
|
if (error) {
|
|
return
|
|
}
|
|
|
|
// Emit an optimistic "response" event at this point,
|
|
// not to rely on the back-and-forth signaling for the sake of the event.
|
|
this.emitter.emit('response', {
|
|
request,
|
|
requestId: requestJson.id,
|
|
response: responseClone,
|
|
isMockedResponse: true,
|
|
})
|
|
}
|
|
)
|
|
|
|
logger.info(
|
|
'sent serialized mocked response to the parent:',
|
|
serializedResponse
|
|
)
|
|
},
|
|
onRequestError: (response) => {
|
|
this.logger.info('received a network error!', { response })
|
|
throw new Error('Not implemented')
|
|
},
|
|
onError: (error) => {
|
|
this.logger.info('request has errored!', { error })
|
|
throw new Error('Not implemented')
|
|
},
|
|
})
|
|
}
|
|
|
|
this.subscriptions.push(() => {
|
|
this.process.removeListener('message', handleChildMessage)
|
|
logger.info('removed the "message" listener from the child process!')
|
|
})
|
|
|
|
logger.info('adding a "message" listener to the child process')
|
|
this.process.addListener('message', handleChildMessage)
|
|
|
|
this.process.once('error', () => this.dispose())
|
|
this.process.once('exit', () => this.dispose())
|
|
}
|
|
}
|