Skip to content

Commit dd1320d

Browse files
kevinbuhmanndevelar
authored andcommitted
feat(electron-updater): add requestHeaders option
Close #1175
1 parent e7e2a82 commit dd1320d

File tree

16 files changed

+235
-203
lines changed

16 files changed

+235
-203
lines changed

docs/Auto Update.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ autoUpdater.logger.transports.file.level = "info"
4444
4545
## Options
4646
47-
Name | Default | Description
48-
--------------------|-------------------------|------------
49-
autoDownload | `true` | Automatically download an update when it is found.
50-
logger | `console` | The logger. You can pass [electron-log](https://github.com/megahertz/electron-log), [winston](https://github.com/winstonjs/winston) or another logger with the following interface: `{ info(), warn(), error() }`. Set it to `null` if you would like to disable a logging feature.
47+
Name | Default | Description
48+
--------------------|-------------------|------------
49+
`autoDownload` | `true` | Automatically download an update when it is found.
50+
`logger` | `console` | The logger. You can pass [electron-log](https://github.com/megahertz/electron-log), [winston](https://github.com/winstonjs/winston) or another logger with the following interface: `{ info(), warn(), error() }`. Set it to `null` if you would like to disable a logging feature.
51+
`requestHeaders` | `null` | The request headers.
5152
5253
## Events
5354

packages/electron-builder-http/src/bintray.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { BintrayOptions } from "./publishOptions"
2-
import { request } from "./httpExecutor"
2+
import { request, configureRequestOptions } from "./httpExecutor"
33

44
export function bintrayRequest<T>(path: string, auth: string | null, data: {[name: string]: any; } | null = null, method?: string): Promise<T> {
5-
return request<T>({hostname: "api.bintray.com", path: path}, auth, data, null, method)
5+
return request<T>(configureRequestOptions({hostname: "api.bintray.com", path: path}, auth, method), data)
66
}
77

88
export interface Version {

packages/electron-builder-http/src/httpExecutor.ts

Lines changed: 105 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Url } from "url"
21
import { createHash } from "crypto"
32
import { Transform } from "stream"
43
import { createWriteStream } from "fs-extra-p"
@@ -8,11 +7,23 @@ import _debug from "debug"
87
import { ProgressCallbackTransform } from "./ProgressCallbackTransform"
98
import { safeLoad } from "js-yaml"
109
import { EventEmitter } from "events"
10+
import { Socket } from "net"
1111

12-
export const debug = _debug("electron-builder")
13-
export const maxRedirects = 10
12+
export interface RequestHeaders {
13+
[key: string]: any
14+
}
15+
16+
export interface Response extends EventEmitter {
17+
statusCode?: number
18+
statusMessage?: string
19+
20+
headers: any
21+
22+
setEncoding(encoding: string): void
23+
}
1424

1525
export interface DownloadOptions {
26+
headers?: RequestHeaders | null
1627
skipDirCreation?: boolean
1728
sha2?: string
1829
onProgress?(progress: any): void
@@ -39,41 +50,48 @@ export function download(url: string, destination: string, options?: DownloadOpt
3950
return executorHolder.httpExecutor.download(url, destination, options)
4051
}
4152

53+
export class HttpError extends Error {
54+
constructor(public readonly response: {statusMessage?: string | undefined, statusCode?: number | undefined, headers?: { [key: string]: string[]; } | undefined}, public description: any | null = null) {
55+
super(response.statusCode + " " + response.statusMessage + (description == null ? "" : ("\n" + JSON.stringify(description, null, " "))) + "\nHeaders: " + JSON.stringify(response.headers, null, " "))
56+
57+
this.name = "HttpError"
58+
}
59+
}
60+
4261
export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
43-
request<T>(url: Url, token?: string | null, data?: {[name: string]: any; } | null, headers?: { [key: string]: any } | null, method?: string): Promise<T> {
44-
const defaultHeaders: any = {"User-Agent": "electron-builder"}
45-
const options = Object.assign({
46-
method: method || "GET",
47-
headers: headers == null ? defaultHeaders : Object.assign(defaultHeaders, headers)
48-
}, url)
62+
protected readonly maxRedirects = 10
63+
protected readonly debug = _debug("electron-builder")
64+
65+
request<T>(options: RequestOptions, data?: { [name: string]: any; } | null): Promise<T> {
66+
options = Object.assign({headers: {"User-Agent": "electron-builder"}}, options)
4967

5068
const encodedData = data == null ? undefined : new Buffer(JSON.stringify(data))
5169
if (encodedData != null) {
5270
options.method = "post"
71+
if (options.headers == null) {
72+
options.headers = {}
73+
}
74+
5375
options.headers["Content-Type"] = "application/json"
5476
options.headers["Content-Length"] = encodedData.length
5577
}
56-
return this.doApiRequest<T>(<any>options, token || null, it => (<any>it).end(encodedData), 0)
78+
return this.doApiRequest<T>(<any>options, it => (<any>it).end(encodedData), 0)
5779
}
5880

59-
protected abstract doApiRequest<T>(options: REQUEST_OPTS, token: string | null, requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void, redirectCount: number): Promise<T>
81+
protected abstract doApiRequest<T>(options: REQUEST_OPTS, requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void, redirectCount: number): Promise<T>
6082

6183
abstract download(url: string, destination: string, options?: DownloadOptions | null): Promise<string>
6284

63-
protected handleResponse(response: Response, options: RequestOptions, resolve: (data?: any) => void, reject: (error: Error) => void, redirectCount: number, token: string | null, requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void) {
64-
if (debug.enabled) {
65-
const safe: any = Object.assign({}, options)
66-
if (safe.headers != null && safe.headers.authorization != null) {
67-
safe.headers.authorization = "<skipped>"
68-
}
69-
debug(`Response status: ${response.statusCode} ${response.statusMessage}, request options: ${JSON.stringify(safe, null, 2)}`)
85+
protected handleResponse(response: Response, options: RequestOptions, resolve: (data?: any) => void, reject: (error: Error) => void, redirectCount: number, requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void) {
86+
if (this.debug.enabled) {
87+
this.debug(`Response status: ${response.statusCode} ${response.statusMessage}, request options: ${dumpRequestOptions(options)}`)
7088
}
7189

7290
// we handle any other >= 400 error on request end (read detailed message in the response body)
7391
if (response.statusCode === 404) {
7492
// error is clear, we don't need to read detailed error description
7593
reject(new HttpError(response, `method: ${options.method} url: https://${options.hostname}${options.path}
76-
94+
7795
Please double check that your authentication token is correct. Due to security reasons actual status maybe not reported, but 404.
7896
`))
7997
return
@@ -91,7 +109,7 @@ export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
91109
return
92110
}
93111

94-
this.doApiRequest(<REQUEST_OPTS>Object.assign({}, options, parseUrl(redirectUrl)), token, requestProcessor, redirectCount)
112+
this.doApiRequest(<REQUEST_OPTS>Object.assign({}, options, parseUrl(redirectUrl)), requestProcessor, redirectCount)
95113
.then(resolve)
96114
.catch(reject)
97115

@@ -129,23 +147,47 @@ export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
129147
}
130148
})
131149
}
132-
}
133150

134-
export class HttpError extends Error {
135-
constructor(public readonly response: {statusMessage?: string | undefined, statusCode?: number | undefined, headers?: { [key: string]: string[]; } | undefined}, public description: any | null = null) {
136-
super(response.statusCode + " " + response.statusMessage + (description == null ? "" : ("\n" + JSON.stringify(description, null, " "))) + "\nHeaders: " + JSON.stringify(response.headers, null, " "))
151+
protected abstract doRequest(options: any, callback: (response: any) => void): any
137152

138-
this.name = "HttpError"
139-
}
140-
}
153+
protected doDownload(requestOptions: any, destination: string, redirectCount: number, options: DownloadOptions, callback: (error: Error | null) => void) {
154+
const request = this.doRequest(requestOptions, (response: Electron.IncomingMessage) => {
155+
if (response.statusCode >= 400) {
156+
callback(new Error(`Cannot download "${requestOptions.protocol || "https"}://${requestOptions.hostname}/${requestOptions.path}", status ${response.statusCode}: ${response.statusMessage}`))
157+
return
158+
}
141159

142-
export interface Response extends EventEmitter {
143-
statusCode?: number
144-
statusMessage?: string
160+
const redirectUrl = safeGetHeader(response, "location")
161+
if (redirectUrl != null) {
162+
if (redirectCount < this.maxRedirects) {
163+
const parsedUrl = parseUrl(redirectUrl)
164+
this.doDownload(Object.assign({}, requestOptions, {
165+
hostname: parsedUrl.hostname,
166+
path: parsedUrl.path,
167+
port: parsedUrl.port == null ? undefined : parsedUrl.port
168+
}), destination, redirectCount++, options, callback)
169+
}
170+
else {
171+
callback(new Error(`Too many redirects (> ${this.maxRedirects})`))
172+
}
173+
return
174+
}
145175

146-
headers: any
176+
configurePipes(options, response, destination, callback)
177+
})
178+
this.addTimeOutHandler(request, callback)
179+
request.on("error", callback)
180+
request.end()
181+
}
147182

148-
setEncoding(encoding: string): void
183+
protected addTimeOutHandler(request: any, callback: (error: Error) => void) {
184+
request.on("socket", function (socket: Socket) {
185+
socket.setTimeout(60 * 1000, () => {
186+
callback(new Error("Request timed out"))
187+
request.abort()
188+
})
189+
})
190+
}
149191
}
150192

151193
class DigestTransform extends Transform {
@@ -166,12 +208,8 @@ class DigestTransform extends Transform {
166208
}
167209
}
168210

169-
export function githubRequest<T>(path: string, token: string | null, data: {[name: string]: any; } | null = null, method?: string): Promise<T> {
170-
return executorHolder.httpExecutor.request<T>({hostname: "api.github.com", path: path}, token, data, {Accept: "application/vnd.github.v3+json"}, method)
171-
}
172-
173-
export function request<T>(url: Url, token: string | null = null, data: {[name: string]: any; } | null = null, headers?: { [key: string]: any } | null, method?: string): Promise<T> {
174-
return executorHolder.httpExecutor.request(url, token, data, headers, method)
211+
export function request<T>(options: RequestOptions, data?: {[name: string]: any; } | null): Promise<T> {
212+
return executorHolder.httpExecutor.request(options, data)
175213
}
176214

177215
function checkSha2(sha2Header: string | null | undefined, sha2: string | null | undefined, callback: (error: Error | null) => void): boolean {
@@ -189,7 +227,7 @@ function checkSha2(sha2Header: string | null | undefined, sha2: string | null |
189227
return true
190228
}
191229

192-
export function safeGetHeader(response: any, headerKey: string) {
230+
function safeGetHeader(response: any, headerKey: string) {
193231
const value = response.headers[headerKey]
194232
if (value == null) {
195233
return null
@@ -203,7 +241,7 @@ export function safeGetHeader(response: any, headerKey: string) {
203241
}
204242
}
205243

206-
export function configurePipes(options: DownloadOptions, response: any, destination: string, callback: (error: Error | null) => void) {
244+
function configurePipes(options: DownloadOptions, response: any, destination: string, callback: (error: Error | null) => void) {
207245
if (!checkSha2(safeGetHeader(response, "X-Checksum-Sha2"), options.sha2, callback)) {
208246
return
209247
}
@@ -230,4 +268,31 @@ export function configurePipes(options: DownloadOptions, response: any, destinat
230268
}
231269

232270
fileOut.on("finish", () => (<any>fileOut.close)(callback))
271+
}
272+
273+
export function configureRequestOptions(options: RequestOptions, token: string | null, method?: string): RequestOptions {
274+
if (method != null) {
275+
options.method = method
276+
}
277+
278+
let headers = options.headers
279+
if (headers == null) {
280+
headers = {}
281+
options.headers = headers
282+
}
283+
if (token != null) {
284+
(<any>headers).authorization = token.startsWith("Basic") ? token : `token ${token}`
285+
}
286+
if (headers["User-Agent"] == null) {
287+
headers["User-Agent"] = "electron-builder"
288+
}
289+
return options
290+
}
291+
292+
export function dumpRequestOptions(options: RequestOptions): string {
293+
const safe: any = Object.assign({}, options)
294+
if (safe.headers != null && safe.headers.authorization != null) {
295+
safe.headers.authorization = "<skipped>"
296+
}
297+
return JSON.stringify(safe, null, 2)
233298
}

packages/electron-builder/src/publish/BintrayPublisher.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { BintrayClient, Version } from "electron-builder-http/out/bintray"
66
import { BintrayOptions } from "electron-builder-http/out/publishOptions"
77
import { ClientRequest } from "http"
88
import { NodeHttpExecutor } from "../util/nodeHttpExecutor"
9-
import { HttpError } from "electron-builder-http"
9+
import { HttpError, configureRequestOptions } from "electron-builder-http"
1010

1111
export class BintrayPublisher extends Publisher {
1212
private _versionPromise: BluebirdPromise<Version>
@@ -58,7 +58,7 @@ export class BintrayPublisher extends Publisher {
5858
let badGatewayCount = 0
5959
for (let i = 0; i < 3; i++) {
6060
try {
61-
return await this.httpExecutor.doApiRequest<any>({
61+
return await this.httpExecutor.doApiRequest<any>(configureRequestOptions({
6262
hostname: "api.bintray.com",
6363
path: `/content/${this.client.owner}/${this.client.repo}/${this.client.packageName}/${version.name}/${fileName}`,
6464
method: "PUT",
@@ -68,7 +68,7 @@ export class BintrayPublisher extends Publisher {
6868
"X-Bintray-Override": "1",
6969
"X-Bintray-Publish": "1",
7070
}
71-
}, this.client.auth, requestProcessor)
71+
}, this.client.auth), requestProcessor)
7272
}
7373
catch (e) {
7474
if (e instanceof HttpError && e.response.statusCode === 502 && badGatewayCount++ < 3) {

packages/electron-builder/src/publish/gitHubPublisher.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import BluebirdPromise from "bluebird-lst-c"
77
import { PublishOptions, Publisher } from "./publisher"
88
import { GithubOptions } from "electron-builder-http/out/publishOptions"
99
import { ClientRequest } from "http"
10-
import { HttpError, githubRequest } from "electron-builder-http"
10+
import { HttpError, configureRequestOptions } from "electron-builder-http"
1111
import { NodeHttpExecutor } from "../util/nodeHttpExecutor"
1212

1313
export interface Release {
@@ -63,7 +63,7 @@ export class GitHubPublisher extends Publisher {
6363

6464
private async getOrCreateRelease(): Promise<Release | null> {
6565
// we don't use "Get a release by tag name" because "tag name" means existing git tag, but we draft release and don't create git tag
66-
const releases = await githubRequest<Array<Release>>(`/repos/${this.info.owner}/${this.info.repo}/releases`, this.token)
66+
const releases = await this.githubRequest<Array<Release>>(`/repos/${this.info.owner}/${this.info.repo}/releases`, this.token)
6767
for (const release of releases) {
6868
if (release.tag_name === this.tag || release.tag_name === this.version) {
6969
if (release.draft || release.prerelease) {
@@ -102,7 +102,7 @@ export class GitHubPublisher extends Publisher {
102102
let badGatewayCount = 0
103103
uploadAttempt: for (let i = 0; i < 3; i++) {
104104
try {
105-
return await this.httpExecutor.doApiRequest<any>({
105+
return await this.httpExecutor.doApiRequest<any>(configureRequestOptions({
106106
hostname: parsedUrl.hostname,
107107
path: parsedUrl.path,
108108
method: "POST",
@@ -112,18 +112,18 @@ export class GitHubPublisher extends Publisher {
112112
"Content-Type": mime.lookup(fileName),
113113
"Content-Length": dataLength
114114
}
115-
}, this.token, requestProcessor)
115+
}, this.token), requestProcessor)
116116
}
117117
catch (e) {
118118
if (e instanceof HttpError) {
119119
if (e.response.statusCode === 422 && e.description != null && e.description.errors != null && e.description.errors[0].code === "already_exists") {
120120
// delete old artifact and re-upload
121121
log(`Artifact ${fileName} already exists, overwrite one`)
122122

123-
const assets = await githubRequest<Array<Asset>>(`/repos/${this.info.owner}/${this.info.repo}/releases/${release.id}/assets`, this.token, null)
123+
const assets = await this.githubRequest<Array<Asset>>(`/repos/${this.info.owner}/${this.info.repo}/releases/${release.id}/assets`, this.token, null)
124124
for (const asset of assets) {
125125
if (asset!.name === fileName) {
126-
await githubRequest<void>(`/repos/${this.info.owner}/${this.info.repo}/releases/assets/${asset!.id}`, this.token, null, "DELETE")
126+
await this.githubRequest<void>(`/repos/${this.info.owner}/${this.info.repo}/releases/assets/${asset!.id}`, this.token, null, "DELETE")
127127
continue uploadAttempt
128128
}
129129
}
@@ -142,7 +142,7 @@ export class GitHubPublisher extends Publisher {
142142
}
143143

144144
private createRelease() {
145-
return githubRequest<Release>(`/repos/${this.info.owner}/${this.info.repo}/releases`, this.token, {
145+
return this.githubRequest<Release>(`/repos/${this.info.owner}/${this.info.repo}/releases`, this.token, {
146146
tag_name: this.tag,
147147
name: this.version,
148148
draft: this.options.draft == null || this.options.draft,
@@ -153,7 +153,7 @@ export class GitHubPublisher extends Publisher {
153153
// test only
154154
//noinspection JSUnusedGlobalSymbols
155155
async getRelease(): Promise<any> {
156-
return githubRequest<Release>(`/repos/${this.info.owner}/${this.info.repo}/releases/${(await this._releasePromise).id}`, this.token)
156+
return this.githubRequest<Release>(`/repos/${this.info.owner}/${this.info.repo}/releases/${(await this._releasePromise).id}`, this.token)
157157
}
158158

159159
//noinspection JSUnusedGlobalSymbols
@@ -165,7 +165,7 @@ export class GitHubPublisher extends Publisher {
165165

166166
for (let i = 0; i < 3; i++) {
167167
try {
168-
return await githubRequest(`/repos/${this.info.owner}/${this.info.repo}/releases/${release.id}`, this.token, null, "DELETE")
168+
return await this.githubRequest(`/repos/${this.info.owner}/${this.info.repo}/releases/${release.id}`, this.token, null, "DELETE")
169169
}
170170
catch (e) {
171171
if (e instanceof HttpError) {
@@ -185,10 +185,11 @@ export class GitHubPublisher extends Publisher {
185185
warn(`Cannot delete release ${release.id}`)
186186
}
187187

188-
// async deleteOldReleases() {
189-
// const releases = await githubRequest<Array<Release>>(`/repos/${this.owner}/${this.repo}/releases`, this.token)
190-
// for (const release of releases) {
191-
// await githubRequest(`/repos/${this.owner}/${this.repo}/releases/${release.id}`, this.token, null, "DELETE")
192-
// }
193-
// }
188+
private githubRequest<T>(path: string, token: string | null, data: {[name: string]: any; } | null = null, method?: string): Promise<T> {
189+
return this.httpExecutor.request<T>(configureRequestOptions({
190+
hostname: "api.github.com",
191+
path: path,
192+
headers: {Accept: "application/vnd.github.v3+json"}
193+
}, token, method), data)
194+
}
194195
}

0 commit comments

Comments
 (0)