| // 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 |
| }) |
| } |