golang.org/x/build@v0.0.0-20240506185731-218518f32b70/buildlet/buildletclient.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package buildlet // import "golang.org/x/build/buildlet" 6 7 import ( 8 "bufio" 9 "context" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "log" 15 "net" 16 "net/http" 17 "net/url" 18 "os" 19 "strings" 20 "sync" 21 "time" 22 ) 23 24 var _ Client = (*client)(nil) 25 26 // NewClient returns a *client that will manipulate ipPort, 27 // authenticated using the provided keypair. 28 // 29 // This constructor returns immediately without testing the host or auth. 30 func NewClient(ipPort string, kp KeyPair) Client { 31 tr := &http.Transport{ 32 Dial: defaultDialer(), 33 DialTLS: kp.tlsDialer(), 34 IdleConnTimeout: time.Minute, 35 } 36 c := &client{ 37 ipPort: ipPort, 38 tls: kp, 39 password: kp.Password(), 40 httpClient: &http.Client{Transport: tr}, 41 closeFuncs: []func(){tr.CloseIdleConnections}, 42 } 43 c.setCommon() 44 return c 45 } 46 47 func (c *client) setCommon() { 48 c.peerDead = make(chan struct{}) 49 c.ctx, c.ctxCancel = context.WithCancel(context.Background()) 50 } 51 52 // SetOnHeartbeatFailure sets a function to be called when heartbeats 53 // against this builder fail, or when the client is destroyed with 54 // Close. The function fn is never called more than once. 55 // SetOnHeartbeatFailure must be set before any use of the buildlet. 56 func (c *client) SetOnHeartbeatFailure(fn func()) { 57 c.heartbeatFailure = fn 58 } 59 60 var ErrClosed = errors.New("buildlet: Client closed") 61 62 // Close destroys and closes down the buildlet, destroying all state 63 // immediately. 64 func (c *client) Close() error { 65 // TODO(bradfitz): have a Client-wide Done channel we set on 66 // all outbound HTTP Requests and close it in the once here? 67 // Then if something was in-flight and somebody else Closes, 68 // the local http.Transport notices, rather than noticing via 69 // the remote machine dying or timing out. 70 c.closeOnce.Do(func() { 71 // Send a best-effort notification to the server to destroy itself. 72 // Don't want too long (since it's likely in a broken state anyway). 73 // Ignore the return value, since we're about to forcefully destroy 74 // it anyway. 75 req, err := http.NewRequest("POST", c.URL()+"/halt", nil) 76 if err != nil { 77 // ignore. 78 } else { 79 _, err = c.doHeaderTimeout(req, 2*time.Second) 80 } 81 if err == nil { 82 err = ErrClosed 83 } 84 for _, fn := range c.closeFuncs { 85 fn() 86 } 87 c.setPeerDead(err) // which will also cause c.heartbeatFailure to run 88 }) 89 return nil 90 } 91 92 func (c *client) setPeerDead(err error) { 93 c.setPeerDeadOnce.Do(func() { 94 c.MarkBroken() 95 if err == nil { 96 err = errors.New("peer dead (no specific error)") 97 } 98 c.deadErr = err 99 close(c.peerDead) 100 }) 101 } 102 103 // SetDescription sets a short description of where the buildlet 104 // connection came from. This is used by the build coordinator status 105 // page, mostly for debugging. 106 func (c *client) SetDescription(v string) { 107 c.desc = v 108 } 109 110 // SetInstanceName sets an instance name for GCE and EC2 buildlets. 111 // This value differs from the buildlet name used in the CLI and web interface. 112 func (c *client) SetInstanceName(v string) { 113 c.instanceName = v 114 } 115 116 // InstanceName gets an instance name for GCE and EC2 buildlets. 117 // This value differs from the buildlet name used in the CLI and web interface. 118 // For non-GCE or EC2 buildlets, this will return an empty string. 119 func (c *client) InstanceName() string { 120 return c.instanceName 121 } 122 123 // SetHTTPClient replaces the underlying HTTP client. 124 // It should only be called before the Client is used. 125 func (c *client) SetHTTPClient(httpClient *http.Client) { 126 c.httpClient = httpClient 127 } 128 129 // SetDialer sets the function that creates a new connection to the buildlet. 130 // By default, net.Dialer.DialContext is used. SetDialer has effect only when 131 // TLS isn't used. 132 // 133 // TODO(bradfitz): this is only used for ssh connections to buildlets, 134 // which previously required the client to do its own net.Dial + 135 // upgrade request. But now that the net/http client supports 136 // read/write bodies for protocol upgrades, we could change how ssh 137 // works and delete this. 138 func (c *client) SetDialer(dialer func(context.Context) (net.Conn, error)) { 139 c.dialer = dialer 140 } 141 142 // defaultDialer returns the net/http package's default Dial function. 143 // Notably, this sets TCP keep-alive values, so when we kill VMs 144 // (whose TCP stacks stop replying, forever), we don't leak file 145 // descriptors for otherwise forever-stalled TCP connections. 146 func defaultDialer() func(network, addr string) (net.Conn, error) { 147 if fn := http.DefaultTransport.(*http.Transport).Dial; fn != nil { 148 return fn 149 } 150 return net.Dial 151 } 152 153 // A client interacts with a single buildlet. 154 type client struct { 155 ipPort string // required, unless remoteBuildlet+baseURL is set 156 tls KeyPair 157 httpClient *http.Client 158 dialer func(context.Context) (net.Conn, error) // nil means to use net.Dialer.DialContext 159 baseURL string // optional baseURL (used by remote buildlets) 160 authUser string // defaults to "gomote", if password is non-empty 161 password string // basic auth password or empty for none 162 remoteBuildlet string // non-empty if for remote buildlets (used by client) 163 name string // optional name for debugging, returned by Name 164 instanceName string // instance name for GCE and EC2 VMs 165 166 closeFuncs []func() // optional extra code to run on close 167 168 ctx context.Context 169 ctxCancel context.CancelFunc 170 heartbeatFailure func() // optional 171 desc string 172 173 closeOnce sync.Once 174 initHeartbeatOnce sync.Once 175 setPeerDeadOnce sync.Once 176 peerDead chan struct{} // closed on peer death 177 deadErr error // guarded by peerDead's close 178 179 mu sync.Mutex 180 broken bool // client is broken in some way 181 } 182 183 func (c *client) String() string { 184 if c == nil { 185 return "(nil buildlet.Client)" 186 } 187 return strings.TrimSpace(c.URL() + " " + c.desc) 188 } 189 190 // RemoteName returns the name of this client's buildlet on the 191 // coordinator. If this buildlet isn't a remote buildlet created via 192 // gomote, this returns the empty string. 193 func (c *client) RemoteName() string { 194 return c.remoteBuildlet 195 } 196 197 // URL returns the buildlet's URL prefix, without a trailing slash. 198 func (c *client) URL() string { 199 if c.baseURL != "" { 200 return strings.TrimRight(c.baseURL, "/") 201 } 202 if !c.tls.IsZero() { 203 return "https://" + strings.TrimSuffix(c.ipPort, ":443") 204 } 205 return "http://" + strings.TrimSuffix(c.ipPort, ":80") 206 } 207 208 func (c *client) IPPort() string { return c.ipPort } 209 210 func (c *client) SetName(name string) { c.name = name } 211 212 // Name returns the name of this buildlet. 213 // It returns the first non-empty string from the name given to 214 // SetName, its remote buildlet name, its ip:port, or "(unnamed-buildlet)" in the case where 215 // ip:port is empty because there's a custom dialer. 216 func (c *client) Name() string { 217 if c.name != "" { 218 return c.name 219 } 220 if c.remoteBuildlet != "" { 221 return c.remoteBuildlet 222 } 223 if c.ipPort != "" { 224 return c.ipPort 225 } 226 return "(unnamed-buildlet)" 227 } 228 229 // MarkBroken marks this client as broken in some way. 230 func (c *client) MarkBroken() { 231 c.mu.Lock() 232 defer c.mu.Unlock() 233 c.broken = true 234 c.ctxCancel() 235 } 236 237 // IsBroken reports whether this client is broken in some way. 238 func (c *client) IsBroken() bool { 239 c.mu.Lock() 240 defer c.mu.Unlock() 241 return c.broken 242 } 243 244 func (c *client) authUsername() string { 245 if c.authUser != "" { 246 return c.authUser 247 } 248 return "gomote" 249 } 250 251 func (c *client) do(req *http.Request) (*http.Response, error) { 252 c.initHeartbeatOnce.Do(c.initHeartbeats) 253 if c.password != "" { 254 req.SetBasicAuth(c.authUsername(), c.password) 255 } 256 if c.remoteBuildlet != "" { 257 req.Header.Set("X-Buildlet-Proxy", c.remoteBuildlet) 258 } 259 return c.httpClient.Do(req) 260 } 261 262 // ProxyTCP connects to the given port on the remote buildlet. 263 // The buildlet client must currently be a gomote client (RemoteName != "") 264 // and the target type must be a VM type running on GCE. This was primarily 265 // created for RDP to Windows machines, but it might get reused for other 266 // purposes in the future. 267 func (c *client) ProxyTCP(port int) (io.ReadWriteCloser, error) { 268 if c.RemoteName() == "" { 269 return nil, errors.New("ProxyTCP currently only supports gomote-created buildlets") 270 } 271 req, err := http.NewRequest("POST", c.URL()+"/tcpproxy", nil) 272 if err != nil { 273 return nil, err 274 } 275 req.Header.Add("X-Target-Port", fmt.Sprint(port)) 276 res, err := c.do(req) 277 if err != nil { 278 return nil, err 279 } 280 if res.StatusCode != http.StatusSwitchingProtocols { 281 slurp, _ := io.ReadAll(io.LimitReader(res.Body, 4<<10)) 282 res.Body.Close() 283 return nil, fmt.Errorf("wanted 101 Switching Protocols; unexpected response: %v, %q", res.Status, slurp) 284 } 285 rwc, ok := res.Body.(io.ReadWriteCloser) 286 if !ok { 287 res.Body.Close() 288 return nil, fmt.Errorf("tcpproxy response was not a Writer") 289 } 290 return rwc, nil 291 } 292 293 // ProxyRoundTripper returns a RoundTripper that sends HTTP requests directly 294 // through to the underlying buildlet, adding auth and X-Buildlet-Proxy headers 295 // as necessary. This is really only intended for use by the coordinator. 296 func (c *client) ProxyRoundTripper() http.RoundTripper { 297 return proxyRoundTripper{c} 298 } 299 300 type proxyRoundTripper struct { 301 c *client 302 } 303 304 func (p proxyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 305 return p.c.do(req) 306 } 307 308 func (c *client) initHeartbeats() { 309 go c.heartbeatLoop() 310 } 311 312 func (c *client) heartbeatLoop() { 313 failInARow := 0 314 for { 315 select { 316 case <-c.peerDead: 317 // Dead for whatever reason (heartbeat, remote 318 // side closed, caller Closed 319 // normally). Regardless, we call the 320 // heartbeatFailure func if set. 321 if c.heartbeatFailure != nil { 322 c.heartbeatFailure() 323 } 324 return 325 case <-time.After(10 * time.Second): 326 t0 := time.Now() 327 if _, err := c.Status(context.Background()); err != nil { 328 failInARow++ 329 if failInARow == 3 { 330 log.Printf("Buildlet %v failed three heartbeats; final error: %v", c, err) 331 c.setPeerDead(fmt.Errorf("Buildlet %v failed heartbeat after %v; marking dead; err=%v", c, time.Since(t0), err)) 332 } 333 } else { 334 failInARow = 0 335 } 336 } 337 } 338 } 339 340 var errHeaderTimeout = errors.New("timeout waiting for headers") 341 342 // doHeaderTimeout calls c.do(req) and returns its results, or 343 // errHeaderTimeout if max elapses first. 344 func (c *client) doHeaderTimeout(req *http.Request, max time.Duration) (res *http.Response, err error) { 345 type resErr struct { 346 res *http.Response 347 err error 348 } 349 350 ctx, cancel := context.WithCancel(req.Context()) 351 352 req = req.WithContext(ctx) 353 354 timer := time.NewTimer(max) 355 defer timer.Stop() 356 357 resErrc := make(chan resErr, 1) 358 go func() { 359 res, err := c.do(req) 360 resErrc <- resErr{res, err} 361 }() 362 363 cleanup := func() { 364 cancel() 365 if re := <-resErrc; re.res != nil { 366 re.res.Body.Close() 367 } 368 } 369 370 select { 371 case re := <-resErrc: 372 if re.err != nil { 373 cancel() 374 return nil, re.err 375 } 376 // Clean up our cancel context above when the caller 377 // reads to the end of the response body or closes. 378 re.res.Body = onEOFReadCloser{re.res.Body, cancel} 379 return re.res, nil 380 case <-c.peerDead: 381 log.Printf("%s: peer dead with %v, waiting for headers for %v", c.Name(), c.deadErr, req.URL.Path) 382 go cleanup() 383 return nil, c.deadErr 384 case <-timer.C: 385 log.Printf("%s: timeout after %v waiting for headers for %v", c.Name(), max, req.URL.Path) 386 go cleanup() 387 return nil, errHeaderTimeout 388 } 389 } 390 391 // doOK sends the request and expects a 200 OK response. 392 func (c *client) doOK(req *http.Request) error { 393 res, err := c.do(req) 394 if err != nil { 395 return err 396 } 397 defer res.Body.Close() 398 if res.StatusCode != http.StatusOK { 399 slurp, _ := io.ReadAll(io.LimitReader(res.Body, 4<<10)) 400 return fmt.Errorf("%v; body: %s", res.Status, slurp) 401 } 402 return nil 403 } 404 405 // PutTar writes files to the remote buildlet, rooted at the relative 406 // directory dir. 407 // If dir is empty, they're placed at the root of the buildlet's work directory. 408 // The dir is created if necessary. 409 // The Reader must be of a tar.gz file. 410 func (c *client) PutTar(ctx context.Context, r io.Reader, dir string) error { 411 req, err := http.NewRequest("PUT", c.URL()+"/writetgz?dir="+url.QueryEscape(dir), r) 412 if err != nil { 413 return err 414 } 415 return c.doOK(req.WithContext(ctx)) 416 } 417 418 // PutTarFromURL tells the buildlet to download the tar.gz file from tarURL 419 // and write it to dir, a relative directory from the workdir. 420 // If dir is empty, they're placed at the root of the buildlet's work directory. 421 // The dir is created if necessary. 422 // The url must be of a tar.gz file. 423 func (c *client) PutTarFromURL(ctx context.Context, tarURL, dir string) error { 424 form := url.Values{ 425 "url": {tarURL}, 426 } 427 req, err := http.NewRequest("POST", c.URL()+"/writetgz?dir="+url.QueryEscape(dir), strings.NewReader(form.Encode())) 428 if err != nil { 429 return err 430 } 431 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 432 return c.doOK(req.WithContext(ctx)) 433 } 434 435 // Put writes the provided file to path (relative to workdir) and sets mode. 436 // It creates any missing parent directories with 0755 permission. 437 func (c *client) Put(ctx context.Context, r io.Reader, path string, mode os.FileMode) error { 438 param := url.Values{ 439 "path": {path}, 440 "mode": {fmt.Sprint(int64(mode))}, 441 } 442 req, err := http.NewRequest("PUT", c.URL()+"/write?"+param.Encode(), r) 443 if err != nil { 444 return err 445 } 446 return c.doOK(req.WithContext(ctx)) 447 } 448 449 // GetTar returns a .tar.gz stream of the given directory, relative to the buildlet's work dir. 450 // The provided dir may be empty to get everything. 451 func (c *client) GetTar(ctx context.Context, dir string) (io.ReadCloser, error) { 452 req, err := http.NewRequest("GET", c.URL()+"/tgz?dir="+url.QueryEscape(dir), nil) 453 if err != nil { 454 return nil, err 455 } 456 res, err := c.do(req.WithContext(ctx)) 457 if err != nil { 458 return nil, err 459 } 460 if res.StatusCode != http.StatusOK { 461 slurp, _ := io.ReadAll(io.LimitReader(res.Body, 4<<10)) 462 res.Body.Close() 463 return nil, fmt.Errorf("%v; body: %s", res.Status, slurp) 464 } 465 return res.Body, nil 466 } 467 468 // ExecOpts are options for a remote command invocation. 469 type ExecOpts struct { 470 // Output is the output of stdout and stderr. 471 // If nil, the output is discarded. 472 Output io.Writer 473 474 // Dir is the directory from which to execute the command, 475 // as an absolute or relative path using the buildlet's native 476 // path separator, or a slash-separated relative path. 477 // If relative, it is relative to the buildlet's work directory. 478 // 479 // Dir is optional. If not specified, it defaults to the directory of 480 // the command, or the work directory if SystemLevel is set. 481 Dir string 482 483 // Args are the arguments to pass to the cmd given to Client.Exec. 484 Args []string 485 486 // ExtraEnv are KEY=VALUE pairs to append to the buildlet 487 // process's environment. 488 ExtraEnv []string 489 490 // Path, if non-nil, specifies the PATH variable of the executed process's 491 // environment. Each path in the list should use separators native to the 492 // buildlet's platform, and a non-nil empty list clears the path. 493 // 494 // The following expansions apply: 495 // - the string "$PATH" expands to any existing PATH element(s) 496 // - the substring "$WORKDIR" expands to buildlet's temp workdir 497 // 498 // After expansion, the list is joined with an OS-specific list 499 // separator and supplied to the executed process as its PATH 500 // environment variable. 501 Path []string 502 503 // SystemLevel controls whether the command is expected to be found outside of 504 // the buildlet's environment. 505 SystemLevel bool 506 507 // Debug, if true, instructs to the buildlet to print extra debug 508 // info to the output before the command begins executing. 509 Debug bool 510 511 // OnStartExec is an optional hook that runs after the 200 OK 512 // response from the buildlet, but before the output begins 513 // writing to Output. 514 OnStartExec func() 515 } 516 517 // ErrTimeout is a sentinel error that represents that waiting 518 // for a command to complete has exceeded the given timeout. 519 var ErrTimeout = errors.New("buildlet: timeout waiting for command to complete") 520 521 // Exec runs cmd on the buildlet. 522 // 523 // cmd may be an absolute or relative path using the buildlet's native path 524 // separator, or a slash-separated relative path. If relative, it is 525 // relative to the buildlet's work directory (not opts.Dir). 526 // 527 // Two errors are returned: one is whether the command succeeded 528 // remotely (remoteErr), and the second (execErr) is whether there 529 // were system errors preventing the command from being started or 530 // seen to completition. If execErr is non-nil, the remoteErr is 531 // meaningless. 532 // 533 // If the context's deadline is exceeded while waiting for the command 534 // to complete, the returned execErr is ErrTimeout. 535 func (c *client) Exec(ctx context.Context, cmd string, opts ExecOpts) (remoteErr, execErr error) { 536 var mode string 537 if opts.SystemLevel { 538 mode = "sys" 539 } 540 path := opts.Path 541 if len(path) == 0 && path != nil { 542 // url.Values doesn't distinguish between a nil slice and 543 // a non-nil zero-length slice, so use this sentinel value. 544 path = []string{"$EMPTY"} 545 } 546 form := url.Values{ 547 "cmd": {cmd}, 548 "mode": {mode}, 549 "dir": {opts.Dir}, 550 "cmdArg": opts.Args, 551 "env": opts.ExtraEnv, 552 "path": path, 553 "debug": {fmt.Sprint(opts.Debug)}, 554 } 555 req, err := http.NewRequest("POST", c.URL()+"/exec", strings.NewReader(form.Encode())) 556 if err != nil { 557 return nil, err 558 } 559 req = req.WithContext(ctx) 560 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 561 562 // The first thing the buildlet's exec handler does is flush the headers, so 563 // 20 seconds should be plenty of time, regardless of where on the planet 564 // (Atlanta, Paris, Sydney, etc.) the reverse buildlet is: 565 res, err := c.doHeaderTimeout(req, 20*time.Second) 566 if err == errHeaderTimeout { 567 // If we don't see headers after all that time, 568 // consider the buildlet to be unhealthy. 569 c.MarkBroken() 570 return nil, errors.New("buildlet: timeout waiting for exec header response") 571 } else if err != nil { 572 return nil, err 573 } 574 defer res.Body.Close() 575 if res.StatusCode != http.StatusOK { 576 slurp, _ := io.ReadAll(io.LimitReader(res.Body, 4<<10)) 577 return nil, fmt.Errorf("buildlet: HTTP status %v: %s", res.Status, slurp) 578 } 579 condRun(opts.OnStartExec) 580 581 type errs struct { 582 remoteErr, execErr error 583 } 584 resc := make(chan errs, 1) 585 go func() { 586 // Stream the output: 587 out := opts.Output 588 if out == nil { 589 out = io.Discard 590 } 591 if _, err := io.Copy(out, res.Body); err != nil { 592 resc <- errs{execErr: fmt.Errorf("error copying response: %w", err)} 593 return 594 } 595 596 // Don't record to the dashboard unless we heard the trailer from 597 // the buildlet, otherwise it was probably some unrelated error 598 // (like the VM being killed, or the buildlet crashing due to 599 // e.g. https://golang.org/issue/9309, since we require a tip 600 // build of the buildlet to get Trailers support) 601 state := res.Trailer.Get("Process-State") 602 if state == "" { 603 resc <- errs{execErr: errors.New("missing Process-State trailer from HTTP response; buildlet built with old (<= 1.4) Go?")} 604 return 605 } 606 if state != "ok" { 607 resc <- errs{remoteErr: errors.New(state)} 608 } else { 609 resc <- errs{} // success 610 } 611 }() 612 select { 613 case res := <-resc: 614 if res.execErr != nil { 615 // Note: We've historically marked the buildlet as unhealthy after 616 // reaching any kind of execution error, even when it's a remote command 617 // execution timeout (see use of ErrTimeout below). 618 // This is certainly on the safer side of avoiding false positive signal, 619 // but maybe someday we'll want to start to rely on the buildlet to report 620 // such a condition and not mark it as unhealthy. 621 622 c.MarkBroken() 623 if errors.Is(res.execErr, context.DeadlineExceeded) { 624 res.execErr = ErrTimeout 625 } 626 } 627 return res.remoteErr, res.execErr 628 case <-c.peerDead: 629 return nil, c.deadErr 630 } 631 } 632 633 // RemoveAll deletes the provided paths, relative to the work directory. 634 func (c *client) RemoveAll(ctx context.Context, paths ...string) error { 635 if len(paths) == 0 { 636 return nil 637 } 638 form := url.Values{"path": paths} 639 req, err := http.NewRequest("POST", c.URL()+"/removeall", strings.NewReader(form.Encode())) 640 if err != nil { 641 return err 642 } 643 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 644 return c.doOK(req.WithContext(ctx)) 645 } 646 647 // Status provides status information about the buildlet. 648 // 649 // A coordinator can use the provided information to decide what, if anything, 650 // to do with a buildlet. 651 type Status struct { 652 Version int // buildlet version, coordinator rejects value that is too old (see minBuildletVersion). 653 } 654 655 // Status returns an Status value describing this buildlet. 656 func (c *client) Status(ctx context.Context) (Status, error) { 657 select { 658 case <-c.peerDead: 659 return Status{}, c.deadErr 660 default: 661 // Continue below. 662 } 663 req, err := http.NewRequest("GET", c.URL()+"/status", nil) 664 if err != nil { 665 return Status{}, err 666 } 667 req = req.WithContext(ctx) 668 resp, err := c.doHeaderTimeout(req, 20*time.Second) // plenty of time 669 if err != nil { 670 return Status{}, err 671 } 672 if resp.StatusCode != http.StatusOK { 673 return Status{}, errors.New(resp.Status) 674 } 675 b, err := io.ReadAll(resp.Body) 676 resp.Body.Close() 677 if err != nil { 678 return Status{}, err 679 } 680 var status Status 681 if err := json.Unmarshal(b, &status); err != nil { 682 return Status{}, err 683 } 684 return status, nil 685 } 686 687 // WorkDir returns the absolute path to the buildlet work directory. 688 func (c *client) WorkDir(ctx context.Context) (string, error) { 689 req, err := http.NewRequest("GET", c.URL()+"/workdir", nil) 690 if err != nil { 691 return "", err 692 } 693 req = req.WithContext(ctx) 694 resp, err := c.doHeaderTimeout(req, 20*time.Second) // plenty of time 695 if err != nil { 696 return "", err 697 } 698 if resp.StatusCode != http.StatusOK { 699 return "", errors.New(resp.Status) 700 } 701 b, err := io.ReadAll(resp.Body) 702 resp.Body.Close() 703 if err != nil { 704 return "", err 705 } 706 return string(b), nil 707 } 708 709 // DirEntry is the information about a file on a buildlet. 710 type DirEntry struct { 711 // Line is of the form "drw-rw-rw\t<name>" and then if a regular file, 712 // also "\t<size>\t<modtime>". in either case, without trailing newline. 713 // TODO: break into parsed fields? 714 Line string 715 } 716 717 func (de DirEntry) String() string { 718 return de.Line 719 } 720 721 // Name returns the relative path to the file, such as "src/net/http/" or "src/net/http/jar.go". 722 func (de DirEntry) Name() string { 723 f := strings.Split(de.Line, "\t") 724 if len(f) < 2 { 725 return "" 726 } 727 return f[1] 728 } 729 730 // Perm returns the permission bits in string form, such as "-rw-r--r--" or "drwxr-xr-x". 731 func (de DirEntry) Perm() string { 732 i := strings.IndexByte(de.Line, '\t') 733 if i == -1 { 734 return "" 735 } 736 return de.Line[:i] 737 } 738 739 // IsDir reports whether de describes a directory. That is, 740 // it tests for the os.ModeDir bit being set in de.Perm(). 741 func (de DirEntry) IsDir() bool { 742 if len(de.Line) == 0 { 743 return false 744 } 745 return de.Line[0] == 'd' 746 } 747 748 // Digest returns the SHA-1 digest of the file, such as "da39a3ee5e6b4b0d3255bfef95601890afd80709". 749 // It returns the empty string if the digest isn't included. 750 func (de DirEntry) Digest() string { 751 f := strings.Split(de.Line, "\t") 752 if len(f) < 5 { 753 return "" 754 } 755 return f[4] 756 } 757 758 // ListDirOpts are options for Client.ListDir. 759 type ListDirOpts struct { 760 // Recursive controls whether the directory is listed 761 // recursively. 762 Recursive bool 763 764 // Skip are the directories to skip, relative to the directory 765 // passed to ListDir. Each item should contain only forward 766 // slashes and not start or end in slashes. 767 Skip []string 768 769 // Digest controls whether the SHA-1 digests of regular files 770 // are returned. 771 Digest bool 772 } 773 774 // ListDir lists the contents of a directory. 775 // The fn callback is run for each entry. 776 // The directory dir itself is not included. 777 func (c *client) ListDir(ctx context.Context, dir string, opts ListDirOpts, fn func(DirEntry)) error { 778 param := url.Values{ 779 "dir": {dir}, 780 "recursive": {fmt.Sprint(opts.Recursive)}, 781 "skip": opts.Skip, 782 "digest": {fmt.Sprint(opts.Digest)}, 783 } 784 req, err := http.NewRequest("GET", c.URL()+"/ls?"+param.Encode(), nil) 785 if err != nil { 786 return err 787 } 788 resp, err := c.do(req.WithContext(ctx)) 789 if err != nil { 790 return err 791 } 792 defer resp.Body.Close() 793 if resp.StatusCode != http.StatusOK { 794 slurp, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10)) 795 return fmt.Errorf("%s: %s", resp.Status, slurp) 796 } 797 sc := bufio.NewScanner(resp.Body) 798 for sc.Scan() { 799 line := strings.TrimSpace(sc.Text()) 800 fn(DirEntry{Line: line}) 801 } 802 return sc.Err() 803 } 804 805 func (c *client) getDialer() func(context.Context) (net.Conn, error) { 806 if !c.tls.IsZero() { 807 return func(_ context.Context) (net.Conn, error) { 808 return c.tls.tlsDialer()("tcp", c.ipPort) 809 } 810 } 811 if c.dialer != nil { 812 return c.dialer 813 } 814 return c.dialWithNetDial 815 } 816 817 func (c *client) dialWithNetDial(ctx context.Context) (net.Conn, error) { 818 var d net.Dialer 819 return d.DialContext(ctx, "tcp", c.ipPort) 820 } 821 822 // ConnectSSH opens an SSH connection to the buildlet for the given username. 823 // The authorizedPubKey must be a line from an ~/.ssh/authorized_keys file 824 // and correspond to the private key to be used to communicate over the net.Conn. 825 func (c *client) ConnectSSH(user, authorizedPubKey string) (net.Conn, error) { 826 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 827 defer cancel() 828 conn, err := c.getDialer()(ctx) 829 if err != nil { 830 return nil, fmt.Errorf("error dialing HTTP connection before SSH upgrade: %v", err) 831 } 832 deadline, _ := ctx.Deadline() 833 conn.SetDeadline(deadline) 834 req, err := http.NewRequest("POST", "/connect-ssh", nil) 835 if err != nil { 836 conn.Close() 837 return nil, err 838 } 839 req.Header.Add("X-Go-Ssh-User", user) 840 req.Header.Add("X-Go-Authorized-Key", authorizedPubKey) 841 if !c.tls.IsZero() { 842 req.SetBasicAuth(c.authUsername(), c.password) 843 } 844 if err := req.Write(conn); err != nil { 845 conn.Close() 846 return nil, fmt.Errorf("writing /connect-ssh HTTP request failed: %v", err) 847 } 848 bufr := bufio.NewReader(conn) 849 res, err := http.ReadResponse(bufr, req) 850 if err != nil { 851 conn.Close() 852 return nil, fmt.Errorf("reading /connect-ssh response: %v", err) 853 } 854 if res.StatusCode != http.StatusSwitchingProtocols { 855 slurp, _ := io.ReadAll(res.Body) 856 conn.Close() 857 return nil, fmt.Errorf("unexpected /connect-ssh response: %v, %s", res.Status, slurp) 858 } 859 conn.SetDeadline(time.Time{}) 860 return conn, nil 861 } 862 863 func condRun(fn func()) { 864 if fn != nil { 865 fn() 866 } 867 } 868 869 type onEOFReadCloser struct { 870 rc io.ReadCloser 871 fn func() 872 } 873 874 func (o onEOFReadCloser) Read(p []byte) (n int, err error) { 875 n, err = o.rc.Read(p) 876 if err == io.EOF { 877 o.fn() 878 } 879 return 880 } 881 882 func (o onEOFReadCloser) Close() error { 883 o.fn() 884 return o.rc.Close() 885 }