Skip to content

Commit 569dd8b

Browse files
Szavadevelar
authored andcommitted
feat: Use Electron.Net to send http requests for auto updater
There was a refactoring around httpRequests and util/restApiRequests, they both forward the calls to an instance of the HttpExecutor interface, which can be Node or Electron. Node is used by default and electron is only used in case it's an electron app running the auto updater Closes #959, #1024
1 parent dfb8890 commit 569dd8b

18 files changed

+662
-377
lines changed

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nsis-auto-updater/package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,11 @@
1313
],
1414
"dependencies": {
1515
"bluebird-lst-c": "^1.0.5",
16-
"debug": "^2.4.4",
16+
"debug": "^2.4.5",
1717
"fs-extra-p": "^3.0.3",
18-
"ini": "^1.3.4",
1918
"js-yaml": "^3.7.0",
2019
"semver": "^5.3.0",
21-
"source-map-support": "^0.4.6",
22-
"tunnel-agent": "^0.4.3"
20+
"source-map-support": "^0.4.6"
2321
},
2422
"typings": "./out/electron-auto-updater.d.ts"
2523
}

nsis-auto-updater/src/BintrayProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class BintrayProvider implements Provider<VersionInfo> {
1717
}
1818
}
1919
catch (e) {
20-
if (e instanceof HttpError && e.response.statusCode === 404) {
20+
if ("response" in e && e.response.statusCode === 404) {
2121
throw new Error(`No latest version, please ensure that user, package and repository correctly configured. Or at least one version is published. ${e.stack || e.message}`)
2222
}
2323
throw e

nsis-auto-updater/src/GenericProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Provider, FileInfo } from "./api"
2-
import { HttpError, request } from "../../src/publish/restApiRequest"
32
import { GenericServerOptions, UpdateInfo } from "../../src/options/publishOptions"
43
import * as url from "url"
54
import * as path from "path"
5+
import { HttpError, request } from "../../src/util/httpExecutor"
66

77
export class GenericProvider implements Provider<UpdateInfo> {
88
private readonly baseUrl = url.parse(this.configuration.url)

nsis-auto-updater/src/GitHubProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Provider, FileInfo } from "./api"
22
import { VersionInfo, GithubOptions, UpdateInfo } from "../../src/options/publishOptions"
3-
import { request, HttpError } from "../../src/publish/restApiRequest"
43
import { validateUpdateInfo } from "./GenericProvider"
54
import * as path from "path"
5+
import { HttpError, request } from "../../src/util/httpExecutor"
66

77
export class GitHubProvider implements Provider<VersionInfo> {
88
constructor(private readonly options: GithubOptions) {

nsis-auto-updater/src/NsisUpdater.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { readFile } from "fs-extra-p"
1212
import { safeLoad } from "js-yaml"
1313
import { GenericProvider } from "./GenericProvider"
1414
import { GitHubProvider } from "./GitHubProvider"
15+
import { executorHolder } from "../../src/util/httpExecutor"
16+
import { ElectronHttpExecutor } from "./electronHttpExecutor"
1517

1618
export class NsisUpdater extends EventEmitter {
1719
private setupPath: string | null
@@ -21,14 +23,32 @@ export class NsisUpdater extends EventEmitter {
2123

2224
private clientPromise: Promise<Provider<any>>
2325

26+
private readonly untilAppReady: Promise<boolean>
27+
2428
private readonly app: any
2529

2630
private quitHandlerAdded = false
2731

2832
constructor(options?: PublishConfiguration | BintrayOptions | GithubOptions) {
2933
super()
3034

31-
this.app = (<any>global).__test_app || require("electron").app
35+
if ((<any>global).__test_app) {
36+
this.app = (<any>global).__test_app
37+
this.untilAppReady = BluebirdPromise.resolve()
38+
}
39+
else {
40+
this.app = require("electron").app
41+
executorHolder.httpExecutor = new ElectronHttpExecutor()
42+
this.untilAppReady = new BluebirdPromise((resolve, reject) => {
43+
if (this.app.isReady()) {
44+
resolve()
45+
}
46+
else {
47+
this.app.on("ready", resolve)
48+
}
49+
})
50+
}
51+
3252

3353
if (options != null) {
3454
this.setFeedURL(options)
@@ -44,6 +64,7 @@ export class NsisUpdater extends EventEmitter {
4464
}
4565

4666
async checkForUpdates(): Promise<UpdateCheckResult> {
67+
await this.untilAppReady
4768
this.emit("checking-for-update")
4869
try {
4970
if (this.clientPromise == null) {
@@ -159,13 +180,16 @@ function createClient(data: string | PublishConfiguration | BintrayOptions | Git
159180
if (typeof data === "string") {
160181
throw new Error("Please pass PublishConfiguration object")
161182
}
162-
else {
163-
const provider = (<PublishConfiguration>data).provider
164-
switch (provider) {
165-
case "github": return new GitHubProvider(<GithubOptions>data)
166-
case "generic": return new GenericProvider(<GenericServerOptions>data)
167-
case "bintray": return new BintrayProvider(<BintrayOptions>data)
168-
default: throw new Error(`Unsupported provider: ${provider}`)
169-
}
183+
184+
const provider = (<PublishConfiguration>data).provider
185+
switch (provider) {
186+
case "github":
187+
return new GitHubProvider(<GithubOptions>data)
188+
case "generic":
189+
return new GenericProvider(<GenericServerOptions>data)
190+
case "bintray":
191+
return new BintrayProvider(<BintrayOptions>data)
192+
default:
193+
throw new Error(`Unsupported provider: ${provider}`)
170194
}
171195
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { Socket } from "net"
2+
import { net } from "electron"
3+
import { createWriteStream, ensureDir } from "fs-extra-p"
4+
import BluebirdPromise from "bluebird-lst-c"
5+
import * as path from "path"
6+
import { HttpExecutor, DownloadOptions, HttpError, DigestTransform } from "../../src/util/httpExecutor"
7+
import { Url } from "url"
8+
import { safeLoad } from "js-yaml"
9+
import _debug from "debug"
10+
import Debugger = debug.Debugger
11+
import { parse as parseUrl } from "url"
12+
13+
export class ElectronHttpExecutor implements HttpExecutor {
14+
private readonly debug: Debugger = _debug("electron-builder")
15+
16+
private readonly maxRedirects = 10
17+
18+
request<T>(url: Url, token: string | null = null, data: {[name: string]: any; } | null = null, method: string = "GET"): Promise<T> {
19+
const options: any = Object.assign({
20+
method: method,
21+
headers: {
22+
"User-Agent": "electron-builder"
23+
}
24+
}, url)
25+
26+
if (url.hostname!!.includes("github") && !url.path!.endsWith(".yml")) {
27+
options.headers.Accept = "application/vnd.github.v3+json"
28+
}
29+
30+
const encodedData = data == null ? undefined : new Buffer(JSON.stringify(data))
31+
if (encodedData != null) {
32+
options.method = "post"
33+
options.headers["Content-Type"] = "application/json"
34+
options.headers["Content-Length"] = encodedData.length
35+
}
36+
return this.doApiRequest<T>(options, token, it => it.end(encodedData))
37+
}
38+
39+
download(url: string, destination: string, options?: DownloadOptions | null): Promise<string> {
40+
return new BluebirdPromise( (resolve, reject) => {
41+
this.doDownload(url, destination, 0, options || {}, (error: Error) => {
42+
if (error == null) {
43+
resolve(destination)
44+
}
45+
else {
46+
reject(error)
47+
}
48+
})
49+
})
50+
}
51+
52+
private addTimeOutHandler(request: Electron.ClientRequest, callback: (error: Error) => void) {
53+
request.on("socket", function (socket: Socket) {
54+
socket.setTimeout(60 * 1000, () => {
55+
callback(new Error("Request timed out"))
56+
request.abort()
57+
})
58+
})
59+
}
60+
61+
private doDownload(url: string, destination: string, redirectCount: number, options: DownloadOptions, callback: (error: Error | null) => void) {
62+
const ensureDirPromise = options.skipDirCreation ? BluebirdPromise.resolve() : ensureDir(path.dirname(destination))
63+
64+
const parsedUrl = parseUrl(url)
65+
// user-agent must be specified, otherwise some host can return 401 unauthorised
66+
67+
//FIXME hack, the electron typings specifies Protocol with capital but the code actually uses with small case
68+
const requestOpts = {
69+
protocol: parsedUrl.protocol,
70+
hostname: parsedUrl.hostname,
71+
path: parsedUrl.path,
72+
headers: {
73+
"User-Agent": "electron-builder"
74+
},
75+
}
76+
77+
const request = net.request(requestOpts, (response: Electron.IncomingMessage) => {
78+
if (response.statusCode >= 400) {
79+
callback(new Error(`Cannot download "${url}", status ${response.statusCode}: ${response.statusMessage}`))
80+
return
81+
}
82+
83+
const redirectUrl = this.safeGetHeader(response, "location")
84+
if (redirectUrl != null) {
85+
if (redirectCount < this.maxRedirects) {
86+
this.doDownload(redirectUrl, destination, redirectCount++, options, callback)
87+
}
88+
else {
89+
callback(new Error("Too many redirects (> " + this.maxRedirects + ")"))
90+
}
91+
return
92+
}
93+
94+
const sha2Header = this.safeGetHeader(response, "X-Checksum-Sha2")
95+
if (sha2Header != null && options.sha2 != null) {
96+
// todo why bintray doesn't send this header always
97+
if (sha2Header == null) {
98+
throw new Error("checksum is required, but server response doesn't contain X-Checksum-Sha2 header")
99+
}
100+
else if (sha2Header !== options.sha2) {
101+
throw new Error(`checksum mismatch: expected ${options.sha2} but got ${sha2Header} (X-Checksum-Sha2 header)`)
102+
}
103+
}
104+
105+
ensureDirPromise
106+
.then(() => {
107+
const fileOut = createWriteStream(destination)
108+
if (options.sha2 == null) {
109+
response.pipe(fileOut)
110+
}
111+
else {
112+
response
113+
.pipe(new DigestTransform(options.sha2))
114+
.pipe(fileOut)
115+
}
116+
117+
fileOut.on("finish", () => (<any>fileOut.close)(callback))
118+
})
119+
.catch(callback)
120+
121+
let ended = false
122+
response.on("end", () => {
123+
ended = true
124+
})
125+
126+
response.on("close", () => {
127+
if (!ended) {
128+
callback(new Error("Request aborted"))
129+
}
130+
})
131+
})
132+
this.addTimeOutHandler(request, callback)
133+
request.on("error", callback)
134+
request.end()
135+
}
136+
137+
private safeGetHeader(response: Electron.IncomingMessage, headerKey: string) {
138+
return response.headers[headerKey] ? response.headers[headerKey].pop() : null
139+
}
140+
141+
142+
doApiRequest<T>(options: Electron.RequestOptions, token: string | null, requestProcessor: (request: Electron.ClientRequest, reject: (error: Error) => void) => void, redirectCount: number = 0): Promise<T> {
143+
const requestOptions: any = options
144+
this.debug(`HTTPS request: ${JSON.stringify(requestOptions, null, 2)}`)
145+
146+
if (token != null) {
147+
(<any>requestOptions.headers).authorization = token.startsWith("Basic") ? token : `token ${token}`
148+
}
149+
150+
requestOptions.protocol = "https:"
151+
return new BluebirdPromise<T>((resolve, reject, onCancel) => {
152+
const request = net.request(options, (response: Electron.IncomingMessage) => {
153+
try {
154+
if (response.statusCode === 404) {
155+
// error is clear, we don't need to read detailed error description
156+
reject(new HttpError(response, `method: ${options.method} url: https://${options.hostname}${options.path}
157+
158+
Please double check that your authentication token is correct. Due to security reasons actual status maybe not reported, but 404.
159+
`))
160+
}
161+
else if (response.statusCode === 204) {
162+
// on DELETE request
163+
resolve()
164+
return
165+
}
166+
167+
const redirectUrl = this.safeGetHeader(response, "location")
168+
if (redirectUrl != null) {
169+
if (redirectCount > 10) {
170+
reject(new Error("Too many redirects (> 10)"))
171+
return
172+
}
173+
174+
if (options.path!.endsWith("/latest")) {
175+
resolve(<any>{location: redirectUrl})
176+
}
177+
else {
178+
this.doApiRequest(Object.assign({}, options, parseUrl(redirectUrl)), token, requestProcessor)
179+
.then(<any>resolve)
180+
.catch(reject)
181+
}
182+
return
183+
}
184+
185+
let data = ""
186+
response.setEncoding("utf8")
187+
response.on("data", (chunk: string) => {
188+
data += chunk
189+
})
190+
191+
response.on("end", () => {
192+
try {
193+
const contentType = response.headers["content-type"]
194+
const isJson = contentType != null && contentType.includes("json")
195+
if (response.statusCode >= 400) {
196+
if (isJson) {
197+
reject(new HttpError(response, JSON.parse(data)))
198+
}
199+
else {
200+
reject(new HttpError(response))
201+
}
202+
}
203+
else {
204+
resolve(data.length === 0 ? null : (isJson || !options.path!.includes(".yml")) ? JSON.parse(data) : safeLoad(data))
205+
}
206+
}
207+
catch (e) {
208+
reject(e)
209+
}
210+
})
211+
}
212+
catch (e) {
213+
reject(e)
214+
}
215+
})
216+
this.addTimeOutHandler(request, reject)
217+
request.on("error", reject)
218+
requestProcessor(request, reject)
219+
onCancel!(() => request.abort())
220+
})
221+
}
222+
}

nsis-auto-updater/tsconfig.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@
2424
"../typings/debug.d.ts",
2525
"../node_modules/@types/node/index.d.ts",
2626
"../node_modules/fs-extra-p/index.d.ts",
27-
"../node_modules/bluebird-lst-c/index.d.ts",
28-
"../src/util/httpRequest.ts",
29-
"../src/publish/restApiRequest.ts",
30-
"../src/publish/restApiRequest.ts",
31-
"../src/publish/bintray.ts"
27+
"../node_modules/bluebird-lst-c/index.d.ts"
3228
],
3329
"include": [
3430
"src/**/*.ts"

0 commit comments

Comments
 (0)