1
1
import electron , { ipcMain } from 'electron' ;
2
2
import fs from 'fs' ;
3
+ import { IncomingMessage } from 'http' ;
3
4
import { setDefaultProtocol } from 'insomnia-url' ;
4
5
import mkdirp from 'mkdirp' ;
5
6
import path from 'path' ;
@@ -13,11 +14,15 @@ import {
13
14
WebSocket ,
14
15
} from 'ws' ;
15
16
17
+ import { AUTH_BASIC , AUTH_BEARER } from '../../common/constants' ;
16
18
import { generateId } from '../../common/misc' ;
17
19
import { websocketRequest } from '../../models' ;
18
20
import * as models from '../../models' ;
21
+ import { RequestAuthentication , RequestHeader } from '../../models/request' ;
19
22
import type { Response } from '../../models/response' ;
20
23
import { BaseWebSocketRequest } from '../../models/websocket-request' ;
24
+ import { getBasicAuthHeader } from '../../network/basic-auth/get-header' ;
25
+ import { getBearerAuthHeader } from '../../network/bearer-auth/get-header' ;
21
26
import { urlMatchesCertHost } from '../../network/url-matches-cert-host' ;
22
27
23
28
export interface WebSocketConnection extends WebSocket {
@@ -96,6 +101,23 @@ function dispatchWebSocketEvent(target: Electron.WebContents, eventChannel: stri
96
101
}
97
102
}
98
103
104
+ const parseResponseAndBuildTimeline = ( url : string , incomingMessage : IncomingMessage , clientRequestHeaders : string ) => {
105
+ const statusMessage = incomingMessage . statusMessage || '' ;
106
+ const statusCode = incomingMessage . statusCode || 0 ;
107
+ const httpVersion = incomingMessage . httpVersion ;
108
+ const responseHeaders = Object . entries ( incomingMessage . headers ) . map ( ( [ name , value ] ) => ( { name, value : value ?. toString ( ) || '' } ) ) ;
109
+ const headersIn = responseHeaders . map ( ( { name, value } ) => `${ name } : ${ value } ` ) . join ( '\n' ) ;
110
+ const timeline = [
111
+ { value : `Preparing request to ${ url } ` , name : 'Text' , timestamp : Date . now ( ) } ,
112
+ { value : `Current time is ${ new Date ( ) . toISOString ( ) } ` , name : 'Text' , timestamp : Date . now ( ) } ,
113
+ { value : 'Using HTTP 1.1' , name : 'Text' , timestamp : Date . now ( ) } ,
114
+ { value : clientRequestHeaders , name : 'HeaderOut' , timestamp : Date . now ( ) } ,
115
+ { value : `HTTP/${ httpVersion } ${ statusCode } ${ statusMessage } ` , name : 'HeaderIn' , timestamp : Date . now ( ) } ,
116
+ { value : headersIn , name : 'HeaderIn' , timestamp : Date . now ( ) } ,
117
+ ] ;
118
+ return { timeline, responseHeaders, statusCode, statusMessage, httpVersion } ;
119
+ } ;
120
+
99
121
async function createWebSocketConnection (
100
122
event : Electron . IpcMainInvokeEvent ,
101
123
options : { requestId : string ; workspaceId : string }
@@ -122,11 +144,24 @@ async function createWebSocketConnection(
122
144
try {
123
145
const eventChannel = `webSocketRequest.connection.${ responseId } .event` ;
124
146
const readyStateChannel = `webSocketRequest.connection.${ request . _id } .readyState` ;
125
-
126
147
// @TODO : Render nunjucks tags in these headers
127
148
const reduceArrayToLowerCaseKeyedDictionary = ( acc : { [ key : string ] : string } , { name, value } : BaseWebSocketRequest [ 'headers' ] [ 0 ] ) =>
128
149
( { ...acc , [ name . toLowerCase ( ) || '' ] : value || '' } ) ;
129
- const headers = request . headers . filter ( ( { value, disabled } ) => ! ! value && ! disabled )
150
+ const headers = request . headers ;
151
+ if ( request . authentication . disabled === false ) {
152
+ if ( request . authentication . type === AUTH_BASIC ) {
153
+ const { username, password, useISO88591 } = request . authentication ;
154
+ const encoding = useISO88591 ? 'latin1' : 'utf8' ;
155
+ headers . push ( getBasicAuthHeader ( username , password , encoding ) ) ;
156
+ }
157
+ if ( request . authentication . type === AUTH_BEARER ) {
158
+ const { token, prefix } = request . authentication ;
159
+ headers . push ( getBearerAuthHeader ( token , prefix ) ) ;
160
+ }
161
+ }
162
+
163
+ const lowerCasedEnabledHeaders = headers
164
+ . filter ( ( { value, disabled } ) => ! ! value && ! disabled )
130
165
. reduce ( reduceArrayToLowerCaseKeyedDictionary , { } ) ;
131
166
132
167
const settings = await models . settings . getOrCreate ( ) ;
@@ -158,34 +193,43 @@ async function createWebSocketConnection(
158
193
} ) ;
159
194
160
195
const ws = new WebSocket ( request . url , {
161
- headers,
196
+ headers : lowerCasedEnabledHeaders ,
162
197
cert : pemCertificates ,
163
198
key : pemCertificateKeys ,
164
199
pfx : pfxCertificates ,
165
200
rejectUnauthorized : settings . validateSSL ,
201
+ followRedirects : true ,
166
202
} ) ;
167
203
WebSocketConnections . set ( options . requestId , ws ) ;
168
204
169
- ws . on ( 'upgrade' , async incoming => {
205
+ ws . on ( 'upgrade' , async incomingMessage => {
170
206
// @ts -expect-error -- private property
171
- const internalRequest = ws . _req ;
172
- // response
173
- const statusMessage = incoming . statusMessage || '' ;
174
- const statusCode = incoming . statusCode || 0 ;
175
- const httpVersion = incoming . httpVersion ;
176
- const responseHeaders = Object . entries ( incoming . headers ) . map ( ( [ name , value ] ) => ( { name, value : value ?. toString ( ) || '' } ) ) ;
177
- const headersIn = responseHeaders . map ( ( { name, value } ) => `${ name } : ${ value } ` ) . join ( '\n' ) ;
178
-
179
- // @TODO : We may want to add set-cookie handling here.
180
- [
181
- { value : `Preparing request to ${ request . url } ` , name : 'Text' , timestamp : Date . now ( ) } ,
182
- { value : `Current time is ${ new Date ( ) . toISOString ( ) } ` , name : 'Text' , timestamp : Date . now ( ) } ,
183
- { value : 'Using HTTP 1.1' , name : 'Text' , timestamp : Date . now ( ) } ,
184
- { value : internalRequest . _header , name : 'HeaderOut' , timestamp : Date . now ( ) } ,
185
- { value : `HTTP/${ httpVersion } ${ statusCode } ${ statusMessage } ` , name : 'HeaderIn' , timestamp : Date . now ( ) } ,
186
- { value : headersIn , name : 'HeaderIn' , timestamp : Date . now ( ) } ,
187
- ] . map ( t => timelineFileStreams . get ( options . requestId ) ?. write ( JSON . stringify ( t ) + '\n' ) ) ;
188
-
207
+ const internalRequestHeader = ws . _req . _header ;
208
+ const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline ( request . url , incomingMessage , internalRequestHeader ) ;
209
+ timeline . map ( t => timelineFileStreams . get ( options . requestId ) ?. write ( JSON . stringify ( t ) + '\n' ) ) ;
210
+ const responsePatch : Partial < Response > = {
211
+ _id : responseId ,
212
+ parentId : request . _id ,
213
+ headers : responseHeaders ,
214
+ url : request . url ,
215
+ statusCode,
216
+ statusMessage,
217
+ httpVersion,
218
+ elapsedTime : performance . now ( ) - start ,
219
+ timelinePath,
220
+ bodyPath : responseBodyPath ,
221
+ // NOTE: required for legacy zip workaround
222
+ bodyCompression : null ,
223
+ } ;
224
+ const settings = await models . settings . getOrCreate ( ) ;
225
+ models . response . create ( responsePatch , settings . maxHistoryResponses ) ;
226
+ models . requestMeta . updateOrCreateByParentId ( request . _id , { activeResponseId : null } ) ;
227
+ } ) ;
228
+ ws . on ( 'unexpected-response' , async ( clientRequest , incomingMessage ) => {
229
+ // @ts -expect-error -- private property
230
+ const internalRequestHeader = clientRequest . _header ;
231
+ const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline ( request . url , incomingMessage , internalRequestHeader ) ;
232
+ timeline . map ( t => timelineFileStreams . get ( options . requestId ) ?. write ( JSON . stringify ( t ) + '\n' ) ) ;
189
233
const responsePatch : Partial < Response > = {
190
234
_id : responseId ,
191
235
parentId : request . _id ,
@@ -203,6 +247,7 @@ async function createWebSocketConnection(
203
247
const settings = await models . settings . getOrCreate ( ) ;
204
248
models . response . create ( responsePatch , settings . maxHistoryResponses ) ;
205
249
models . requestMeta . updateOrCreateByParentId ( request . _id , { activeResponseId : null } ) ;
250
+ deleteRequestMaps ( request . _id , `Unexpected response ${ incomingMessage . statusCode } ` ) ;
206
251
} ) ;
207
252
208
253
ws . addEventListener ( 'open' , ( ) => {
@@ -413,3 +458,31 @@ electron.app.on('window-all-closed', () => {
413
458
ws . close ( ) ;
414
459
} ) ;
415
460
} ) ;
461
+
462
+ export function getAuthHeader ( authentication : RequestAuthentication ) : RequestHeader | undefined {
463
+ if ( ! authentication || authentication . disabled ) {
464
+ return ;
465
+ }
466
+
467
+ switch ( authentication . type ) {
468
+ case 'basic' : {
469
+ const { username, password, useISO88591 } = authentication ;
470
+ const encoding = useISO88591 ? 'latin1' : 'utf8' ;
471
+ const header = getBasicAuthHeader ( username , password , encoding ) ;
472
+ return header ;
473
+ }
474
+
475
+ case 'bearer' : {
476
+ const { token, prefix } = authentication ;
477
+ return getBearerAuthHeader ( token , prefix ) ;
478
+ }
479
+
480
+ case 'digest' : {
481
+ return ;
482
+ }
483
+
484
+ default : {
485
+ return ;
486
+ }
487
+ }
488
+ }
0 commit comments