- Delete old Vite+Svelte frontend - Initialize new SvelteKit project with TypeScript - Configure Tailwind CSS v4 + DaisyUI - Implement JWT authentication with auto-refresh - Create login page with form validation (Zod) - Add protected route guards - Update Docker configuration for single-stage build - Add E2E tests with Playwright (6/11 passing) - Fix Svelte 5 reactivity with $state() runes Known issues: - 5 E2E tests failing (timing/async issues) - Token refresh implementation needs debugging - Validation error display timing
838 lines
38 KiB
JavaScript
838 lines
38 KiB
JavaScript
"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 () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__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 });
|
|
const PropertySymbol = __importStar(require("../PropertySymbol.cjs"));
|
|
const Headers_js_1 = __importDefault(require("./Headers.cjs"));
|
|
const FetchRequestReferrerUtility_js_1 = __importDefault(require("./utilities/FetchRequestReferrerUtility.cjs"));
|
|
const DOMExceptionNameEnum_js_1 = __importDefault(require("../exception/DOMExceptionNameEnum.cjs"));
|
|
const http_1 = __importDefault(require("http"));
|
|
const https_1 = __importDefault(require("https"));
|
|
const zlib_1 = __importDefault(require("zlib"));
|
|
const url_1 = require("url");
|
|
const fs_1 = __importDefault(require("fs"));
|
|
const path_1 = __importDefault(require("path"));
|
|
const stream_1 = __importDefault(require("stream"));
|
|
const DataURIParser_js_1 = __importDefault(require("./data-uri/DataURIParser.cjs"));
|
|
const FetchCORSUtility_js_1 = __importDefault(require("./utilities/FetchCORSUtility.cjs"));
|
|
const Response_js_1 = __importDefault(require("./Response.cjs"));
|
|
const CachedResponseStateEnum_js_1 = __importDefault(require("./cache/response/CachedResponseStateEnum.cjs"));
|
|
const FetchRequestHeaderUtility_js_1 = __importDefault(require("./utilities/FetchRequestHeaderUtility.cjs"));
|
|
const FetchRequestValidationUtility_js_1 = __importDefault(require("./utilities/FetchRequestValidationUtility.cjs"));
|
|
const FetchResponseRedirectUtility_js_1 = __importDefault(require("./utilities/FetchResponseRedirectUtility.cjs"));
|
|
const FetchResponseHeaderUtility_js_1 = __importDefault(require("./utilities/FetchResponseHeaderUtility.cjs"));
|
|
const FetchHTTPSCertificate_js_1 = __importDefault(require("./certificate/FetchHTTPSCertificate.cjs"));
|
|
const buffer_1 = require("buffer");
|
|
const FetchBodyUtility_js_1 = __importDefault(require("./utilities/FetchBodyUtility.cjs"));
|
|
const VirtualServerUtility_js_1 = __importDefault(require("./utilities/VirtualServerUtility.cjs"));
|
|
const PreloadUtility_js_1 = __importDefault(require("./preload/PreloadUtility.cjs"));
|
|
const LAST_CHUNK = buffer_1.Buffer.from('0\r\n\r\n');
|
|
/**
|
|
* Handles fetch requests.
|
|
*
|
|
* Based on:
|
|
* https://github.com/node-fetch/node-fetch/blob/main/src/index.js
|
|
*
|
|
* @see https://fetch.spec.whatwg.org/#http-network-fetch
|
|
*/
|
|
class Fetch {
|
|
reject = null;
|
|
resolve = null;
|
|
listeners = {
|
|
onSignalAbort: this.onSignalAbort.bind(this)
|
|
};
|
|
isChunkedTransfer = false;
|
|
isProperLastChunkReceived = false;
|
|
previousChunk = null;
|
|
nodeRequest = null;
|
|
nodeResponse = null;
|
|
response = null;
|
|
responseHeaders = null;
|
|
interceptor;
|
|
request;
|
|
redirectCount = 0;
|
|
disableCache;
|
|
disableSameOriginPolicy;
|
|
disablePreload;
|
|
#browserFrame;
|
|
#window;
|
|
#unfilteredHeaders = null;
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param options Options.
|
|
* @param options.browserFrame Browser frame.
|
|
* @param options.window Window.
|
|
* @param options.url URL.
|
|
* @param [options.init] Init.
|
|
* @param [options.redirectCount] Redirect count.
|
|
* @param [options.contentType] Content Type.
|
|
* @param [options.disableCache] Disables the use of cached responses. It will still store the response in the cache.
|
|
* @param [options.disableSameOriginPolicy] Disables the Same-Origin policy.
|
|
* @param [options.unfilteredHeaders] Unfiltered headers - necessary for preflight requests.
|
|
* @param [options.disablePreload] Disables the use of preloaded responses.
|
|
*/
|
|
constructor(options) {
|
|
this.#browserFrame = options.browserFrame;
|
|
this.#window = options.window;
|
|
this.#unfilteredHeaders = options.unfilteredHeaders ?? null;
|
|
this.request =
|
|
typeof options.url === 'string' || options.url instanceof url_1.URL
|
|
? new options.window.Request(options.url, options.init)
|
|
: options.url;
|
|
if (options.contentType) {
|
|
this.request[PropertySymbol.contentType] = options.contentType;
|
|
}
|
|
this.redirectCount = options.redirectCount ?? 0;
|
|
this.disableCache = options.disableCache ?? false;
|
|
this.disableSameOriginPolicy =
|
|
options.disableSameOriginPolicy ??
|
|
this.#browserFrame.page.context.browser.settings.fetch.disableSameOriginPolicy ??
|
|
false;
|
|
this.interceptor = this.#browserFrame.page.context.browser.settings.fetch.interceptor;
|
|
this.disablePreload = options.disablePreload ?? false;
|
|
}
|
|
/**
|
|
* Sends request.
|
|
*
|
|
* @returns Response.
|
|
*/
|
|
async send() {
|
|
FetchRequestReferrerUtility_js_1.default.prepareRequest(new url_1.URL(this.#window.location.href), this.request);
|
|
if (this.interceptor?.beforeAsyncRequest) {
|
|
const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask();
|
|
const response = await this.interceptor.beforeAsyncRequest({
|
|
request: this.request,
|
|
window: this.#window
|
|
});
|
|
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
|
|
if (response instanceof Response_js_1.default) {
|
|
return response;
|
|
}
|
|
}
|
|
FetchRequestValidationUtility_js_1.default.validateSchema(this.request);
|
|
if (this.request.signal[PropertySymbol.aborted]) {
|
|
if (this.request.signal[PropertySymbol.reason] !== undefined) {
|
|
throw this.request.signal[PropertySymbol.reason];
|
|
}
|
|
throw new this[PropertySymbol.window].DOMException('signal is aborted without reason', DOMExceptionNameEnum_js_1.default.abortError);
|
|
}
|
|
if (this.request[PropertySymbol.url].protocol === 'data:') {
|
|
const result = DataURIParser_js_1.default.parse(this.request.url);
|
|
this.response = new this.#window.Response(result.buffer, {
|
|
headers: { 'Content-Type': result.type }
|
|
});
|
|
const interceptedResponse = this.interceptor?.afterAsyncResponse
|
|
? await this.interceptor.afterAsyncResponse({
|
|
window: this.#window,
|
|
response: this.response,
|
|
request: this.request
|
|
})
|
|
: undefined;
|
|
return interceptedResponse instanceof Response_js_1.default ? interceptedResponse : this.response;
|
|
}
|
|
// Security check for "https" to "http" requests.
|
|
if (this.request[PropertySymbol.url].protocol === 'http:' &&
|
|
this.#window.location.protocol === 'https:') {
|
|
throw new this.#window.DOMException(`Mixed Content: The page at '${this.#window.location.href}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${this.request.url}'. This request has been blocked; the content must be served over HTTPS.`, DOMExceptionNameEnum_js_1.default.securityError);
|
|
}
|
|
if (!this.disableCache) {
|
|
const cachedResponse = await this.getCachedResponse();
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
}
|
|
if (!this.disablePreload) {
|
|
const preloadKey = PreloadUtility_js_1.default.getKey({
|
|
url: this.request.url,
|
|
destination: 'fetch',
|
|
mode: this.request.mode,
|
|
credentialsMode: this.request.credentials
|
|
});
|
|
const preloadEntry = this.#window.document[PropertySymbol.preloads].get(preloadKey);
|
|
if (preloadEntry) {
|
|
this.#window.document[PropertySymbol.preloads].delete(preloadKey);
|
|
if (preloadEntry.response) {
|
|
return preloadEntry.response;
|
|
}
|
|
const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask();
|
|
const response = await preloadEntry.onResponseAvailable();
|
|
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
|
|
return response;
|
|
}
|
|
}
|
|
const virtualServerResponse = await this.getVirtualServerResponse();
|
|
if (virtualServerResponse) {
|
|
return virtualServerResponse;
|
|
}
|
|
if (!this.disableSameOriginPolicy) {
|
|
const compliesWithCrossOriginPolicy = await this.compliesWithCrossOriginPolicy();
|
|
if (!compliesWithCrossOriginPolicy) {
|
|
this.#browserFrame?.page?.console.warn(`Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at "${this.request.url}".`);
|
|
throw new this.#window.DOMException(`Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at "${this.request.url}".`, DOMExceptionNameEnum_js_1.default.networkError);
|
|
}
|
|
}
|
|
return await this.sendRequest();
|
|
}
|
|
/**
|
|
* Returns cached response.
|
|
*
|
|
* @returns Response.
|
|
*/
|
|
async getCachedResponse() {
|
|
if (this.disableCache) {
|
|
return null;
|
|
}
|
|
let cachedResponse = this.#browserFrame.page.context.responseCache.get(this.request);
|
|
if (!cachedResponse || cachedResponse.response.waitingForBody) {
|
|
return null;
|
|
}
|
|
if (cachedResponse.state === CachedResponseStateEnum_js_1.default.stale) {
|
|
const headers = new Headers_js_1.default(cachedResponse.request.headers);
|
|
if (cachedResponse.etag) {
|
|
headers.set('If-None-Match', cachedResponse.etag);
|
|
}
|
|
else {
|
|
if (!cachedResponse.lastModified) {
|
|
return null;
|
|
}
|
|
headers.set('If-Modified-Since', new Date(cachedResponse.lastModified).toUTCString());
|
|
}
|
|
const fetch = new Fetch({
|
|
browserFrame: this.#browserFrame,
|
|
window: this.#window,
|
|
url: this.request.url,
|
|
init: { headers, method: cachedResponse.request.method },
|
|
disableCache: true,
|
|
disableSameOriginPolicy: true
|
|
});
|
|
if (cachedResponse.etag || !cachedResponse.staleWhileRevalidate) {
|
|
const validateResponse = await fetch.send();
|
|
const body = validateResponse.status !== 304 ? await validateResponse.buffer() : null;
|
|
cachedResponse = this.#browserFrame.page.context.responseCache.add(this.request, {
|
|
...validateResponse,
|
|
body,
|
|
waitingForBody: false
|
|
});
|
|
if (validateResponse.status !== 304) {
|
|
const response = new this.#window.Response(body, {
|
|
status: validateResponse.status,
|
|
statusText: validateResponse.statusText,
|
|
headers: validateResponse.headers
|
|
});
|
|
response.url = validateResponse.url;
|
|
response[PropertySymbol.cachedResponse] = cachedResponse;
|
|
return response;
|
|
}
|
|
}
|
|
else {
|
|
fetch.send().then((response) => {
|
|
response.buffer().then((body) => {
|
|
this.#browserFrame.page.context.responseCache.add(this.request, {
|
|
...response,
|
|
body,
|
|
waitingForBody: false
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
if (!cachedResponse || cachedResponse.response.waitingForBody) {
|
|
return null;
|
|
}
|
|
const response = new this.#window.Response(cachedResponse.response.body, {
|
|
status: cachedResponse.response.status,
|
|
statusText: cachedResponse.response.statusText,
|
|
headers: cachedResponse.response.headers
|
|
});
|
|
response.url = cachedResponse.response.url;
|
|
response[PropertySymbol.cachedResponse] = cachedResponse;
|
|
return response;
|
|
}
|
|
/**
|
|
* Returns virtual server response.
|
|
*
|
|
* @returns Response.
|
|
*/
|
|
async getVirtualServerResponse() {
|
|
const filePath = VirtualServerUtility_js_1.default.getFilepath(this.#window, this.request.url);
|
|
if (!filePath) {
|
|
return null;
|
|
}
|
|
const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask();
|
|
if (this.request.method !== 'GET') {
|
|
this.#browserFrame?.page?.console.error(`${this.request.method} ${this.request.url} 404 (Not Found)`);
|
|
const response = VirtualServerUtility_js_1.default.getNotFoundResponse(this.#window);
|
|
const interceptedResponse = this.interceptor?.afterAsyncResponse
|
|
? await this.interceptor.afterAsyncResponse({
|
|
window: this.#window,
|
|
response: await response,
|
|
request: this.request
|
|
})
|
|
: undefined;
|
|
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
|
|
return interceptedResponse instanceof Response_js_1.default ? interceptedResponse : response;
|
|
}
|
|
let buffer;
|
|
try {
|
|
const stat = await fs_1.default.promises.stat(filePath);
|
|
buffer = await fs_1.default.promises.readFile(stat.isDirectory() ? path_1.default.join(filePath, 'index.html') : filePath);
|
|
}
|
|
catch (error) {
|
|
this.#browserFrame?.page?.console.error(`${this.request.method} ${this.request.url} 404 (Not Found)`);
|
|
const response = VirtualServerUtility_js_1.default.getNotFoundResponse(this.#window);
|
|
const interceptedResponse = this.interceptor?.afterAsyncResponse
|
|
? await this.interceptor.afterAsyncResponse({
|
|
window: this.#window,
|
|
response: await response,
|
|
request: this.request
|
|
})
|
|
: undefined;
|
|
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
|
|
return interceptedResponse instanceof Response_js_1.default ? interceptedResponse : response;
|
|
}
|
|
const body = new this.#window.ReadableStream({
|
|
start(controller) {
|
|
setTimeout(() => {
|
|
controller.enqueue(buffer);
|
|
controller.close();
|
|
});
|
|
}
|
|
});
|
|
const response = new this.#window.Response(body);
|
|
response[PropertySymbol.buffer] = buffer;
|
|
response.url = this.request.url;
|
|
const interceptedResponse = this.interceptor?.afterAsyncResponse
|
|
? await this.interceptor.afterAsyncResponse({
|
|
window: this.#window,
|
|
response: await response,
|
|
request: this.request
|
|
})
|
|
: undefined;
|
|
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
|
|
const returnResponse = interceptedResponse instanceof Response_js_1.default ? interceptedResponse : response;
|
|
const cachedResponse = {
|
|
...returnResponse,
|
|
body: buffer,
|
|
waitingForBody: false
|
|
};
|
|
response[PropertySymbol.cachedResponse] = this.#browserFrame.page?.context?.responseCache.add(this.request, cachedResponse);
|
|
return returnResponse;
|
|
}
|
|
/**
|
|
* Checks if the request complies with the Cross-Origin policy.
|
|
*
|
|
* @returns True if it complies with the policy.
|
|
*/
|
|
async compliesWithCrossOriginPolicy() {
|
|
if (this.disableSameOriginPolicy ||
|
|
!FetchCORSUtility_js_1.default.isCORS(this.#window.location.href, this.request[PropertySymbol.url])) {
|
|
return true;
|
|
}
|
|
const cachedPreflightResponse = this.#browserFrame.page.context.preflightResponseCache.get(this.request);
|
|
if (cachedPreflightResponse) {
|
|
if (cachedPreflightResponse.allowOrigin !== '*' &&
|
|
cachedPreflightResponse.allowOrigin !== this.#window.location.origin) {
|
|
return false;
|
|
}
|
|
if (cachedPreflightResponse.allowMethods.length !== 0 &&
|
|
!cachedPreflightResponse.allowMethods.includes(this.request.method)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
const requestHeaders = [];
|
|
for (const [header] of this.request.headers) {
|
|
requestHeaders.push(header.toLowerCase());
|
|
}
|
|
const corsHeaders = new Headers_js_1.default({
|
|
'Access-Control-Request-Method': this.request.method,
|
|
Origin: this.#window.location.origin
|
|
});
|
|
if (requestHeaders.length > 0) {
|
|
// This intentionally does not use "combine" (comma + space), as the spec dictates.
|
|
// See https://fetch.spec.whatwg.org/#cors-preflight-fetch for more details.
|
|
// Sorting the headers is not required, but can optimize cache hits.
|
|
corsHeaders.set('Access-Control-Request-Headers', requestHeaders.slice().sort().join(','));
|
|
}
|
|
const fetch = new Fetch({
|
|
browserFrame: this.#browserFrame,
|
|
window: this.#window,
|
|
url: this.request.url,
|
|
init: { method: 'OPTIONS' },
|
|
disableCache: true,
|
|
disableSameOriginPolicy: true,
|
|
unfilteredHeaders: corsHeaders
|
|
});
|
|
const response = await fetch.send();
|
|
if (!response.ok) {
|
|
return false;
|
|
}
|
|
const allowOrigin = response.headers.get('Access-Control-Allow-Origin');
|
|
if (!allowOrigin) {
|
|
return false;
|
|
}
|
|
if (allowOrigin !== '*' && allowOrigin !== this.#window.location.origin) {
|
|
return false;
|
|
}
|
|
const allowMethods = [];
|
|
if (response.headers.has('Access-Control-Allow-Methods')) {
|
|
const allowMethodsHeader = response.headers.get('Access-Control-Allow-Methods');
|
|
if (allowMethodsHeader !== '*') {
|
|
for (const method of allowMethodsHeader.split(',')) {
|
|
allowMethods.push(method.trim().toUpperCase());
|
|
}
|
|
}
|
|
}
|
|
if (allowMethods.length !== 0 && !allowMethods.includes(this.request.method)) {
|
|
return false;
|
|
}
|
|
// TODO: Add support for more Access-Control-Allow-* headers.
|
|
return true;
|
|
}
|
|
/**
|
|
* Sends request.
|
|
*
|
|
* @returns Response.
|
|
*/
|
|
sendRequest() {
|
|
return new Promise((resolve, reject) => {
|
|
const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(() => this.onAsyncTaskManagerAbort());
|
|
if (this.resolve) {
|
|
throw new this.#window.Error('Fetch already sent.');
|
|
}
|
|
this.resolve = async (response) => {
|
|
// We can end up here when closing down the browser frame and there is an ongoing request.
|
|
// Therefore, we need to check if browserFrame.page.context is still available.
|
|
if (!this.disableCache &&
|
|
response instanceof Response_js_1.default &&
|
|
this.#browserFrame.page &&
|
|
this.#browserFrame.page.context) {
|
|
response[PropertySymbol.cachedResponse] =
|
|
this.#browserFrame.page.context.responseCache.add(this.request, {
|
|
...response,
|
|
headers: this.responseHeaders,
|
|
body: response[PropertySymbol.buffer],
|
|
waitingForBody: !response[PropertySymbol.buffer] && !!response.body
|
|
});
|
|
}
|
|
const interceptedResponse = this.interceptor?.afterAsyncResponse
|
|
? await this.interceptor.afterAsyncResponse({
|
|
window: this.#window,
|
|
response: await response,
|
|
request: this.request
|
|
})
|
|
: undefined;
|
|
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
|
|
const returnResponse = interceptedResponse instanceof Response_js_1.default ? interceptedResponse : response;
|
|
// The browser outputs errors to the console when the response is not ok.
|
|
if (returnResponse instanceof Response_js_1.default && !returnResponse.ok) {
|
|
this.#browserFrame?.page?.console.error(`${this.request.method} ${this.request.url} ${returnResponse.status} (${returnResponse.statusText})`);
|
|
}
|
|
resolve(returnResponse);
|
|
};
|
|
this.reject = (error) => {
|
|
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
|
|
reject(error);
|
|
};
|
|
this.request.signal.addEventListener('abort', this.listeners.onSignalAbort);
|
|
const send = (this.request[PropertySymbol.url].protocol === 'https:' ? https_1.default : http_1.default).request;
|
|
this.nodeRequest = send(this.request[PropertySymbol.url].href, {
|
|
method: this.request.method,
|
|
headers: FetchRequestHeaderUtility_js_1.default.getRequestHeaders({
|
|
browserFrame: this.#browserFrame,
|
|
window: this.#window,
|
|
request: this.request,
|
|
baseHeaders: this.#unfilteredHeaders
|
|
}),
|
|
agent: false,
|
|
rejectUnauthorized: !this.#browserFrame.page.context.browser.settings.fetch.disableStrictSSL,
|
|
key: this.request[PropertySymbol.url].protocol === 'https:'
|
|
? FetchHTTPSCertificate_js_1.default.key
|
|
: undefined,
|
|
cert: this.request[PropertySymbol.url].protocol === 'https:'
|
|
? FetchHTTPSCertificate_js_1.default.cert
|
|
: undefined
|
|
});
|
|
this.nodeRequest.on('error', this.onError.bind(this));
|
|
this.nodeRequest.on('socket', this.onSocket.bind(this));
|
|
this.nodeRequest.on('response', this.onResponse.bind(this));
|
|
if (this.request.body === null) {
|
|
this.nodeRequest.end();
|
|
}
|
|
else {
|
|
stream_1.default.pipeline(this.request.body, this.nodeRequest, (error) => {
|
|
if (error) {
|
|
this.onError(error);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Event listener for "socket" event.
|
|
*
|
|
* @param socket Socket.
|
|
*/
|
|
onSocket(socket) {
|
|
const onSocketClose = () => {
|
|
if (this.isChunkedTransfer && !this.isProperLastChunkReceived) {
|
|
const error = new this.#window.DOMException('Premature close.', DOMExceptionNameEnum_js_1.default.networkError);
|
|
if (this.response && this.response.body) {
|
|
this.response.body[PropertySymbol.error] = error;
|
|
if (!this.response.body.locked) {
|
|
this.response.body.cancel(error);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const onData = (buffer) => {
|
|
this.isProperLastChunkReceived = buffer_1.Buffer.compare(buffer.slice(-5), LAST_CHUNK) === 0;
|
|
// Sometimes final 0-length chunk and end of message code are in separate packets.
|
|
if (!this.isProperLastChunkReceived && this.previousChunk) {
|
|
this.isProperLastChunkReceived =
|
|
buffer_1.Buffer.compare(this.previousChunk.slice(-3), LAST_CHUNK.slice(0, 3)) === 0 &&
|
|
buffer_1.Buffer.compare(buffer.slice(-2), LAST_CHUNK.slice(3)) === 0;
|
|
}
|
|
this.previousChunk = buffer;
|
|
};
|
|
socket.prependListener('close', onSocketClose);
|
|
socket.on('data', onData);
|
|
this.nodeRequest.on('close', () => {
|
|
socket.removeListener('close', onSocketClose);
|
|
socket.removeListener('data', onData);
|
|
});
|
|
}
|
|
/**
|
|
* Event listener for signal "abort" event.
|
|
*
|
|
* @param event Event.
|
|
*/
|
|
onSignalAbort(event) {
|
|
this.finalizeRequest();
|
|
this.abort(event.target?.reason);
|
|
}
|
|
/**
|
|
* Event listener for request "error" event.
|
|
*
|
|
* @param error Error.
|
|
*/
|
|
onError(error) {
|
|
this.finalizeRequest();
|
|
this.#browserFrame?.page?.console.error(error);
|
|
this.reject(new this.#window.DOMException(`Failed to execute "fetch()" on "Window" with URL "${this.request.url}": ${error.message}`, DOMExceptionNameEnum_js_1.default.networkError));
|
|
}
|
|
/**
|
|
* Triggered when the async task manager aborts.
|
|
*/
|
|
onAsyncTaskManagerAbort() {
|
|
const error = new this.#window.DOMException('The operation was aborted.', DOMExceptionNameEnum_js_1.default.abortError);
|
|
this.request[PropertySymbol.aborted] = true;
|
|
if (this.request.body) {
|
|
this.request.body[PropertySymbol.error] = error;
|
|
}
|
|
if (this.listeners.onSignalAbort) {
|
|
this.request.signal.removeEventListener('abort', this.listeners.onSignalAbort);
|
|
}
|
|
if (this.nodeRequest && !this.nodeRequest.destroyed) {
|
|
this.nodeRequest.destroy(error);
|
|
}
|
|
if (this.nodeResponse && !this.nodeResponse.destroyed) {
|
|
this.nodeResponse.destroy(error);
|
|
}
|
|
if (this.response && this.response.body) {
|
|
this.response.body[PropertySymbol.error] = error;
|
|
if (!this.response.body.locked) {
|
|
this.response.body.cancel(error);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Event listener for request "response" event.
|
|
*
|
|
* @param nodeResponse Node response.
|
|
*/
|
|
onResponse(nodeResponse) {
|
|
// Needed for handling bad endings of chunked transfer.
|
|
this.isChunkedTransfer =
|
|
nodeResponse.headers['transfer-encoding'] === 'chunked' &&
|
|
!nodeResponse.headers['content-length'];
|
|
this.nodeRequest.setTimeout(0);
|
|
this.responseHeaders = FetchResponseHeaderUtility_js_1.default.parseResponseHeaders({
|
|
browserFrame: this.#browserFrame,
|
|
requestURL: this.request[PropertySymbol.url],
|
|
rawHeaders: nodeResponse.rawHeaders
|
|
});
|
|
if (this.handleRedirectResponse(nodeResponse, this.responseHeaders)) {
|
|
return;
|
|
}
|
|
nodeResponse.once('end', () => this.request.signal.removeEventListener('abort', this.listeners.onSignalAbort));
|
|
let body = stream_1.default.pipeline(nodeResponse, new stream_1.default.PassThrough(), (error) => {
|
|
if (error) {
|
|
// Ignore error as it is forwarded to the response body.
|
|
}
|
|
});
|
|
const responseOptions = {
|
|
status: nodeResponse.statusCode,
|
|
statusText: nodeResponse.statusMessage,
|
|
headers: this.responseHeaders
|
|
};
|
|
const contentEncodingHeader = this.responseHeaders.get('Content-Encoding');
|
|
if (this.request.method === 'HEAD' ||
|
|
contentEncodingHeader === null ||
|
|
nodeResponse.statusCode === 204 ||
|
|
nodeResponse.statusCode === 304) {
|
|
this.response = new this.#window.Response(FetchBodyUtility_js_1.default.nodeToWebStream(body), responseOptions);
|
|
this.response.redirected = this.redirectCount > 0;
|
|
this.response.url = this.request.url;
|
|
this.resolve(this.response);
|
|
return;
|
|
}
|
|
// For GZip
|
|
if (contentEncodingHeader === 'gzip' || contentEncodingHeader === 'x-gzip') {
|
|
// Be less strict when decoding compressed responses.
|
|
// Sometimes servers send slightly invalid responses that are still accepted by common browsers.
|
|
// "cURL" always uses Z_SYNC_FLUSH.
|
|
const zlibOptions = {
|
|
flush: zlib_1.default.constants.Z_SYNC_FLUSH,
|
|
finishFlush: zlib_1.default.constants.Z_SYNC_FLUSH
|
|
};
|
|
body = stream_1.default.pipeline(body, zlib_1.default.createGunzip(zlibOptions), (error) => {
|
|
if (error) {
|
|
// Ignore error as it is forwarded to the response body.
|
|
}
|
|
});
|
|
this.response = new this.#window.Response(FetchBodyUtility_js_1.default.nodeToWebStream(body), responseOptions);
|
|
this.response.redirected = this.redirectCount > 0;
|
|
this.response.url = this.request.url;
|
|
this.resolve(this.response);
|
|
return;
|
|
}
|
|
// For Deflate
|
|
if (contentEncodingHeader === 'deflate' || contentEncodingHeader === 'x-deflate') {
|
|
// Handle the infamous raw deflate response from old servers
|
|
// A hack for old IIS and Apache servers
|
|
const raw = stream_1.default.pipeline(nodeResponse, new stream_1.default.PassThrough(), (error) => {
|
|
if (error) {
|
|
// Ignore error as it is forwarded to the response body.
|
|
}
|
|
});
|
|
raw.on('data', (chunk) => {
|
|
// See http://stackoverflow.com/questions/37519828
|
|
if ((chunk[0] & 0x0f) === 0x08) {
|
|
body = stream_1.default.pipeline(body, zlib_1.default.createInflate(), (error) => {
|
|
if (error) {
|
|
// Ignore error as the fetch() promise has already been resolved.
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
body = stream_1.default.pipeline(body, zlib_1.default.createInflateRaw(), (error) => {
|
|
if (error) {
|
|
// Ignore error as it is forwarded to the response body.
|
|
}
|
|
});
|
|
}
|
|
this.response = new this.#window.Response(FetchBodyUtility_js_1.default.nodeToWebStream(body), responseOptions);
|
|
this.response.redirected = this.redirectCount > 0;
|
|
this.response.url = this.request.url;
|
|
this.resolve(this.response);
|
|
});
|
|
raw.on('end', () => {
|
|
// Some old IIS servers return zero-length OK deflate responses, so 'data' is never emitted.
|
|
if (!this.response) {
|
|
this.response = new this.#window.Response(FetchBodyUtility_js_1.default.nodeToWebStream(body), responseOptions);
|
|
this.response.redirected = this.redirectCount > 0;
|
|
this.response.url = this.request.url;
|
|
this.resolve(this.response);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
// For BR
|
|
if (contentEncodingHeader === 'br') {
|
|
body = stream_1.default.pipeline(body, zlib_1.default.createBrotliDecompress(), (error) => {
|
|
if (error) {
|
|
// Ignore error as it is forwarded to the response body.
|
|
}
|
|
});
|
|
this.response = new this.#window.Response(FetchBodyUtility_js_1.default.nodeToWebStream(body), responseOptions);
|
|
this.response.redirected = this.redirectCount > 0;
|
|
this.response.url = this.request.url;
|
|
this.resolve(this.response);
|
|
return;
|
|
}
|
|
// Otherwise, use response as is
|
|
this.response = new this.#window.Response(FetchBodyUtility_js_1.default.nodeToWebStream(body), responseOptions);
|
|
this.response.redirected = this.redirectCount > 0;
|
|
this.response.url = this.request.url;
|
|
this.resolve(this.response);
|
|
}
|
|
/**
|
|
* Handles redirect response.
|
|
*
|
|
* @param nodeResponse Node response.
|
|
* @param responseHeaders Headers.
|
|
* @returns True if redirect response was handled, false otherwise.
|
|
*/
|
|
handleRedirectResponse(nodeResponse, responseHeaders) {
|
|
if (!FetchResponseRedirectUtility_js_1.default.isRedirect(nodeResponse.statusCode)) {
|
|
return false;
|
|
}
|
|
switch (this.request.redirect) {
|
|
case 'error':
|
|
this.finalizeRequest();
|
|
this.reject(new this.#window.DOMException(`URI requested responds with a redirect, redirect mode is set to "error": ${this.request.url}`, DOMExceptionNameEnum_js_1.default.abortError));
|
|
return true;
|
|
case 'manual':
|
|
// Nothing to do
|
|
return false;
|
|
case 'follow':
|
|
const locationHeader = responseHeaders.get('Location');
|
|
const shouldBecomeGetRequest = nodeResponse.statusCode === 303 ||
|
|
((nodeResponse.statusCode === 301 || nodeResponse.statusCode === 302) &&
|
|
this.request.method === 'POST');
|
|
let locationURL = null;
|
|
if (locationHeader !== null) {
|
|
try {
|
|
locationURL = new url_1.URL(locationHeader, this.request.url);
|
|
}
|
|
catch {
|
|
this.finalizeRequest();
|
|
this.reject(new this.#window.DOMException(`URI requested responds with an invalid redirect URL: ${locationHeader}`, DOMExceptionNameEnum_js_1.default.uriMismatchError));
|
|
return true;
|
|
}
|
|
}
|
|
if (locationURL === null) {
|
|
return false;
|
|
}
|
|
if (FetchResponseRedirectUtility_js_1.default.isMaxRedirectsReached(this.redirectCount)) {
|
|
this.finalizeRequest();
|
|
this.reject(new this.#window.DOMException(`Maximum redirects reached at: ${this.request.url}`, DOMExceptionNameEnum_js_1.default.networkError));
|
|
return true;
|
|
}
|
|
const headers = new Headers_js_1.default(this.request.headers);
|
|
const requestInit = {
|
|
method: this.request.method,
|
|
signal: this.request.signal,
|
|
referrer: this.request.referrer,
|
|
referrerPolicy: this.request.referrerPolicy,
|
|
credentials: this.request.credentials,
|
|
headers,
|
|
body: this.request[PropertySymbol.bodyBuffer]
|
|
};
|
|
if (this.request.credentials === 'omit' ||
|
|
(this.request.credentials === 'same-origin' &&
|
|
FetchCORSUtility_js_1.default.isCORS(this.#window.location.href, locationURL))) {
|
|
headers.delete('authorization');
|
|
headers.delete('www-authenticate');
|
|
headers.delete('cookie');
|
|
headers.delete('cookie2');
|
|
}
|
|
if (this.request.signal[PropertySymbol.aborted]) {
|
|
this.abort(this.request.signal[PropertySymbol.reason]);
|
|
return true;
|
|
}
|
|
if (shouldBecomeGetRequest) {
|
|
requestInit.method = 'GET';
|
|
requestInit.body = undefined;
|
|
headers.delete('Content-Length');
|
|
headers.delete('Content-Type');
|
|
}
|
|
const responseReferrerPolicy = FetchRequestReferrerUtility_js_1.default.getReferrerPolicyFromHeader(headers);
|
|
if (responseReferrerPolicy) {
|
|
requestInit.referrerPolicy = responseReferrerPolicy;
|
|
}
|
|
const fetch = new Fetch({
|
|
browserFrame: this.#browserFrame,
|
|
window: this.#window,
|
|
url: locationURL,
|
|
init: requestInit,
|
|
redirectCount: this.redirectCount + 1,
|
|
contentType: !shouldBecomeGetRequest
|
|
? this.request[PropertySymbol.contentType]
|
|
: undefined
|
|
});
|
|
this.finalizeRequest();
|
|
fetch
|
|
.send()
|
|
.then((response) => this.resolve(response))
|
|
.catch((error) => this.reject(error));
|
|
return true;
|
|
default:
|
|
this.finalizeRequest();
|
|
this.reject(new this.#window.DOMException(`Redirect option '${this.request.redirect}' is not a valid value of IRequestRedirect`));
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* Finalizes the request.
|
|
*/
|
|
finalizeRequest() {
|
|
this.request.signal.removeEventListener('abort', this.listeners.onSignalAbort);
|
|
this.nodeRequest.destroy();
|
|
}
|
|
/**
|
|
* Aborts the request.
|
|
*
|
|
* @param reason Reason.
|
|
*/
|
|
abort(reason) {
|
|
const error = new this.#window.DOMException('The operation was aborted.' + (reason ? ' ' + reason.toString() : ''), DOMExceptionNameEnum_js_1.default.abortError);
|
|
this.request[PropertySymbol.aborted] = true;
|
|
if (this.request.body) {
|
|
this.request.body[PropertySymbol.error] = error;
|
|
}
|
|
if (this.nodeRequest && !this.nodeRequest.destroyed) {
|
|
this.nodeRequest.destroy(error);
|
|
}
|
|
if (this.nodeResponse && !this.nodeResponse.destroyed) {
|
|
this.nodeResponse.destroy(error);
|
|
}
|
|
if (this.response && this.response.body) {
|
|
this.response.body[PropertySymbol.error] = error;
|
|
if (!this.response.body.locked) {
|
|
this.response.body.cancel(error);
|
|
}
|
|
}
|
|
if (this.reject) {
|
|
this.reject(reason !== undefined ? reason : error);
|
|
}
|
|
}
|
|
}
|
|
exports.default = Fetch;
|
|
//# sourceMappingURL=Fetch.cjs.map
|