From e73edb54817acb8f0e548916a97a9ed9340dfc3f Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 21 Jun 2023 15:31:28 +0200 Subject: [PATCH] feat: allow headers to be sent with `session.downloadURL()` (#38785) --- docs/api/session.md | 4 +- shell/browser/api/electron_api_session.cc | 18 +++- shell/browser/api/electron_api_session.h | 2 +- spec/api-session-spec.ts | 100 ++++++++++++++++++++++ 4 files changed, 120 insertions(+), 4 deletions(-) diff --git a/docs/api/session.md b/docs/api/session.md index e999a5207b91..ed5ff247f9f0 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -1284,9 +1284,11 @@ reused for new connections. Returns `Promise` - resolves with blob data. -#### `ses.downloadURL(url)` +#### `ses.downloadURL(url[, options])` * `url` string +* `options` Object (optional) + * `headers` Record (optional) - HTTP request headers. Initiates a download of the resource at `url`. The API will generate a [DownloadItem](download-item.md) that can be accessed diff --git a/shell/browser/api/electron_api_session.cc b/shell/browser/api/electron_api_session.cc index b78d0ee273cd..82594de091e1 100644 --- a/shell/browser/api/electron_api_session.cc +++ b/shell/browser/api/electron_api_session.cc @@ -804,10 +804,24 @@ v8::Local Session::GetBlobData(v8::Isolate* isolate, return holder->ReadAll(isolate); } -void Session::DownloadURL(const GURL& url) { - auto* download_manager = browser_context()->GetDownloadManager(); +void Session::DownloadURL(const GURL& url, gin::Arguments* args) { + std::map headers; + gin_helper::Dictionary options; + if (args->GetNext(&options)) { + if (options.Has("headers") && !options.Get("headers", &headers)) { + args->ThrowTypeError("Invalid value for headers - must be an object"); + return; + } + } + auto download_params = std::make_unique( url, MISSING_TRAFFIC_ANNOTATION); + + for (const auto& [name, value] : headers) { + download_params->add_request_header(name, value); + } + + auto* download_manager = browser_context()->GetDownloadManager(); download_manager->DownloadUrl(std::move(download_params)); } diff --git a/shell/browser/api/electron_api_session.h b/shell/browser/api/electron_api_session.h index 111101743048..dec77b2c5eca 100644 --- a/shell/browser/api/electron_api_session.h +++ b/shell/browser/api/electron_api_session.h @@ -131,7 +131,7 @@ class Session : public gin::Wrappable, bool IsPersistent(); v8::Local GetBlobData(v8::Isolate* isolate, const std::string& uuid); - void DownloadURL(const GURL& url); + void DownloadURL(const GURL& url, gin::Arguments* args); void CreateInterruptedDownload(const gin_helper::Dictionary& options); void SetPreloads(const std::vector& preloads); std::vector GetPreloads() const; diff --git a/spec/api-session-spec.ts b/spec/api-session-spec.ts index 5105574571ac..7d483e6f9b96 100644 --- a/spec/api-session-spec.ts +++ b/spec/api-session-spec.ts @@ -788,6 +788,7 @@ describe('session module', () => { const contentDisposition = 'inline; filename="mock.pdf"'; let port: number; let downloadServer: http.Server; + before(async () => { downloadServer = http.createServer((req, res) => { res.writeHead(200, { @@ -799,9 +800,11 @@ describe('session module', () => { }); port = (await listen(downloadServer)).port; }); + after(async () => { await new Promise(resolve => downloadServer.close(resolve)); }); + afterEach(closeAllWindows); const isPathEqual = (path1: string, path2: string) => { @@ -840,6 +843,103 @@ describe('session module', () => { session.defaultSession.downloadURL(`${url}:${port}`); }); + it('can download using session.downloadURL with a valid auth header', async () => { + const server = http.createServer((req, res) => { + const { authorization } = req.headers; + if (!authorization || authorization !== 'Basic i-am-an-auth-header') { + res.statusCode = 401; + res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"'); + res.end(); + } else { + res.writeHead(200, { + 'Content-Length': mockPDF.length, + 'Content-Type': 'application/pdf', + 'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition + }); + res.end(mockPDF); + } + }); + + const { port } = await listen(server); + + const downloadDone: Promise = new Promise((resolve) => { + session.defaultSession.once('will-download', (e, item) => { + item.savePath = downloadFilePath; + item.on('done', () => { + try { + resolve(item); + } catch {} + }); + }); + }); + + session.defaultSession.downloadURL(`${url}:${port}`, { + headers: { + Authorization: 'Basic i-am-an-auth-header' + } + }); + + const item = await downloadDone; + expect(item.getState()).to.equal('completed'); + expect(item.getFilename()).to.equal('mock.pdf'); + expect(item.getMimeType()).to.equal('application/pdf'); + expect(item.getReceivedBytes()).to.equal(mockPDF.length); + expect(item.getTotalBytes()).to.equal(mockPDF.length); + expect(item.getContentDisposition()).to.equal(contentDisposition); + }); + + it('throws when session.downloadURL is called with invalid headers', () => { + expect(() => { + session.defaultSession.downloadURL(`${url}:${port}`, { + // @ts-ignore this line is intentionally incorrect + headers: 'i-am-a-bad-header' + }); + }).to.throw(/Invalid value for headers - must be an object/); + }); + + it('can download using session.downloadURL with an invalid auth header', async () => { + const server = http.createServer((req, res) => { + const { authorization } = req.headers; + if (!authorization || authorization !== 'Basic i-am-an-auth-header') { + res.statusCode = 401; + res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"'); + res.end(); + } else { + res.writeHead(200, { + 'Content-Length': mockPDF.length, + 'Content-Type': 'application/pdf', + 'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition + }); + res.end(mockPDF); + } + }); + + const { port } = await listen(server); + + const downloadFailed: Promise = new Promise((resolve) => { + session.defaultSession.once('will-download', (_, item) => { + item.savePath = downloadFilePath; + item.on('done', (e, state) => { + console.log(state); + try { + resolve(item); + } catch {} + }); + }); + }); + + session.defaultSession.downloadURL(`${url}:${port}`, { + headers: { + Authorization: 'wtf-is-this' + } + }); + + const item = await downloadFailed; + expect(item.getState()).to.equal('interrupted'); + expect(item.getReceivedBytes()).to.equal(0); + expect(item.getTotalBytes()).to.equal(0); + }); + it('can download using WebContents.downloadURL', (done) => { const w = new BrowserWindow({ show: false }); w.webContents.session.once('will-download', function (e, item) {