blob: a2d911c3392078bfe0e5e8727fbb8370464d0fa4 [file] [log] [blame]
Brad Fitzpatrick95713102014-12-29 12:29:13 -08001// 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
Brad Fitzpatrick5a3fc032015-01-03 20:12:38 -08005// +build buildlet
6
Brad Fitzpatrick95713102014-12-29 12:29:13 -08007// The buildlet is an HTTP server that untars content to disk and runs
8// commands it has untarred, streaming their output back over HTTP.
9// It is part of Go's continuous build system.
10//
11// This program intentionally allows remote code execution, and
12// provides no security of its own. It is assumed that any user uses
13// it with an appropriately-configured firewall between their VM
14// instances.
15package main // import "golang.org/x/tools/dashboard/buildlet"
16
Brad Fitzpatrick95713102014-12-29 12:29:13 -080017import (
18 "archive/tar"
19 "compress/gzip"
Brad Fitzpatrick6078e102015-01-13 12:41:09 -080020 "crypto/tls"
Brad Fitzpatrick95713102014-12-29 12:29:13 -080021 "flag"
22 "fmt"
23 "io"
24 "io/ioutil"
25 "log"
Brad Fitzpatrick6078e102015-01-13 12:41:09 -080026 "net"
Brad Fitzpatrick95713102014-12-29 12:29:13 -080027 "net/http"
28 "os"
29 "os/exec"
30 "path/filepath"
31 "runtime"
32 "strings"
33 "sync"
Brad Fitzpatrick6078e102015-01-13 12:41:09 -080034 "time"
Brad Fitzpatrick5a3fc032015-01-03 20:12:38 -080035
36 "google.golang.org/cloud/compute/metadata"
Brad Fitzpatrick95713102014-12-29 12:29:13 -080037)
38
39var (
40 scratchDir = flag.String("scratchdir", "", "Temporary directory to use. The contents of this directory may be deleted at any time. If empty, TempDir is used to create one.")
41 listenAddr = flag.String("listen", defaultListenAddr(), "address to listen on. Warning: this service is inherently insecure and offers no protection of its own. Do not expose this port to the world.")
42)
43
44func defaultListenAddr() string {
Brad Fitzpatrick30ec0612015-01-07 14:08:28 -080045 if runtime.GOOS == "darwin" {
46 // Darwin will never run on GCE, so let's always
47 // listen on a high port (so we don't need to be
48 // root).
49 return ":5936"
50 }
Brad Fitzpatrick6078e102015-01-13 12:41:09 -080051 if !metadata.OnGCE() {
52 return "localhost:5936"
Brad Fitzpatrick95713102014-12-29 12:29:13 -080053 }
Brad Fitzpatrick6078e102015-01-13 12:41:09 -080054 // In production, default to port 80 or 443, depending on
55 // whether TLS is configured.
56 if metadataValue("tls-cert") != "" {
57 return ":443"
58 }
59 return ":80"
Brad Fitzpatrick95713102014-12-29 12:29:13 -080060}
61
62func main() {
63 flag.Parse()
Brad Fitzpatrick5a3fc032015-01-03 20:12:38 -080064 if !metadata.OnGCE() && !strings.HasPrefix(*listenAddr, "localhost:") {
Brad Fitzpatrick95713102014-12-29 12:29:13 -080065 log.Printf("** WARNING *** This server is unsafe and offers no security. Be careful.")
66 }
Brad Fitzpatrick04319632015-01-07 20:56:15 -080067 if runtime.GOOS == "plan9" {
68 // Plan 9 is too slow on GCE, so stop running run.rc after the basics.
69 // See https://golang.org/cl/2522 and https://golang.org/issue/9491
70 // TODO(bradfitz): once the buildlet has environment variable support,
71 // the coordinator can send this in, and this variable can be part of
72 // the build configuration struct instead of hard-coded here.
73 // But no need for environment variables quite yet.
74 os.Setenv("GOTESTONLY", "std")
75 }
76
Brad Fitzpatrick95713102014-12-29 12:29:13 -080077 if *scratchDir == "" {
78 dir, err := ioutil.TempDir("", "buildlet-scatch")
79 if err != nil {
80 log.Fatalf("error creating scratchdir with ioutil.TempDir: %v", err)
81 }
82 *scratchDir = dir
83 }
84 if _, err := os.Lstat(*scratchDir); err != nil {
85 log.Fatalf("invalid --scratchdir %q: %v", *scratchDir, err)
86 }
Brad Fitzpatrick95713102014-12-29 12:29:13 -080087 http.HandleFunc("/", handleRoot)
Brad Fitzpatrick6078e102015-01-13 12:41:09 -080088
89 password := metadataValue("password")
90 http.Handle("/writetgz", requirePassword{http.HandlerFunc(handleWriteTGZ), password})
91 http.Handle("/exec", requirePassword{http.HandlerFunc(handleExec), password})
Brad Fitzpatrick95713102014-12-29 12:29:13 -080092 // TODO: removeall
Brad Fitzpatrick6078e102015-01-13 12:41:09 -080093
94 tlsCert, tlsKey := metadataValue("tls-cert"), metadataValue("tls-key")
95 if (tlsCert == "") != (tlsKey == "") {
96 log.Fatalf("tls-cert and tls-key must both be supplied, or neither.")
97 }
98
Brad Fitzpatrick95713102014-12-29 12:29:13 -080099 log.Printf("Listening on %s ...", *listenAddr)
Brad Fitzpatrick6078e102015-01-13 12:41:09 -0800100 ln, err := net.Listen("tcp", *listenAddr)
101 if err != nil {
102 log.Fatalf("Failed to listen on %s: %v", *listenAddr, err)
103 }
104 ln = tcpKeepAliveListener{ln.(*net.TCPListener)}
105
106 var srv http.Server
107 if tlsCert != "" {
108 cert, err := tls.X509KeyPair([]byte(tlsCert), []byte(tlsKey))
109 if err != nil {
110 log.Fatalf("TLS cert error: %v", err)
111 }
112 tlsConf := &tls.Config{
113 Certificates: []tls.Certificate{cert},
114 }
115 ln = tls.NewListener(ln, tlsConf)
116 }
117
118 log.Fatalf("Serve: %v", srv.Serve(ln))
119}
120
121// metadataValue returns the GCE metadata instance value for the given key.
122//
123// If not running on GCE, it falls back to using environment variables
124// for local development.
125func metadataValue(key string) string {
126 // The common case:
127 if metadata.OnGCE() {
128 v, err := metadata.InstanceAttributeValue(key)
129 if err != nil {
130 log.Fatalf("metadata.InstanceAttributeValue(%q): %v", key, err)
131 }
132 return v
133 }
134
135 // Else let developers use environment variables to fake
136 // metadata keys, for local testing.
137 envKey := "GCEMETA_" + strings.Replace(key, "-", "_", -1)
138 v := os.Getenv(envKey)
139 // Respect curl-style '@' prefix to mean the rest is a filename.
140 if strings.HasPrefix(v, "@") {
141 slurp, err := ioutil.ReadFile(v[1:])
142 if err != nil {
143 log.Fatalf("Error reading file for GCEMETA_%v: %v", key, err)
144 }
145 return string(slurp)
146 }
147 if v == "" {
148 log.Printf("Warning: not running on GCE, and no %v environment variable defined", envKey)
149 }
150 return v
151}
152
153// tcpKeepAliveListener is a net.Listener that sets TCP keep-alive
154// timeouts on accepted connections.
155type tcpKeepAliveListener struct {
156 *net.TCPListener
157}
158
159func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
160 tc, err := ln.AcceptTCP()
161 if err != nil {
162 return
163 }
164 tc.SetKeepAlive(true)
165 tc.SetKeepAlivePeriod(3 * time.Minute)
166 return tc, nil
Brad Fitzpatrick95713102014-12-29 12:29:13 -0800167}
168
169func handleRoot(w http.ResponseWriter, r *http.Request) {
Brad Fitzpatrick6078e102015-01-13 12:41:09 -0800170 fmt.Fprintf(w, "buildlet running on %s-%s\n", runtime.GOOS, runtime.GOARCH)
Brad Fitzpatrick95713102014-12-29 12:29:13 -0800171}
172
173func handleWriteTGZ(w http.ResponseWriter, r *http.Request) {
174 if r.Method != "PUT" {
175 http.Error(w, "requires PUT method", http.StatusBadRequest)
176 return
177 }
178 err := untar(r.Body, *scratchDir)
179 if err != nil {
180 status := http.StatusInternalServerError
181 if he, ok := err.(httpStatuser); ok {
182 status = he.httpStatus()
183 }
184 http.Error(w, err.Error(), status)
185 return
186 }
187 io.WriteString(w, "OK")
188}
189
190// untar reads the gzip-compressed tar file from r and writes it into dir.
191func untar(r io.Reader, dir string) error {
192 zr, err := gzip.NewReader(r)
193 if err != nil {
194 return badRequest("requires gzip-compressed body: " + err.Error())
195 }
196 tr := tar.NewReader(zr)
197 for {
198 f, err := tr.Next()
199 if err == io.EOF {
200 break
201 }
202 if err != nil {
203 log.Printf("tar reading error: %v", err)
204 return badRequest("tar error: " + err.Error())
205 }
206 if !validRelPath(f.Name) {
207 return badRequest(fmt.Sprintf("tar file contained invalid name %q", f.Name))
208 }
209 rel := filepath.FromSlash(f.Name)
210 abs := filepath.Join(dir, rel)
211
212 fi := f.FileInfo()
213 mode := fi.Mode()
214 switch {
215 case mode.IsRegular():
216 // Make the directory. This is redundant because it should
217 // already be made by a directory entry in the tar
218 // beforehand. Thus, don't check for errors; the next
219 // write will fail with the same error.
220 os.MkdirAll(filepath.Dir(abs), 0755)
221 wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
222 if err != nil {
223 return err
224 }
225 n, err := io.Copy(wf, tr)
226 if closeErr := wf.Close(); closeErr != nil && err == nil {
227 err = closeErr
228 }
229 if err != nil {
230 return fmt.Errorf("error writing to %s: %v", abs, err)
231 }
232 if n != f.Size {
233 return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
234 }
235 log.Printf("wrote %s", abs)
236 case mode.IsDir():
237 if err := os.MkdirAll(abs, 0755); err != nil {
238 return err
239 }
240 default:
241 return badRequest(fmt.Sprintf("tar file entry %s contained unsupported file type %v", f.Name, mode))
242 }
243 }
244 return nil
245}
246
Brad Fitzpatrick411cd8a2014-12-29 20:51:32 -0800247// Process-State is an HTTP Trailer set in the /exec handler to "ok"
248// on success, or os.ProcessState.String() on failure.
249const hdrProcessState = "Process-State"
250
Brad Fitzpatrick95713102014-12-29 12:29:13 -0800251func handleExec(w http.ResponseWriter, r *http.Request) {
252 if r.Method != "POST" {
253 http.Error(w, "requires POST method", http.StatusBadRequest)
254 return
255 }
Brad Fitzpatrick411cd8a2014-12-29 20:51:32 -0800256 if r.ProtoMajor*10+r.ProtoMinor < 11 {
257 // We need trailers, only available in HTTP/1.1 or HTTP/2.
258 http.Error(w, "HTTP/1.1 or higher required", http.StatusBadRequest)
259 return
260 }
261
262 w.Header().Set("Trailer", hdrProcessState) // declare it so we can set it
263
Brad Fitzpatrick95713102014-12-29 12:29:13 -0800264 cmdPath := r.FormValue("cmd") // required
265 if !validRelPath(cmdPath) {
266 http.Error(w, "requires 'cmd' parameter", http.StatusBadRequest)
267 return
268 }
Brad Fitzpatrick411cd8a2014-12-29 20:51:32 -0800269 if f, ok := w.(http.Flusher); ok {
270 f.Flush()
271 }
272
Brad Fitzpatrick95713102014-12-29 12:29:13 -0800273 absCmd := filepath.Join(*scratchDir, filepath.FromSlash(cmdPath))
274 cmd := exec.Command(absCmd, r.PostForm["cmdArg"]...)
275 cmd.Dir = filepath.Dir(absCmd)
276 cmdOutput := &flushWriter{w: w}
277 cmd.Stdout = cmdOutput
278 cmd.Stderr = cmdOutput
279 err := cmd.Run()
Brad Fitzpatrick411cd8a2014-12-29 20:51:32 -0800280 state := "ok"
281 if err != nil {
282 if ps := cmd.ProcessState; ps != nil {
283 state = ps.String()
284 } else {
285 state = err.Error()
286 }
287 }
288 w.Header().Set(hdrProcessState, state)
289 log.Printf("Run = %s", state)
Brad Fitzpatrick95713102014-12-29 12:29:13 -0800290}
291
292// flushWriter is an io.Writer wrapper that writes to w and
293// Flushes the output immediately, if w is an http.Flusher.
294type flushWriter struct {
295 mu sync.Mutex
296 w http.ResponseWriter
297}
298
299func (hw *flushWriter) Write(p []byte) (n int, err error) {
300 hw.mu.Lock()
301 defer hw.mu.Unlock()
302 n, err = hw.w.Write(p)
303 if f, ok := hw.w.(http.Flusher); ok {
304 f.Flush()
305 }
306 return
307}
308
309func validRelPath(p string) bool {
310 if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
311 return false
312 }
313 return true
314}
315
316type httpStatuser interface {
317 error
318 httpStatus() int
319}
320
321type httpError struct {
322 statusCode int
323 msg string
324}
325
326func (he httpError) Error() string { return he.msg }
327func (he httpError) httpStatus() int { return he.statusCode }
328
329func badRequest(msg string) error {
330 return httpError{http.StatusBadRequest, msg}
331}
Brad Fitzpatrick6078e102015-01-13 12:41:09 -0800332
333// requirePassword is an http.Handler auth wrapper that enforces a
334// HTTP Basic password. The username is ignored.
335type requirePassword struct {
336 h http.Handler
337 password string // empty means no password
338}
339
340func (h requirePassword) ServeHTTP(w http.ResponseWriter, r *http.Request) {
341 _, gotPass, _ := r.BasicAuth()
342 if h.password != "" && h.password != gotPass {
343 http.Error(w, "invalid password", http.StatusForbidden)
344 return
345 }
346 h.h.ServeHTTP(w, r)
347}