From 1ff5b9a2620a46f1420fd17207c313b3bab8b8cf Mon Sep 17 00:00:00 2001 From: Mas0nShi Date: Sun, 3 Jul 2022 23:04:56 +0800 Subject: [PATCH] #526@minor: Continue Adds support for XMLHttpRequest (Sync) and remove outdated annotation, fixes some typo error. --- .../happy-dom/src/event/IEventListener.ts | 2 +- packages/happy-dom/src/fetch/FetchHandler.ts | 10 ++- .../src/fetch/ResourceFetchHandler.ts | 4 +- .../happy-dom/src/nodes/document/Document.ts | 2 - packages/happy-dom/src/window/IWindow.ts | 4 +- .../src/xml-http-request/XMLHttpRequest.ts | 60 ++++++++++++--- .../XMLHttpRequestSyncWorker.ts | 73 +++++++++++++++++++ 7 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 packages/happy-dom/src/xml-http-request/XMLHttpRequestSyncWorker.ts diff --git a/packages/happy-dom/src/event/IEventListener.ts b/packages/happy-dom/src/event/IEventListener.ts index b731998d7..0c9e5cbee 100644 --- a/packages/happy-dom/src/event/IEventListener.ts +++ b/packages/happy-dom/src/event/IEventListener.ts @@ -7,7 +7,7 @@ export default interface IEventListener { /** * Handles event. * - * @param type Event type. + * @param event */ handleEvent(event: Event): void; } diff --git a/packages/happy-dom/src/fetch/FetchHandler.ts b/packages/happy-dom/src/fetch/FetchHandler.ts index 755b20292..f4da6a206 100644 --- a/packages/happy-dom/src/fetch/FetchHandler.ts +++ b/packages/happy-dom/src/fetch/FetchHandler.ts @@ -10,7 +10,7 @@ import NodeFetch from 'node-fetch'; */ export default class FetchHandler { /** - * Returns resource data asynchonously. + * Returns resource data asynchronously. * * @param document Document. * @param url URL to resource. @@ -20,7 +20,13 @@ export default class FetchHandler { public static fetch(document: IDocument, url: string, init?: IRequestInit): Promise { // We want to only load NodeFetch when it is needed to improve performance and not have direct dependencies to server side packages. const taskManager = document.defaultView.happyDOM.asyncTaskManager; - + // We need set referer to solve anti-hotlinking. + // And the browser will set the referer to the origin of the page. + if (init) { + if (!init.headers['referer']) { + init.headers['referer'] = document.defaultView.location.origin; + } + } return new Promise((resolve, reject) => { const taskID = taskManager.startTask(); diff --git a/packages/happy-dom/src/fetch/ResourceFetchHandler.ts b/packages/happy-dom/src/fetch/ResourceFetchHandler.ts index 176880555..5e83c2c84 100644 --- a/packages/happy-dom/src/fetch/ResourceFetchHandler.ts +++ b/packages/happy-dom/src/fetch/ResourceFetchHandler.ts @@ -7,7 +7,7 @@ import IDocument from '../nodes/document/IDocument'; */ export default class ResourceFetchHandler { /** - * Returns resource data asynchonously. + * Returns resource data asynchronously. * * @param document Document. * @param url URL. @@ -24,7 +24,7 @@ export default class ResourceFetchHandler { } /** - * Returns resource data synchonously. + * Returns resource data synchronously. * * @param document Document. * @param url URL. diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index e3fcd72dd..e47532179 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -64,7 +64,6 @@ export default class Document extends Node implements IDocument { /** * Creates an instance of Document. * - * @param defaultView Default view. */ constructor() { super(); @@ -764,7 +763,6 @@ export default class Document extends Node implements IDocument { * * @param node Node to import. * @param [deep=false] Set to "true" if the clone should be deep. - * @param Imported Node. */ public importNode(node: INode, deep = false): INode { if (!(node instanceof Node)) { diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 1574bcbf3..88fa0b7aa 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -310,7 +310,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global { * Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data). * * @see https://developer.mozilla.org/en-US/docs/Web/API/btoa - * @param data Binay data. + * @param data Binary data. * @returns Base64-encoded string. */ btoa(data: unknown): string; @@ -321,7 +321,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global { * @see https://developer.mozilla.org/en-US/docs/Web/API/atob * @see https://infra.spec.whatwg.org/#forgiving-base64-encode. * @see Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. - * @param data Binay string. + * @param data Binary string. * @returns An ASCII string containing decoded data from encodedData. */ atob(data: unknown): string; diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index ea28b8fba..8e40e19c8 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -21,6 +21,8 @@ import { MajorNodeVersion, IXMLHttpRequestOptions } from './XMLHttpReqeustUtility'; +import { spawnSync } from "child_process"; +const SyncWorkerFile = require.resolve ? require.resolve('./XMLHttpRequestSyncWorker') : null; /** * References: https://github.com/souldreamer/xhr2-cookies. @@ -46,13 +48,15 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { public upload = new XMLHttpRequestUpload(); public responseUrl = ''; public withCredentials = false; - // TODO: another way to set proxy? + public nodejsHttpAgent: HttpAgent = http.globalAgent; public nodejsHttpsAgent: HttpsAgent = https.globalAgent; private readonly anonymous: boolean; private method: string | null = null; private url: URL | null = null; + private auth: string | null = null; + private body: string | Buffer | ArrayBuffer | ArrayBufferView; private sync = false; private headers: { [header: string]: string } = {}; private loweredHeaders: { [lowercaseHeader: string]: string } = {}; @@ -60,7 +64,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { private _request: ClientRequest | null = null; private _response: IncomingMessage | null = null; // @ts-ignore - private _error: Error | null = null; + private _error: Error | string | null = null; private responseParts: Buffer[] | null = null; private responseHeaders: { [lowercaseHeader: string]: string } | null = null; private loadedBytes = 0; @@ -141,10 +145,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.method = method; this.url = xhrUrl; + this.auth = `${this.url.username || ''}:${this.url.password || ''}` this.sync = !async; - this.headers = {}; + // this.headers = {}; this.loweredHeaders = {}; - this.mimeOverride = null; + // this.mimeOverride = null; this.setReadyState(XMLHttpRequest.OPENED); this._request = null; this._response = null; @@ -155,6 +160,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.loadedBytes = 0; this.totalBytes = 0; this.lengthComputable = false; + } /** @@ -246,7 +252,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { if ( this.responseHeaders == null || name == null || - this.readyState in [XMLHttpRequest.OPENED, XMLHttpRequest.UNSENT] + this.readyState === XMLHttpRequest.OPENED || + this.readyState === XMLHttpRequest.UNSENT ) { return null; } @@ -263,7 +270,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { public getAllResponseHeaders(): string { if ( this.responseHeaders == null || - this.readyState in [XMLHttpRequest.OPENED, XMLHttpRequest.UNSENT] + this.readyState === XMLHttpRequest.OPENED || + this.readyState === XMLHttpRequest.UNSENT ) { return ''; } @@ -278,7 +286,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param mimeType The MIME type to use. */ public overrideMimeType(mimeType: string): void { - if (this.readyState in [XMLHttpRequest.LOADING, XMLHttpRequest.DONE]) { + if (this.readyState === XMLHttpRequest.LOADING || this.readyState === XMLHttpRequest.DONE) { throw new DOMException( 'overrideMimeType() not allowed in LOADING or DONE', DOMExceptionNameEnum.invalidStateError @@ -315,7 +323,12 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { */ private sendHttp(data?: string | Buffer | ArrayBuffer | ArrayBufferView): void { const { _defaultView } = XMLHttpRequest; + this.body = data; + if (this.sync) { + const params = this._serialParams(); + const res = spawnSync(process.execPath, [SyncWorkerFile], { input: params, maxBuffer: Infinity }); + res; // TODO: sync not implemented. throw new Error('Synchronous XHR processing not implemented'); } @@ -351,7 +364,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { hostname: this.url.hostname, port: +this.url.port, path: this.url.pathname, - auth: `${this.url.username || ''}:${this.url.password || ''}`, + auth: this.auth, method: this.method, headers: this.headers, agent @@ -381,7 +394,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { Connection: 'keep-alive', Host: this.url.host, 'User-Agent': _defaultView.navigator.userAgent, - ...(this.anonymous ? { Referer: 'about:blank' } : {}) + ...(this.anonymous ? { Referer: 'about:blank' } : { Referer: _defaultView.location.origin }) }; this.upload.finalizeHeaders(this.headers, this.loweredHeaders); } @@ -651,4 +664,33 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { const charset = /;\s*charset=(.*)$/.exec(this.responseHeaders['content-type'] || ''); return Array.isArray(charset) ? charset[1] : 'utf-8'; } + + public _syncGetError(): Error { + return this._error; + } + public _syncSetErrorString(error: string): void { + this._error = error; + } + + private _serialParams(): string { + const { _defaultView } = XMLHttpRequest; + const serials = { + sync: this.sync, + withCredentials: this.withCredentials, + mimeType: this.mimeOverride, + username: this.url.username, + password: this.url.password, + auth: this.auth, + method: this.method, + responseType: this.responseType, + headers: this.headers, + uri: this.url.href, + timeout: this.timeout, + body: this.body, + + cookie: _defaultView.document.cookie, + origin: _defaultView.location.href + }; + return JSON.stringify(serials); + } } diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequestSyncWorker.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequestSyncWorker.ts new file mode 100644 index 000000000..ed97d8eaa --- /dev/null +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequestSyncWorker.ts @@ -0,0 +1,73 @@ +import Window from '../window/Window'; +import * as util from 'util'; + +const window = new Window(); +const xhr = new window.XMLHttpRequest(); + +const chunks = []; + +process.stdin.on('data', (chunk) => { + chunks.push(chunk); +}); + +process.stdin.on('end', () => { + const buffer = Buffer.concat(chunks); + + const serials = JSON.parse(buffer.toString()); + if (serials.body && serials.body.type === 'Buffer' && serials.body.data) { + serials.body = Buffer.from(serials.body.data); + } + if (serials.origin) { + window.location.href = serials.origin; + } + + if (serials.cookie) { + window.document.cookie = serials.cookie; + } + + xhr.overrideMimeType(serials.mimeType); + xhr.open(serials.method, serials.uri, true, serials.user, serials.password); + if (serials.headers) { + Object.keys(serials.headers).forEach((key) => { + xhr.setRequestHeader(key, serials.headers[key]); + }); + } + + + xhr.timeout = serials.timeout; + + try { + xhr.addEventListener('loadend', () => { + if (xhr._syncGetError()) { + const err = xhr._syncGetError(); + xhr._syncSetErrorString(err.stack || util.inspect(err)); + } + + process.stdout.write( + JSON.stringify({ + responseURL: xhr.responseUrl, + responseText: xhr.responseText, + status: xhr.status, + statusText: xhr.statusText, + }), + () => { + process.exit(0); + } + ); + }); + + xhr.send(serials.body); + } catch (error) { + // Properties.error += error.stack || util.inspect(error); + process.stdout.write( + JSON.stringify({ + responseURL: xhr.responseUrl, + status: xhr.status, + statusText: xhr.statusText + }), + () => { + process.exit(0); + } + ); + } +});