Browse Source

feat: allow headers to be sent with `session.downloadURL()` (#38871)

* feat: allow headers to be sent with session.downloadURL

Co-authored-by: Shelley Vohr <[email protected]>

* Update shell/browser/api/electron_api_session.cc

Co-authored-by: Charles Kerr <[email protected]>

Co-authored-by: Shelley Vohr <[email protected]>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <[email protected]>
trop[bot] 1 year ago
parent
commit
76f4291130

+ 3 - 1
docs/api/session.md

@@ -1284,9 +1284,11 @@ reused for new connections.
 
 Returns `Promise<Buffer>` - resolves with blob data.
 
-#### `ses.downloadURL(url)`
+#### `ses.downloadURL(url[, options])`
 
 * `url` string
+* `options` Object (optional)
+  * `headers` Record<string, string> (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

+ 16 - 2
shell/browser/api/electron_api_session.cc

@@ -804,10 +804,24 @@ v8::Local<v8::Promise> 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<std::string, std::string> 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<download::DownloadUrlParameters>(
       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));
 }
 

+ 1 - 1
shell/browser/api/electron_api_session.h

@@ -131,7 +131,7 @@ class Session : public gin::Wrappable<Session>,
   bool IsPersistent();
   v8::Local<v8::Promise> 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<base::FilePath>& preloads);
   std::vector<base::FilePath> GetPreloads() const;

+ 100 - 0
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<Electron.DownloadItem> = 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<Electron.DownloadItem> = 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) {