1
- import { Url } from "url"
2
1
import { createHash } from "crypto"
3
2
import { Transform } from "stream"
4
3
import { createWriteStream } from "fs-extra-p"
@@ -8,11 +7,23 @@ import _debug from "debug"
8
7
import { ProgressCallbackTransform } from "./ProgressCallbackTransform"
9
8
import { safeLoad } from "js-yaml"
10
9
import { EventEmitter } from "events"
10
+ import { Socket } from "net"
11
11
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
+ }
14
24
15
25
export interface DownloadOptions {
26
+ headers ?: RequestHeaders | null
16
27
skipDirCreation ?: boolean
17
28
sha2 ?: string
18
29
onProgress ?( progress : any ) : void
@@ -39,41 +50,48 @@ export function download(url: string, destination: string, options?: DownloadOpt
39
50
return executorHolder . httpExecutor . download ( url , destination , options )
40
51
}
41
52
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
+
42
61
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 )
49
67
50
68
const encodedData = data == null ? undefined : new Buffer ( JSON . stringify ( data ) )
51
69
if ( encodedData != null ) {
52
70
options . method = "post"
71
+ if ( options . headers == null ) {
72
+ options . headers = { }
73
+ }
74
+
53
75
options . headers [ "Content-Type" ] = "application/json"
54
76
options . headers [ "Content-Length" ] = encodedData . length
55
77
}
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 )
57
79
}
58
80
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 >
60
82
61
83
abstract download ( url : string , destination : string , options ?: DownloadOptions | null ) : Promise < string >
62
84
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 ) } ` )
70
88
}
71
89
72
90
// we handle any other >= 400 error on request end (read detailed message in the response body)
73
91
if ( response . statusCode === 404 ) {
74
92
// error is clear, we don't need to read detailed error description
75
93
reject ( new HttpError ( response , `method: ${ options . method } url: https://${ options . hostname } ${ options . path }
76
-
94
+
77
95
Please double check that your authentication token is correct. Due to security reasons actual status maybe not reported, but 404.
78
96
` ) )
79
97
return
@@ -91,7 +109,7 @@ export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
91
109
return
92
110
}
93
111
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 )
95
113
. then ( resolve )
96
114
. catch ( reject )
97
115
@@ -129,23 +147,47 @@ export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
129
147
}
130
148
} )
131
149
}
132
- }
133
150
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
137
152
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
+ }
141
159
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
+ }
145
175
146
- headers : any
176
+ configurePipes ( options , response , destination , callback )
177
+ } )
178
+ this . addTimeOutHandler ( request , callback )
179
+ request . on ( "error" , callback )
180
+ request . end ( )
181
+ }
147
182
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
+ }
149
191
}
150
192
151
193
class DigestTransform extends Transform {
@@ -166,12 +208,8 @@ class DigestTransform extends Transform {
166
208
}
167
209
}
168
210
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 )
175
213
}
176
214
177
215
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 |
189
227
return true
190
228
}
191
229
192
- export function safeGetHeader ( response : any , headerKey : string ) {
230
+ function safeGetHeader ( response : any , headerKey : string ) {
193
231
const value = response . headers [ headerKey ]
194
232
if ( value == null ) {
195
233
return null
@@ -203,7 +241,7 @@ export function safeGetHeader(response: any, headerKey: string) {
203
241
}
204
242
}
205
243
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 ) {
207
245
if ( ! checkSha2 ( safeGetHeader ( response , "X-Checksum-Sha2" ) , options . sha2 , callback ) ) {
208
246
return
209
247
}
@@ -230,4 +268,31 @@ export function configurePipes(options: DownloadOptions, response: any, destinat
230
268
}
231
269
232
270
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 )
233
298
}
0 commit comments