Skip to content

Commit

Permalink
fix(updater): github private repo
Browse files Browse the repository at this point in the history
Close #1370
  • Loading branch information
develar committed Mar 15, 2017
1 parent e5682a0 commit fb24e26
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 68 deletions.
16 changes: 13 additions & 3 deletions packages/electron-builder-http/src/httpExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,9 @@ export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
return
}

this.doApiRequest(<REQUEST_OPTS>Object.assign({}, options, parseUrl(redirectUrl)), cancellationToken, requestProcessor, redirectCount)
this.doApiRequest(<REQUEST_OPTS>Object.assign({}, removeAuthHeader(options), parseUrl(redirectUrl)), cancellationToken, requestProcessor, redirectCount)
.then(resolve)
.catch(reject)

return
}

Expand Down Expand Up @@ -160,7 +159,7 @@ export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
if (redirectUrl != null) {
if (redirectCount < this.maxRedirects) {
const parsedUrl = parseUrl(redirectUrl)
this.doDownload(Object.assign({}, requestOptions, {
this.doDownload(Object.assign({}, removeAuthHeader(requestOptions), {
hostname: parsedUrl.hostname,
path: parsedUrl.path,
port: parsedUrl.port == null ? undefined : parsedUrl.port
Expand Down Expand Up @@ -305,4 +304,15 @@ export function dumpRequestOptions(options: RequestOptions): string {
safe.headers.authorization = "<skipped>"
}
return JSON.stringify(safe, null, 2)
}

function removeAuthHeader(requestOptions: RequestOptions): RequestOptions {
const result = Object.assign({}, requestOptions)
// github redirect to amazon s3 - avoid error "Only one auth mechanism allowed"
if (result.headers != null) {
result.headers = Object.assign({}, result.headers)
delete result.headers.Authorization
delete result.headers.authorization
}
return result
}
5 changes: 5 additions & 0 deletions packages/electron-builder-http/src/publishOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export interface GithubOptions extends PublishConfiguration {
* @default https
*/
readonly protocol?: "https" | "http" | null

/**
* The access token to support auto-update from private github repositories. Never specify it in the configuration files. Only for [setFeedURL](module:electron-updater/out/AppUpdater.AppUpdater+setFeedURL).
*/
readonly token?: string | null
}

export function githubUrl(options: GithubOptions) {
Expand Down
8 changes: 5 additions & 3 deletions packages/electron-updater/src/AppUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,11 +276,13 @@ function createClient(data: string | PublishConfiguration) {
const provider = (<PublishConfiguration>data).provider
switch (provider) {
case "github":
if (process.env.GH_TOKEN == null) {
return new GitHubProvider(<GithubOptions>data)
const githubOptions = <GithubOptions>data
const token = process.env.GH_TOKEN || githubOptions.token
if (token == null) {
return new GitHubProvider(githubOptions)
}
else {
return new PrivateGitHubProvider(<GithubOptions>data)
return new PrivateGitHubProvider(githubOptions, token)
}

case "s3": {
Expand Down
25 changes: 15 additions & 10 deletions packages/electron-updater/src/GitHubProvider.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
import { HttpError, request } from "electron-builder-http"
import { CancellationToken } from "electron-builder-http/out/CancellationToken"
import { GithubOptions, githubUrl, UpdateInfo, VersionInfo } from "electron-builder-http/out/publishOptions"
import { GithubOptions, githubUrl, UpdateInfo } from "electron-builder-http/out/publishOptions"
import { RequestOptions } from "http"
import * as path from "path"
import { parse as parseUrl } from "url"
import { FileInfo, formatUrl, getChannelFilename, getCurrentPlatform, getDefaultChannelName, Provider } from "./api"
import { validateUpdateInfo } from "./GenericProvider"

export class GitHubProvider extends Provider<VersionInfo> {
export abstract class BaseGitHubProvider<T extends UpdateInfo> extends Provider<T> {
// so, we don't need to parse port (because node http doesn't support host as url does)
private readonly baseUrl: RequestOptions

constructor(private readonly options: GithubOptions) {
protected readonly baseUrl: RequestOptions
constructor(protected readonly options: GithubOptions, baseHost: string) {
super()

const baseUrl = parseUrl(`${options.protocol || "https"}://${options.host || "github.com"}`)
const baseUrl = parseUrl(`${options.protocol || "https"}://${options.host || baseHost}`)
this.baseUrl = {
protocol: baseUrl.protocol,
hostname: baseUrl.hostname,
port: <any>baseUrl.port,
}
}
}

export class GitHubProvider extends BaseGitHubProvider<UpdateInfo> {
constructor(protected readonly options: GithubOptions) {
super(options, "github.com")
}

async getLatestVersion(): Promise<UpdateInfo> {
const basePath = this.getBasePath()
const basePath = this.basePath
const cancellationToken = new CancellationToken()
const version = await this.getLatestVersionString(basePath, cancellationToken)
let result: any
Expand Down Expand Up @@ -61,7 +67,7 @@ export class GitHubProvider extends Provider<VersionInfo> {
}
}

private getBasePath() {
protected get basePath() {
return `/${this.options.owner}/${this.options.repo}/releases`
}

Expand All @@ -70,12 +76,11 @@ export class GitHubProvider extends Provider<VersionInfo> {
return <any>versionInfo
}

const basePath = this.getBasePath()
// space is not supported on GitHub
const name = versionInfo.githubArtifactName || path.posix.basename(versionInfo.path).replace(/ /g, "-")
return {
name: name,
url: formatUrl(Object.assign({path: `${basePath}/download/v${versionInfo.version}/${name}`}, this.baseUrl)),
url: formatUrl(Object.assign({path: `${this.basePath}/download/v${versionInfo.version}/${name}`}, this.baseUrl)),
sha2: versionInfo.sha2,
}
}
Expand Down
90 changes: 39 additions & 51 deletions packages/electron-updater/src/PrivateGitHubProvider.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,34 @@
import { HttpError, request } from "electron-builder-http"
import { CancellationToken } from "electron-builder-http/out/CancellationToken"
import { GithubOptions, UpdateInfo, VersionInfo } from "electron-builder-http/out/publishOptions"
import { GithubOptions, UpdateInfo } from "electron-builder-http/out/publishOptions"
import { RequestOptions } from "http"
import { safeLoad } from "js-yaml"
import * as path from "path"
import { parse as parseUrl } from "url"
import { FileInfo, formatUrl, getChannelFilename, getCurrentPlatform, getDefaultChannelName, Provider } from "./api"
import { FileInfo, formatUrl, getChannelFilename, getCurrentPlatform, getDefaultChannelName } from "./api"
import { validateUpdateInfo } from "./GenericProvider"
import { BaseGitHubProvider } from "./GitHubProvider"

export class PrivateGitHubProvider extends Provider<VersionInfo> {
// so, we don't need to parse port (because node http doesn't support host as url does)
private readonly baseUrl: RequestOptions
private apiResult: any
interface PrivateGitHubUpdateInfo extends UpdateInfo {
assets: Array<Asset>
}

constructor(private readonly options: GithubOptions) {
super()

const baseUrl = parseUrl(`${options.protocol || "https"}://${options.host || "api.github.com"}`)
this.baseUrl = {
protocol: baseUrl.protocol,
hostname: baseUrl.hostname,
port: <any>baseUrl.port,
}
export class PrivateGitHubProvider extends BaseGitHubProvider<PrivateGitHubUpdateInfo> {
constructor(options: GithubOptions, private readonly token: string) {
super(options, "api.github.com")
}

async getLatestVersion(): Promise<UpdateInfo> {
const basePath = this.getBasePath()
async getLatestVersion(): Promise<PrivateGitHubUpdateInfo> {
const basePath = this.basePath
const cancellationToken = new CancellationToken()
let result: any
const channelFile = getChannelFilename(getDefaultChannelName())
const versionUrl = await this.getLatestVersionUrl(basePath, cancellationToken, channelFile)
const assetPath = parseUrl(versionUrl).path

const assets = await this.getLatestVersionInfo(basePath, cancellationToken)
const requestOptions = Object.assign({
path: `${assetPath}?access_token=${process.env.GH_TOKEN}`,
headers: Object.assign({
Accept: "application/octet-stream",
"User-Agent": this.options.owner
}, this.requestHeaders)
}, this.baseUrl)
headers: this.configureHeaders("application/octet-stream")
}, parseUrl(assets.find(it => it.name == channelFile)!.url))
let result: any
try {
result = await request<UpdateInfo>(requestOptions, cancellationToken)
//Maybe better to parse in httpExecutor ?
if (typeof result === "string") {
if (getCurrentPlatform() === "darwin") {
result = JSON.parse(result)
}
else {
result = safeLoad(result)
}
}
}
catch (e) {
if (e instanceof HttpError && e.response.statusCode === 404) {
Expand All @@ -61,54 +41,62 @@ export class PrivateGitHubProvider extends Provider<VersionInfo> {
if (getCurrentPlatform() === "darwin") {
result.releaseJsonUrl = `${this.options.protocol || "https"}://${this.options.host || "api.github.com"}${requestOptions.path}`
}
(<PrivateGitHubUpdateInfo>result).assets = assets
return result
}

private async getLatestVersionUrl(basePath: string, cancellationToken: CancellationToken, channelFile: string): Promise<string> {
private configureHeaders(accept: string) {
return Object.assign({
Accept: accept,
Authorization: `token ${this.token}`,
}, this.requestHeaders)
}

private async getLatestVersionInfo(basePath: string, cancellationToken: CancellationToken): Promise<Array<Asset>> {
const requestOptions: RequestOptions = Object.assign({
path: `${basePath}/latest?access_token=${process.env.GH_TOKEN}`,
headers: Object.assign({Accept: "application/json", "User-Agent": this.options.owner}, this.requestHeaders)
path: `${basePath}/latest`,
headers: this.configureHeaders("application/vnd.github.v3+json"),
}, this.baseUrl)
try {
this.apiResult = (await request<any>(requestOptions, cancellationToken))
return this.apiResult.assets.find((elem: any) => {
return elem.name == channelFile
}).url
return (await request<any>(requestOptions, cancellationToken)).assets
}
catch (e) {
throw new Error(`Unable to find latest version on GitHub (${formatUrl(<any>requestOptions)}), please ensure a production release exists: ${e.stack || e.message}`)
}
}

private getBasePath() {
private get basePath() {
return `/repos/${this.options.owner}/${this.options.repo}/releases`
}

async getUpdateFile(versionInfo: UpdateInfo): Promise<FileInfo> {
async getUpdateFile(versionInfo: PrivateGitHubUpdateInfo): Promise<FileInfo> {
const headers = {
Accept: "application/octet-stream",
"User-Agent": this.options.owner,
Authorization: `token ${process.env.GH_TOKEN}`
Authorization: `token ${this.token}`
}

// space is not supported on GitHub
if (getCurrentPlatform() === "darwin") {
const info = <any>versionInfo
const name = info.url.split("/").pop()
const assetPath = parseUrl(this.apiResult.assets.find((it: any) => it.name == name).url).path
const assetPath = parseUrl(versionInfo.assets.find(it => it.name == name)!.url).path
info.url = formatUrl(Object.assign({path: `${assetPath}`}, this.baseUrl))
info.headers = headers
return info
}
else {
const name = versionInfo.githubArtifactName || path.posix.basename(versionInfo.path).replace(/ /g, "-")
const assetPath = parseUrl(this.apiResult.assets.find((it: any) => it.name == name).url).path
return {
name: name,
url: formatUrl(Object.assign({path: `${assetPath}`}, this.baseUrl)),
url: versionInfo.assets.find(it => it.name == name)!.url,
sha2: versionInfo.sha2,
headers: headers,
}
}
}
}

interface Asset {
name: string
url: string
}
5 changes: 4 additions & 1 deletion test/jestSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ test.ifLinux = process.platform === "linux" ? test : skip
test.ifLinuxOrDevMac = process.platform === "linux" || (!isCi && isMac) ? test : skip

delete process.env.CSC_NAME
delete process.env.GH_TOKEN
if (process.env.TEST_APP_TMP_DIR == null) {
delete process.env.GH_TOKEN
}

process.env.CSC_IDENTITY_AUTO_DISCOVERY = "false"
if (!process.env.USE_HARD_LINKS) {
process.env.USE_HARD_LINKS = "true"
Expand Down
12 changes: 12 additions & 0 deletions test/out/__snapshots__/nsisUpdaterTest.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ Object {
}
`;

exports[`file url github private 1`] = `
Object {
"headers": Object {
"Accept": "application/octet-stream",
"Authorization": "token fad40e29d04dc522e3ba03a5468339e191acd82d",
},
"name": "TestApp-Setup-1.1.0.exe",
"sha2": "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
"url": "https://api.github.com/repos/develar/__test_nsis_release_private/releases/assets/3403081",
}
`;

exports[`sha2 mismatch error event 1`] = `
Object {
"name": "TestApp Setup 1.1.0.exe",
Expand Down
23 changes: 23 additions & 0 deletions test/src/nsisUpdaterTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,29 @@ test("file url github", async () => {
expect(actualEvents).toEqual(expectedEvents)
})

test.skip("file url github private", async () => {
const updater = new NsisUpdater()
updater.updateConfigPath = await writeUpdateConfig(<GithubOptions>{
provider: "github",
owner: "develar",
repo: "__test_nsis_release_private",
})

const actualEvents: Array<string> = []
const expectedEvents = ["checking-for-update", "update-available", "update-downloaded"]
for (const eventName of expectedEvents) {
updater.addListener(eventName, () => {
actualEvents.push(eventName)
})
}

const updateCheckResult = await updater.checkForUpdates()
expect(updateCheckResult.fileInfo).toMatchSnapshot()
await assertThat(path.join(await updateCheckResult.downloadPromise)).isFile()

expect(actualEvents).toEqual(expectedEvents)
})

test("test error", async () => {
const updater: NsisUpdater = new NsisUpdater()

Expand Down

0 comments on commit fb24e26

Please sign in to comment.