Source file src/cmd/go/internal/vcweb/script.go

     1  // Copyright 2022 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  package vcweb
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"cmd/internal/script"
    11  	"context"
    12  	"errors"
    13  	"fmt"
    14  	"internal/txtar"
    15  	"io"
    16  	"log"
    17  	"net/http"
    18  	"os"
    19  	"os/exec"
    20  	"path/filepath"
    21  	"runtime"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"golang.org/x/mod/module"
    27  	"golang.org/x/mod/zip"
    28  )
    29  
    30  // newScriptEngine returns a script engine augmented with commands for
    31  // reproducing version-control repositories by replaying commits.
    32  func newScriptEngine() *script.Engine {
    33  	conds := script.DefaultConds()
    34  
    35  	interrupt := func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
    36  	gracePeriod := 30 * time.Second // arbitrary
    37  
    38  	cmds := script.DefaultCmds()
    39  	cmds["at"] = scriptAt()
    40  	cmds["bzr"] = script.Program("bzr", interrupt, gracePeriod)
    41  	cmds["fossil"] = script.Program("fossil", interrupt, gracePeriod)
    42  	cmds["git"] = script.Program("git", interrupt, gracePeriod)
    43  	cmds["hg"] = script.Program("hg", interrupt, gracePeriod)
    44  	cmds["handle"] = scriptHandle()
    45  	cmds["modzip"] = scriptModzip()
    46  	cmds["svnadmin"] = script.Program("svnadmin", interrupt, gracePeriod)
    47  	cmds["svn"] = script.Program("svn", interrupt, gracePeriod)
    48  	cmds["unquote"] = scriptUnquote()
    49  
    50  	return &script.Engine{
    51  		Cmds:  cmds,
    52  		Conds: conds,
    53  	}
    54  }
    55  
    56  // loadScript interprets the given script content using the vcweb script engine.
    57  // loadScript always returns either a non-nil handler or a non-nil error.
    58  //
    59  // The script content must be a txtar archive with a comment containing a script
    60  // with exactly one "handle" command and zero or more VCS commands to prepare
    61  // the repository to be served.
    62  func (s *Server) loadScript(ctx context.Context, logger *log.Logger, scriptPath string, scriptContent []byte, workDir string) (http.Handler, error) {
    63  	ar := txtar.Parse(scriptContent)
    64  
    65  	if err := os.MkdirAll(workDir, 0755); err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	st, err := s.newState(ctx, workDir)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	if err := st.ExtractFiles(ar); err != nil {
    74  		return nil, err
    75  	}
    76  
    77  	scriptName := filepath.Base(scriptPath)
    78  	scriptLog := new(strings.Builder)
    79  	err = s.engine.Execute(st, scriptName, bufio.NewReader(bytes.NewReader(ar.Comment)), scriptLog)
    80  	closeErr := st.CloseAndWait(scriptLog)
    81  	logger.Printf("%s:", scriptName)
    82  	io.WriteString(logger.Writer(), scriptLog.String())
    83  	io.WriteString(logger.Writer(), "\n")
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	if closeErr != nil {
    88  		return nil, err
    89  	}
    90  
    91  	sc, err := getScriptCtx(st)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  	if sc.handler == nil {
    96  		return nil, errors.New("script completed without setting handler")
    97  	}
    98  	return sc.handler, nil
    99  }
   100  
   101  // newState returns a new script.State for executing scripts in workDir.
   102  func (s *Server) newState(ctx context.Context, workDir string) (*script.State, error) {
   103  	ctx = &scriptCtx{
   104  		Context: ctx,
   105  		server:  s,
   106  	}
   107  
   108  	st, err := script.NewState(ctx, workDir, s.env)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	return st, nil
   113  }
   114  
   115  // scriptEnviron returns a new environment that attempts to provide predictable
   116  // behavior for the supported version-control tools.
   117  func scriptEnviron(homeDir string) []string {
   118  	env := []string{
   119  		"USER=gopher",
   120  		homeEnvName() + "=" + homeDir,
   121  		"GIT_CONFIG_NOSYSTEM=1",
   122  		"HGRCPATH=" + filepath.Join(homeDir, ".hgrc"),
   123  		"HGENCODING=utf-8",
   124  	}
   125  	// Preserve additional environment variables that may be needed by VCS tools.
   126  	for _, k := range []string{
   127  		pathEnvName(),
   128  		tempEnvName(),
   129  		"SYSTEMROOT",        // must be preserved on Windows to find DLLs; golang.org/issue/25210
   130  		"WINDIR",            // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711
   131  		"ComSpec",           // must be preserved on Windows to be able to run Batch files; golang.org/issue/56555
   132  		"DYLD_LIBRARY_PATH", // must be preserved on macOS systems to find shared libraries
   133  		"LD_LIBRARY_PATH",   // must be preserved on Unix systems to find shared libraries
   134  		"LIBRARY_PATH",      // allow override of non-standard static library paths
   135  		"PYTHONPATH",        // may be needed by hg to find imported modules
   136  	} {
   137  		if v, ok := os.LookupEnv(k); ok {
   138  			env = append(env, k+"="+v)
   139  		}
   140  	}
   141  
   142  	if os.Getenv("GO_BUILDER_NAME") != "" || os.Getenv("GIT_TRACE_CURL") == "1" {
   143  		// To help diagnose https://go.dev/issue/52545,
   144  		// enable tracing for Git HTTPS requests.
   145  		env = append(env,
   146  			"GIT_TRACE_CURL=1",
   147  			"GIT_TRACE_CURL_NO_DATA=1",
   148  			"GIT_REDACT_COOKIES=o,SSO,GSSO_Uberproxy")
   149  	}
   150  
   151  	return env
   152  }
   153  
   154  // homeEnvName returns the environment variable used by os.UserHomeDir
   155  // to locate the user's home directory.
   156  func homeEnvName() string {
   157  	switch runtime.GOOS {
   158  	case "windows":
   159  		return "USERPROFILE"
   160  	case "plan9":
   161  		return "home"
   162  	default:
   163  		return "HOME"
   164  	}
   165  }
   166  
   167  // tempEnvName returns the environment variable used by os.TempDir
   168  // to locate the default directory for temporary files.
   169  func tempEnvName() string {
   170  	switch runtime.GOOS {
   171  	case "windows":
   172  		return "TMP"
   173  	case "plan9":
   174  		return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine
   175  	default:
   176  		return "TMPDIR"
   177  	}
   178  }
   179  
   180  // pathEnvName returns the environment variable used by exec.LookPath to
   181  // identify directories to search for executables.
   182  func pathEnvName() string {
   183  	switch runtime.GOOS {
   184  	case "plan9":
   185  		return "path"
   186  	default:
   187  		return "PATH"
   188  	}
   189  }
   190  
   191  // A scriptCtx is a context.Context that stores additional state for script
   192  // commands.
   193  type scriptCtx struct {
   194  	context.Context
   195  	server      *Server
   196  	commitTime  time.Time
   197  	handlerName string
   198  	handler     http.Handler
   199  }
   200  
   201  // scriptCtxKey is the key associating the *scriptCtx in a script's Context..
   202  type scriptCtxKey struct{}
   203  
   204  func (sc *scriptCtx) Value(key any) any {
   205  	if key == (scriptCtxKey{}) {
   206  		return sc
   207  	}
   208  	return sc.Context.Value(key)
   209  }
   210  
   211  func getScriptCtx(st *script.State) (*scriptCtx, error) {
   212  	sc, ok := st.Context().Value(scriptCtxKey{}).(*scriptCtx)
   213  	if !ok {
   214  		return nil, errors.New("scriptCtx not found in State.Context")
   215  	}
   216  	return sc, nil
   217  }
   218  
   219  func scriptAt() script.Cmd {
   220  	return script.Command(
   221  		script.CmdUsage{
   222  			Summary: "set the current commit time for all version control systems",
   223  			Args:    "time",
   224  			Detail: []string{
   225  				"The argument must be an absolute timestamp in RFC3339 format.",
   226  			},
   227  		},
   228  		func(st *script.State, args ...string) (script.WaitFunc, error) {
   229  			if len(args) != 1 {
   230  				return nil, script.ErrUsage
   231  			}
   232  
   233  			sc, err := getScriptCtx(st)
   234  			if err != nil {
   235  				return nil, err
   236  			}
   237  
   238  			sc.commitTime, err = time.ParseInLocation(time.RFC3339, args[0], time.UTC)
   239  			if err == nil {
   240  				st.Setenv("GIT_COMMITTER_DATE", args[0])
   241  				st.Setenv("GIT_AUTHOR_DATE", args[0])
   242  			}
   243  			return nil, err
   244  		})
   245  }
   246  
   247  func scriptHandle() script.Cmd {
   248  	return script.Command(
   249  		script.CmdUsage{
   250  			Summary: "set the HTTP handler that will serve the script's output",
   251  			Args:    "handler [dir]",
   252  			Detail: []string{
   253  				"The handler will be passed the script's current working directory and environment as arguments.",
   254  				"Valid handlers include 'dir' (for general http.Dir serving), 'bzr', 'fossil', 'git', and 'hg'",
   255  			},
   256  		},
   257  		func(st *script.State, args ...string) (script.WaitFunc, error) {
   258  			if len(args) == 0 || len(args) > 2 {
   259  				return nil, script.ErrUsage
   260  			}
   261  
   262  			sc, err := getScriptCtx(st)
   263  			if err != nil {
   264  				return nil, err
   265  			}
   266  
   267  			if sc.handler != nil {
   268  				return nil, fmt.Errorf("server handler already set to %s", sc.handlerName)
   269  			}
   270  
   271  			name := args[0]
   272  			h, ok := sc.server.vcsHandlers[name]
   273  			if !ok {
   274  				return nil, fmt.Errorf("unrecognized VCS %q", name)
   275  			}
   276  			sc.handlerName = name
   277  			if !h.Available() {
   278  				return nil, ServerNotInstalledError{name}
   279  			}
   280  
   281  			dir := st.Getwd()
   282  			if len(args) >= 2 {
   283  				dir = st.Path(args[1])
   284  			}
   285  			sc.handler, err = h.Handler(dir, st.Environ(), sc.server.logger)
   286  			return nil, err
   287  		})
   288  }
   289  
   290  func scriptModzip() script.Cmd {
   291  	return script.Command(
   292  		script.CmdUsage{
   293  			Summary: "create a Go module zip file from a directory",
   294  			Args:    "zipfile path@version dir",
   295  		},
   296  		func(st *script.State, args ...string) (wait script.WaitFunc, err error) {
   297  			if len(args) != 3 {
   298  				return nil, script.ErrUsage
   299  			}
   300  			zipPath := st.Path(args[0])
   301  			mPath, version, ok := strings.Cut(args[1], "@")
   302  			if !ok {
   303  				return nil, script.ErrUsage
   304  			}
   305  			dir := st.Path(args[2])
   306  
   307  			if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil {
   308  				return nil, err
   309  			}
   310  			f, err := os.Create(zipPath)
   311  			if err != nil {
   312  				return nil, err
   313  			}
   314  			defer func() {
   315  				if closeErr := f.Close(); err == nil {
   316  					err = closeErr
   317  				}
   318  			}()
   319  
   320  			return nil, zip.CreateFromDir(f, module.Version{Path: mPath, Version: version}, dir)
   321  		})
   322  }
   323  
   324  func scriptUnquote() script.Cmd {
   325  	return script.Command(
   326  		script.CmdUsage{
   327  			Summary: "unquote the argument as a Go string",
   328  			Args:    "string",
   329  		},
   330  		func(st *script.State, args ...string) (script.WaitFunc, error) {
   331  			if len(args) != 1 {
   332  				return nil, script.ErrUsage
   333  			}
   334  
   335  			s, err := strconv.Unquote(`"` + args[0] + `"`)
   336  			if err != nil {
   337  				return nil, err
   338  			}
   339  
   340  			wait := func(*script.State) (stdout, stderr string, err error) {
   341  				return s, "", nil
   342  			}
   343  			return wait, nil
   344  		})
   345  }
   346  

View as plain text