@@ -296,6 +296,11 @@ func proxyBuildletHTTP(w http.ResponseWriter, r *http.Request) {
296
296
return
297
297
}
298
298
299
+ if r .Method == "POST" && r .URL .Path == "/tcpproxy" {
300
+ proxyBuildletTCP (w , r , rb )
301
+ return
302
+ }
303
+
299
304
outReq , err := http .NewRequest (r .Method , rb .buildlet .URL ()+ r .URL .Path + "?" + r .URL .RawQuery , r .Body )
300
305
if err != nil {
301
306
log .Printf ("bad proxy request: %v" , err )
@@ -317,6 +322,87 @@ func proxyBuildletHTTP(w http.ResponseWriter, r *http.Request) {
317
322
proxy .ServeHTTP (w , outReq )
318
323
}
319
324
325
+ // proxyBuildletTCP handles connecting to and proxying between a
326
+ // backend buildlet VM's TCP port and the client. This is called once
327
+ // it's already authenticated by proxyBuildletHTTP.
328
+ func proxyBuildletTCP (w http.ResponseWriter , r * http.Request , rb * remoteBuildlet ) {
329
+ if r .ProtoMajor > 1 {
330
+ // TODO: deal with HTTP/2 requests if https://farmer.golang.org enables it later.
331
+ // Currently it does not, as other handlers Hijack too. We'd need to teach clients
332
+ // when to explicitly disable HTTP/1, or update the protocols to do read/write
333
+ // bodies instead of 101 Switching Protocols.
334
+ http .Error (w , "unexpected HTTP/2 request" , http .StatusInternalServerError )
335
+ return
336
+ }
337
+ hj , ok := w .(http.Hijacker )
338
+ if ! ok {
339
+ http .Error (w , "not a Hijacker" , http .StatusInternalServerError )
340
+ return
341
+ }
342
+ // The target port is a header instead of a query parameter for no real reason other
343
+ // than being consistent with the reverse buildlet registration headers.
344
+ port , err := strconv .Atoi (r .Header .Get ("X-Target-Port" ))
345
+ if err != nil {
346
+ http .Error (w , "invalid or missing X-Target-Port" , http .StatusBadRequest )
347
+ return
348
+ }
349
+ hc , ok := dashboard .Hosts [rb .HostType ]
350
+ if ! ok || ! hc .IsVM () {
351
+ // TODO: implement support for non-VM types if/when needed.
352
+ http .Error (w , fmt .Sprintf ("unsupported non-VM host type %q" , rb .HostType ), http .StatusBadRequest )
353
+ return
354
+ }
355
+ ip , _ , err := net .SplitHostPort (rb .buildlet .IPPort ())
356
+ if err != nil {
357
+ http .Error (w , fmt .Sprintf ("unexpected backend ip:port %q" , rb .buildlet .IPPort ()), http .StatusInternalServerError )
358
+ return
359
+ }
360
+
361
+ c , err := (& net.Dialer {}).DialContext (r .Context (), "tcp" , net .JoinHostPort (ip , fmt .Sprint (port )))
362
+ if err != nil {
363
+ http .Error (w , fmt .Sprintf ("failed to connect to port %v: %v" , port , err ), http .StatusInternalServerError )
364
+ return
365
+ }
366
+ defer c .Close ()
367
+
368
+ // Hijack early so we can check for any unexpected buffered
369
+ // request data without doing a potentially blocking
370
+ // r.Body.Read. Also it's nice to be able to WriteString the
371
+ // response header explicitly. But using w.WriteHeader+w.Flush
372
+ // would probably also work. Somewhat arbitrary to do it early.
373
+ cc , buf , err := hj .Hijack ()
374
+ if err != nil {
375
+ http .Error (w , fmt .Sprintf ("Hijack: %v" , err ), http .StatusInternalServerError )
376
+ return
377
+ }
378
+ defer cc .Close ()
379
+
380
+ if buf .Reader .Buffered () != 0 {
381
+ io .WriteString (cc , "HTTP/1.0 400 Bad Request\r \n \r \n Unexpected buffered data.\n " )
382
+ return
383
+ }
384
+
385
+ // If we send a 101 response with an Upgrade header and a
386
+ // "Connection: Upgrade" header, that makes net/http's
387
+ // *Response.isProtocolSwitch() return true, which gives us a
388
+ // writable Response.Body on the client side, which simplifies
389
+ // the gomote code.
390
+ io .WriteString (cc , "HTTP/1.1 101 Switching Protocols\r \n Upgrade: tcpproxy\r \n Connection: upgrade\r \n \r \n " )
391
+
392
+ errc := make (chan error , 2 )
393
+ // Copy from HTTP client to backend.
394
+ go func () {
395
+ _ , err := io .Copy (c , cc )
396
+ errc <- err
397
+ }()
398
+ // And copy from backend to the HTTP client.
399
+ go func () {
400
+ _ , err := io .Copy (cc , c )
401
+ errc <- err
402
+ }()
403
+ <- errc
404
+ }
405
+
320
406
func requireBuildletProxyAuth (h http.Handler ) http.Handler {
321
407
return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
322
408
user , pass , ok := r .BasicAuth ()
0 commit comments