1
- import { ExtensionMessage } from "./common" ;
1
+ import {
2
+ ExtensionMessage ,
3
+ WebSocketMessage ,
4
+ getApprovedHosts ,
5
+ addApprovedHost
6
+ } from "./common" ;
2
7
3
8
export class SailConnector {
4
9
private port : chrome . runtime . Port ;
@@ -13,7 +18,7 @@ export class SailConnector {
13
18
this . port = chrome . runtime . connectNative ( "com.coder.sail" ) ;
14
19
this . port . onMessage . addListener ( ( message ) => {
15
20
if ( ! message . url ) {
16
- return reject ( "Invalid handshaking message" ) ;
21
+ return reject ( "Invalid handshake message" ) ;
17
22
}
18
23
19
24
resolve ( message . url ) ;
@@ -37,26 +42,145 @@ export class SailConnector {
37
42
}
38
43
}
39
44
45
+ // Get the sail URL.
40
46
const connector = new SailConnector ( ) ;
41
47
let connectError : string | undefined = "Not connected yet" ;
42
48
connector . connect ( ) . then ( ( ) => connectError = undefined ) . catch ( ( ex ) => {
43
49
connectError = `Failed to connect: ${ ex . toString ( ) } ` ;
44
50
} ) ;
45
51
52
+ // doConnection attempts to connect to Sail over WebSocket.
53
+ const doConnection = ( socketUrl : string , projectUrl : string , onMessage : ( data : WebSocketMessage ) => void ) : Promise < WebSocket > => {
54
+ return new Promise < WebSocket > ( ( resolve , reject ) => {
55
+ const socket = new WebSocket ( socketUrl ) ;
56
+ socket . addEventListener ( "open" , ( ) => {
57
+ socket . send ( JSON . stringify ( {
58
+ project : projectUrl ,
59
+ } ) ) ;
60
+
61
+ resolve ( socket ) ;
62
+ } ) ;
63
+ socket . addEventListener ( "close" , ( event ) => {
64
+ const v = `sail socket was closed: ${ event . code } ` ;
65
+ onMessage ( { type : "error" , v } ) ;
66
+ reject ( v ) ;
67
+ } ) ;
68
+
69
+ socket . addEventListener ( "message" , ( event ) => {
70
+ const data = JSON . parse ( event . data ) ;
71
+ if ( ! data ) {
72
+ return ;
73
+ }
74
+ const type = data . type ;
75
+ const content = type === "data" ? atob ( data . v ) : data . v ;
76
+
77
+ switch ( type ) {
78
+ case "data" :
79
+ case "error" :
80
+ onMessage ( { type, v : content } ) ;
81
+ break ;
82
+ default :
83
+ throw new Error ( "unknown message type: " + type ) ;
84
+ }
85
+ } ) ;
86
+ } ) ;
87
+ } ;
88
+
46
89
chrome . runtime . onMessage . addListener ( ( data : ExtensionMessage , sender , sendResponse : ( msg : ExtensionMessage ) => void ) => {
47
90
if ( data . type === "sail" ) {
48
- connector . connect ( ) . then ( ( url ) => {
49
- sendResponse ( {
50
- type : "sail" ,
51
- url,
52
- } )
53
- } ) . catch ( ( ex ) => {
54
- sendResponse ( {
55
- type : "sail" ,
56
- error : ex . toString ( ) ,
91
+ if ( data . projectUrl ) {
92
+ // Launch a sail connection.
93
+ if ( ! sender . tab ) {
94
+ // Only allow from content scripts.
95
+ return ;
96
+ }
97
+
98
+ // Check that the tab is an approved host, otherwise ask
99
+ // the user for permission before launching Sail.
100
+ const url = new URL ( sender . tab . url ) ;
101
+ const host = url . hostname ;
102
+ getApprovedHosts ( )
103
+ . then ( ( hosts ) => {
104
+ for ( let h of hosts ) {
105
+ if ( h === host || ( h . startsWith ( "." ) && ( host === h . substr ( 1 ) || host . endsWith ( h ) ) ) ) {
106
+ // Approved host.
107
+ return true ;
108
+ }
109
+ }
110
+
111
+ // If not approved, ask for approval.
112
+ return new Promise ( ( resolve , reject ) => {
113
+ chrome . tabs . executeScript ( sender . tab . id , {
114
+ code : `confirm("Launch Sail? This will add this host to your approved hosts list.")` ,
115
+ } , ( result ) => {
116
+ if ( chrome . runtime . lastError ) {
117
+ return reject ( chrome . runtime . lastError . message ) ;
118
+ }
119
+
120
+ if ( result ) {
121
+ // The user approved the confirm dialog.
122
+ addApprovedHost ( host )
123
+ . then ( ( ) => resolve ( true ) )
124
+ . catch ( reject ) ;
125
+ return ;
126
+ }
127
+
128
+ return false ;
129
+ } ) ;
130
+ } ) ;
131
+ } )
132
+ . then ( ( approved ) => {
133
+ if ( ! approved ) {
134
+ return ;
135
+ }
136
+
137
+ // Start Sail.
138
+ // onMessage forwards WebSocketMessages to the tab that
139
+ // launched Sail.
140
+ const onMessage = ( message : WebSocketMessage ) => {
141
+ chrome . tabs . sendMessage ( sender . tab . id , message ) ;
142
+ } ;
143
+ connector . connect ( ) . then ( ( sailUrl ) => {
144
+ const socketUrl = sailUrl . replace ( "http:" , "ws:" ) + "/api/v1/run" ;
145
+ return doConnection ( socketUrl , data . projectUrl , onMessage ) . then ( ( conn ) => {
146
+ sendResponse ( {
147
+ type : "sail" ,
148
+ } ) ;
149
+ } ) ;
150
+ } ) . catch ( ( ex ) => {
151
+ sendResponse ( {
152
+ type : "sail" ,
153
+ error : ex . toString ( ) ,
154
+ } ) ;
155
+ } ) ;
156
+ } )
157
+ . catch ( ( ex ) => {
158
+ sendResponse ( {
159
+ type : "sail" ,
160
+ error : ex . toString ( ) ,
161
+ } ) ;
162
+
163
+ } ) ;
164
+ } else {
165
+ // Check if we can get a sail URL.
166
+ connector . connect ( ) . then ( ( ) => {
167
+ sendResponse ( {
168
+ type : "sail" ,
169
+ } )
170
+ } ) . catch ( ( ex ) => {
171
+ sendResponse ( {
172
+ type : "sail" ,
173
+ error : ex . toString ( ) ,
174
+ } ) ;
57
175
} ) ;
58
- } ) ;
176
+ }
59
177
60
178
return true ;
61
179
}
62
180
} ) ;
181
+
182
+ // Open the config page when the browser action is clicked.
183
+ chrome . browserAction . onClicked . addListener ( ( ) => {
184
+ const url = chrome . runtime . getURL ( "/out/config.html" ) ;
185
+ chrome . tabs . create ( { url } ) ;
186
+ } ) ;
0 commit comments