electron: autoUpdater does not work when have authenticated proxy

  • Electron version: 0.36.7
  • Operating system: Windows

The autoUpdater module in electron does not appear to be attempting to update all my users on windows from 3.0.1 to 3.1.0. It took a clean reinstall of the application on a few computers that I am aware of before it decided there was an update too download.

Upon sniffing out the traffic I could see that an update check request was being sent to fetch the RELEASES file and then didn’t attempt to download the obviously available update.

I don’t see how it can work for around 80% of users (that number is a guestimate) and not work for the rest.

The update server is here http://update.googleplaymusicdesktopplayer.com/update/{platform}/{version} The update code is here https://github.com/MarshallOfSound/Google-Play-Music-Desktop-Player-UNOFFICIAL-/blob/master/src/main/features/core/autoUpdater.js

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 1
  • Comments: 60 (19 by maintainers)

Most upvoted comments

I finally got everything working so I just wanted to report back here quickly while I’m waiting for a release to build.

To corroborate @scottrippey’s comments, here are the basic steps for using autoUpdater with a local directory for Mac (darwin) and Windows (win32):

Mac (darwin)

  1. Create a temp directory to store your update files:

    import { app } from "electron";
    import path from "path";
    import fs from "fs";
    
    // Set global temporary directory for things like auto update downloads, creating it if it doesn't exist already.
    global.tempPath = path.join( app.getPath( "temp" ), "NTWRK" );
    if ( !fs.existsSync( global.tempPath ) ) fs.mkdirSync( global.tempPath );
    
  2. Download .zip file containing the updated .app.

    This needs to be the .zip file. It’s what autoUpdater is expecting on the darwin platform.

    download( mainWindow, urlToZipFile, {
      directory: global.tempPath,
      onProgress: progress => mainWindow.webContents.send( "downloadProgress", progress )
    } );
    

    The above should help get you started, but you’ll need to flesh the rest of the download out, especially receiving progress from the Renderer thread.

  3. Create a feed.json file in the global.tempPath directory with a url key that points to our downloaded .zip file:

    import fs from "fs";
    
    // We need to manually create a feed.json file with a `url` key that points to our local `.zip` update file.
    const json = { url: `file://${ global.tempPath }/${ downloadedZipFilename }` };
    
    fs.writeFileSync( global.tempPath + "/feed.json", JSON.stringify( json ) );
    

    Note the file path has to be prefaced with file:// on darwin. Not so with win32. Great find @scottrippey!

  4. Set FeedURL of autoUpdater to that feed.json file:

    feedURL = `file://${ global.tempPath }/feed.json`;
    
    autoUpdater.setFeedURL( feedURL );
    

    Note the file path has to be prefaced with file:// on darwin. Not so with win32. Great find @scottrippey!

  5. Check for updates via autoUpdater:

    autoUpdater.checkForUpdates();
    

Windows (win32)

  1. Create a temp directory to store your update files:

    import { app } from "electron";
    import path from "path";
    import fs from "fs";
    
    // Set global temporary directory for things like auto update downloads, creating it if it doesn't exist already.
    global.tempPath = path.join( app.getPath( "temp" ), "NTWRK" );
    if ( !fs.existsSync( global.tempPath ) ) fs.mkdirSync( global.tempPath );
    
  2. Download .nupkg file.

    This needs to be the .nupkg file. It’s what autoUpdater is expecting on the win32 platform.

    download( mainWindow, urlToNupkg, {
      directory: global.tempPath,
      onProgress: progress => mainWindow.webContents.send( "downloadProgress", progress )
    } );
    

    The above should help get you started, but you’ll need to flesh the rest of the download out, especially receiving progress from the Renderer thread.

  3. Download or generate a RELEASES file.

    If you look above, @scottrippey outlines how to create this file yourself. In our case, we’re using electron-forge and it is automatically generated and published to our release server so we only needed to download it.

    download( mainWindow, urlToRELEASES, {
      directory: global.tempPath,
      onProgress: progress => mainWindow.webContents.send( "downloadProgress", progress )
    } );
    

    The above should help get you started, but you’ll need to flesh the rest of the download out, especially receiving progress from the Renderer thread.

  4. Set FeedURL of autoUpdater to the global.tempPath directory where both the .nupkg and RELEASES files are:

    feedURL = global.tempPath;
    
    autoUpdater.setFeedURL( feedURL );
    

    Note that on win32 you do not need to preface the feedURL with file://. Great discovery @scottrippey!

  5. Check for updates via autoUpdater:

    autoUpdater.checkForUpdates();
    

Party

You now have a way to get around network proxies, pass your own headers and even get download progress. So dance!

80s kid dancing

Great work @joshuapinter ! I left a lot of “gaps” but it looks like you nailed it. To summarize the main differences between Mac/Windows:

  1. On Windows, both URLs can be file paths. On Mac, both must be file:// URLs.
  2. On Windows, you must have a RELEASES file that points to a -full.nuget file. On Mac, you must have a feed.json file that points to a .zip file.
  3. On Windows, you call setFeedUrl(tempPath) and it finds the RELEASES file. On Mac, you call setFeedUrl(path.join(tempPath, 'feed.json').
download( mainWindow, urlToNupkg, {
  directory: global.tempPath,
  onProgress: progress => mainWindow.webContents.send( "downloadProgress", progress )
} );

Sorry, where is download function’s location?

hi all, everything seems to be working fine for me on this except that it never seems to trigger the autoUpdate.on(‘update-downloaded’) event? Anyone experienced this?

Sorry for a 3rd comment, but I just couldn’t let this rest! I found a way to achieve the “temporary folder” hack on Mac too! Squirrel allows a file:// URL for setFeedURL!

As with the Windows approach, first, I download the -mac.zip file to a temporary folder. Second, I generate a feed.json manifest in the same temp folder, which points to file://${tempFolder}/${tempFile}-mac.zip. Third, I call setFeedURL(`file://${tempFolder}/feed.json`).

This “temporary folder” approach lets me use my own electron.net requests for all network activity, which means it handles proxies, basic auth, proxy auth, and custom headers, for both Mac and Windows!

Now I REALLY want to make this code open-source! I will bring this up at work 😃

I just wanted to throw an idea out there that I’m using to satisfy a requirement for an internal tool for work. We don’t necessarily need to download the files ourselves. We just need to add things to redirect the request to another location that can resolve them. My problem isn’t quite the same, as I just need to have a password on my update server, which of course, squirrel for Windows doesn’t support (even with the credentials in the URL as per http scheme spec). I’d be happy to release a POC example once I’ve finished and tested it, but what if instead of parsing and downloading packages ourselves, effectively eliminating the elegance and speed of delta updates, we just create a proxy on localhost. Then, when squirrel goes looking for an update, we receive all of its requests, and can do with them what we wish. In my case, I can simply inject an authorization header, rewrite the host, and voila! Authentication headers are now added to the request before they reach the update server.

Side note: According to my research, it looks like squirrel is an external program that runs on-demand when electron asks for it. It does not appear that squirrel makes any requests outside of the app’s lifecycle, because it only kills the app when it’s already downloaded and ready to start the new version, so there’s no reason the app couldn’t be its own update server. Only the URL makes it into squirrel, and it doesn’t support basic auth, so there’s no hope of getting squirrel to make the requests properly (this term is used loosely, since this is kind of an edge case). However, electron has an http server, and squirrel can request via HTTP, which means the application can be squirrel’s update server and it doesn’t matter if squirrel supports the protocols used on the public-facing server. I’m not sure if this works or not for authenticated proxies, since squirrel would need to bypass them, but maybe there’s an exception if it’s sending requests to localhost?

I’ve found an alternative solution that works for basic proxies.

Within my app, I have added a app.on("login", ... handler, which prompts the user for credentials. It works great, except for the autoUpdater. I was hoping that autoUpdater was using electron.net, so that it would fire the login event, but it does not fire. I get the impression that the updater runs outside of Electron.

This is true however you can listen for the autoUpdater login event instead. See https://www.electron.build/auto-update#updatersignal

autoUpdater.on('login', async (authInfo, callback) => {
  const [username, password] = ...... // prompt on UI, use cache, other solution to acquire credentials
  callback(username, password)
})
autoUpdater.requestHeaders = {'User-Agent': userAgent}
// Will cause a login event
const {info, provider} = await autoUpdater.getUpdateInfoAndProvider()
const updateAvailable = await autoUpdater.isUpdateAvailable(info)

This was really worked. I have been success.

@joshuapinter did you have any issues with file protocol when implementing local feed auto update? I can’t get this to work, because I keep getting error “Error: Protocol “file:” not supported”.

I’ve found an alternative solution that works for basic proxies.

Within my app, I have added a app.on("login", ... handler, which prompts the user for credentials. It works great, except for the autoUpdater.

I was hoping that autoUpdater was using electron.net, so that it would fire the login event, but it does not fire. I get the impression that the updater runs outside of Electron.

This is true however you can listen for the autoUpdater login event instead. See https://www.electron.build/auto-update#updatersignal

autoUpdater.on('login', async (authInfo, callback) => {
  const [username, password] = ...... // prompt on UI, use cache, other solution to acquire credentials
  callback(username, password)
})
autoUpdater.requestHeaders = {'User-Agent': userAgent}
// Will cause a login event
const {info, provider} = await autoUpdater.getUpdateInfoAndProvider()
const updateAvailable = await autoUpdater.isUpdateAvailable(info)

I don’t mind sharing some snippets, though!

On Mac, I create a temp folder, and in it I put app-name-1.2.3-mac.zip, and I generate a feed.json that points to it. On Windows, I create a temp folder, and in it I put app-name-1.2.3-full.nuget and I generate a RELEASES that points to it. To generate the RELEASES file, it requires a certain format; here’s a snippet of how I do that:

import hasha from 'hasha';
import fse from 'fs-extra';
import temp from 'temp';
import path from 'path';
...
        const tempFolder = temp.track().mkdirSync('name-of-app-update-');
        const tempFilename = 'name-of-app-1.2.3-full.nuget';
        const tempFilePath = path.join(tempFolder, tempFilename);
        // On Windows, autoUpdater will check the feedURL for a 'RELEASES' manifest:
        const checksum = await hasha.fromFile(tempFilePath, { algorithm: 'sha1', encoding: 'hex' });
        const filesize = (await fse.stat(tempFilePath)).size;
        const RELEASES = `${checksum} ${tempFilename} ${filesize}`;
        await fse.writeFile(path.join(tempFolder, 'RELEASES'), RELEASES);

Then I call autoUpdater.setFeedUrl(tempFolder); autoUpdater.checkForUpdates() and it picks it up!

So, the problem at the end of the day is, if you have an authenticated proxy on Windows, you need to pop UI to prompt for username/passwords at the time of request (i.e. not just in a system settings dialog). This is deeply stupid, but it is what it is.

To mitigate this, you could download RELEASES and the latest NuPkg file yourself with Chromium (as well as implement the login callback to actually show UI in-app), then point your updater URL to that temp folder. This is deeply obnoxious, but so are proxy servers.

Is nupkg file still required for auto update on Windows? I found squirrel.windows, which is used for generating nupkg file, is deprecated and can’t see the latest update from its repository. On the other hand, I found squirrel.windows is still a solution to auto update an electron app on Windows. https://www.electronjs.org/docs/latest/api/auto-updater#windows What is the best way to implement auto update on windows? @joshuapinter @scottrippey

I have finished and tested my authentication proxy example and am happy to report that it works! Unfortunately, it’s not the most secure thing in the world, but I’d say it’s about as secure as bundling your update password with the application, which you’d probably have to do anyway unless you were to prompt the user for it on every launch. You could also start the server just long enough to get an update in order to mitigate the risk.

https://gist.github.com/sploders101/23719534c683216baf76b816098e614b

Ultimately, I think the most useful way to go about this would be to implement an RPC-esque proxy on stdin/out on both squirrel and electron, which could be handled on the JS side similarly to HTTP requests, and could be enabled via a command line flag on the squirrel exe, but I’m not sure how likely that is to be implemented.

Hello. For myself I found a solution.

First step: Request the latest version available. (For example using Electron Release Server you can get a list of versions - http://download.myapp.com/api/version)

Second step: Compare the current version and available.

Third step: Download file available version to a temporary folder(temporaryDirPath) - http://download.myapp.com/download/${availableVersion}/windows_64/MyApp-0.1.1-full.nupkg

Fourth step: UptoUpdater set the path, but not url. And checkForUpdates.

autoUpdater.setFeedURL(temporaryDirPath);
autoUpdater.checkForUpdates();