From 84fc58b0824d85fc595a693229bcbb153f2eea45 Mon Sep 17 00:00:00 2001 From: Vladimir Airikh Date: Tue, 11 Jun 2019 16:47:27 +0300 Subject: [PATCH] fix `Implement support for proxying pages inside ASAR archives` (close #2033) --- package.json | 1 + src/request-pipeline/file-request.ts | 67 ++++++++++++---- .../server/data/file-in-asar-archive/app.asar | Bin 0 -> 85 bytes test/server/proxy-test.js | 72 ++++++++++++++++++ 4 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 test/server/data/file-in-asar-archive/app.asar diff --git a/package.json b/package.json index 34fc8b3c97..2bec954402 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "acorn-hammerhead": "^0.2.0", + "asar": "^2.0.1", "bowser": "1.6.0", "brotli": "^1.3.1", "crypto-md5": "^1.0.0", diff --git a/src/request-pipeline/file-request.ts b/src/request-pipeline/file-request.ts index 031e8cc9a1..842f2135b4 100644 --- a/src/request-pipeline/file-request.ts +++ b/src/request-pipeline/file-request.ts @@ -5,34 +5,71 @@ import { EventEmitter } from 'events'; import { parse } from 'url'; import { MESSAGE, getText } from '../messages'; import { stat, access } from '../utils/promisified-functions'; +import asar from 'asar'; +import { toReadableStream } from '../utils/buffer'; -const DISK_RE: RegExp = /^\/[A-Za-z]:/; +const DISK_RE: RegExp = /^\/[A-Za-z]:/; +const ASAR_ARCHIVE_PATH: RegExp = /^.*\.asar/; -const TARGET_IS_NOT_FILE = 'The target of the operation is not a file'; +const TARGET_IS_NOT_FILE = 'The target of the operation is not a file'; +const ASAR_ARCHIVE_TARGET_IS_NOT_FILE = 'The asar archive target of the operation is not a file'; export default class FileRequest extends EventEmitter { url: string; path: string; + asarArchivePath: string; constructor (url: string) { super(); - this.url = url; - this.path = FileRequest._getPath(url); + this.url = url; + this.path = FileRequest._getPath(url); + this.asarArchivePath = FileRequest._getAsarArchivePath(this.path); this._initEvents(); } _initEvents () { - stat(this.path) - .then((stats: fs.Stats) => { - if (!stats.isFile()) - throw new Error(TARGET_IS_NOT_FILE); - - return access(this.path, fs.constants.R_OK); - }) - .then(() => this._onOpen()) - .catch((err: Error) => this._onError(err)); + if (this.asarArchivePath) { + stat(this.asarArchivePath) + .catch(() => { + throw new Error(ASAR_ARCHIVE_TARGET_IS_NOT_FILE); + }) + .then(() => access(this.asarArchivePath, fs.constants.R_OK)) + .then(() => this._onOpen()) + .catch((err: Error) => { + + return this._onError(err); + }); + } + else { + stat(this.path) + .then((stats: fs.Stats) => { + if (!stats.isFile()) + throw new Error(TARGET_IS_NOT_FILE); + + return access(this.path, fs.constants.R_OK); + }) + .then(() => this._onOpen()) + .catch((err: Error) => this._onError(err)); + } + } + + static _getAsarArchivePath (path: string) : string { + const match = path.match(ASAR_ARCHIVE_PATH); + + return match ? match[0] : ''; + } + + static _getFilePathInAsarArchive (path: string) : string { + return path.replace(ASAR_ARCHIVE_PATH, '.'); + } + + _getAsarFileReadStream (path: string) : any { + const filePath = FileRequest._getFilePathInAsarArchive(path); + const file = asar.extractFile(this.asarArchivePath, filePath); + + return toReadableStream(file); } static _getPath (proxiedUrl: string): string { @@ -51,7 +88,9 @@ export default class FileRequest extends EventEmitter { } _onOpen () { - let stream = fs.createReadStream(this.path); + let stream = this.asarArchivePath + ? this._getAsarFileReadStream(this.path) + : fs.createReadStream(this.path); stream = Object.assign(stream, { statusCode: 200, diff --git a/test/server/data/file-in-asar-archive/app.asar b/test/server/data/file-in-asar-archive/app.asar new file mode 100644 index 0000000000000000000000000000000000000000..62a00f08c0d7b5d26dc566cf44ed92133aaa272f GIT binary patch literal 85 zcmZQ!U|_HSViO?N1LA6>w9K5;VkN6;rQ)Jwy^@L&Ff+3%RmsZ8P)8|0Ev+~eD5hkf aR9joiz>rv+Sfr3xl$?=SmTIK{)dB!YgBOMX literal 0 HcmV?d00001 diff --git a/test/server/proxy-test.js b/test/server/proxy-test.js index 5bfe313a1b..d38c14fe50 100644 --- a/test/server/proxy-test.js +++ b/test/server/proxy-test.js @@ -1633,6 +1633,78 @@ describe('Proxy', () => { request(options); }); + + it('Should pass an error to the session if target (an "asar" archive) does not exist (GH-2033)', done => { + const url = getFileProtocolUrl('./data/file-in-asar-archive/non-exist-asar-archive.asar/non-exist-file.txt'); + + session.id = 'sessionId'; + + session.handlePageError = (ctx, err) => { + expect(err).contains([ + 'Failed to read a file at ' + url + ' because of the error:', + '', + 'The asar archive target of the operation is not a file' + ].join('\n')); + + ctx.res.end(); + done(); + return true; + }; + + const options = { + url: proxy.openSession(url, session), + headers: { + accept: 'text/html,*/*;q=0.1' + } + }; + + request(options); + }); + + it('Should pass an error to the session if target (a file in an "asar" archive) does not exist (GH-2033)', done => { + const url = getFileProtocolUrl('./data/file-in-asar-archive/app.asar/non-exist-file.txt'); + + session.id = 'sessionId'; + + session.handlePageError = (ctx, err) => { + expect(err).contains([ + 'Failed to read a file at ' + url + ' because of the error:', + '', + 'Cannot read property \'link\' of undefined' + ].join('\n')); + + ctx.res.end(); + done(); + return true; + }; + + const options = { + url: proxy.openSession(url, session), + headers: { + accept: 'text/html,*/*;q=0.1' + } + }; + + request(options); + }); + + it('Should resolve an asar archive file (GH-2033)', () => { + session.id = 'sessionId'; + + const fileUrl = getFileProtocolUrl('./data/file-in-asar-archive/app.asar/src.txt'); + + const options = { + url: proxy.openSession(fileUrl, session), + headers: { + accept: '*/*' + } + }; + + return request(options) + .then(body => { + expect(body).eql('asar archive: src.txt'); + }); + }); }); describe('State switching', () => {