// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !appengine

// Package socket implements a WebSocket-based playground backend.
// Clients connect to a websocket handler and send run/kill commands, and
// the server sends the output and exit status of the running processes.
// Multiple clients running multiple processes may be served concurrently.
// The wire format is JSON and is described by the Message type.
//
// This will not run on App Engine as WebSockets are not supported there.
package socket // import "golang.org/x/tools/playground/socket"

import (
	"bytes"
	"encoding/json"
	"errors"
	"go/parser"
	"go/token"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"slices"
	"strings"
	"time"
	"unicode/utf8"

	"golang.org/x/net/websocket"
	"golang.org/x/tools/txtar"
)

// RunScripts specifies whether the socket handler should execute shell scripts
// (snippets that start with a shebang).
var RunScripts = true

// Environ provides an environment when a binary, such as the go tool, is
// invoked.
var Environ func() []string = os.Environ

const (
	// The maximum number of messages to send per session (avoid flooding).
	msgLimit = 1000

	// Batch messages sent in this interval and send as a single message.
	msgDelay = 10 * time.Millisecond
)

// Message is the wire format for the websocket connection to the browser.
// It is used for both sending output messages and receiving commands, as
// distinguished by the Kind field.
type Message struct {
	Id      string // client-provided unique id for the process
	Kind    string // in: "run", "kill" out: "stdout", "stderr", "end"
	Body    string
	Options *Options `json:",omitempty"`
}

// Options specify additional message options.
type Options struct {
	Race bool // use -race flag when building code (for "run" only)
}

// NewHandler returns a websocket server which checks the origin of requests.
func NewHandler(origin *url.URL) websocket.Server {
	return websocket.Server{
		Config:    websocket.Config{Origin: origin},
		Handshake: handshake,
		Handler:   websocket.Handler(socketHandler),
	}
}

// handshake checks the origin of a request during the websocket handshake.
func handshake(c *websocket.Config, req *http.Request) error {
	o, err := websocket.Origin(c, req)
	if err != nil {
		log.Println("bad websocket origin:", err)
		return websocket.ErrBadWebSocketOrigin
	}
	_, port, err := net.SplitHostPort(c.Origin.Host)
	if err != nil {
		log.Println("bad websocket origin:", err)
		return websocket.ErrBadWebSocketOrigin
	}
	ok := c.Origin.Scheme == o.Scheme && (c.Origin.Host == o.Host || c.Origin.Host == net.JoinHostPort(o.Host, port))
	if !ok {
		log.Println("bad websocket origin:", o)
		return websocket.ErrBadWebSocketOrigin
	}
	log.Println("accepting connection from:", req.RemoteAddr)
	return nil
}

// socketHandler handles the websocket connection for a given present session.
// It handles transcoding Messages to and from JSON format, and starting
// and killing processes.
func socketHandler(c *websocket.Conn) {
	in, out := make(chan *Message), make(chan *Message)
	errc := make(chan error, 1)

	// Decode messages from client and send to the in channel.
	go func() {
		dec := json.NewDecoder(c)
		for {
			var m Message
			if err := dec.Decode(&m); err != nil {
				errc <- err
				return
			}
			in <- &m
		}
	}()

	// Receive messages from the out channel and encode to the client.
	go func() {
		enc := json.NewEncoder(c)
		for m := range out {
			if err := enc.Encode(m); err != nil {
				errc <- err
				return
			}
		}
	}()
	defer close(out)

	// Start and kill processes and handle errors.
	proc := make(map[string]*process)
	for {
		select {
		case m := <-in:
			switch m.Kind {
			case "run":
				log.Println("running snippet from:", c.Request().RemoteAddr)
				proc[m.Id].Kill()
				proc[m.Id] = startProcess(m.Id, m.Body, out, m.Options)
			case "kill":
				proc[m.Id].Kill()
			}
		case err := <-errc:
			if err != io.EOF {
				// A encode or decode has failed; bail.
				log.Println(err)
			}
			// Shut down any running processes.
			for _, p := range proc {
				p.Kill()
			}
			return
		}
	}
}

// process represents a running process.
type process struct {
	out  chan<- *Message
	done chan struct{} // closed when wait completes
	run  *exec.Cmd
	path string
}

// startProcess builds and runs the given program, sending its output
// and end event as Messages on the provided channel.
func startProcess(id, body string, dest chan<- *Message, opt *Options) *process {
	var (
		done = make(chan struct{})
		out  = make(chan *Message)
		p    = &process{out: out, done: done}
	)
	go func() {
		defer close(done)
		for m := range buffer(limiter(out, p), time.After) {
			m.Id = id
			dest <- m
		}
	}()
	var err error
	if path, args := shebang(body); path != "" {
		if RunScripts {
			err = p.startProcess(path, args, body)
		} else {
			err = errors.New("script execution is not allowed")
		}
	} else {
		err = p.start(body, opt)
	}
	if err != nil {
		p.end(err)
		return nil
	}
	go func() {
		p.end(p.run.Wait())
	}()
	return p
}

// end sends an "end" message to the client, containing the process id and the
// given error value. It also removes the binary, if present.
func (p *process) end(err error) {
	if p.path != "" {
		defer os.RemoveAll(p.path)
	}
	m := &Message{Kind: "end"}
	if err != nil {
		m.Body = err.Error()
	}
	p.out <- m
	close(p.out)
}

// A killer provides a mechanism to terminate a process.
// The Kill method returns only once the process has exited.
type killer interface {
	Kill()
}

// limiter returns a channel that wraps the given channel.
// It receives Messages from the given channel and sends them to the returned
// channel until it passes msgLimit messages, at which point it will kill the
// process and pass only the "end" message.
// When the given channel is closed, or when the "end" message is received,
// it closes the returned channel.
func limiter(in <-chan *Message, p killer) <-chan *Message {
	out := make(chan *Message)
	go func() {
		defer close(out)
		n := 0
		for m := range in {
			switch {
			case n < msgLimit || m.Kind == "end":
				out <- m
				if m.Kind == "end" {
					return
				}
			case n == msgLimit:
				// Kill in a goroutine as Kill will not return
				// until the process' output has been
				// processed, and we're doing that in this loop.
				go p.Kill()
			default:
				continue // don't increment
			}
			n++
		}
	}()
	return out
}

// buffer returns a channel that wraps the given channel. It receives messages
// from the given channel and sends them to the returned channel.
// Message bodies are gathered over the period msgDelay and coalesced into a
// single Message before they are passed on. Messages of the same kind are
// coalesced; when a message of a different kind is received, any buffered
// messages are flushed. When the given channel is closed, buffer flushes the
// remaining buffered messages and closes the returned channel.
// The timeAfter func should be time.After. It exists for testing.
func buffer(in <-chan *Message, timeAfter func(time.Duration) <-chan time.Time) <-chan *Message {
	out := make(chan *Message)
	go func() {
		defer close(out)
		var (
			tc    <-chan time.Time
			buf   []byte
			kind  string
			flush = func() {
				if len(buf) == 0 {
					return
				}
				out <- &Message{Kind: kind, Body: safeString(buf)}
				buf = buf[:0] // recycle buffer
				kind = ""
			}
		)
		for {
			select {
			case m, ok := <-in:
				if !ok {
					flush()
					return
				}
				if m.Kind == "end" {
					flush()
					out <- m
					return
				}
				if kind != m.Kind {
					flush()
					kind = m.Kind
					if tc == nil {
						tc = timeAfter(msgDelay)
					}
				}
				buf = append(buf, m.Body...)
			case <-tc:
				flush()
				tc = nil
			}
		}
	}()
	return out
}

// Kill stops the process if it is running and waits for it to exit.
func (p *process) Kill() {
	if p == nil || p.run == nil {
		return
	}
	p.run.Process.Kill()
	<-p.done // block until process exits
}

// shebang looks for a shebang ('#!') at the beginning of the passed string.
// If found, it returns the path and args after the shebang.
// args includes the command as args[0].
func shebang(body string) (path string, args []string) {
	body = strings.TrimSpace(body)
	if !strings.HasPrefix(body, "#!") {
		return "", nil
	}
	if i := strings.Index(body, "\n"); i >= 0 {
		body = body[:i]
	}
	fs := strings.Fields(body[2:])
	return fs[0], fs
}

// startProcess starts a given program given its path and passing the given body
// to the command standard input.
func (p *process) startProcess(path string, args []string, body string) error {
	cmd := &exec.Cmd{
		Path:   path,
		Args:   args,
		Stdin:  strings.NewReader(body),
		Stdout: &messageWriter{kind: "stdout", out: p.out},
		Stderr: &messageWriter{kind: "stderr", out: p.out},
	}
	if err := cmd.Start(); err != nil {
		return err
	}
	p.run = cmd
	return nil
}

// start builds and starts the given program, sending its output to p.out,
// and stores the running *exec.Cmd in the run field.
func (p *process) start(body string, opt *Options) error {
	// We "go build" and then exec the binary so that the
	// resultant *exec.Cmd is a handle to the user's program
	// (rather than the go tool process).
	// This makes Kill work.

	path, err := os.MkdirTemp("", "present-")
	if err != nil {
		return err
	}
	p.path = path // to be removed by p.end

	out := "prog"
	if runtime.GOOS == "windows" {
		out = "prog.exe"
	}
	bin := filepath.Join(path, out)

	// write body to x.go files
	a := txtar.Parse([]byte(body))
	if len(a.Comment) != 0 {
		a.Files = append(a.Files, txtar.File{Name: "prog.go", Data: a.Comment})
		a.Comment = nil
	}
	hasModfile := false
	for _, f := range a.Files {
		err = os.WriteFile(filepath.Join(path, f.Name), f.Data, 0666)
		if err != nil {
			return err
		}
		if f.Name == "go.mod" {
			hasModfile = true
		}
	}

	// build x.go, creating x
	args := []string{"go", "build", "-tags", "OMIT"}
	if opt != nil && opt.Race {
		p.out <- &Message{
			Kind: "stderr",
			Body: "Running with race detector.\n",
		}
		args = append(args, "-race")
	}
	args = append(args, "-o", bin)
	cmd := p.cmd(path, args...)
	if !hasModfile {
		cmd.Env = append(cmd.Env, "GO111MODULE=off")
	}
	cmd.Stdout = cmd.Stderr // send compiler output to stderr
	if err := cmd.Run(); err != nil {
		return err
	}

	// run x
	if isNacl() {
		cmd, err = p.naclCmd(bin)
		if err != nil {
			return err
		}
	} else {
		cmd = p.cmd("", bin)
	}
	if opt != nil && opt.Race {
		cmd.Env = append(cmd.Env, "GOMAXPROCS=2")
	}
	if err := cmd.Start(); err != nil {
		// If we failed to exec, that might be because they built
		// a non-main package instead of an executable.
		// Check and report that.
		if name, err := packageName(body); err == nil && name != "main" {
			return errors.New(`executable programs must use "package main"`)
		}
		return err
	}
	p.run = cmd
	return nil
}

// cmd builds an *exec.Cmd that writes its standard output and error to the
// process' output channel.
func (p *process) cmd(dir string, args ...string) *exec.Cmd {
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Dir = dir
	cmd.Env = Environ()
	cmd.Stdout = &messageWriter{kind: "stdout", out: p.out}
	cmd.Stderr = &messageWriter{kind: "stderr", out: p.out}
	return cmd
}

func isNacl() bool {
	return slices.Contains(append(Environ(), os.Environ()...), "GOOS=nacl")
}

// naclCmd returns an *exec.Cmd that executes bin under native client.
func (p *process) naclCmd(bin string) (*exec.Cmd, error) {
	pwd, err := os.Getwd()
	if err != nil {
		return nil, err
	}
	var args []string
	env := []string{
		"NACLENV_GOOS=" + runtime.GOOS,
		"NACLENV_GOROOT=/go",
		"NACLENV_NACLPWD=" + strings.Replace(pwd, runtime.GOROOT(), "/go", 1),
	}
	switch runtime.GOARCH {
	case "amd64":
		env = append(env, "NACLENV_GOARCH=amd64p32")
		args = []string{"sel_ldr_x86_64"}
	case "386":
		env = append(env, "NACLENV_GOARCH=386")
		args = []string{"sel_ldr_x86_32"}
	case "arm":
		env = append(env, "NACLENV_GOARCH=arm")
		selLdr, err := exec.LookPath("sel_ldr_arm")
		if err != nil {
			return nil, err
		}
		args = []string{"nacl_helper_bootstrap_arm", selLdr, "--reserved_at_zero=0xXXXXXXXXXXXXXXXX"}
	default:
		return nil, errors.New("native client does not support GOARCH=" + runtime.GOARCH)
	}

	cmd := p.cmd("", append(args, "-l", "/dev/null", "-S", "-e", bin)...)
	cmd.Env = append(cmd.Env, env...)

	return cmd, nil
}

func packageName(body string) (string, error) {
	f, err := parser.ParseFile(token.NewFileSet(), "prog.go",
		strings.NewReader(body), parser.PackageClauseOnly)
	if err != nil {
		return "", err
	}
	return f.Name.String(), nil
}

// messageWriter is an io.Writer that converts all writes to Message sends on
// the out channel with the specified id and kind.
type messageWriter struct {
	kind string
	out  chan<- *Message
}

func (w *messageWriter) Write(b []byte) (n int, err error) {
	w.out <- &Message{Kind: w.kind, Body: safeString(b)}
	return len(b), nil
}

// safeString returns b as a valid UTF-8 string.
func safeString(b []byte) string {
	if utf8.Valid(b) {
		return string(b)
	}
	var buf bytes.Buffer
	for len(b) > 0 {
		r, size := utf8.DecodeRune(b)
		b = b[size:]
		buf.WriteRune(r)
	}
	return buf.String()
}
