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
+ }
0 commit comments