Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | /**
* I took inspiration in few stack exchange posts
* and Tencent vConsole library (MIT)
* by wrapping the XMLHttpRequest object in a Proxy
* we can intercept the network requests
* in not-so-hacky way
* */
import NetworkMessage from './networkMessage'
import { RequestState, INetworkMessage, RequestResponseData } from './types';
import { formatByteSize, genStringBody, getStringResponseByType, getURL } from './utils'
export class ResponseProxyHandler<T extends Response> implements ProxyHandler<T> {
public resp: Response
public item: NetworkMessage
constructor(resp: T, item: NetworkMessage) {
this.resp = resp
this.item = item
this.mockReader()
}
public set(target: T, key: string, value: (args: any[]) => any) {
return Reflect.set(target, key, value)
}
public get(target: T, key: string) {
const value = Reflect.get(target, key)
switch (key) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'json':
case 'text':
return () => {
this.item.responseType = <any>key.toLowerCase()
// @ts-ignore
return value.apply(target).then((resp: any) => {
this.item.response = getStringResponseByType(this.item.responseType, resp)
return resp
})
}
}
if (typeof value === 'function') {
return value.bind(target)
} else {
return value
}
}
protected mockReader() {
let readerReceivedValue: Uint8Array
if (!this.resp.body) {
// some browsers do not return `body` in some cases, like `OPTIONS` method
return
}
if (typeof this.resp.body.getReader !== 'function') {
return
}
const _getReader = this.resp.body.getReader
// @ts-ignore
this.resp.body.getReader = () => {
const reader = <ReturnType<typeof _getReader>>_getReader.apply(this.resp.body)
// when readyState is already 4,
// it's not a chunked stream, or it had already been read.
// so should not update status.
if (this.item.readyState === RequestState.DONE) {
return reader
}
const _read = reader.read
const _cancel = reader.cancel
this.item.responseType = 'arraybuffer'
// @ts-ignore
reader.read = () => {
return (<ReturnType<typeof _read>>_read.apply(reader)).then(
(result: ReadableStreamReadResult<Uint8Array>) => {
if (!readerReceivedValue) {
// @ts-ignore
readerReceivedValue = new Uint8Array(result.value)
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const newValue = new Uint8Array(readerReceivedValue.length + result.value!.length)
newValue.set(readerReceivedValue)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
newValue.set(result.value!, readerReceivedValue.length)
readerReceivedValue = newValue
}
this.item.endTime = performance.now()
this.item.duration = this.item.endTime - (this.item.startTime || this.item.endTime)
this.item.readyState = result.done ? 4 : 3
this.item.statusText = result.done ? String(this.item.status) : 'Loading'
this.item.responseSize = readerReceivedValue.length
this.item.responseSizeText = formatByteSize(this.item.responseSize)
if (result.done) {
this.item.response = getStringResponseByType(
this.item.responseType,
readerReceivedValue,
)
}
return result
},
)
}
reader.cancel = (...args) => {
this.item.cancelState = 2
this.item.statusText = 'Cancel'
this.item.endTime = performance.now()
this.item.duration = this.item.endTime - (this.item.startTime || this.item.endTime)
this.item.response = getStringResponseByType(this.item.responseType, readerReceivedValue)
return _cancel.apply(reader, args)
}
return reader
}
}
}
export class FetchProxyHandler<T extends typeof fetch> implements ProxyHandler<T> {
constructor(
private readonly ignoredHeaders: boolean | string[],
private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
private readonly sendMessage: (item: INetworkMessage) => void,
private readonly isServiceUrl: (url: string) => boolean,
private readonly tokenUrlMatcher?: (url: string) => boolean,
) {}
public apply(target: T, _: typeof window, argsList: [RequestInfo | URL, RequestInit]) {
const input = argsList[0]
const init = argsList[1]
if (
!input ||
// @ts-ignore
(typeof input !== 'string' && !input?.url)
) {
return <ReturnType<T>>target.apply(window, argsList)
}
const isORUrl =
input instanceof URL || typeof input === 'string'
? this.isServiceUrl(String(input))
: this.isServiceUrl(String(input.url))
if (isORUrl) {
return target.apply(window, argsList)
}
const item = new NetworkMessage(this.ignoredHeaders, this.setSessionTokenHeader, this.sanitize)
this.beforeFetch(item, input as RequestInfo, init)
this.setSessionTokenHeader((name, value) => {
if (this.tokenUrlMatcher !== undefined) {
if (!this.tokenUrlMatcher(item.url)) {
return
}
}
if (argsList[1] === undefined && argsList[0] instanceof Request) {
return argsList[0].headers.append(name, value)
} else {
if (!argsList[1]) argsList[1] = {}
if (argsList[1].headers === undefined) {
argsList[1] = { ...argsList[1], headers: {} }
}
if (argsList[1].headers instanceof Headers) {
argsList[1].headers.append(name, value)
} else if (Array.isArray(argsList[1].headers)) {
argsList[1].headers.push([name, value])
} else {
// @ts-ignore
argsList[1].headers[name] = value
}
}
})
return (<ReturnType<T>>target.apply(window, argsList))
.then(this.afterFetch(item))
.catch((e) => {
// mock finally
item.endTime = performance.now()
item.duration = item.endTime - (item.startTime || item.endTime)
throw e
})
}
protected beforeFetch(item: NetworkMessage, input: RequestInfo | string, init?: RequestInit) {
let url: URL,
method = 'GET',
requestHeader: HeadersInit = {}
// handle `input` content
if (typeof input === 'string') {
// when `input` is a string
method = init?.method || 'GET'
url = getURL(input)
requestHeader = init?.headers || {}
} else {
// when `input` is a `Request` object
method = input.method || 'GET'
url = getURL(input.url)
requestHeader = input.headers
}
item.method = <NetworkMessage['method']>method
item.requestType = 'fetch'
item.requestHeader = requestHeader
item.url = url.toString()
item.name = (url.pathname.split('/').pop() || '') + url.search
item.status = 0
item.statusText = 'Pending'
item.readyState = 1
if (!item.startTime) {
// UNSENT
item.startTime = performance.now()
}
if (Object.prototype.toString.call(requestHeader) === '[object Headers]') {
item.requestHeader = {}
for (const [key, value] of <Headers>requestHeader) {
item.requestHeader[key] = value
}
} else {
item.requestHeader = requestHeader
}
// save GET data
if (url.search && url.searchParams) {
item.getData = {}
for (const [key, value] of url.searchParams) {
item.getData[key] = value
}
}
// save POST data
if (init?.body) {
item.requestData = genStringBody(init.body)
}
}
protected afterFetch(item: NetworkMessage) {
return (resp: Response) => {
item.endTime = performance.now()
item.duration = item.endTime - (item.startTime || item.endTime)
item.status = resp.status
item.statusText = String(resp.status)
let isChunked = false
item.header = {}
for (const [key, value] of resp.headers) {
item.header[key] = value
isChunked = value.toLowerCase().indexOf('chunked') > -1 ? true : isChunked
}
if (isChunked) {
// when `transfer-encoding` is chunked, the response is a stream which is under loading,
// so the `readyState` should be 3 (Loading),
// and the response should NOT be `clone()` which will affect stream reading.
item.readyState = 3
} else {
// Otherwise, not chunked, the response is not a stream,
// so it's completed and can be cloned for `text()` calling.
item.readyState = 4
this.handleResponseBody(resp.clone(), item)
.then((responseValue: string | ArrayBuffer) => {
item.responseSize =
typeof responseValue === 'string' ? responseValue.length : responseValue.byteLength
item.responseSizeText = formatByteSize(item.responseSize)
item.response = getStringResponseByType(item.responseType, responseValue)
const msg = item.getMessage()
if (msg) {
this.sendMessage(msg)
}
})
.catch((e) => {
if (e.name !== 'AbortError') {
throw e
} else {
// ignore AbortError
}
})
}
return new Proxy(resp, new ResponseProxyHandler(resp, item))
}
}
protected handleResponseBody(resp: Response, item: NetworkMessage) {
// parse response body by Content-Type
const contentType = resp.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
item.responseType = 'json'
return resp.text()
} else if (
contentType &&
(contentType.includes('text/html') || contentType.includes('text/plain'))
) {
item.responseType = 'text'
return resp.text()
} else {
item.responseType = 'arraybuffer'
return resp.arrayBuffer()
}
}
}
export default class FetchProxy {
public static create(
ignoredHeaders: boolean | string[],
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
sanitize: (data: RequestResponseData) => RequestResponseData | null,
sendMessage: (item: INetworkMessage) => void,
isServiceUrl: (url: string) => boolean,
tokenUrlMatcher?: (url: string) => boolean,
) {
return new Proxy(
fetch,
new FetchProxyHandler(
ignoredHeaders,
setSessionTokenHeader,
sanitize,
sendMessage,
isServiceUrl,
tokenUrlMatcher,
),
)
}
}
|