UNPKG

taiko

Version:

Taiko is a Node.js library for automating Chromium based browsers

285 lines (264 loc) 7.95 kB
const { isFunction, isString, isObject } = require("../helper"); const { eventHandler } = require("../eventBus"); const headersMap = new Map(); const defaultErrorReason = "Failed"; let fetch; let userEnabledIntercept = false; let interceptors = []; const createdSessionListener = async (client) => { let resolve; eventHandler.emit( "handlerActingOnNewSession", new Promise((r) => { resolve = r; }), ); fetch = client.Fetch; if (userEnabledIntercept) { await enableFetchIntercept(); } resolve(); }; eventHandler.on("createdSession", createdSessionListener); const enableFetchIntercept = async () => { await fetch.enable({ patterns: [{ urlPattern: "*" }], }); await fetch.requestPaused(handleInterceptor); }; const extractHostName = (url) => { let { protocol, href, host } = new URL(url); if (protocol === "file:") { host = href; } return host; }; const addExtraHeadersToRequest = (p) => { const options = { requestId: p.requestId }; const host = extractHostName(p.request.url); const headers = headersMap.get(host); for (const headerName in headers) { if (!(headerName in p.request.headers)) { options.headers = options.headers || {}; options.headers[headerName] = headers[headerName]; } } if (options.headers) { options.headers = headerArray(options.headers); } return options; }; const filterInterceptorsAndWarnIfNeeded = (requestUrl) => { const matches = interceptors.filter((interceptor) => getMatchingInterceptor(interceptor, requestUrl), ); const matchesLen = matches.length; if (matchesLen > 1) { const matchesURL = matches.map((e) => `"${e.requestUrl}"`).join(","); console.warn( `WARNING: More than one intercept [${matchesURL}] found for request "${requestUrl}".\n Applying: intercept("${ matches[matchesLen - 1].requestUrl }", "${matches[matchesLen - 1].action}")`, ); } return matches.pop(); }; const warnInterceptFailed = (p) => { console.warn(`WARNING: Could not intercept request ${p.request.url}`); }; const handleInterceptor = (p) => { let options = addExtraHeadersToRequest(p); const interceptor = filterInterceptorsAndWarnIfNeeded(p.request.url); if (!interceptor) { fetch.continueRequest(options).catch(() => {}); return; } interceptor.count = interceptor.count - 1; switch (true) { //Blocks matching url case !interceptor.action: options.errorReason = defaultErrorReason; fetch.failRequest(options); break; //Mocks response and/or request based on callback case isFunction(interceptor.action): p.continue = (override) => overrideRequest(p, override, options); p.respond = (mock) => { options = mockResponse(mock, options); fetch.fulfillRequest(options).catch(() => warnInterceptFailed(p)); }; interceptor.action(p); break; //Redirects to given url case isString(interceptor.action): if ( !/^https?:\/\//i.test(interceptor.action) && !/^file/i.test(interceptor.action) ) { interceptor.action = `http://${interceptor.action}`; } options.url = interceptor.action; fetch.continueRequest(options).catch(() => warnInterceptFailed(p)); break; //Mocks response with given object case isObject(interceptor.action): options = mockResponse(interceptor.action, options); fetch.fulfillRequest(options).catch(() => warnInterceptFailed(p)); break; //Continue default request if none of the above matches default: fetch.continueRequest(options).catch(() => {}); } }; const mockResponse = (response, options) => { const responseBodyJson = isObject(response.body) ? JSON.stringify(response.body) : response.body; const responseBody = Buffer.from(responseBodyJson || ""); const responseHeaders = {}; if (response.headers) { for (const header of Object.keys(response.headers)) { responseHeaders[header.toLowerCase()] = response.headers[header]; } } if (response.contentType) { responseHeaders["content-type"] = response.contentType; } if (responseBody && !("content-length" in responseHeaders)) { responseHeaders["content-length"] = Buffer.byteLength(responseBody); } options.body = responseBody.toString("base64"); options.responseCode = response.status || 200; options.responsePhrase = statusTexts[response.status || 200]; options.responseHeaders = headerArray(responseHeaders); return options; }; const headerArray = (headers) => { const result = []; for (const name in headers) { if (!Object.is(headers[name], undefined)) { result.push({ name, value: `${headers[name]}` }); } } return result; }; const overrideRequest = (p, override, options) => { if (override) { override.postData = override.postData ? Buffer.from(override.postData).toString("base64") : undefined; for (const key in override) { options[key] = override[key]; } } fetch.continueRequest(options).catch(() => warnInterceptFailed(p)); }; const statusTexts = { 100: "Continue", 101: "Switching Protocols", 102: "Processing", 200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 206: "Partial Content", 207: "Multi-Status", 208: "Already Reported", 209: "IM Used", 300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 306: "Switch Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect", 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 418: "I'm a teapot", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 510: "Not Extended", 511: "Network Authentication Required", }; const getMatchingInterceptor = (interceptor, url) => { if (interceptor.count === 0) { return false; } if (url === interceptor.requestUrl) { return true; } const re = new RegExp(interceptor.requestUrl); const matches = url.match(re); if (matches === null || matches.length <= 0) { return decodeURI(url).match(re); } return matches; }; const addInterceptor = async (requestWithAction) => { interceptors.push(requestWithAction); if (!userEnabledIntercept) { userEnabledIntercept = true; await enableFetchIntercept(); } }; const resetInterceptor = (url) => { const originalLength = interceptors.length; interceptors = interceptors.filter( (interceptor) => !getMatchingInterceptor(interceptor, url), ); return originalLength !== interceptors.length; }; const resetInterceptors = () => { interceptors = []; userEnabledIntercept = false; }; const setHTTPHeaders = async (headers, url) => { const host = extractHostName(url); headersMap.set(host, headers); if (!userEnabledIntercept) { userEnabledIntercept = true; await enableFetchIntercept(); } }; module.exports = { addInterceptor, resetInterceptor, resetInterceptors, handleInterceptor, setHTTPHeaders, };