blob: c35b46f735115f337ea5aaff563c1770b88303a2 [file] [log] [blame]
// Copyright 2022 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.
package vcweb
import (
"bufio"
"bytes"
"cmd/go/internal/script"
"context"
"errors"
"fmt"
"internal/txtar"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"golang.org/x/mod/module"
"golang.org/x/mod/zip"
)
// newScriptEngine returns a script engine augmented with commands for
// reproducing version-control repositories by replaying commits.
func newScriptEngine() *script.Engine {
conds := script.DefaultConds()
interrupt := func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
gracePeriod := 30 * time.Second // arbitrary
cmds := script.DefaultCmds()
cmds["at"] = scriptAt()
cmds["bzr"] = script.Program("bzr", interrupt, gracePeriod)
cmds["fossil"] = script.Program("fossil", interrupt, gracePeriod)
cmds["git"] = script.Program("git", interrupt, gracePeriod)
cmds["hg"] = script.Program("hg", interrupt, gracePeriod)
cmds["handle"] = scriptHandle()
cmds["modzip"] = scriptModzip()
cmds["svnadmin"] = script.Program("svnadmin", interrupt, gracePeriod)
cmds["svn"] = script.Program("svn", interrupt, gracePeriod)
cmds["unquote"] = scriptUnquote()
return &script.Engine{
Cmds: cmds,
Conds: conds,
}
}
// loadScript interprets the given script content using the vcweb script engine.
// loadScript always returns either a non-nil handler or a non-nil error.
//
// The script content must be a txtar archive with a comment containing a script
// with exactly one "handle" command and zero or more VCS commands to prepare
// the repository to be served.
func (s *Server) loadScript(ctx context.Context, logger *log.Logger, scriptPath string, scriptContent []byte, workDir string) (http.Handler, error) {
ar := txtar.Parse(scriptContent)
if err := os.MkdirAll(workDir, 0755); err != nil {
return nil, err
}
st, err := s.newState(ctx, workDir)
if err != nil {
return nil, err
}
if err := st.ExtractFiles(ar); err != nil {
return nil, err
}
scriptName := filepath.Base(scriptPath)
scriptLog := new(strings.Builder)
err = s.engine.Execute(st, scriptName, bufio.NewReader(bytes.NewReader(ar.Comment)), scriptLog)
closeErr := st.CloseAndWait(scriptLog)
logger.Printf("%s:", scriptName)
io.WriteString(logger.Writer(), scriptLog.String())
io.WriteString(logger.Writer(), "\n")
if err != nil {
return nil, err
}
if closeErr != nil {
return nil, err
}
sc, err := getScriptCtx(st)
if err != nil {
return nil, err
}
if sc.handler == nil {
return nil, errors.New("script completed without setting handler")
}
return sc.handler, nil
}
// newState returns a new script.State for executing scripts in workDir.
func (s *Server) newState(ctx context.Context, workDir string) (*script.State, error) {
ctx = &scriptCtx{
Context: ctx,
server: s,
}
st, err := script.NewState(ctx, workDir, s.env)
if err != nil {
return nil, err
}
return st, nil
}
// scriptEnviron returns a new environment that attempts to provide predictable
// behavior for the supported version-control tools.
func scriptEnviron(homeDir string) []string {
env := []string{
"USER=gopher",
homeEnvName() + "=" + homeDir,
"GIT_CONFIG_NOSYSTEM=1",
"HGRCPATH=" + filepath.Join(homeDir, ".hgrc"),
"HGENCODING=utf-8",
}
// Preserve additional environment variables that may be needed by VCS tools.
for _, k := range []string{
pathEnvName(),
tempEnvName(),
"SYSTEMROOT", // must be preserved on Windows to find DLLs; golang.org/issue/25210
"WINDIR", // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711
"ComSpec", // must be preserved on Windows to be able to run Batch files; golang.org/issue/56555
"DYLD_LIBRARY_PATH", // must be preserved on macOS systems to find shared libraries
"LD_LIBRARY_PATH", // must be preserved on Unix systems to find shared libraries
"LIBRARY_PATH", // allow override of non-standard static library paths
"PYTHONPATH", // may be needed by hg to find imported modules
} {
if v, ok := os.LookupEnv(k); ok {
env = append(env, k+"="+v)
}
}
if os.Getenv("GO_BUILDER_NAME") != "" || os.Getenv("GIT_TRACE_CURL") == "1" {
// To help diagnose https://go.dev/issue/52545,
// enable tracing for Git HTTPS requests.
env = append(env,
"GIT_TRACE_CURL=1",
"GIT_TRACE_CURL_NO_DATA=1",
"GIT_REDACT_COOKIES=o,SSO,GSSO_Uberproxy")
}
return env
}
// homeEnvName returns the environment variable used by os.UserHomeDir
// to locate the user's home directory.
func homeEnvName() string {
switch runtime.GOOS {
case "windows":
return "USERPROFILE"
case "plan9":
return "home"
default:
return "HOME"
}
}
// tempEnvName returns the environment variable used by os.TempDir
// to locate the default directory for temporary files.
func tempEnvName() string {
switch runtime.GOOS {
case "windows":
return "TMP"
case "plan9":
return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine
default:
return "TMPDIR"
}
}
// pathEnvName returns the environment variable used by exec.LookPath to
// identify directories to search for executables.
func pathEnvName() string {
switch runtime.GOOS {
case "plan9":
return "path"
default:
return "PATH"
}
}
// A scriptCtx is a context.Context that stores additional state for script
// commands.
type scriptCtx struct {
context.Context
server *Server
commitTime time.Time
handlerName string
handler http.Handler
}
// scriptCtxKey is the key associating the *scriptCtx in a script's Context..
type scriptCtxKey struct{}
func (sc *scriptCtx) Value(key any) any {
if key == (scriptCtxKey{}) {
return sc
}
return sc.Context.Value(key)
}
func getScriptCtx(st *script.State) (*scriptCtx, error) {
sc, ok := st.Context().Value(scriptCtxKey{}).(*scriptCtx)
if !ok {
return nil, errors.New("scriptCtx not found in State.Context")
}
return sc, nil
}
func scriptAt() script.Cmd {
return script.Command(
script.CmdUsage{
Summary: "set the current commit time for all version control systems",
Args: "time",
Detail: []string{
"The argument must be an absolute timestamp in RFC3339 format.",
},
},
func(st *script.State, args ...string) (script.WaitFunc, error) {
if len(args) != 1 {
return nil, script.ErrUsage
}
sc, err := getScriptCtx(st)
if err != nil {
return nil, err
}
sc.commitTime, err = time.ParseInLocation(time.RFC3339, args[0], time.UTC)
if err == nil {
st.Setenv("GIT_COMMITTER_DATE", args[0])
st.Setenv("GIT_AUTHOR_DATE", args[0])
}
return nil, err
})
}
func scriptHandle() script.Cmd {
return script.Command(
script.CmdUsage{
Summary: "set the HTTP handler that will serve the script's output",
Args: "handler [dir]",
Detail: []string{
"The handler will be passed the script's current working directory and environment as arguments.",
"Valid handlers include 'dir' (for general http.Dir serving), 'bzr', 'fossil', 'git', and 'hg'",
},
},
func(st *script.State, args ...string) (script.WaitFunc, error) {
if len(args) == 0 || len(args) > 2 {
return nil, script.ErrUsage
}
sc, err := getScriptCtx(st)
if err != nil {
return nil, err
}
if sc.handler != nil {
return nil, fmt.Errorf("server handler already set to %s", sc.handlerName)
}
name := args[0]
h, ok := sc.server.vcsHandlers[name]
if !ok {
return nil, fmt.Errorf("unrecognized VCS %q", name)
}
sc.handlerName = name
if !h.Available() {
return nil, ServerNotInstalledError{name}
}
dir := st.Getwd()
if len(args) >= 2 {
dir = st.Path(args[1])
}
sc.handler, err = h.Handler(dir, st.Environ(), sc.server.logger)
return nil, err
})
}
func scriptModzip() script.Cmd {
return script.Command(
script.CmdUsage{
Summary: "create a Go module zip file from a directory",
Args: "zipfile path@version dir",
},
func(st *script.State, args ...string) (wait script.WaitFunc, err error) {
if len(args) != 3 {
return nil, script.ErrUsage
}
zipPath := st.Path(args[0])
mPath, version, ok := strings.Cut(args[1], "@")
if !ok {
return nil, script.ErrUsage
}
dir := st.Path(args[2])
if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil {
return nil, err
}
f, err := os.Create(zipPath)
if err != nil {
return nil, err
}
defer func() {
if closeErr := f.Close(); err == nil {
err = closeErr
}
}()
return nil, zip.CreateFromDir(f, module.Version{Path: mPath, Version: version}, dir)
})
}
func scriptUnquote() script.Cmd {
return script.Command(
script.CmdUsage{
Summary: "unquote the argument as a Go string",
Args: "string",
},
func(st *script.State, args ...string) (script.WaitFunc, error) {
if len(args) != 1 {
return nil, script.ErrUsage
}
s, err := strconv.Unquote(`"` + args[0] + `"`)
if err != nil {
return nil, err
}
wait := func(*script.State) (stdout, stderr string, err error) {
return s, "", nil
}
return wait, nil
})
}