3
3
package main
4
4
5
5
import (
6
+ "bufio"
6
7
"bytes"
7
8
"context"
8
9
"fmt"
@@ -15,8 +16,10 @@ import (
15
16
"syscall/js"
16
17
"time"
17
18
19
+ "github.com/coder/coder/v2/coderd/util/ptr"
18
20
"github.com/coder/wush/overlay"
19
21
"github.com/coder/wush/tsserver"
22
+ "github.com/pion/webrtc/v4"
20
23
"golang.org/x/crypto/ssh"
21
24
"golang.org/x/xerrors"
22
25
"tailscale.com/ipn/store"
@@ -46,10 +49,6 @@ func main() {
46
49
promiseConstructor := js .Global ().Get ("Promise" )
47
50
return promiseConstructor .New (handler )
48
51
}))
49
- js .Global ().Set ("exitWush" , js .FuncOf (func (this js.Value , args []js.Value ) any {
50
- // close(ch)
51
- return nil
52
- }))
53
52
54
53
// Keep the main function running
55
54
<- make (chan struct {}, 0 )
@@ -66,7 +65,12 @@ func newWush(cfg js.Value) map[string]any {
66
65
panic (err )
67
66
}
68
67
69
- ov := overlay .NewWasmOverlay (log .Printf , dm , cfg .Get ("onNewPeer" ))
68
+ ov := overlay .NewWasmOverlay (log .Printf , dm ,
69
+ cfg .Get ("onNewPeer" ),
70
+ cfg .Get ("onWebrtcOffer" ),
71
+ cfg .Get ("onWebrtcAnswer" ),
72
+ cfg .Get ("onWebrtcCandidate" ),
73
+ )
70
74
71
75
err = ov .PickDERPHome (ctx )
72
76
if err != nil {
@@ -116,9 +120,10 @@ func newWush(cfg js.Value) map[string]any {
116
120
}
117
121
118
122
return map [string ]any {
119
- "derp_id" : ov .DerpRegionID ,
120
- "derp_name" : ov .DerpMap .Regions [int (ov .DerpRegionID )].RegionName ,
121
- "auth_key" : ov .ClientAuth ().AuthKey (),
123
+ "derp_id" : ov .DerpRegionID ,
124
+ "derp_name" : ov .DerpMap .Regions [int (ov .DerpRegionID )].RegionName ,
125
+ "derp_latency" : ov .DerpLatency .Milliseconds (),
126
+ "auth_key" : ov .ClientAuth ().AuthKey (),
122
127
}
123
128
}),
124
129
"stop" : js .FuncOf (func (this js.Value , args []js.Value ) any {
@@ -131,14 +136,14 @@ func newWush(cfg js.Value) map[string]any {
131
136
return nil
132
137
}),
133
138
"ssh" : js .FuncOf (func (this js.Value , args []js.Value ) any {
134
- if len (args ) != 1 {
135
- log .Printf ("Usage: ssh({} )" )
139
+ if len (args ) != 2 {
140
+ log .Printf ("Usage: ssh(peer, config )" )
136
141
return nil
137
142
}
138
143
139
144
sess := & sshSession {
140
145
ts : ts ,
141
- cfg : args [0 ],
146
+ cfg : args [1 ],
142
147
}
143
148
144
149
go sess .Run ()
@@ -160,9 +165,9 @@ func newWush(cfg js.Value) map[string]any {
160
165
reject := promiseArgs [1 ]
161
166
162
167
go func () {
163
- if len (args ) != 1 {
168
+ if len (args ) != 2 {
164
169
errorConstructor := js .Global ().Get ("Error" )
165
- errorObject := errorConstructor .New ("Usage: connect(authKey)" )
170
+ errorObject := errorConstructor .New ("Usage: connect(authKey, offer )" )
166
171
reject .Invoke (errorObject )
167
172
return
168
173
}
@@ -172,7 +177,18 @@ func newWush(cfg js.Value) map[string]any {
172
177
authKey = args [0 ].String ()
173
178
} else {
174
179
errorConstructor := js .Global ().Get ("Error" )
175
- errorObject := errorConstructor .New ("Usage: connect(authKey)" )
180
+ errorObject := errorConstructor .New ("Usage: connect(authKey, offer)" )
181
+ reject .Invoke (errorObject )
182
+ return
183
+ }
184
+
185
+ var offer webrtc.SessionDescription
186
+ if jsOffer := args [1 ]; jsOffer .Type () == js .TypeObject {
187
+ offer .SDP = jsOffer .Get ("sdp" ).String ()
188
+ offer .Type = webrtc .NewSDPType (jsOffer .Get ("type" ).String ())
189
+ } else {
190
+ errorConstructor := js .Global ().Get ("Error" )
191
+ errorObject := errorConstructor .New ("Usage: connect(authKey, offer)" )
176
192
reject .Invoke (errorObject )
177
193
return
178
194
}
@@ -187,11 +203,11 @@ func newWush(cfg js.Value) map[string]any {
187
203
}
188
204
189
205
ctx , cancel := context .WithCancel (context .Background ())
190
- peer , err := ov .Connect (ctx , ca )
206
+ peer , err := ov .Connect (ctx , ca , offer )
191
207
if err != nil {
192
208
cancel ()
193
209
errorConstructor := js .Global ().Get ("Error" )
194
- errorObject := errorConstructor .New (fmt .Errorf ("parse authkey : %w" , err ).Error ())
210
+ errorObject := errorConstructor .New (fmt .Errorf ("connect to peer : %w" , err ).Error ())
195
211
reject .Invoke (errorObject )
196
212
return
197
213
}
@@ -200,6 +216,7 @@ func newWush(cfg js.Value) map[string]any {
200
216
"id" : js .ValueOf (peer .ID ),
201
217
"name" : js .ValueOf (peer .Name ),
202
218
"ip" : js .ValueOf (peer .IP .String ()),
219
+ "type" : js .ValueOf (peer .Type ),
203
220
"cancel" : js .FuncOf (func (this js.Value , args []js.Value ) any {
204
221
cancel ()
205
222
return nil
@@ -220,61 +237,34 @@ func newWush(cfg js.Value) map[string]any {
220
237
221
238
if len (args ) != 5 {
222
239
errorConstructor := js .Global ().Get ("Error" )
223
- errorObject := errorConstructor .New ("Usage: transfer(peer, file )" )
240
+ errorObject := errorConstructor .New ("Usage: transfer(peer, fileName, sizeBytes, stream, onProgress )" )
224
241
reject .Invoke (errorObject )
225
242
return nil
226
243
}
227
244
228
245
peer := args [0 ]
229
246
ip := peer .Get ("ip" ).String ()
230
247
fileName := args [1 ].String ()
231
- sizeBytes := args [2 ].Int ()
248
+ sizeBytes := int64 ( args [2 ].Int () )
232
249
stream := args [3 ]
233
- streamHelper := args [4 ]
234
-
235
- pr , pw := io .Pipe ()
236
-
237
- goCallback := js .FuncOf (func (this js.Value , args []js.Value ) interface {} {
238
- promiseConstructor := js .Global ().Get ("Promise" )
239
- return promiseConstructor .New (js .FuncOf (func (this js.Value , promiseArgs []js.Value ) any {
240
- resolve := promiseArgs [0 ]
241
- _ = promiseArgs [1 ]
242
- go func () {
243
- if len (args ) == 0 || args [0 ].IsNull () || args [0 ].IsUndefined () {
244
- pw .Close ()
245
- resolve .Invoke ()
246
- return
247
- }
248
-
249
- fmt .Println ("in go callback" )
250
- // Convert the JavaScript Uint8Array to a Go byte slice
251
- uint8Array := args [0 ]
252
- fmt .Println ("type is" , uint8Array .Type ().String ())
253
- length := uint8Array .Get ("length" ).Int ()
254
- buf := make ([]byte , length )
255
- js .CopyBytesToGo (buf , uint8Array )
256
-
257
- fmt .Println ("sending data to channel" )
258
- // Send the data to the channel
259
- if _ , err := pw .Write (buf ); err != nil {
260
- pw .CloseWithError (err )
261
- }
262
- fmt .Println ("callback finished" )
263
-
264
- // Resolve the promise
265
- resolve .Invoke ()
266
- }()
267
- return nil
268
- }))
269
- })
250
+ onProgress := args [4 ]
270
251
271
252
go func () {
272
- defer goCallback .Release ()
273
-
274
- streamHelper .Invoke (stream , goCallback )
275
-
276
- hc := ts .HTTPClient ()
277
- req , err := http .NewRequest (http .MethodPost , fmt .Sprintf ("http://%s:4444/%s" , ip , fileName ), pr )
253
+ startTime := time .Now ()
254
+ reader := & jsStreamReader {
255
+ reader : stream .Call ("getReader" ),
256
+ onProgress : onProgress ,
257
+ totalSize : sizeBytes ,
258
+ }
259
+ bufferSize := 1024 * 1024
260
+ hc := & http.Client {
261
+ Transport : & http.Transport {
262
+ DialContext : ts .Dial ,
263
+ ReadBufferSize : bufferSize ,
264
+ WriteBufferSize : bufferSize ,
265
+ },
266
+ }
267
+ req , err := http .NewRequest (http .MethodPost , fmt .Sprintf ("http://%s:4444/%s" , ip , fileName ), bufio .NewReaderSize (reader , bufferSize ))
278
268
if err != nil {
279
269
errorConstructor := js .Global ().Get ("Error" )
280
270
errorObject := errorConstructor .New (err .Error ())
@@ -283,6 +273,7 @@ func newWush(cfg js.Value) map[string]any {
283
273
}
284
274
req .ContentLength = int64 (sizeBytes )
285
275
276
+ fmt .Printf ("Starting transfer of %d bytes\n " , sizeBytes )
286
277
res , err := hc .Do (req )
287
278
if err != nil {
288
279
errorConstructor := js .Global ().Get ("Error" )
@@ -295,7 +286,10 @@ func newWush(cfg js.Value) map[string]any {
295
286
bod := bytes .NewBuffer (nil )
296
287
_ , _ = io .Copy (bod , res .Body )
297
288
298
- fmt .Println (bod .String ())
289
+ duration := time .Since (startTime )
290
+ speed := float64 (sizeBytes ) / duration .Seconds () / 1024 / 1024 // MB/s
291
+ fmt .Printf ("Transfer completed in %v. Speed: %.2f MB/s\n " , duration , speed )
292
+
299
293
resolve .Invoke ()
300
294
}()
301
295
@@ -305,6 +299,36 @@ func newWush(cfg js.Value) map[string]any {
305
299
promiseConstructor := js .Global ().Get ("Promise" )
306
300
return promiseConstructor .New (handler )
307
301
}),
302
+
303
+ "sendWebrtcCandidate" : js .FuncOf (func (this js.Value , args []js.Value ) any {
304
+ peer := args [0 ].String ()
305
+ candidate := args [1 ]
306
+
307
+ ov .SendWebrtcCandidate (peer , webrtc.ICECandidateInit {
308
+ Candidate : candidate .Get ("candidate" ).String (),
309
+ SDPMLineIndex : ptr .Ref (uint16 (candidate .Get ("sdpMLineIndex" ).Int ())),
310
+ SDPMid : ptr .Ref (candidate .Get ("sdpMid" ).String ()),
311
+ UsernameFragment : ptr .Ref (candidate .Get ("sdpMid" ).String ()),
312
+ })
313
+
314
+ return nil
315
+ }),
316
+
317
+ "parseAuthKey" : js .FuncOf (func (this js.Value , args []js.Value ) any {
318
+ authKey := args [0 ].String ()
319
+
320
+ var ca overlay.ClientAuth
321
+ _ = ca .Parse (authKey )
322
+ typ := "cli"
323
+ if ca .Web {
324
+ typ = "web"
325
+ }
326
+
327
+ return map [string ]any {
328
+ "id" : js .ValueOf (ca .ReceiverPublicKey .String ()),
329
+ "type" : js .ValueOf (typ ),
330
+ }
331
+ }),
308
332
}
309
333
}
310
334
@@ -359,7 +383,7 @@ func (s *sshSession) Run() {
359
383
ctx , cancel := context .WithTimeout (context .Background (), time .Duration (timeoutSeconds * float64 (time .Second )))
360
384
defer cancel ()
361
385
reportProgress (fmt .Sprintf ("Connecting..." ))
362
- c , err := s .ts .Dial (ctx , "tcp" , net .JoinHostPort ("100.64.0.0 " , "3" ))
386
+ c , err := s .ts .Dial (ctx , "tcp" , net .JoinHostPort ("fd7a:115c:a1e0::1 " , "3" ))
363
387
if err != nil {
364
388
writeError ("Dial" , err )
365
389
return
@@ -538,8 +562,8 @@ func cpH(onIncomingFile js.Value, downloadFile js.Value) http.HandlerFunc {
538
562
539
563
// Read the entire stream and pass it to JavaScript
540
564
for {
541
- // Read up to 16KB at a time
542
- buf := make ([]byte , 16384 )
565
+ // Read up to 1MB at a time
566
+ buf := make ([]byte , 1024 * 1024 )
543
567
n , err := r .Body .Read (buf )
544
568
if err != nil && err != io .EOF {
545
569
// Tell the controller we have an error
@@ -582,3 +606,73 @@ func cpH(onIncomingFile js.Value, downloadFile js.Value) http.HandlerFunc {
582
606
downloadFile .Invoke (peer , fiName , r .ContentLength , readableStream )
583
607
}
584
608
}
609
+
610
+ // jsStreamReader implements io.Reader for JavaScript streams
611
+ type jsStreamReader struct {
612
+ reader js.Value
613
+ onProgress js.Value
614
+ bytesRead int64
615
+ totalSize int64
616
+ buffer bytes.Buffer
617
+ }
618
+
619
+ func (r * jsStreamReader ) Read (p []byte ) (n int , err error ) {
620
+ if r .bytesRead >= r .totalSize {
621
+ return 0 , io .EOF
622
+ }
623
+
624
+ fmt .Printf ("Read %d bytes\n " , len (p ))
625
+
626
+ // If we have buffered data, use it first
627
+ if r .buffer .Len () > 0 {
628
+ n , _ = r .buffer .Read (p )
629
+ r .bytesRead += int64 (n )
630
+
631
+ if r .onProgress .Truthy () {
632
+ r .onProgress .Invoke (r .bytesRead )
633
+ }
634
+ return n , nil
635
+ }
636
+
637
+ // Only read from stream if buffer is empty
638
+ promise := r .reader .Call ("read" )
639
+ result := await (promise )
640
+
641
+ if result .Get ("done" ).Bool () {
642
+ if r .bytesRead < r .totalSize {
643
+ return 0 , fmt .Errorf ("stream ended prematurely at %d/%d bytes" , r .bytesRead , r .totalSize )
644
+ }
645
+ return 0 , io .EOF
646
+ }
647
+
648
+ // Get the chunk from JavaScript and write it to our buffer
649
+ value := result .Get ("value" )
650
+ chunk := make ([]byte , value .Length ())
651
+ js .CopyBytesToGo (chunk , value )
652
+ r .buffer .Write (chunk )
653
+
654
+ // Now read what we can into p
655
+ n , _ = r .buffer .Read (p )
656
+ r .bytesRead += int64 (n )
657
+
658
+ if r .onProgress .Truthy () {
659
+ r .onProgress .Invoke (r .bytesRead )
660
+ }
661
+
662
+ return n , nil
663
+ }
664
+
665
+ // Helper function to await a JavaScript promise
666
+ func await (promise js.Value ) js.Value {
667
+ done := make (chan js.Value )
668
+ promise .Call ("then" , js .FuncOf (func (_ js.Value , args []js.Value ) interface {} {
669
+ done <- args [0 ]
670
+ return nil
671
+ }))
672
+ return <- done
673
+ }
674
+
675
+ func (r * jsStreamReader ) Close () error {
676
+ r .reader .Call ("releaseLock" )
677
+ return nil
678
+ }
0 commit comments