| 1 | // Copyright 2012 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 | //go:build !appengine |
| 6 | // +build !appengine |
| 7 | |
| 8 | // Package socket implements an WebSocket-based playground backend. |
| 9 | // Clients connect to a websocket handler and send run/kill commands, and |
| 10 | // the server sends the output and exit status of the running processes. |
| 11 | // Multiple clients running multiple processes may be served concurrently. |
| 12 | // The wire format is JSON and is described by the Message type. |
| 13 | // |
| 14 | // This will not run on App Engine as WebSockets are not supported there. |
| 15 | package socket // import "golang.org/x/tools/playground/socket" |
| 16 | |
| 17 | import ( |
| 18 | "bytes" |
| 19 | "encoding/json" |
| 20 | "errors" |
| 21 | "go/parser" |
| 22 | "go/token" |
| 23 | exec "golang.org/x/sys/execabs" |
| 24 | "io" |
| 25 | "io/ioutil" |
| 26 | "log" |
| 27 | "net" |
| 28 | "net/http" |
| 29 | "net/url" |
| 30 | "os" |
| 31 | "path/filepath" |
| 32 | "runtime" |
| 33 | "strings" |
| 34 | "time" |
| 35 | "unicode/utf8" |
| 36 | |
| 37 | "golang.org/x/net/websocket" |
| 38 | "golang.org/x/tools/txtar" |
| 39 | ) |
| 40 | |
| 41 | // RunScripts specifies whether the socket handler should execute shell scripts |
| 42 | // (snippets that start with a shebang). |
| 43 | var RunScripts = true |
| 44 | |
| 45 | // Environ provides an environment when a binary, such as the go tool, is |
| 46 | // invoked. |
| 47 | var Environ func() []string = os.Environ |
| 48 | |
| 49 | const ( |
| 50 | // The maximum number of messages to send per session (avoid flooding). |
| 51 | msgLimit = 1000 |
| 52 | |
| 53 | // Batch messages sent in this interval and send as a single message. |
| 54 | msgDelay = 10 * time.Millisecond |
| 55 | ) |
| 56 | |
| 57 | // Message is the wire format for the websocket connection to the browser. |
| 58 | // It is used for both sending output messages and receiving commands, as |
| 59 | // distinguished by the Kind field. |
| 60 | type Message struct { |
| 61 | Id string // client-provided unique id for the process |
| 62 | Kind string // in: "run", "kill" out: "stdout", "stderr", "end" |
| 63 | Body string |
| 64 | Options *Options `json:",omitempty"` |
| 65 | } |
| 66 | |
| 67 | // Options specify additional message options. |
| 68 | type Options struct { |
| 69 | Race bool // use -race flag when building code (for "run" only) |
| 70 | } |
| 71 | |
| 72 | // NewHandler returns a websocket server which checks the origin of requests. |
| 73 | func NewHandler(origin *url.URL) websocket.Server { |
| 74 | return websocket.Server{ |
| 75 | Config: websocket.Config{Origin: origin}, |
| 76 | Handshake: handshake, |
| 77 | Handler: websocket.Handler(socketHandler), |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | // handshake checks the origin of a request during the websocket handshake. |
| 82 | func handshake(c *websocket.Config, req *http.Request) error { |
| 83 | o, err := websocket.Origin(c, req) |
| 84 | if err != nil { |
| 85 | log.Println("bad websocket origin:", err) |
| 86 | return websocket.ErrBadWebSocketOrigin |
| 87 | } |
| 88 | _, port, err := net.SplitHostPort(c.Origin.Host) |
| 89 | if err != nil { |
| 90 | log.Println("bad websocket origin:", err) |
| 91 | return websocket.ErrBadWebSocketOrigin |
| 92 | } |
| 93 | ok := c.Origin.Scheme == o.Scheme && (c.Origin.Host == o.Host || c.Origin.Host == net.JoinHostPort(o.Host, port)) |
| 94 | if !ok { |
| 95 | log.Println("bad websocket origin:", o) |
| 96 | return websocket.ErrBadWebSocketOrigin |
| 97 | } |
| 98 | log.Println("accepting connection from:", req.RemoteAddr) |
| 99 | return nil |
| 100 | } |
| 101 | |
| 102 | // socketHandler handles the websocket connection for a given present session. |
| 103 | // It handles transcoding Messages to and from JSON format, and starting |
| 104 | // and killing processes. |
| 105 | func socketHandler(c *websocket.Conn) { |
| 106 | in, out := make(chan *Message), make(chan *Message) |
| 107 | errc := make(chan error, 1) |
| 108 | |
| 109 | // Decode messages from client and send to the in channel. |
| 110 | go func() { |
| 111 | dec := json.NewDecoder(c) |
| 112 | for { |
| 113 | var m Message |
| 114 | if err := dec.Decode(&m); err != nil { |
| 115 | errc <- err |
| 116 | return |
| 117 | } |
| 118 | in <- &m |
| 119 | } |
| 120 | }() |
| 121 | |
| 122 | // Receive messages from the out channel and encode to the client. |
| 123 | go func() { |
| 124 | enc := json.NewEncoder(c) |
| 125 | for m := range out { |
| 126 | if err := enc.Encode(m); err != nil { |
| 127 | errc <- err |
| 128 | return |
| 129 | } |
| 130 | } |
| 131 | }() |
| 132 | defer close(out) |
| 133 | |
| 134 | // Start and kill processes and handle errors. |
| 135 | proc := make(map[string]*process) |
| 136 | for { |
| 137 | select { |
| 138 | case m := <-in: |
| 139 | switch m.Kind { |
| 140 | case "run": |
| 141 | log.Println("running snippet from:", c.Request().RemoteAddr) |
| 142 | proc[m.Id].Kill() |
| 143 | proc[m.Id] = startProcess(m.Id, m.Body, out, m.Options) |
| 144 | case "kill": |
| 145 | proc[m.Id].Kill() |
| 146 | } |
| 147 | case err := <-errc: |
| 148 | if err != io.EOF { |
| 149 | // A encode or decode has failed; bail. |
| 150 | log.Println(err) |
| 151 | } |
| 152 | // Shut down any running processes. |
| 153 | for _, p := range proc { |
| 154 | p.Kill() |
| 155 | } |
| 156 | return |
| 157 | } |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | // process represents a running process. |
| 162 | type process struct { |
| 163 | out chan<- *Message |
| 164 | done chan struct{} // closed when wait completes |
| 165 | run *exec.Cmd |
| 166 | path string |
| 167 | } |
| 168 | |
| 169 | // startProcess builds and runs the given program, sending its output |
| 170 | // and end event as Messages on the provided channel. |
| 171 | func startProcess(id, body string, dest chan<- *Message, opt *Options) *process { |
| 172 | var ( |
| 173 | done = make(chan struct{}) |
| 174 | out = make(chan *Message) |
| 175 | p = &process{out: out, done: done} |
| 176 | ) |
| 177 | go func() { |
| 178 | defer close(done) |
| 179 | for m := range buffer(limiter(out, p), time.After) { |
| 180 | m.Id = id |
| 181 | dest <- m |
| 182 | } |
| 183 | }() |
| 184 | var err error |
| 185 | if path, args := shebang(body); path != "" { |
| 186 | if RunScripts { |
| 187 | err = p.startProcess(path, args, body) |
| 188 | } else { |
| 189 | err = errors.New("script execution is not allowed") |
| 190 | } |
| 191 | } else { |
| 192 | err = p.start(body, opt) |
| 193 | } |
| 194 | if err != nil { |
| 195 | p.end(err) |
| 196 | return nil |
| 197 | } |
| 198 | go func() { |
| 199 | p.end(p.run.Wait()) |
| 200 | }() |
| 201 | return p |
| 202 | } |
| 203 | |
| 204 | // end sends an "end" message to the client, containing the process id and the |
| 205 | // given error value. It also removes the binary, if present. |
| 206 | func (p *process) end(err error) { |
| 207 | if p.path != "" { |
| 208 | defer os.RemoveAll(p.path) |
| 209 | } |
| 210 | m := &Message{Kind: "end"} |
| 211 | if err != nil { |
| 212 | m.Body = err.Error() |
| 213 | } |
| 214 | p.out <- m |
| 215 | close(p.out) |
| 216 | } |
| 217 | |
| 218 | // A killer provides a mechanism to terminate a process. |
| 219 | // The Kill method returns only once the process has exited. |
| 220 | type killer interface { |
| 221 | Kill() |
| 222 | } |
| 223 | |
| 224 | // limiter returns a channel that wraps the given channel. |
| 225 | // It receives Messages from the given channel and sends them to the returned |
| 226 | // channel until it passes msgLimit messages, at which point it will kill the |
| 227 | // process and pass only the "end" message. |
| 228 | // When the given channel is closed, or when the "end" message is received, |
| 229 | // it closes the returned channel. |
| 230 | func limiter(in <-chan *Message, p killer) <-chan *Message { |
| 231 | out := make(chan *Message) |
| 232 | go func() { |
| 233 | defer close(out) |
| 234 | n := 0 |
| 235 | for m := range in { |
| 236 | switch { |
| 237 | case n < msgLimit || m.Kind == "end": |
| 238 | out <- m |
| 239 | if m.Kind == "end" { |
| 240 | return |
| 241 | } |
| 242 | case n == msgLimit: |
| 243 | // Kill in a goroutine as Kill will not return |
| 244 | // until the process' output has been |
| 245 | // processed, and we're doing that in this loop. |
| 246 | go p.Kill() |
| 247 | default: |
| 248 | continue // don't increment |
| 249 | } |
| 250 | n++ |
| 251 | } |
| 252 | }() |
| 253 | return out |
| 254 | } |
| 255 | |
| 256 | // buffer returns a channel that wraps the given channel. It receives messages |
| 257 | // from the given channel and sends them to the returned channel. |
| 258 | // Message bodies are gathered over the period msgDelay and coalesced into a |
| 259 | // single Message before they are passed on. Messages of the same kind are |
| 260 | // coalesced; when a message of a different kind is received, any buffered |
| 261 | // messages are flushed. When the given channel is closed, buffer flushes the |
| 262 | // remaining buffered messages and closes the returned channel. |
| 263 | // The timeAfter func should be time.After. It exists for testing. |
| 264 | func buffer(in <-chan *Message, timeAfter func(time.Duration) <-chan time.Time) <-chan *Message { |
| 265 | out := make(chan *Message) |
| 266 | go func() { |
| 267 | defer close(out) |
| 268 | var ( |
| 269 | tc <-chan time.Time |
| 270 | buf []byte |
| 271 | kind string |
| 272 | flush = func() { |
| 273 | if len(buf) == 0 { |
| 274 | return |
| 275 | } |
| 276 | out <- &Message{Kind: kind, Body: safeString(buf)} |
| 277 | buf = buf[:0] // recycle buffer |
| 278 | kind = "" |
| 279 | } |
| 280 | ) |
| 281 | for { |
| 282 | select { |
| 283 | case m, ok := <-in: |
| 284 | if !ok { |
| 285 | flush() |
| 286 | return |
| 287 | } |
| 288 | if m.Kind == "end" { |
| 289 | flush() |
| 290 | out <- m |
| 291 | return |
| 292 | } |
| 293 | if kind != m.Kind { |
| 294 | flush() |
| 295 | kind = m.Kind |
| 296 | if tc == nil { |
| 297 | tc = timeAfter(msgDelay) |
| 298 | } |
| 299 | } |
| 300 | buf = append(buf, m.Body...) |
| 301 | case <-tc: |
| 302 | flush() |
| 303 | tc = nil |
| 304 | } |
| 305 | } |
| 306 | }() |
| 307 | return out |
| 308 | } |
| 309 | |
| 310 | // Kill stops the process if it is running and waits for it to exit. |
| 311 | func (p *process) Kill() { |
| 312 | if p == nil || p.run == nil { |
| 313 | return |
| 314 | } |
| 315 | p.run.Process.Kill() |
| 316 | <-p.done // block until process exits |
| 317 | } |
| 318 | |
| 319 | // shebang looks for a shebang ('#!') at the beginning of the passed string. |
| 320 | // If found, it returns the path and args after the shebang. |
| 321 | // args includes the command as args[0]. |
| 322 | func shebang(body string) (path string, args []string) { |
| 323 | body = strings.TrimSpace(body) |
| 324 | if !strings.HasPrefix(body, "#!") { |
| 325 | return "", nil |
| 326 | } |
| 327 | if i := strings.Index(body, "\n"); i >= 0 { |
| 328 | body = body[:i] |
| 329 | } |
| 330 | fs := strings.Fields(body[2:]) |
| 331 | return fs[0], fs |
| 332 | } |
| 333 | |
| 334 | // startProcess starts a given program given its path and passing the given body |
| 335 | // to the command standard input. |
| 336 | func (p *process) startProcess(path string, args []string, body string) error { |
| 337 | cmd := &exec.Cmd{ |
| 338 | Path: path, |
| 339 | Args: args, |
| 340 | Stdin: strings.NewReader(body), |
| 341 | Stdout: &messageWriter{kind: "stdout", out: p.out}, |
| 342 | Stderr: &messageWriter{kind: "stderr", out: p.out}, |
| 343 | } |
| 344 | if err := cmd.Start(); err != nil { |
| 345 | return err |
| 346 | } |
| 347 | p.run = cmd |
| 348 | return nil |
| 349 | } |
| 350 | |
| 351 | // start builds and starts the given program, sending its output to p.out, |
| 352 | // and stores the running *exec.Cmd in the run field. |
| 353 | func (p *process) start(body string, opt *Options) error { |
| 354 | // We "go build" and then exec the binary so that the |
| 355 | // resultant *exec.Cmd is a handle to the user's program |
| 356 | // (rather than the go tool process). |
| 357 | // This makes Kill work. |
| 358 | |
| 359 | path, err := ioutil.TempDir("", "present-") |
| 360 | if err != nil { |
| 361 | return err |
| 362 | } |
| 363 | p.path = path // to be removed by p.end |
| 364 | |
| 365 | out := "prog" |
| 366 | if runtime.GOOS == "windows" { |
| 367 | out = "prog.exe" |
| 368 | } |
| 369 | bin := filepath.Join(path, out) |
| 370 | |
| 371 | // write body to x.go files |
| 372 | a := txtar.Parse([]byte(body)) |
| 373 | if len(a.Comment) != 0 { |
| 374 | a.Files = append(a.Files, txtar.File{Name: "prog.go", Data: a.Comment}) |
| 375 | a.Comment = nil |
| 376 | } |
| 377 | hasModfile := false |
| 378 | for _, f := range a.Files { |
| 379 | err = ioutil.WriteFile(filepath.Join(path, f.Name), f.Data, 0666) |
| 380 | if err != nil { |
| 381 | return err |
| 382 | } |
| 383 | if f.Name == "go.mod" { |
| 384 | hasModfile = true |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | // build x.go, creating x |
| 389 | args := []string{"go", "build", "-tags", "OMIT"} |
| 390 | if opt != nil && opt.Race { |
| 391 | p.out <- &Message{ |
| 392 | Kind: "stderr", |
| 393 | Body: "Running with race detector.\n", |
| 394 | } |
| 395 | args = append(args, "-race") |
| 396 | } |
| 397 | args = append(args, "-o", bin) |
| 398 | cmd := p.cmd(path, args...) |
| 399 | if !hasModfile { |
| 400 | cmd.Env = append(cmd.Env, "GO111MODULE=off") |
| 401 | } |
| 402 | cmd.Stdout = cmd.Stderr // send compiler output to stderr |
| 403 | if err := cmd.Run(); err != nil { |
| 404 | return err |
| 405 | } |
| 406 | |
| 407 | // run x |
| 408 | if isNacl() { |
| 409 | cmd, err = p.naclCmd(bin) |
| 410 | if err != nil { |
| 411 | return err |
| 412 | } |
| 413 | } else { |
| 414 | cmd = p.cmd("", bin) |
| 415 | } |
| 416 | if opt != nil && opt.Race { |
| 417 | cmd.Env = append(cmd.Env, "GOMAXPROCS=2") |
| 418 | } |
| 419 | if err := cmd.Start(); err != nil { |
| 420 | // If we failed to exec, that might be because they built |
| 421 | // a non-main package instead of an executable. |
| 422 | // Check and report that. |
| 423 | if name, err := packageName(body); err == nil && name != "main" { |
| 424 | return errors.New(`executable programs must use "package main"`) |
| 425 | } |
| 426 | return err |
| 427 | } |
| 428 | p.run = cmd |
| 429 | return nil |
| 430 | } |
| 431 | |
| 432 | // cmd builds an *exec.Cmd that writes its standard output and error to the |
| 433 | // process' output channel. |
| 434 | func (p *process) cmd(dir string, args ...string) *exec.Cmd { |
| 435 | cmd := exec.Command(args[0], args[1:]...) |
| 436 | cmd.Dir = dir |
| 437 | cmd.Env = Environ() |
| 438 | cmd.Stdout = &messageWriter{kind: "stdout", out: p.out} |
| 439 | cmd.Stderr = &messageWriter{kind: "stderr", out: p.out} |
| 440 | return cmd |
| 441 | } |
| 442 | |
| 443 | func isNacl() bool { |
| 444 | for _, v := range append(Environ(), os.Environ()...) { |
| 445 | if v == "GOOS=nacl" { |
| 446 | return true |
| 447 | } |
| 448 | } |
| 449 | return false |
| 450 | } |
| 451 | |
| 452 | // naclCmd returns an *exec.Cmd that executes bin under native client. |
| 453 | func (p *process) naclCmd(bin string) (*exec.Cmd, error) { |
| 454 | pwd, err := os.Getwd() |
| 455 | if err != nil { |
| 456 | return nil, err |
| 457 | } |
| 458 | var args []string |
| 459 | env := []string{ |
| 460 | "NACLENV_GOOS=" + runtime.GOOS, |
| 461 | "NACLENV_GOROOT=/go", |
| 462 | "NACLENV_NACLPWD=" + strings.Replace(pwd, runtime.GOROOT(), "/go", 1), |
| 463 | } |
| 464 | switch runtime.GOARCH { |
| 465 | case "amd64": |
| 466 | env = append(env, "NACLENV_GOARCH=amd64p32") |
| 467 | args = []string{"sel_ldr_x86_64"} |
| 468 | case "386": |
| 469 | env = append(env, "NACLENV_GOARCH=386") |
| 470 | args = []string{"sel_ldr_x86_32"} |
| 471 | case "arm": |
| 472 | env = append(env, "NACLENV_GOARCH=arm") |
| 473 | selLdr, err := exec.LookPath("sel_ldr_arm") |
| 474 | if err != nil { |
| 475 | return nil, err |
| 476 | } |
| 477 | args = []string{"nacl_helper_bootstrap_arm", selLdr, "--reserved_at_zero=0xXXXXXXXXXXXXXXXX"} |
| 478 | default: |
| 479 | return nil, errors.New("native client does not support GOARCH=" + runtime.GOARCH) |
| 480 | } |
| 481 | |
| 482 | cmd := p.cmd("", append(args, "-l", "/dev/null", "-S", "-e", bin)...) |
| 483 | cmd.Env = append(cmd.Env, env...) |
| 484 | |
| 485 | return cmd, nil |
| 486 | } |
| 487 | |
| 488 | func packageName(body string) (string, error) { |
| 489 | f, err := parser.ParseFile(token.NewFileSet(), "prog.go", |
| 490 | strings.NewReader(body), parser.PackageClauseOnly) |
| 491 | if err != nil { |
| 492 | return "", err |
| 493 | } |
| 494 | return f.Name.String(), nil |
| 495 | } |
| 496 | |
| 497 | // messageWriter is an io.Writer that converts all writes to Message sends on |
| 498 | // the out channel with the specified id and kind. |
| 499 | type messageWriter struct { |
| 500 | kind string |
| 501 | out chan<- *Message |
| 502 | } |
| 503 | |
| 504 | func (w *messageWriter) Write(b []byte) (n int, err error) { |
| 505 | w.out <- &Message{Kind: w.kind, Body: safeString(b)} |
| 506 | return len(b), nil |
| 507 | } |
| 508 | |
| 509 | // safeString returns b as a valid UTF-8 string. |
| 510 | func safeString(b []byte) string { |
| 511 | if utf8.Valid(b) { |
| 512 | return string(b) |
| 513 | } |
| 514 | var buf bytes.Buffer |
| 515 | for len(b) > 0 { |
| 516 | r, size := utf8.DecodeRune(b) |
| 517 | b = b[size:] |
| 518 | buf.WriteRune(r) |
| 519 | } |
| 520 | return buf.String() |
| 521 | } |
| 522 |
Members