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