golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/buildlet/buildlet.go (about) 1 // Copyright 2014 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 // The buildlet is an HTTP server that untars content to disk and runs 6 // commands it has untarred, streaming their output back over HTTP. 7 // It is part of Go's continuous build system. 8 // 9 // This program intentionally allows remote code execution, and 10 // provides no security of its own. It is assumed that any user uses 11 // it with an appropriately-configured firewall between their VM 12 // instances. 13 package main // import "golang.org/x/build/cmd/buildlet" 14 15 import ( 16 "archive/tar" 17 "bytes" 18 "compress/gzip" 19 "context" 20 "crypto/sha1" 21 "crypto/tls" 22 "encoding/json" 23 "errors" 24 "flag" 25 "fmt" 26 "io" 27 "io/fs" 28 "log" 29 "net" 30 "net/http" 31 "net/url" 32 "os" 33 "os/exec" 34 "path" 35 "path/filepath" 36 "runtime" 37 "strconv" 38 "strings" 39 "sync" 40 "time" 41 42 "cloud.google.com/go/compute/metadata" 43 "github.com/aws/aws-sdk-go/aws" 44 "github.com/aws/aws-sdk-go/aws/ec2metadata" 45 "github.com/aws/aws-sdk-go/aws/session" 46 "github.com/gliderlabs/ssh" 47 "golang.org/x/build/buildlet" 48 "golang.org/x/build/internal/cloud" 49 "golang.org/x/build/internal/envutil" 50 "golang.org/x/build/pargzip" 51 ) 52 53 var ( 54 haltEntireOS = flag.Bool("halt", true, "halt OS in /halt handler. If false, the buildlet process just ends.") 55 rebootOnHalt = flag.Bool("reboot", false, "reboot system in /halt handler.") 56 workDir = flag.String("workdir", "", "Temporary directory to use. The contents of this directory may be deleted at any time. If empty, TempDir is used to create one.") 57 listenAddr = flag.String("listen", "AUTO", "address to listen on. Unused in reverse mode. Warning: this service is inherently insecure and offers no protection of its own. Do not expose this port to the world.") 58 reverseType = flag.String("reverse-type", "", "if non-empty, go into reverse mode where the buildlet dials the coordinator instead of listening for connections. The value is the dashboard/builders.go Hosts map key, naming a HostConfig. This buildlet will receive work for any BuildConfig specifying this named HostConfig.") 59 coordinator = flag.String("coordinator", "localhost:8119", "address of coordinator, in production use farmer.golang.org. Only used in reverse mode.") 60 hostname = flag.String("hostname", "", "hostname to advertise to coordinator for reverse mode; default is actual hostname") 61 healthAddr = flag.String("health-addr", "0.0.0.0:8080", "For reverse buildlets, address to listen for /healthz requests separately from the reverse dialer to the coordinator.") 62 version = flag.Bool("version", false, "print buildlet version and exit") 63 gomoteServerAddr = flag.String("gomote-server-addr", "gomotessh.golang.org:443", "Gomote server address and port") 64 swarmingBot = flag.Bool("swarming-bot", false, "start the buildlet on a swarming bot") 65 ) 66 67 // Bump this whenever something notable happens, or when another 68 // component needs a certain feature. This shows on the coordinator 69 // per reverse client, and is also accessible via the buildlet 70 // package's client API (via the Status method). 71 // 72 // Notable versions: 73 // 74 // 3: switched to revdial protocol 75 // 5: reverse dialing uses timeouts+tcp keepalives, pargzip fix 76 // 7: version bumps while debugging revdial hang (Issue 12816) 77 // 8: mac screensaver disabled 78 // 11: move from self-signed cert to LetsEncrypt (Issue 16442) 79 // 15: ssh support 80 // 16: make macstadium builders always haltEntireOS 81 // 17: make macstadium halts use sudo 82 // 18: set TMPDIR and GOCACHE 83 // 21: GO_BUILDER_SET_GOPROXY=coordinator support 84 // 22: TrimSpace the reverse buildlet's gobuildkey contents 85 // 23: revdial v2 86 // 24: removeAllIncludingReadonly 87 // 25: use removeAllIncludingReadonly for all work area cleanup 88 // 26: clean up path validation and normalization 89 // 27: export GOPLSCACHE=$workdir/goplscache 90 // 28: add support for gomote server 91 const buildletVersion = 28 92 93 func defaultListenAddr() string { 94 if runtime.GOOS == "darwin" { 95 // Darwin will never run on GCE, so let's always 96 // listen on a high port (so we don't need to be 97 // root). 98 return ":5936" 99 } 100 // check if env is dev 101 if !metadata.OnGCE() && !onEC2() { 102 return "localhost:5936" 103 } 104 // In production, default to port 80 or 443, depending on 105 // whether TLS is configured. 106 if metadataValue(metaKeyTLSCert) != "" { 107 return ":443" 108 } 109 return ":80" 110 } 111 112 // Functionality set non-nil by some platforms: 113 var ( 114 configureSerialLogOutput func() 115 setOSRlimit func() error 116 ) 117 118 // If non-empty, the $TMPDIR, $GOCACHE, and $GOPLSCACHE environment 119 // variables to use for child processes. 120 var ( 121 processTmpDirEnv string 122 processGoCacheEnv string 123 processGoplsCacheEnv string 124 ) 125 126 const ( 127 metaKeyPassword = "password" 128 metaKeyTLSCert = "tls-cert" 129 metaKeyTLSkey = "tls-key" 130 ) 131 132 func main() { 133 builderEnv := os.Getenv("GO_BUILDER_ENV") 134 defer teardownOnce() 135 onGCE := metadata.OnGCE() 136 switch runtime.GOOS { 137 case "plan9": 138 if onGCE { 139 log.SetOutput(&gcePlan9LogWriter{w: os.Stderr}) 140 } 141 case "linux": 142 if onGCE && !inKube { 143 if w, err := os.OpenFile("/dev/console", os.O_WRONLY, 0); err == nil { 144 log.SetOutput(w) 145 } 146 } 147 case "windows": 148 if onGCE { 149 configureSerialLogOutput() 150 } 151 } 152 153 flag.Parse() 154 if *version { 155 fmt.Printf("buildlet version %v (%s-%s)\n", buildletVersion, runtime.GOOS, runtime.GOARCH) 156 fmt.Printf("built with %v\n", runtime.Version()) 157 os.Exit(0) 158 } 159 log.Printf("buildlet starting.") 160 161 if builderEnv == "android-amd64-emu" { 162 startAndroidEmulator() 163 } 164 165 // Optimize emphemeral filesystems. Prefer speed over safety, 166 // since these VMs only last for the duration of one build. 167 switch runtime.GOOS { 168 case "openbsd", "freebsd", "netbsd": 169 makeBSDFilesystemFast() 170 } 171 if setOSRlimit != nil { 172 err := setOSRlimit() 173 if err != nil { 174 log.Fatalf("setOSRLimit: %v", err) 175 } 176 log.Printf("set OS rlimits.") 177 } 178 179 isReverse := *reverseType != "" 180 181 if *listenAddr == "AUTO" && !isReverse { 182 v := defaultListenAddr() 183 log.Printf("Will listen on %s", v) 184 *listenAddr = v 185 } 186 187 if !onGCE && !isReverse && !onEC2() && !strings.HasPrefix(*listenAddr, "localhost:") { 188 log.Printf("** WARNING *** This server is unsafe and offers no security. Be careful.") 189 } 190 if onGCE { 191 fixMTU() 192 } 193 if *workDir == "" && setWorkdirToTmpfs != nil { 194 setWorkdirToTmpfs() 195 } 196 if *workDir == "" { 197 switch runtime.GOOS { 198 case "windows": 199 // We want a short path on Windows, due to 200 // Windows issues with maximum path lengths. 201 *workDir = `C:\workdir` 202 if err := os.MkdirAll(*workDir, 0755); err != nil { 203 log.Fatalf("error creating workdir: %v", err) 204 } 205 default: 206 wdName := "workdir" 207 if *reverseType != "" { 208 wdName += "-" + *reverseType 209 } 210 dir := filepath.Join(os.TempDir(), wdName) 211 removeAllAndMkdir(dir) 212 *workDir = dir 213 } 214 } 215 216 os.Setenv("WORKDIR", *workDir) // mostly for demos 217 218 if _, err := os.Lstat(*workDir); err != nil { 219 log.Fatalf("invalid --workdir %q: %v", *workDir, err) 220 } 221 222 // Set up and clean $TMPDIR and $GOCACHE directories. 223 if runtime.GOOS != "plan9" { // go.dev/cl/207283 seems to indicate plan9 should work, but someone needs to test it. 224 processTmpDirEnv = filepath.Join(*workDir, "tmp") 225 removeAllAndMkdir(processTmpDirEnv) 226 227 processGoCacheEnv = filepath.Join(*workDir, "gocache") 228 removeAllAndMkdir(processGoCacheEnv) 229 230 processGoplsCacheEnv = filepath.Join(*workDir, "goplscache") 231 removeAllAndMkdir(processGoplsCacheEnv) 232 } 233 234 http.HandleFunc("/", handleRoot) 235 http.HandleFunc("/debug/x", handleX) 236 237 var password string 238 if !isReverse { 239 password = metadataValue(metaKeyPassword) 240 } 241 requireAuth := func(handler func(w http.ResponseWriter, r *http.Request)) http.Handler { 242 return requirePasswordHandler{http.HandlerFunc(handler), password} 243 } 244 http.Handle("/debug/goroutines", requireAuth(handleGoroutines)) 245 http.Handle("/writetgz", requireAuth(handleWriteTGZ)) 246 http.Handle("/write", requireAuth(handleWrite)) 247 http.Handle("/exec", requireAuth(handleExec)) 248 http.Handle("/halt", requireAuth(handleHalt)) 249 http.Handle("/tgz", requireAuth(handleGetTGZ)) 250 http.Handle("/removeall", requireAuth(handleRemoveAll)) 251 http.Handle("/workdir", requireAuth(handleWorkDir)) 252 http.Handle("/status", requireAuth(handleStatus)) 253 http.Handle("/ls", requireAuth(handleLs)) 254 http.Handle("/connect-ssh", requireAuth(handleConnectSSH)) 255 http.HandleFunc("/healthz", handleHealthz) 256 257 if !isReverse && !*swarmingBot { 258 listenForCoordinator() 259 } else { 260 go func() { 261 if err := serveReverseHealth(); err != nil { 262 log.Printf("Error in serveReverseHealth: %v", err) 263 } 264 }() 265 ln, err := dialServer() 266 if err != nil { 267 log.Fatalf("Error dialing server: %v", err) 268 } 269 srv := &http.Server{} 270 err = srv.Serve(ln) 271 log.Printf("http.Serve on reverse connection complete: %v", err) 272 log.Printf("buildlet reverse mode exiting.") 273 if *haltEntireOS { 274 // The coordinator disconnects before doHalt has time to 275 // execute. handleHalt has a 1s delay. 276 time.Sleep(5 * time.Second) 277 } 278 os.Exit(0) 279 } 280 } 281 282 type teardownFunc func() 283 284 var ( 285 tdOnce sync.Once 286 teardownOnce func() = func() { tdOnce.Do(teardown) } 287 teardownFuncs []teardownFunc 288 ) 289 290 func teardown() { 291 for _, f := range teardownFuncs { 292 f() 293 } 294 } 295 296 func dialServer() (net.Listener, error) { 297 if *swarmingBot { 298 return dialGomoteServer() 299 } 300 return dialCoordinator() 301 } 302 303 func listenForCoordinator() { 304 tlsCert, tlsKey := metadataValue(metaKeyTLSCert), metadataValue(metaKeyTLSkey) 305 if (tlsCert == "") != (tlsKey == "") { 306 log.Fatalf("tls-cert and tls-key must both be supplied, or neither.") 307 } 308 309 log.Printf("Listening on %s ...", *listenAddr) 310 ln, err := net.Listen("tcp", *listenAddr) 311 if err != nil { 312 log.Fatalf("Failed to listen on %s: %v", *listenAddr, err) 313 } 314 ln = tcpKeepAliveListener{ln.(*net.TCPListener)} 315 316 var srv http.Server 317 if tlsCert != "" { 318 cert, err := tls.X509KeyPair([]byte(tlsCert), []byte(tlsKey)) 319 if err != nil { 320 log.Fatalf("TLS cert error: %v", err) 321 } 322 tlsConf := &tls.Config{ 323 Certificates: []tls.Certificate{cert}, 324 } 325 ln = tls.NewListener(ln, tlsConf) 326 } 327 328 serveErr := make(chan error, 1) 329 go func() { 330 serveErr <- srv.Serve(ln) 331 }() 332 333 signalChan := make(chan os.Signal, 1) 334 if registerSignal != nil { 335 registerSignal(signalChan) 336 } 337 select { 338 case sig := <-signalChan: 339 log.Printf("received signal %v; shutting down gracefully.", sig) 340 case err := <-serveErr: 341 log.Fatalf("Serve: %v", err) 342 } 343 time.AfterFunc(5*time.Second, func() { 344 log.Printf("timeout shutting down gracefully; exiting immediately") 345 os.Exit(1) 346 }) 347 if err := srv.Shutdown(context.Background()); err != nil { 348 log.Printf("Graceful shutdown error: %v; exiting immediately instead", err) 349 os.Exit(1) 350 } 351 log.Printf("graceful shutdown complete.") 352 os.Exit(0) 353 } 354 355 // registerSignal if non-nil registers shutdown signals with the provided chan. 356 var registerSignal func(chan<- os.Signal) 357 358 var inKube = os.Getenv("KUBERNETES_SERVICE_HOST") != "" 359 360 var ( 361 // ec2UD contains a copy of the EC2 vm user data retrieved from the metadata. 362 ec2UD *cloud.EC2UserData 363 // ec2MdC is an EC2 metadata client. 364 ec2MdC *ec2metadata.EC2Metadata 365 ) 366 367 // onEC2 evaluates if the buildlet is running on an EC2 instance. 368 func onEC2() bool { 369 if ec2MdC != nil { 370 return ec2MdC.Available() 371 } 372 cfg := aws.NewConfig() 373 // TODO(golang/go#42604) - Improve detection of our qemu forwarded 374 // metadata service for Windows ARM VMs running on EC2. 375 if runtime.GOOS == "windows" && runtime.GOARCH == "arm64" { 376 cfg = cfg.WithEndpoint("http://10.0.2.100:8173/latest") 377 } 378 ses, err := session.NewSession(cfg) 379 if err != nil { 380 log.Printf("unable to create aws session: %s", err) 381 return false 382 } 383 ec2MdC = ec2metadata.New(ses, cfg) 384 return ec2MdC.Available() 385 } 386 387 // mdValueFromUserData maps a metadata key value into the corresponding 388 // EC2UserData value. If a mapping is not found, an empty string is returned. 389 func mdValueFromUserData(ud *cloud.EC2UserData, key string) string { 390 switch key { 391 case metaKeyTLSCert: 392 return ud.TLSCert 393 case metaKeyTLSkey: 394 return ud.TLSKey 395 case metaKeyPassword: 396 return ud.TLSPassword 397 default: 398 return "" 399 } 400 } 401 402 // metadataValue returns the GCE metadata instance value for the given key. 403 // If the instance is on EC2 the corresponding value will be extracted from 404 // the user data available via the metadata. 405 // If the metadata is not defined, the returned string is empty. 406 // 407 // If not running on GCE or EC2, it falls back to using environment variables 408 // for local development. 409 func metadataValue(key string) string { 410 // The common case (on GCE, but not in Kubernetes): 411 if metadata.OnGCE() && !inKube { 412 v, err := metadata.InstanceAttributeValue(key) 413 if _, notDefined := err.(metadata.NotDefinedError); notDefined { 414 return "" 415 } 416 if err != nil { 417 log.Fatalf("metadata.InstanceAttributeValue(%q): %v", key, err) 418 } 419 return v 420 } 421 422 if onEC2() { 423 if ec2UD != nil { 424 return mdValueFromUserData(ec2UD, key) 425 } 426 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 427 defer cancel() 428 ec2MetaJson, err := ec2MdC.GetUserDataWithContext(ctx) 429 if err != nil { 430 log.Fatalf("unable to retrieve EC2 user data: %v", err) 431 } 432 ec2UD = &cloud.EC2UserData{} 433 err = json.Unmarshal([]byte(ec2MetaJson), ec2UD) 434 if err != nil { 435 log.Fatalf("unable to unmarshal user data json: %v", err) 436 } 437 return mdValueFromUserData(ec2UD, key) 438 } 439 440 // Else allow use of environment variables to fake 441 // metadata keys, for Kubernetes pods or local testing. 442 envKey := "META_" + strings.Replace(key, "-", "_", -1) 443 v := os.Getenv(envKey) 444 // Respect curl-style '@' prefix to mean the rest is a filename. 445 if strings.HasPrefix(v, "@") { 446 slurp, err := os.ReadFile(v[1:]) 447 if err != nil { 448 log.Fatalf("Error reading file for GCEMETA_%v: %v", key, err) 449 } 450 return string(slurp) 451 } 452 if v == "" { 453 log.Printf("Warning: not running on GCE, and no %v environment variable defined", envKey) 454 } 455 return v 456 } 457 458 // tcpKeepAliveListener is a net.Listener that sets TCP keep-alive 459 // timeouts on accepted connections. 460 type tcpKeepAliveListener struct { 461 *net.TCPListener 462 } 463 464 func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 465 tc, err := ln.AcceptTCP() 466 if err != nil { 467 return 468 } 469 tc.SetKeepAlive(true) 470 tc.SetKeepAlivePeriod(3 * time.Minute) 471 return tc, nil 472 } 473 474 func fixMTU_freebsd() error { return fixMTU_ifconfig("vtnet0") } 475 func fixMTU_openbsd() error { return fixMTU_ifconfig("vio0") } 476 func fixMTU_ifconfig(iface string) error { 477 out, err := exec.Command("/sbin/ifconfig", iface, "mtu", "1460").CombinedOutput() 478 if err != nil { 479 return fmt.Errorf("/sbin/ifconfig %s mtu 1460: %v, %s", iface, err, out) 480 } 481 return nil 482 } 483 484 func fixMTU_plan9() error { 485 f, err := os.OpenFile("/net/ipifc/0/ctl", os.O_WRONLY, 0) 486 if err != nil { 487 return err 488 } 489 if _, err := io.WriteString(f, "mtu 1460\n"); err != nil { 490 f.Close() 491 return err 492 } 493 return f.Close() 494 } 495 496 func fixMTU() { 497 fn, ok := map[string]func() error{ 498 "openbsd": fixMTU_openbsd, 499 "freebsd": fixMTU_freebsd, 500 "plan9": fixMTU_plan9, 501 }[runtime.GOOS] 502 if ok { 503 if err := fn(); err != nil { 504 log.Printf("Failed to set MTU: %v", err) 505 } else { 506 log.Printf("Adjusted MTU.") 507 } 508 } 509 } 510 511 // flushWriter is an io.Writer that Flushes after each Write if the 512 // underlying Writer implements http.Flusher. 513 type flushWriter struct { 514 rw http.ResponseWriter 515 } 516 517 func (fw flushWriter) Write(p []byte) (n int, err error) { 518 n, err = fw.rw.Write(p) 519 if f, ok := fw.rw.(http.Flusher); ok { 520 f.Flush() 521 } 522 return 523 } 524 525 func handleRoot(w http.ResponseWriter, r *http.Request) { 526 if r.URL.Path != "/" { 527 http.NotFound(w, r) 528 return 529 } 530 fmt.Fprintf(w, "buildlet running on %s-%s\n", runtime.GOOS, runtime.GOARCH) 531 } 532 533 func handleGoroutines(w http.ResponseWriter, r *http.Request) { 534 log.Printf("Dumping goroutines.") 535 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 536 buf := make([]byte, 2<<20) 537 buf = buf[:runtime.Stack(buf, true)] 538 w.Write(buf) 539 log.Printf("Dumped goroutines.") 540 } 541 542 // unauthenticated /debug/x handler, to test MTU settings. 543 func handleX(w http.ResponseWriter, r *http.Request) { 544 n, _ := strconv.Atoi(r.FormValue("n")) 545 if n > 1<<20 { 546 n = 1 << 20 547 } 548 log.Printf("Dumping %d X.", n) 549 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 550 buf := make([]byte, n) 551 for i := range buf { 552 buf[i] = 'X' 553 } 554 w.Write(buf) 555 log.Printf("Dumped X.") 556 } 557 558 func handleGetTGZ(w http.ResponseWriter, r *http.Request) { 559 if r.Method != "GET" { 560 http.Error(w, "requires GET method", http.StatusBadRequest) 561 return 562 } 563 if !mkdirAllWorkdirOr500(w) { 564 return 565 } 566 dir, err := nativeRelPath(r.FormValue("dir")) 567 if err != nil { 568 http.Error(w, "invalid 'dir' parameter: "+err.Error(), http.StatusBadRequest) 569 return 570 } 571 572 zw := pargzip.NewWriter(w) 573 tw := tar.NewWriter(zw) 574 base := filepath.Join(*workDir, dir) 575 err = filepath.Walk(base, func(path string, fi os.FileInfo, err error) error { 576 if err != nil { 577 return err 578 } 579 rel := strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(path, base)), "/") 580 var linkName string 581 if fi.Mode()&os.ModeSymlink != 0 { 582 linkName, err = os.Readlink(path) 583 if err != nil { 584 return err 585 } 586 } 587 th, err := tar.FileInfoHeader(fi, linkName) 588 if err != nil { 589 return err 590 } 591 th.Name = rel 592 if fi.IsDir() && !strings.HasSuffix(th.Name, "/") { 593 th.Name += "/" 594 } 595 if th.Name == "/" { 596 return nil 597 } 598 if err := tw.WriteHeader(th); err != nil { 599 return err 600 } 601 if fi.Mode().IsRegular() { 602 f, err := os.Open(path) 603 if err != nil { 604 return err 605 } 606 defer f.Close() 607 if _, err := io.Copy(tw, f); err != nil { 608 return err 609 } 610 } 611 return nil 612 }) 613 if err != nil { 614 log.Printf("Walk error: %v", err) 615 panic(http.ErrAbortHandler) 616 } 617 tw.Close() 618 zw.Close() 619 } 620 621 func handleWriteTGZ(w http.ResponseWriter, r *http.Request) { 622 if !mkdirAllWorkdirOr500(w) { 623 return 624 } 625 urlParam, _ := url.ParseQuery(r.URL.RawQuery) 626 baseDir := *workDir 627 if dir := urlParam.Get("dir"); dir != "" { 628 var err error 629 dir, err = nativeRelPath(dir) 630 if err != nil { 631 log.Printf("writetgz: bogus dir %q", dir) 632 http.Error(w, "invalid 'dir' parameter: "+err.Error(), http.StatusBadRequest) 633 return 634 } 635 baseDir = filepath.Join(baseDir, dir) 636 637 // Special case: if the directory is "go1.4" and it already exists, do nothing. 638 // This lets clients do a blind write to it and not do extra work. 639 if r.Method == "POST" && dir == "go1.4" { 640 if fi, err := os.Stat(baseDir); err == nil && fi.IsDir() { 641 log.Printf("writetgz: skipping URL puttar to go1.4 dir; already exists") 642 io.WriteString(w, "SKIP") 643 return 644 } 645 } 646 647 if err := os.MkdirAll(baseDir, 0755); err != nil { 648 log.Printf("writetgz: %v", err) 649 http.Error(w, "mkdir of base: "+err.Error(), http.StatusInternalServerError) 650 return 651 } 652 } 653 654 var tgz io.Reader 655 var urlStr string 656 switch r.Method { 657 case "PUT": 658 tgz = r.Body 659 log.Printf("writetgz: untarring Request.Body into %s", baseDir) 660 case "POST": 661 urlStr = r.FormValue("url") 662 if urlStr == "" { 663 log.Printf("writetgz: missing url POST param") 664 http.Error(w, "missing url POST param", http.StatusBadRequest) 665 return 666 } 667 t0 := time.Now() 668 res, err := http.Get(urlStr) 669 if err != nil { 670 log.Printf("writetgz: failed to fetch tgz URL %s: %v", urlStr, err) 671 http.Error(w, fmt.Sprintf("fetching URL %s: %v", urlStr, err), http.StatusInternalServerError) 672 return 673 } 674 defer res.Body.Close() 675 if res.StatusCode != http.StatusOK { 676 log.Printf("writetgz: failed to fetch tgz URL %s: status=%v", urlStr, res.Status) 677 http.Error(w, fmt.Sprintf("writetgz: fetching provided URL %q: %s", urlStr, res.Status), http.StatusInternalServerError) 678 return 679 } 680 tgz = res.Body 681 log.Printf("writetgz: untarring %s (got headers in %v) into %s", urlStr, time.Since(t0), baseDir) 682 default: 683 log.Printf("writetgz: invalid method %q", r.Method) 684 http.Error(w, "requires PUT or POST method", http.StatusBadRequest) 685 return 686 } 687 688 err := untar(tgz, baseDir) 689 if err != nil { 690 http.Error(w, err.Error(), httpStatus(err)) 691 return 692 } 693 io.WriteString(w, "OK") 694 } 695 696 func handleWrite(w http.ResponseWriter, r *http.Request) { 697 if r.Method != "PUT" { 698 http.Error(w, "requires POST method", http.StatusBadRequest) 699 return 700 } 701 702 param, _ := url.ParseQuery(r.URL.RawQuery) 703 704 path := param.Get("path") 705 if _, err := nativeRelPath(path); err != nil { 706 http.Error(w, "invalid 'path' parameter: "+err.Error(), http.StatusBadRequest) 707 return 708 } 709 path = filepath.FromSlash(path) 710 path = filepath.Join(*workDir, path) 711 712 modeInt, err := strconv.ParseInt(param.Get("mode"), 10, 64) 713 mode := os.FileMode(modeInt) 714 if err != nil || !mode.IsRegular() { 715 http.Error(w, "bad mode", http.StatusBadRequest) 716 return 717 } 718 719 // Make the parent directory, along with any necessary parents, if needed. 720 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 721 http.Error(w, err.Error(), http.StatusInternalServerError) 722 return 723 } 724 725 if err := writeFile(r.Body, path, mode); err != nil { 726 http.Error(w, err.Error(), http.StatusInternalServerError) 727 return 728 } 729 730 io.WriteString(w, "OK") 731 } 732 733 func writeFile(r io.Reader, path string, mode os.FileMode) error { 734 if runtime.GOOS == "darwin" && mode&0111 != 0 { 735 // The darwin kernel caches binary signatures and SIGKILLs 736 // binaries with mismatched signatures. Overwriting a binary 737 // with O_TRUNC does not clear the cache, rendering the new 738 // copy unusable. Removing the original file first does clear 739 // the cache. See #54132. 740 err := os.Remove(path) 741 if err != nil && !errors.Is(err, fs.ErrNotExist) { 742 return err 743 } 744 } 745 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) 746 if err != nil { 747 return err 748 } 749 if _, err := io.Copy(f, r); err != nil { 750 f.Close() 751 return err 752 } 753 // Try to set the mode again, in case the file already existed. 754 if runtime.GOOS != "windows" { 755 if err := f.Chmod(mode); err != nil { 756 f.Close() 757 return err 758 } 759 } 760 return f.Close() 761 } 762 763 // untar reads the gzip-compressed tar file from r and writes it into dir. 764 func untar(r io.Reader, dir string) (err error) { 765 t0 := time.Now() 766 nFiles := 0 767 madeDir := map[string]bool{} 768 defer func() { 769 td := time.Since(t0) 770 if err == nil { 771 log.Printf("extracted tarball into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td) 772 } else { 773 log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err) 774 } 775 }() 776 zr, err := gzip.NewReader(r) 777 if err != nil { 778 return badRequestf("requires gzip-compressed body: %w", err) 779 } 780 tr := tar.NewReader(zr) 781 loggedChtimesError := false 782 for { 783 f, err := tr.Next() 784 if err == io.EOF { 785 break 786 } 787 if err != nil { 788 log.Printf("tar reading error: %v", err) 789 return badRequestf("tar error: %w", err) 790 } 791 if f.Typeflag == tar.TypeXGlobalHeader { 792 // golang.org/issue/22748: git archive exports 793 // a global header ('g') which after Go 1.9 794 // (for a bit?) contained an empty filename. 795 // Ignore it. 796 continue 797 } 798 rel, err := nativeRelPath(f.Name) 799 if err != nil { 800 return badRequestf("tar file contained invalid name %q: %v", f.Name, err) 801 } 802 abs := filepath.Join(dir, rel) 803 804 fi := f.FileInfo() 805 mode := fi.Mode() 806 switch { 807 case mode.IsRegular(): 808 // Make the directory. This is redundant because it should 809 // already be made by a directory entry in the tar 810 // beforehand. Thus, don't check for errors; the next 811 // write will fail with the same error. 812 dir := filepath.Dir(abs) 813 if !madeDir[dir] { 814 if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { 815 return err 816 } 817 madeDir[dir] = true 818 } 819 if runtime.GOOS == "darwin" && mode&0111 != 0 { 820 // See comment in writeFile. 821 err := os.Remove(abs) 822 if err != nil && !errors.Is(err, fs.ErrNotExist) { 823 return err 824 } 825 } 826 wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()) 827 if err != nil { 828 return err 829 } 830 n, err := io.Copy(wf, tr) 831 if closeErr := wf.Close(); closeErr != nil && err == nil { 832 err = closeErr 833 } 834 if err != nil { 835 return fmt.Errorf("error writing to %s: %v", abs, err) 836 } 837 if n != f.Size { 838 return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size) 839 } 840 modTime := f.ModTime 841 if modTime.After(t0) { 842 // Clamp modtimes at system time. See 843 // golang.org/issue/19062 when clock on 844 // buildlet was behind the gitmirror server 845 // doing the git-archive. 846 modTime = t0 847 } 848 if !modTime.IsZero() { 849 if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError { 850 // benign error. Gerrit doesn't even set the 851 // modtime in these, and we don't end up relying 852 // on it anywhere (the gomote push command relies 853 // on digests only), so this is a little pointless 854 // for now. 855 log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err) 856 loggedChtimesError = true // once is enough 857 } 858 } 859 nFiles++ 860 case mode.IsDir(): 861 if err := os.MkdirAll(abs, 0755); err != nil { 862 return err 863 } 864 madeDir[abs] = true 865 case mode&os.ModeSymlink != 0: 866 // TODO: ignore these for now. They were breaking x/build tests. 867 // Implement these if/when we ever have a test that needs them. 868 // But maybe we'd have to skip creating them on Windows for some builders 869 // without permissions. 870 default: 871 return badRequestf("tar file entry %s contained unsupported file type %v", f.Name, mode) 872 } 873 } 874 return nil 875 } 876 877 // Process-State is an HTTP Trailer set in the /exec handler to "ok" 878 // on success, or os.ProcessState.String() on failure. 879 const hdrProcessState = "Process-State" 880 881 func handleExec(w http.ResponseWriter, r *http.Request) { 882 cn := w.(http.CloseNotifier) 883 clientGone := cn.CloseNotify() 884 handlerDone := make(chan bool) 885 defer close(handlerDone) 886 887 if r.Method != "POST" { 888 http.Error(w, "requires POST method", http.StatusBadRequest) 889 return 890 } 891 if r.ProtoMajor*10+r.ProtoMinor < 11 { 892 // We need trailers, only available in HTTP/1.1 or HTTP/2. 893 http.Error(w, "HTTP/1.1 or higher required", http.StatusBadRequest) 894 return 895 } 896 // Create *workDir and any needed temporary subdirectories. 897 if !mkdirAllWorkdirOr500(w) { 898 return 899 } 900 for _, dir := range []string{processTmpDirEnv, processGoCacheEnv, processGoplsCacheEnv} { 901 if dir == "" { 902 continue 903 } 904 if err := os.MkdirAll(dir, 0755); err != nil { 905 http.Error(w, err.Error(), http.StatusInternalServerError) 906 return 907 } 908 } 909 if err := checkAndroidEmulator(); err != nil { 910 http.Error(w, "android emulator not running: "+err.Error(), http.StatusInternalServerError) 911 return 912 } 913 914 w.Header().Set("Trailer", hdrProcessState) // declare it so we can set it 915 916 sysMode := r.FormValue("mode") == "sys" 917 debug, _ := strconv.ParseBool(r.FormValue("debug")) 918 919 absCmd, err := absExecCmd(r.FormValue("cmd"), sysMode) // required 920 if err != nil { 921 http.Error(w, "invalid 'cmd' parameter: "+err.Error(), httpStatus(err)) 922 return 923 } 924 925 absDir, err := absExecDir(r.FormValue("dir"), sysMode, filepath.Dir(absCmd)) // optional 926 if err != nil { 927 http.Error(w, "invalid 'dir' parameter: "+err.Error(), httpStatus(err)) 928 return 929 } 930 931 if f, ok := w.(http.Flusher); ok { 932 f.Flush() 933 } 934 935 postEnv := r.PostForm["env"] 936 937 goarch := "amd64" // unless we find otherwise 938 if v := envutil.Get(runtime.GOOS, postEnv, "GOARCH"); v != "" { 939 goarch = v 940 } 941 if v, _ := strconv.ParseBool(envutil.Get(runtime.GOOS, postEnv, "GO_DISABLE_OUTBOUND_NETWORK")); v { 942 disableOutboundNetwork() 943 } 944 945 env := append(baseEnv(goarch), postEnv...) 946 if v := processTmpDirEnv; v != "" { 947 env = append(env, "TMPDIR="+v) 948 } 949 if v := processGoCacheEnv; v != "" { 950 env = append(env, "GOCACHE="+v) 951 } 952 if v := processGoplsCacheEnv; v != "" { 953 env = append(env, "GOPLSCACHE="+v) 954 } 955 if path := r.PostForm["path"]; len(path) > 0 { 956 if kv, ok := pathEnv(runtime.GOOS, env, path, *workDir); ok { 957 env = append(env, kv) 958 } 959 } 960 env = envutil.Dedup(runtime.GOOS, env) 961 962 var cmd *exec.Cmd 963 if needsBashWrapper(absCmd) { 964 cmd = exec.Command("bash", absCmd) 965 } else { 966 cmd = exec.Command(absCmd) 967 } 968 cmd.Args = append(cmd.Args, r.PostForm["cmdArg"]...) 969 cmd.Env = env 970 envutil.SetDir(cmd, absDir) 971 cmdOutput := flushWriter{w} 972 cmd.Stdout = cmdOutput 973 cmd.Stderr = cmdOutput 974 975 log.Printf("[%p] Running %s with args %q and env %q in dir %s", 976 cmd, cmd.Path, cmd.Args, cmd.Env, cmd.Dir) 977 978 if debug { 979 fmt.Fprintf(cmdOutput, ":: Running %s with args %q and env %q in dir %s\n\n", 980 cmd.Path, cmd.Args, cmd.Env, cmd.Dir) 981 } 982 983 t0 := time.Now() 984 err = cmd.Start() 985 if err == nil { 986 go func() { 987 select { 988 case <-clientGone: 989 err := killProcessTree(cmd.Process) 990 if err != nil { 991 log.Printf("Kill failed: %v", err) 992 } 993 case <-handlerDone: 994 return 995 } 996 }() 997 err = cmd.Wait() 998 } 999 state := "ok" 1000 if err != nil { 1001 if ps := cmd.ProcessState; ps != nil { 1002 state = ps.String() 1003 } else { 1004 state = err.Error() 1005 } 1006 } 1007 w.Header().Set(hdrProcessState, state) 1008 log.Printf("[%p] Run = %s, after %v", cmd, state, time.Since(t0)) 1009 } 1010 1011 // absExecCmd returns the native, absolute path corresponding to the "cmd" 1012 // argument passed to the "exec" endpoint. 1013 func absExecCmd(cmdArg string, sysMode bool) (absCmd string, err error) { 1014 if cmdArg == "" { 1015 return "", badRequestf("requires 'cmd' parameter") 1016 } 1017 1018 if filepath.IsAbs(cmdArg) { 1019 return filepath.Clean(cmdArg), nil 1020 } 1021 1022 relCmd, err := nativeRelPath(cmdArg) 1023 if err != nil { 1024 return "", badRequestf("invalid 'cmd' parameter: %w", err) 1025 } 1026 1027 if strings.Contains(relCmd, string(filepath.Separator)) { 1028 if sysMode { 1029 return "", badRequestf("'sys' mode requires absolute or system 'cmd' path") 1030 } 1031 return filepath.Join(*workDir, filepath.FromSlash(cmdArg)), nil 1032 } 1033 1034 if !sysMode { 1035 absCmd, err = exec.LookPath(filepath.Join(*workDir, cmdArg)) 1036 if err == nil { 1037 return absCmd, nil 1038 } 1039 // Not found in workdir; treat as a system command even if sysMode is false. 1040 } 1041 1042 absCmd, err = exec.LookPath(cmdArg) 1043 if err != nil { 1044 return "", httpError{http.StatusUnprocessableEntity, fmt.Errorf("command %q not found", cmdArg)} 1045 } 1046 return absCmd, nil 1047 } 1048 1049 // absExecDir returns the native, absolute path corresponding to the "dir" 1050 // argument passed to the "exec" endpoint. 1051 func absExecDir(dirArg string, sysMode bool, cmdDir string) (absDir string, err error) { 1052 if dirArg == "" { 1053 if sysMode { 1054 return *workDir, nil 1055 } 1056 return cmdDir, nil 1057 } 1058 1059 if filepath.IsAbs(dirArg) { 1060 return filepath.Clean(dirArg), nil 1061 } 1062 1063 relDir, err := nativeRelPath(dirArg) 1064 if err != nil { 1065 return "", badRequestf("invalid 'dir' parameter: %w", err) 1066 } 1067 return filepath.Join(*workDir, relDir), nil 1068 } 1069 1070 // needsBashWrapper reports whether the given command needs to 1071 // run through bash. 1072 func needsBashWrapper(cmd string) bool { 1073 if !strings.HasSuffix(cmd, ".bash") { 1074 return false 1075 } 1076 // The mobile platforms can't execute shell scripts directly. 1077 ismobile := runtime.GOOS == "android" || runtime.GOOS == "ios" 1078 return ismobile 1079 } 1080 1081 // pathNotExist reports whether path does not exist. 1082 func pathNotExist(path string) bool { 1083 _, err := os.Stat(path) 1084 return os.IsNotExist(err) 1085 } 1086 1087 // pathEnv returns a key=value string for the system path variable 1088 // (either PATH or path depending on the platform) with values 1089 // substituted from env: 1090 // - the string "$PATH" expands to the original value of the path variable 1091 // - the string "$WORKDIR" expands to the provided workDir 1092 // - the string "$EMPTY" expands to the empty string 1093 // 1094 // The "ok" result reports whether kv differs from the path found in env. 1095 func pathEnv(goos string, env, path []string, workDir string) (kv string, ok bool) { 1096 pathVar := "PATH" 1097 if goos == "plan9" { 1098 pathVar = "path" 1099 } 1100 1101 orig := envutil.Get(goos, env, pathVar) 1102 r := strings.NewReplacer( 1103 "$PATH", orig, 1104 "$WORKDIR", workDir, 1105 "$EMPTY", "", 1106 ) 1107 1108 // Apply substitutions to a copy of the path argument. 1109 subst := make([]string, 0, len(path)) 1110 for _, elem := range path { 1111 if s := r.Replace(elem); s != "" { 1112 subst = append(subst, s) 1113 } 1114 } 1115 kv = pathVar + "=" + strings.Join(subst, pathListSeparator(goos)) 1116 v := kv[len(pathVar)+1:] 1117 return kv, v != orig 1118 } 1119 1120 func pathListSeparator(goos string) string { 1121 switch goos { 1122 case "windows": 1123 return ";" 1124 case "plan9": 1125 return "\x00" 1126 default: 1127 return ":" 1128 } 1129 } 1130 1131 var ( 1132 defaultBootstrap string 1133 defaultBootstrapOnce sync.Once 1134 ) 1135 1136 func baseEnv(goarch string) []string { 1137 var env []string 1138 if runtime.GOOS == "windows" { 1139 env = windowsBaseEnv(goarch) 1140 } else { 1141 env = os.Environ() 1142 } 1143 1144 defaultBootstrapOnce.Do(func() { 1145 defaultBootstrap = filepath.Join(*workDir, "go1.4") 1146 1147 // Prefer buildlet process's inherited GOROOT_BOOTSTRAP if 1148 // there was one and our default doesn't exist. 1149 if v := os.Getenv("GOROOT_BOOTSTRAP"); v != "" && v != defaultBootstrap { 1150 if pathNotExist(defaultBootstrap) { 1151 defaultBootstrap = v 1152 } 1153 } 1154 }) 1155 env = append(env, "GOROOT_BOOTSTRAP="+defaultBootstrap) 1156 1157 return env 1158 } 1159 1160 func windowsBaseEnv(goarch string) (e []string) { 1161 e = append(e, "GOBUILDEXIT=1") // exit all.bat with completion status 1162 1163 for _, pair := range os.Environ() { 1164 const pathEq = "PATH=" 1165 if hasPrefixFold(pair, pathEq) { 1166 e = append(e, "PATH="+windowsPath(pair[len(pathEq):], goarch)) 1167 } else { 1168 e = append(e, pair) 1169 } 1170 } 1171 return e 1172 } 1173 1174 // hasPrefixFold is a case-insensitive strings.HasPrefix. 1175 func hasPrefixFold(s, prefix string) bool { 1176 return len(s) >= len(prefix) && strings.EqualFold(s[:len(prefix)], prefix) 1177 } 1178 1179 // windowsPath cleans the windows %PATH% environment. 1180 // is64Bit is whether this is a windows-amd64-* builder. 1181 // The PATH is assumed to be that of the image described in env/windows/README. 1182 func windowsPath(old string, goarch string) string { 1183 vv := filepath.SplitList(old) 1184 newPath := make([]string, 0, len(vv)) 1185 is64Bit := goarch != "386" 1186 1187 // for windows-buildlet-v2 images 1188 for _, v := range vv { 1189 // The base VM image has both the 32-bit and 64-bit gcc installed. 1190 // They're both in the environment, so scrub the one 1191 // we don't want (TDM-GCC-64 or TDM-GCC-32). 1192 // 1193 // This is not present in arm64 images. 1194 if strings.Contains(v, "TDM-GCC-") { 1195 gcc64 := strings.Contains(v, "TDM-GCC-64") 1196 if is64Bit != gcc64 { 1197 continue 1198 } 1199 } 1200 newPath = append(newPath, v) 1201 } 1202 1203 switch goarch { 1204 case "arm64": 1205 newPath = append(newPath, `C:\godep\llvm-aarch64\bin`) 1206 case "386": 1207 newPath = append(newPath, `C:\godep\gcc32\bin`) 1208 default: 1209 newPath = append(newPath, `C:\godep\gcc64\bin`) 1210 } 1211 1212 return strings.Join(newPath, string(filepath.ListSeparator)) 1213 } 1214 1215 func handleHalt(w http.ResponseWriter, r *http.Request) { 1216 if r.Method != "POST" { 1217 http.Error(w, "requires POST method", http.StatusBadRequest) 1218 return 1219 } 1220 1221 // Do the halt in 1 second, to give the HTTP response time to 1222 // complete. 1223 // 1224 // TODO(bradfitz): maybe prevent any (unlikely) future HTTP 1225 // requests from doing anything from this point on in the 1226 // remaining second. 1227 log.Printf("Halting in 1 second.") 1228 time.AfterFunc(1*time.Second, func() { 1229 teardownOnce() 1230 if *rebootOnHalt { 1231 doReboot() 1232 } 1233 if *haltEntireOS { 1234 doHalt() 1235 } 1236 log.Printf("Ending buildlet process due to halt.") 1237 os.Exit(0) 1238 return 1239 }) 1240 } 1241 1242 func doHalt() { 1243 log.Printf("Halting machine.") 1244 // Backup mechanism, if exec hangs for any reason: 1245 time.AfterFunc(5*time.Second, func() { os.Exit(0) }) 1246 var err error 1247 switch runtime.GOOS { 1248 case "openbsd": 1249 // Quick, no fs flush, and power down: 1250 err = exec.Command("halt", "-q", "-n", "-p").Run() 1251 case "freebsd": 1252 // Power off (-p), via halt (-o), now. 1253 err = exec.Command("shutdown", "-p", "-o", "now").Run() 1254 case "linux": 1255 // Don't sync (-n), force without shutdown (-f), and power off (-p). 1256 err = exec.Command("/bin/halt", "-n", "-f", "-p").Run() 1257 case "plan9": 1258 err = exec.Command("fshalt").Run() 1259 case "darwin": 1260 switch os.Getenv("GO_BUILDER_ENV") { 1261 case "macstadium_vm", "qemu_vm": 1262 // Fast, sloppy, unsafe, because we're never reusing this VM again. 1263 err = exec.Command("/usr/bin/sudo", "/sbin/halt", "-n", "-q", "-l").Run() 1264 default: 1265 err = errors.New("not respecting -halt flag on macOS in unknown environment") 1266 } 1267 case "windows": 1268 err = errors.New("not respecting -halt flag on Windows in unknown environment") 1269 if runtime.GOARCH == "arm64" { 1270 err = exec.Command("shutdown", "/s").Run() 1271 } 1272 default: 1273 err = errors.New("no system-specific halt command run; will just end buildlet process") 1274 } 1275 log.Printf("Shutdown: %v", err) 1276 log.Printf("Ending buildlet process post-halt") 1277 os.Exit(0) 1278 } 1279 1280 func doReboot() { 1281 log.Printf("Rebooting machine.") 1282 var err error 1283 switch runtime.GOOS { 1284 case "windows": 1285 err = exec.Command("shutdown", "/r").Run() 1286 default: 1287 err = exec.Command("reboot").Run() 1288 } 1289 log.Printf("Reboot: %v", err) 1290 log.Printf("Ending buildlet process post-halt") 1291 os.Exit(0) 1292 } 1293 1294 func handleRemoveAll(w http.ResponseWriter, r *http.Request) { 1295 if r.Method != "POST" { 1296 http.Error(w, "requires POST method", http.StatusBadRequest) 1297 return 1298 } 1299 if err := r.ParseForm(); err != nil { 1300 http.Error(w, err.Error(), http.StatusBadRequest) 1301 return 1302 } 1303 paths := r.Form["path"] 1304 if len(paths) == 0 { 1305 http.Error(w, "requires 'path' parameter", http.StatusBadRequest) 1306 return 1307 } 1308 for _, p := range paths { 1309 if _, err := nativeRelPath(p); err != nil { 1310 http.Error(w, "invalid 'path' parameter: "+err.Error(), http.StatusBadRequest) 1311 return 1312 } 1313 } 1314 for _, p := range paths { 1315 log.Printf("Removing %s", p) 1316 fullDir := filepath.Join(*workDir, filepath.FromSlash(p)) 1317 err := removeAllIncludingReadonly(fullDir) 1318 if p == "." && err != nil { 1319 // If workDir is a mountpoint and/or contains a binary 1320 // using it, we can get a "Device or resource busy" error. 1321 // See if it's now empty and ignore the error. 1322 if f, oerr := os.Open(*workDir); oerr == nil { 1323 if all, derr := f.Readdirnames(-1); derr == nil && len(all) == 0 { 1324 log.Printf("Ignoring fail of RemoveAll(.)") 1325 err = nil 1326 } else { 1327 log.Printf("Readdir = %q, %v", all, derr) 1328 } 1329 f.Close() 1330 } else { 1331 log.Printf("Failed to open workdir: %v", oerr) 1332 } 1333 } 1334 if err != nil { 1335 http.Error(w, err.Error(), http.StatusInternalServerError) 1336 return 1337 } 1338 } 1339 } 1340 1341 // mkdirAllWorkdirOr500 reports whether *workDir either exists or was created. 1342 // If it returns false, it also writes an HTTP 500 error to w. 1343 // This is used by callers to verify *workDir exists, even if it might've been 1344 // deleted previously. 1345 func mkdirAllWorkdirOr500(w http.ResponseWriter) bool { 1346 if err := os.MkdirAll(*workDir, 0755); err != nil { 1347 http.Error(w, err.Error(), http.StatusInternalServerError) 1348 return false 1349 } 1350 return true 1351 } 1352 1353 func handleWorkDir(w http.ResponseWriter, r *http.Request) { 1354 if r.Method != "GET" { 1355 http.Error(w, "requires GET method", http.StatusBadRequest) 1356 return 1357 } 1358 fmt.Fprint(w, *workDir) 1359 } 1360 1361 func handleStatus(w http.ResponseWriter, r *http.Request) { 1362 if r.Method != "GET" { 1363 http.Error(w, "requires GET method", http.StatusBadRequest) 1364 return 1365 } 1366 status := buildlet.Status{ 1367 Version: buildletVersion, 1368 } 1369 b, err := json.Marshal(status) 1370 if err != nil { 1371 http.Error(w, err.Error(), http.StatusInternalServerError) 1372 return 1373 } 1374 w.Header().Set("Content-Type", "application/json; charset=utf-8") 1375 w.Write(b) 1376 } 1377 1378 func handleLs(w http.ResponseWriter, r *http.Request) { 1379 if r.Method != "GET" { 1380 http.Error(w, "requires GET method", http.StatusBadRequest) 1381 return 1382 } 1383 1384 dir := r.FormValue("dir") 1385 if dir != "" { 1386 var err error 1387 dir, err = nativeRelPath(dir) 1388 if err != nil { 1389 http.Error(w, "invalid 'dir' parameter: "+err.Error(), http.StatusBadRequest) 1390 return 1391 } 1392 } 1393 1394 recursive, _ := strconv.ParseBool(r.FormValue("recursive")) 1395 digest, _ := strconv.ParseBool(r.FormValue("digest")) 1396 skip := r.Form["skip"] // '/'-separated relative dirs 1397 1398 if !mkdirAllWorkdirOr500(w) { 1399 return 1400 } 1401 1402 base := filepath.Join(*workDir, filepath.FromSlash(dir)) 1403 anyOutput := false 1404 err := filepath.Walk(base, func(path string, fi os.FileInfo, err error) error { 1405 if err != nil { 1406 return err 1407 } 1408 rel := strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(path, base)), "/") 1409 if rel == "" && fi.IsDir() { 1410 return nil 1411 } 1412 if fi.IsDir() { 1413 for _, v := range skip { 1414 if rel == v { 1415 return filepath.SkipDir 1416 } 1417 } 1418 } 1419 anyOutput = true 1420 fmt.Fprintf(w, "%s\t%s", fi.Mode(), rel) 1421 if fi.Mode().IsRegular() { 1422 fmt.Fprintf(w, "\t%d\t%s", fi.Size(), fi.ModTime().UTC().Format(time.RFC3339)) 1423 if digest { 1424 if sha1, err := fileSHA1(path); err != nil { 1425 return err 1426 } else { 1427 io.WriteString(w, "\t"+sha1) 1428 } 1429 } 1430 } else if fi.Mode().IsDir() { 1431 io.WriteString(w, "/") 1432 } 1433 io.WriteString(w, "\n") 1434 if fi.IsDir() && !recursive { 1435 return filepath.SkipDir 1436 } 1437 return nil 1438 }) 1439 if err != nil { 1440 log.Printf("Walk error: %v", err) 1441 if anyOutput { 1442 // Decent way to signal failure to the caller, since it'll break 1443 // the chunked response, rather than have a valid EOF. 1444 conn, _, _ := w.(http.Hijacker).Hijack() 1445 conn.Close() 1446 return 1447 } 1448 http.Error(w, "Walk error: "+err.Error(), 500) 1449 return 1450 } 1451 } 1452 1453 func useBuildletSSHServer() bool { 1454 return *swarmingBot && runtime.GOOS != "plan9" 1455 } 1456 1457 func handleConnectSSH(w http.ResponseWriter, r *http.Request) { 1458 if r.Method != "POST" { 1459 http.Error(w, "requires POST method", http.StatusBadRequest) 1460 return 1461 } 1462 if r.ContentLength != 0 { 1463 http.Error(w, "requires zero Content-Length", http.StatusBadRequest) 1464 return 1465 } 1466 sshUser := r.Header.Get("X-Go-Ssh-User") 1467 authKey := r.Header.Get("X-Go-Authorized-Key") 1468 if sshUser != "" && authKey != "" { 1469 if err := appendSSHAuthorizedKey(sshUser, authKey); err != nil { 1470 http.Error(w, "adding ssh authorized key: "+err.Error(), http.StatusBadRequest) 1471 return 1472 } 1473 } 1474 1475 sshServerOnce.Do(startSSHServer) 1476 1477 var sshConn net.Conn 1478 var err error 1479 1480 // In theory we shouldn't need retries here at all, but the 1481 // startSSHServerLinux's use of sshd -D is kinda sketchy and 1482 // restarts the process whenever we connect to it, so in case 1483 // it's just down between restarts, try a few times. 5 tries 1484 // and 5 seconds seems plenty. 1485 const maxTries = 5 1486 for try := 1; try <= maxTries; try++ { 1487 sshConn, err = net.Dial("tcp", "localhost:"+sshPort()) 1488 if err == nil { 1489 break 1490 } 1491 if try == maxTries { 1492 http.Error(w, err.Error(), http.StatusBadGateway) 1493 return 1494 } 1495 time.Sleep(time.Second) 1496 } 1497 defer sshConn.Close() 1498 hj, ok := w.(http.Hijacker) 1499 if !ok { 1500 log.Printf("conn can't hijack for ssh proxy; HTTP/2 enabled by default?") 1501 http.Error(w, "conn can't hijack", http.StatusInternalServerError) 1502 return 1503 } 1504 conn, _, err := hj.Hijack() 1505 if err != nil { 1506 log.Printf("ssh hijack error: %v", err) 1507 http.Error(w, "ssh hijack error: "+err.Error(), http.StatusInternalServerError) 1508 return 1509 } 1510 defer conn.Close() 1511 fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: ssh\r\nConnection: Upgrade\r\n\r\n") 1512 errc := make(chan error, 1) 1513 go func() { 1514 _, err := io.Copy(sshConn, conn) 1515 errc <- err 1516 }() 1517 go func() { 1518 _, err := io.Copy(conn, sshConn) 1519 errc <- err 1520 }() 1521 <-errc 1522 } 1523 1524 var buildletSSHServer *ssh.Server 1525 var buldletAuthKeys []byte 1526 1527 // sshPort returns the port to use for the local SSH server. 1528 func sshPort() string { 1529 // use port 2222 regardless of where the buildlet is running. 1530 if useBuildletSSHServer() { 1531 return "2222" 1532 } 1533 1534 // runningInCOS is whether we're running under GCE's Container-Optimized OS (COS). 1535 const runningInCOS = runtime.GOOS == "linux" && runtime.GOARCH == "amd64" 1536 1537 if runningInCOS { 1538 // If running in COS, we can't use port 22, as the system's sshd is already using it. 1539 // Our container runs in the system network namespace, not isolated as is typical 1540 // in Docker or Kubernetes. So use another high port. See https://golang.org/issue/26969. 1541 return "2200" 1542 } 1543 return "22" 1544 } 1545 1546 var sshServerOnce sync.Once 1547 1548 // startSSHServer starts an SSH server. 1549 func startSSHServer() { 1550 if useBuildletSSHServer() { 1551 startSSHServerSwarming() 1552 return 1553 } 1554 if inLinuxContainer() { 1555 startSSHServerLinux() 1556 return 1557 } 1558 if runtime.GOOS == "netbsd" { 1559 startSSHServerNetBSD() 1560 return 1561 } 1562 1563 log.Printf("start ssh server: don't know how to start SSH server on this host type") 1564 } 1565 1566 // inLinuxContainer reports whether it looks like we're on Linux running inside a container. 1567 func inLinuxContainer() bool { 1568 if runtime.GOOS != "linux" { 1569 return false 1570 } 1571 if numProcs() >= 4 { 1572 // There should 1 process running (this buildlet 1573 // binary) if we're in Docker. Maybe 2 if something 1574 // else is happening. But if there are 4 or more, 1575 // we'll be paranoid and assuming we're running on a 1576 // user or host system and don't want to start an ssh 1577 // server. 1578 return false 1579 } 1580 // TODO: use a more explicit env variable or on-disk signal 1581 // that we're in a Go buildlet Docker image. But for now, this 1582 // seems to be consistently true: 1583 fi, err := os.Stat("/usr/local/bin/stage0") 1584 return err == nil && fi.Mode().IsRegular() 1585 } 1586 1587 // startSSHServerLinux starts an SSH server on a Linux system. 1588 func startSSHServerLinux() { 1589 log.Printf("start ssh server for linux") 1590 1591 // First, create the privsep directory, otherwise we get a successful cmd.Start, 1592 // but this error message and then an exit: 1593 // Missing privilege separation directory: /var/run/sshd 1594 if err := os.MkdirAll("/var/run/sshd", 0700); err != nil { 1595 log.Printf("creating /var/run/sshd: %v", err) 1596 return 1597 } 1598 1599 // The AWS Docker images don't have ssh host keys in 1600 // their image, at least as of 2017-07-23. So make them first. 1601 // These are the types sshd -D complains about currently. 1602 if runtime.GOARCH == "arm" { 1603 for _, keyType := range []string{"rsa", "dsa", "ed25519", "ecdsa"} { 1604 file := "/etc/ssh/ssh_host_" + keyType + "_key" 1605 if _, err := os.Stat(file); err == nil { 1606 continue 1607 } 1608 out, err := exec.Command("/usr/bin/ssh-keygen", "-f", file, "-N", "", "-t", keyType).CombinedOutput() 1609 log.Printf("ssh-keygen of type %s: err=%v, %s\n", keyType, err, out) 1610 } 1611 } 1612 1613 go func() { 1614 for { 1615 // TODO: using sshd -D isn't great as it only 1616 // handles a single connection and exits. 1617 // Maybe run in sshd -i (inetd) mode instead, 1618 // and hook that up to the buildlet directly? 1619 t0 := time.Now() 1620 cmd := exec.Command("/usr/sbin/sshd", "-D", "-p", sshPort(), "-d", "-d") 1621 cmd.Stderr = os.Stderr 1622 err := cmd.Start() 1623 if err != nil { 1624 log.Printf("starting sshd: %v", err) 1625 return 1626 } 1627 log.Printf("sshd started.") 1628 log.Printf("sshd exited: %v; restarting", cmd.Wait()) 1629 if d := time.Since(t0); d < time.Second { 1630 time.Sleep(time.Second - d) 1631 } 1632 } 1633 }() 1634 waitLocalSSH() 1635 } 1636 1637 func startSSHServerNetBSD() { 1638 cmd := exec.Command("/etc/rc.d/sshd", "start") 1639 err := cmd.Start() 1640 if err != nil { 1641 log.Printf("starting sshd: %v", err) 1642 return 1643 } 1644 log.Printf("sshd started.") 1645 waitLocalSSH() 1646 } 1647 1648 // waitLocalSSH waits for sshd to start accepting connections. 1649 func waitLocalSSH() { 1650 for i := 0; i < 40; i++ { 1651 time.Sleep(10 * time.Millisecond * time.Duration(i+1)) 1652 c, err := net.Dial("tcp", "localhost:"+sshPort()) 1653 if err == nil { 1654 c.Close() 1655 log.Printf("sshd connected.") 1656 return 1657 } 1658 } 1659 log.Printf("timeout waiting for sshd to come up") 1660 } 1661 1662 func numProcs() int { 1663 n := 0 1664 fis, _ := os.ReadDir("/proc") 1665 for _, fi := range fis { 1666 if _, err := strconv.Atoi(fi.Name()); err == nil { 1667 n++ 1668 } 1669 } 1670 return n 1671 } 1672 1673 func fileSHA1(path string) (string, error) { 1674 f, err := os.Open(path) 1675 if err != nil { 1676 return "", err 1677 } 1678 defer f.Close() 1679 s1 := sha1.New() 1680 if _, err := io.Copy(s1, f); err != nil { 1681 return "", err 1682 } 1683 return fmt.Sprintf("%x", s1.Sum(nil)), nil 1684 } 1685 1686 // nativeRelPath verifies that p is a non-empty relative path 1687 // using either slashes or the buildlet's native path separator, 1688 // and returns it canonicalized to the native path separator. 1689 func nativeRelPath(p string) (string, error) { 1690 if p == "" { 1691 return "", errors.New("path not provided") 1692 } 1693 1694 if filepath.Separator != '/' && strings.Contains(p, string(filepath.Separator)) { 1695 clean := filepath.Clean(p) 1696 if filepath.IsAbs(clean) { 1697 return "", fmt.Errorf("path %q is not relative", p) 1698 } 1699 if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) { 1700 return "", fmt.Errorf("path %q refers to a parent directory", p) 1701 } 1702 if strings.HasPrefix(p, string(filepath.Separator)) || filepath.VolumeName(clean) != "" { 1703 // On Windows, this catches semi-relative paths like "C:" (meaning “the 1704 // current working directory on volume C:”) and "\windows" (meaning “the 1705 // windows subdirectory of the current drive letter”). 1706 return "", fmt.Errorf("path %q is relative to volume", p) 1707 } 1708 return p, nil 1709 } 1710 1711 clean := path.Clean(p) 1712 if path.IsAbs(clean) { 1713 return "", fmt.Errorf("path %q is not relative", p) 1714 } 1715 if clean == ".." || strings.HasPrefix(clean, "../") { 1716 return "", fmt.Errorf("path %q refers to a parent directory", p) 1717 } 1718 canon := filepath.FromSlash(p) 1719 if filepath.VolumeName(canon) != "" { 1720 return "", fmt.Errorf("path %q begins with a native volume name", p) 1721 } 1722 return canon, nil 1723 } 1724 1725 // An httpError wraps an error with a corresponding HTTP status code. 1726 type httpError struct { 1727 statusCode int 1728 err error 1729 } 1730 1731 func (he httpError) Error() string { return he.err.Error() } 1732 func (he httpError) Unwrap() error { return he.err } 1733 func (he httpError) httpStatus() int { return he.statusCode } 1734 1735 // badRequestf returns an httpError with status 400 and an error constructed by 1736 // formatting the given arguments. 1737 func badRequestf(format string, args ...interface{}) error { 1738 return httpError{http.StatusBadRequest, fmt.Errorf(format, args...)} 1739 } 1740 1741 // httpStatus returns the httpStatus of err if it is or wraps an httpError, 1742 // or StatusInternalServerError otherwise. 1743 func httpStatus(err error) int { 1744 var he httpError 1745 if !errors.As(err, &he) { 1746 return http.StatusInternalServerError 1747 } 1748 return he.statusCode 1749 } 1750 1751 // requirePasswordHandler is an http.Handler auth wrapper that enforces an 1752 // HTTP Basic password. The username is ignored. 1753 type requirePasswordHandler struct { 1754 h http.Handler 1755 password string // empty means no password 1756 } 1757 1758 func (h requirePasswordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 1759 _, gotPass, _ := r.BasicAuth() 1760 if h.password != "" && h.password != gotPass { 1761 http.Error(w, "invalid password", http.StatusForbidden) 1762 return 1763 } 1764 h.h.ServeHTTP(w, r) 1765 } 1766 1767 // gcePlan9LogWriter truncates log writes to 128 bytes, 1768 // to work around a GCE serial port bug affecting Plan 9. 1769 type gcePlan9LogWriter struct { 1770 w io.Writer 1771 buf []byte 1772 } 1773 1774 func (pw *gcePlan9LogWriter) Write(p []byte) (n int, err error) { 1775 const max = 128 - len("\n\x00") 1776 if len(p) < max { 1777 return pw.w.Write(p) 1778 } 1779 if pw.buf == nil { 1780 pw.buf = make([]byte, max+1) 1781 } 1782 n = copy(pw.buf[:max], p) 1783 pw.buf[n] = '\n' 1784 return pw.w.Write(pw.buf[:n+1]) 1785 } 1786 1787 var killProcessTree = killProcessTreeUnix 1788 1789 func killProcessTreeUnix(p *os.Process) error { 1790 return p.Kill() 1791 } 1792 1793 func vmwareGetInfo(key string) string { 1794 cmd := exec.Command("/Library/Application Support/VMware Tools/vmware-tools-daemon", 1795 "--cmd", 1796 "info-get "+key) 1797 var stdout, stderr bytes.Buffer 1798 cmd.Stdout = &stdout 1799 cmd.Stderr = &stderr 1800 err := cmd.Run() 1801 if err != nil { 1802 if strings.Contains(stderr.String(), "No value found") { 1803 return "" 1804 } 1805 log.Fatalf("Error running vmware-tools-daemon --cmd 'info-get %s': %v, %s\n%s", key, err, stderr.Bytes(), stdout.Bytes()) 1806 } 1807 return strings.TrimSpace(stdout.String()) 1808 } 1809 1810 func makeBSDFilesystemFast() { 1811 if !metadata.OnGCE() { 1812 log.Printf("Not on GCE; not remounting root filesystem.") 1813 return 1814 } 1815 btype, err := metadata.InstanceAttributeValue("buildlet-host-type") 1816 if _, ok := err.(metadata.NotDefinedError); ok && len(btype) == 0 { 1817 log.Printf("Not remounting root filesystem due to missing buildlet-host-type metadata.") 1818 return 1819 } 1820 if err != nil { 1821 log.Printf("Not remounting root filesystem due to failure getting builder type instance metadata: %v", err) 1822 return 1823 } 1824 // Tested on OpenBSD, FreeBSD, and NetBSD: 1825 out, err := exec.Command("/sbin/mount", "-u", "-o", "async,noatime", "/").CombinedOutput() 1826 if err != nil { 1827 log.Printf("Warning: failed to remount %s root filesystem with async,noatime: %v, %s", runtime.GOOS, err, out) 1828 return 1829 } 1830 log.Printf("Remounted / with async,noatime.") 1831 } 1832 1833 func appendSSHAuthorizedKey(sshUser, authKey string) error { 1834 if *swarmingBot { 1835 buldletAuthKeys = append(buldletAuthKeys, []byte(fmt.Sprintf("%s\n%s\n", sshUser, authKey))...) 1836 return nil 1837 } 1838 var homeRoot string 1839 switch runtime.GOOS { 1840 case "darwin": 1841 homeRoot = "/Users" 1842 case "plan9": 1843 return fmt.Errorf("ssh not supported on %v", runtime.GOOS) 1844 case "windows": 1845 homeRoot = `C:\Users` 1846 default: 1847 homeRoot = "/home" 1848 if runtime.GOOS == "freebsd" { 1849 if fi, err := os.Stat("/usr/home/" + sshUser); err == nil && fi.IsDir() { 1850 homeRoot = "/usr/home" 1851 } 1852 } 1853 if sshUser == "root" { 1854 homeRoot = "/" 1855 } 1856 } 1857 sshDir := filepath.Join(homeRoot, sshUser, ".ssh") 1858 if err := os.MkdirAll(sshDir, 0700); err != nil { 1859 return err 1860 } 1861 if err := os.Chmod(sshDir, 0700); err != nil { 1862 return err 1863 } 1864 authFile := filepath.Join(sshDir, "authorized_keys") 1865 exist, err := os.ReadFile(authFile) 1866 if err != nil && !os.IsNotExist(err) { 1867 return err 1868 } 1869 if strings.Contains(string(exist), authKey) { 1870 return nil 1871 } 1872 f, err := os.OpenFile(authFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) 1873 if err != nil { 1874 return err 1875 } 1876 if _, err := fmt.Fprintf(f, "%s\n", authKey); err != nil { 1877 f.Close() 1878 return err 1879 } 1880 if err := f.Close(); err != nil { 1881 return err 1882 } 1883 if runtime.GOOS == "freebsd" { 1884 exec.Command("/usr/sbin/chown", "-R", sshUser, sshDir).Run() 1885 } 1886 if runtime.GOOS == "windows" { 1887 if res, err := exec.Command("icacls.exe", authFile, "/grant", `NT SERVICE\sshd:(R)`).CombinedOutput(); err != nil { 1888 return fmt.Errorf("setting permissions on authorized_keys with: %v\n%s", err, res) 1889 } 1890 } 1891 return nil 1892 } 1893 1894 // setWorkdirToTmpfs sets the *workDir (--workdir) flag to /workdir 1895 // if the flag is empty and /workdir is a tmpfs mount, as it is on the various 1896 // hosts that use rundockerbuildlet. 1897 // 1898 // It is set non-nil on operating systems where the functionality is 1899 // needed & available. Currently we only use it on Linux. 1900 var setWorkdirToTmpfs func() 1901 1902 func initBaseUnixEnv() { 1903 if os.Getenv("USER") == "" { 1904 os.Setenv("USER", "root") 1905 } 1906 if os.Getenv("HOME") == "" { 1907 os.Setenv("HOME", "/root") 1908 } 1909 } 1910 1911 // removeAllAndMkdir calls removeAllIncludingReadonly and then os.Mkdir on the given 1912 // dir, failing the process if either step fails. 1913 func removeAllAndMkdir(dir string) { 1914 if err := removeAllIncludingReadonly(dir); err != nil { 1915 log.Fatal(err) 1916 } 1917 if err := os.Mkdir(dir, 0755); err != nil { 1918 log.Fatal(err) 1919 } 1920 } 1921 1922 // removeAllIncludingReadonly is like os.RemoveAll except that it'll 1923 // also try to change permissions to work around permission errors 1924 // when deleting. 1925 func removeAllIncludingReadonly(dir string) error { 1926 err := os.RemoveAll(dir) 1927 if err == nil || !os.IsPermission(err) { 1928 return err 1929 } 1930 // Make a best effort (ignoring errors) attempt to make all 1931 // files and directories writable before we try to delete them 1932 // all again. 1933 filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { 1934 const ownerWritable = 0200 1935 if err != nil || fi.Mode().Perm()&ownerWritable != 0 { 1936 return nil 1937 } 1938 os.Chmod(path, fi.Mode().Perm()|ownerWritable) 1939 return nil 1940 }) 1941 return os.RemoveAll(dir) 1942 } 1943 1944 var ( 1945 androidEmuDead = make(chan error) // closed on death 1946 androidEmuErr error // set prior to channel close 1947 ) 1948 1949 func startAndroidEmulator() { 1950 cmd := exec.Command("/android/sdk/emulator/emulator", 1951 "@android-avd", 1952 "-no-audio", 1953 "-no-window", 1954 "-no-boot-anim", 1955 "-no-snapshot-save", 1956 "-wipe-data", // required to prevent a hang with -no-window when recovering from a snapshot? 1957 ) 1958 log.Printf("running Android emulator: %v", cmd.Args) 1959 cmd.Stdout = os.Stdout 1960 cmd.Stderr = os.Stderr 1961 if err := cmd.Start(); err != nil { 1962 log.Fatalf("failed to start Android emulator: %v", err) 1963 } 1964 go func() { 1965 err := cmd.Wait() 1966 if err == nil { 1967 err = errors.New("exited without error") 1968 } 1969 androidEmuErr = err 1970 close(androidEmuDead) 1971 }() 1972 } 1973 1974 // checkAndroidEmulator returns an error if this machine is an Android builder 1975 // and the Android emulator process has exited. 1976 func checkAndroidEmulator() error { 1977 select { 1978 case <-androidEmuDead: 1979 return androidEmuErr 1980 default: 1981 return nil 1982 } 1983 } 1984 1985 var disableNetOnce sync.Once 1986 1987 func disableOutboundNetwork() { 1988 if runtime.GOOS != "linux" { 1989 return 1990 } 1991 disableNetOnce.Do(disableOutboundNetworkLinux) 1992 } 1993 1994 func disableOutboundNetworkLinux() { 1995 iptables, err := exec.LookPath("iptables-legacy") 1996 if err != nil { 1997 // Some older distributions, such as Debian Stretch, don't yet have nftables, 1998 // so "iptables" gets us the legacy version whose rules syntax is used below. 1999 iptables, err = exec.LookPath("iptables") 2000 if err != nil { 2001 log.Println("disableOutboundNetworkLinux failed to find iptables:", err) 2002 return 2003 } 2004 } 2005 runOrLog(exec.Command(iptables, "-I", "OUTPUT", "2", "-m", "state", "--state", "NEW", "-d", "10.0.0.0/8", "-p", "tcp", "-j", "ACCEPT")) 2006 runOrLog(exec.Command(iptables, "-I", "OUTPUT", "3", "-m", "state", "--state", "NEW", "-p", "tcp", "--dport", "443", "-j", "REJECT", "--reject-with", "icmp-host-prohibited")) 2007 runOrLog(exec.Command(iptables, "-I", "OUTPUT", "3", "-m", "state", "--state", "NEW", "-p", "tcp", "--dport", "80", "-j", "REJECT", "--reject-with", "icmp-host-prohibited")) 2008 runOrLog(exec.Command(iptables, "-I", "OUTPUT", "3", "-m", "state", "--state", "NEW", "-p", "tcp", "--dport", "22", "-j", "REJECT", "--reject-with", "icmp-host-prohibited")) 2009 } 2010 2011 func runOrLog(cmd *exec.Cmd) { 2012 out, err := cmd.CombinedOutput() 2013 if err != nil { 2014 log.Printf("failed to run %s: %v, %s", cmd.Args, err, out) 2015 } 2016 } 2017 2018 // handleHealthz always returns 200 OK. 2019 func handleHealthz(w http.ResponseWriter, _ *http.Request) { 2020 fmt.Fprintln(w, "ok") 2021 } 2022 2023 // serveReverseHealth serves /healthz requests on healthAddr for 2024 // reverse buildlets. 2025 // 2026 // This can be used to monitor the health of guest buildlets, such as 2027 // the Windows ARM64 qemu guest buildlet. 2028 func serveReverseHealth() error { 2029 m := &http.ServeMux{} 2030 m.HandleFunc("/healthz", handleHealthz) 2031 return http.ListenAndServe(*healthAddr, m) 2032 } 2033 2034 func shell() string { 2035 switch runtime.GOOS { 2036 case "linux": 2037 return "bash" 2038 case "windows": 2039 return `C:\Windows\System32\cmd.exe` 2040 default: 2041 return os.Getenv("SHELL") 2042 } 2043 }