Skip to content

Commit 6ff1c55

Browse files
committed
cmd/coordinator: add buildlet TCP proxy handler, for gomote rdp
Updates golang/go#26090 Change-Id: I095f70baceb23cf28fcd70a78fd72df29603370e Reviewed-on: https://go-review.googlesource.com/c/build/+/207357 Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 8a3c1d9 commit 6ff1c55

File tree

1 file changed

+86
-0
lines changed

1 file changed

+86
-0
lines changed

cmd/coordinator/remote.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ func proxyBuildletHTTP(w http.ResponseWriter, r *http.Request) {
296296
return
297297
}
298298

299+
if r.Method == "POST" && r.URL.Path == "/tcpproxy" {
300+
proxyBuildletTCP(w, r, rb)
301+
return
302+
}
303+
299304
outReq, err := http.NewRequest(r.Method, rb.buildlet.URL()+r.URL.Path+"?"+r.URL.RawQuery, r.Body)
300305
if err != nil {
301306
log.Printf("bad proxy request: %v", err)
@@ -317,6 +322,87 @@ func proxyBuildletHTTP(w http.ResponseWriter, r *http.Request) {
317322
proxy.ServeHTTP(w, outReq)
318323
}
319324

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\nUnexpected 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\nUpgrade: tcpproxy\r\nConnection: 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+
320406
func requireBuildletProxyAuth(h http.Handler) http.Handler {
321407
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
322408
user, pass, ok := r.BasicAuth()

0 commit comments

Comments
 (0)