1
1
/* eslint-disable no-console */
2
- // FIXME: Update this file to be type safe and remove this and next line
3
- // @ts -strict-ignore
4
- import { homedir } from "os" ;
5
-
6
- import * as NodeIPC from "node-ipc" ;
2
+ import { ChildProcess , spawn } from "child_process" ;
3
+ import * as fs from "fs" ;
4
+ import * as path from "path" ;
7
5
8
6
// eslint-disable-next-line no-restricted-imports
9
7
import { MessageCommon } from "../../src/models/native-messaging/message-common" ;
@@ -13,11 +11,6 @@ import { UnencryptedMessageResponse } from "../../src/models/native-messaging/un
13
11
import Deferred from "./deferred" ;
14
12
import { race } from "./race" ;
15
13
16
- NodeIPC . config . id = "native-messaging-test-runner" ;
17
- NodeIPC . config . maxRetries = 0 ;
18
- NodeIPC . config . silent = true ;
19
-
20
- const DESKTOP_APP_PATH = `${ homedir } /tmp/app.bitwarden` ;
21
14
const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000 ; // 10 seconds
22
15
23
16
export type MessageHandler = ( MessageCommon ) => void ;
@@ -42,6 +35,10 @@ export default class IPCService {
42
35
// A set of deferred promises that are awaiting socket connection
43
36
private awaitingConnection = new Set < Deferred < void > > ( ) ;
44
37
38
+ // The IPC desktop_proxy process
39
+ private process ?: ChildProcess ;
40
+ private processOutputBuffer = Buffer . alloc ( 0 ) ;
41
+
45
42
constructor (
46
43
private socketName : string ,
47
44
private messageHandler : MessageHandler ,
@@ -72,47 +69,47 @@ export default class IPCService {
72
69
private _connect ( ) {
73
70
this . connectionState = IPCConnectionState . Connecting ;
74
71
75
- NodeIPC . connectTo ( this . socketName , DESKTOP_APP_PATH , ( ) => {
76
- // Process incoming message
77
- this . getSocket ( ) . on ( "message" , ( message : any ) => {
78
- this . processMessage ( message ) ;
79
- } ) ;
72
+ const proxyPath = selectProxyPath ( ) ;
73
+ console . log ( `[IPCService] connecting to proxy at ${ proxyPath } ` ) ;
80
74
81
- this . getSocket ( ) . on ( "error" , ( error : Error ) => {
82
- // Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
83
- // invoked multiple times each time a connection error happens
84
- console . log ( "[IPCService] errored" ) ;
85
- console . log (
86
- "\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m" ,
87
- ) ;
88
- this . awaitingConnection . forEach ( ( deferred ) => {
89
- console . log ( `rejecting: ${ deferred } ` ) ;
90
- deferred . reject ( error ) ;
91
- } ) ;
92
- this . awaitingConnection . clear ( ) ;
93
- } ) ;
75
+ this . process = spawn ( proxyPath , process . argv . slice ( 1 ) , {
76
+ cwd : process . cwd ( ) ,
77
+ stdio : "pipe" ,
78
+ shell : false ,
79
+ } ) ;
94
80
95
- this . getSocket ( ) . on ( "connect " , ( ) => {
96
- console . log ( "[IPCService] connected" ) ;
97
- this . connectionState = IPCConnectionState . Connected ;
81
+ this . process . stdout . on ( "data " , ( data : Buffer ) => {
82
+ this . processIpcMessage ( data ) ;
83
+ } ) ;
98
84
99
- this . awaitingConnection . forEach ( ( deferred ) => {
100
- deferred . resolve ( null ) ;
101
- } ) ;
102
- this . awaitingConnection . clear ( ) ;
103
- } ) ;
85
+ this . process . stderr . on ( "data" , ( data : Buffer ) => {
86
+ console . error ( `proxy log: ${ data } ` ) ;
87
+ } ) ;
104
88
105
- this . getSocket ( ) . on ( "disconnect" , ( ) => {
106
- console . log ( "[IPCService] disconnected" ) ;
107
- this . connectionState = IPCConnectionState . Disconnected ;
89
+ this . process . on ( "error" , ( error ) => {
90
+ // Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
91
+ // invoked multiple times each time a connection error happens
92
+ console . log ( "[IPCService] errored" ) ;
93
+ console . log (
94
+ "\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m" ,
95
+ ) ;
96
+ this . awaitingConnection . forEach ( ( deferred ) => {
97
+ console . log ( `rejecting: ${ deferred } ` ) ;
98
+ deferred . reject ( error ) ;
108
99
} ) ;
100
+ this . awaitingConnection . clear ( ) ;
101
+ } ) ;
102
+
103
+ this . process . on ( "exit" , ( ) => {
104
+ console . log ( "[IPCService] disconnected" ) ;
105
+ this . connectionState = IPCConnectionState . Disconnected ;
109
106
} ) ;
110
107
}
111
108
112
109
disconnect ( ) {
113
110
console . log ( "[IPCService] disconnecting..." ) ;
114
111
if ( this . connectionState !== IPCConnectionState . Disconnected ) {
115
- NodeIPC . disconnect ( this . socketName ) ;
112
+ this . process ?. kill ( ) ;
116
113
}
117
114
}
118
115
@@ -133,7 +130,7 @@ export default class IPCService {
133
130
134
131
this . pendingMessages . set ( message . messageId , deferred ) ;
135
132
136
- this . getSocket ( ) . emit ( "message" , message ) ;
133
+ this . sendIpcMessage ( message ) ;
137
134
138
135
try {
139
136
// Since we can not guarantee that a response message will ever be sent, we put a timeout
@@ -151,8 +148,56 @@ export default class IPCService {
151
148
}
152
149
}
153
150
154
- private getSocket ( ) {
155
- return NodeIPC . of [ this . socketName ] ;
151
+ // As we're using the desktop_proxy to communicate with the native messaging directly,
152
+ // the messages need to follow Native Messaging Host protocol (uint32 size followed by message).
153
+ // https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol
154
+ private sendIpcMessage ( message : MessageCommon ) {
155
+ const messageStr = JSON . stringify ( message ) ;
156
+ const buffer = Buffer . alloc ( 4 + messageStr . length ) ;
157
+ buffer . writeUInt32LE ( messageStr . length , 0 ) ;
158
+ buffer . write ( messageStr , 4 ) ;
159
+
160
+ this . process ?. stdin . write ( buffer ) ;
161
+ }
162
+
163
+ private processIpcMessage ( data : Buffer ) {
164
+ this . processOutputBuffer = Buffer . concat ( [ this . processOutputBuffer , data ] ) ;
165
+
166
+ // We might receive more than one IPC message per data event, so we need to process them all
167
+ // We continue as long as we have at least 4 + 1 bytes in the buffer, where the first 4 bytes
168
+ // represent the message length and the 5th byte is the message
169
+ while ( this . processOutputBuffer . length > 4 ) {
170
+ // Read the message length and ensure we have the full message
171
+ const msgLength = this . processOutputBuffer . readUInt32LE ( 0 ) ;
172
+ if ( msgLength + 4 < this . processOutputBuffer . length ) {
173
+ return ;
174
+ }
175
+
176
+ // Parse the message from the buffer
177
+ const messageStr = this . processOutputBuffer . subarray ( 4 , msgLength + 4 ) . toString ( ) ;
178
+ const message = JSON . parse ( messageStr ) ;
179
+
180
+ // Store the remaining buffer, which is part of the next message
181
+ this . processOutputBuffer = this . processOutputBuffer . subarray ( msgLength + 4 ) ;
182
+
183
+ // Process the connect/disconnect messages separately
184
+ if ( message ?. command === "connected" ) {
185
+ console . log ( "[IPCService] connected" ) ;
186
+ this . connectionState = IPCConnectionState . Connected ;
187
+
188
+ this . awaitingConnection . forEach ( ( deferred ) => {
189
+ deferred . resolve ( null ) ;
190
+ } ) ;
191
+ this . awaitingConnection . clear ( ) ;
192
+ continue ;
193
+ } else if ( message ?. command === "disconnected" ) {
194
+ console . log ( "[IPCService] disconnected" ) ;
195
+ this . connectionState = IPCConnectionState . Disconnected ;
196
+ continue ;
197
+ }
198
+
199
+ this . processMessage ( message ) ;
200
+ }
156
201
}
157
202
158
203
private processMessage ( message : any ) {
@@ -172,3 +217,41 @@ export default class IPCService {
172
217
}
173
218
}
174
219
}
220
+
221
+ function selectProxyPath ( ) : string {
222
+ const proxyExtension = process . platform === "win32" ? ".exe" : "" ;
223
+
224
+ // If the PROXY_PATH environment variable is set, use that
225
+ if ( process . env . PROXY_PATH ) {
226
+ if ( ! fs . existsSync ( process . env . PROXY_PATH ) ) {
227
+ throw new Error ( `PROXY_PATH is set to ${ process . env . PROXY_PATH } but the file does not exist` ) ;
228
+ }
229
+ return process . env . PROXY_PATH ;
230
+ }
231
+
232
+ // Otherwise try the debug build if present
233
+ const debugProxyPath = path . join (
234
+ __dirname ,
235
+ ".." ,
236
+ ".." ,
237
+ ".." ,
238
+ ".." ,
239
+ ".." ,
240
+ ".." ,
241
+ "desktop_native" ,
242
+ "target" ,
243
+ "debug" ,
244
+ `desktop_proxy${ proxyExtension } ` ,
245
+ ) ;
246
+ if ( fs . existsSync ( debugProxyPath ) ) {
247
+ return debugProxyPath ;
248
+ }
249
+
250
+ // On MacOS, try the release build (sandboxed)
251
+ const macReleaseProxyPath = `/Applications/Bitwarden.app/Contents/MacOS/desktop_proxy${ proxyExtension } ` ;
252
+ if ( process . platform === "darwin" && fs . existsSync ( macReleaseProxyPath ) ) {
253
+ return macReleaseProxyPath ;
254
+ }
255
+
256
+ throw new Error ( "Could not find the desktop_proxy executable" ) ;
257
+ }
0 commit comments