github.com/neohugo/neohugo@v0.123.8/common/hexec/exec.go (about)

     1  // Copyright 2020 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package hexec
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"os/exec"
    24  	"regexp"
    25  	"strings"
    26  
    27  	"github.com/cli/safeexec"
    28  	"github.com/neohugo/neohugo/config"
    29  	"github.com/neohugo/neohugo/config/security"
    30  )
    31  
    32  var WithDir = func(dir string) func(c *commandeer) {
    33  	return func(c *commandeer) {
    34  		c.dir = dir
    35  	}
    36  }
    37  
    38  var WithContext = func(ctx context.Context) func(c *commandeer) {
    39  	return func(c *commandeer) {
    40  		c.ctx = ctx
    41  	}
    42  }
    43  
    44  var WithStdout = func(w io.Writer) func(c *commandeer) {
    45  	return func(c *commandeer) {
    46  		c.stdout = w
    47  	}
    48  }
    49  
    50  var WithStderr = func(w io.Writer) func(c *commandeer) {
    51  	return func(c *commandeer) {
    52  		c.stderr = w
    53  	}
    54  }
    55  
    56  var WithStdin = func(r io.Reader) func(c *commandeer) {
    57  	return func(c *commandeer) {
    58  		c.stdin = r
    59  	}
    60  }
    61  
    62  var WithEnviron = func(env []string) func(c *commandeer) {
    63  	return func(c *commandeer) {
    64  		setOrAppend := func(s string) {
    65  			k1, _ := config.SplitEnvVar(s)
    66  			var found bool
    67  			for i, v := range c.env {
    68  				k2, _ := config.SplitEnvVar(v)
    69  				if k1 == k2 {
    70  					found = true
    71  					c.env[i] = s
    72  				}
    73  			}
    74  
    75  			if !found {
    76  				c.env = append(c.env, s)
    77  			}
    78  		}
    79  
    80  		for _, s := range env {
    81  			setOrAppend(s)
    82  		}
    83  	}
    84  }
    85  
    86  // New creates a new Exec using the provided security config.
    87  func New(cfg security.Config) *Exec {
    88  	var baseEnviron []string
    89  	for _, v := range os.Environ() {
    90  		k, _ := config.SplitEnvVar(v)
    91  		if cfg.Exec.OsEnv.Accept(k) {
    92  			baseEnviron = append(baseEnviron, v)
    93  		}
    94  	}
    95  
    96  	return &Exec{
    97  		sc:          cfg,
    98  		baseEnviron: baseEnviron,
    99  	}
   100  }
   101  
   102  // IsNotFound reports whether this is an error about a binary not found.
   103  func IsNotFound(err error) bool {
   104  	var notFoundErr *NotFoundError
   105  	return errors.As(err, &notFoundErr)
   106  }
   107  
   108  // SafeCommand is a wrapper around os/exec Command which uses a LookPath
   109  // implementation that does not search in current directory before looking in PATH.
   110  // See https://github.com/cli/safeexec and the linked issues.
   111  func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
   112  	bin, err := safeexec.LookPath(name)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	return exec.Command(bin, arg...), nil
   118  }
   119  
   120  // Exec enforces a security policy for commands run via os/exec.
   121  type Exec struct {
   122  	sc security.Config
   123  
   124  	// os.Environ filtered by the Exec.OsEnviron whitelist filter.
   125  	baseEnviron []string
   126  }
   127  
   128  // New will fail if name is not allowed according to the configured security policy.
   129  // Else a configured Runner will be returned ready to be Run.
   130  func (e *Exec) New(name string, arg ...any) (Runner, error) {
   131  	if err := e.sc.CheckAllowedExec(name); err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	env := make([]string, len(e.baseEnviron))
   136  	copy(env, e.baseEnviron)
   137  
   138  	cm := &commandeer{
   139  		name: name,
   140  		env:  env,
   141  	}
   142  
   143  	return cm.command(arg...)
   144  }
   145  
   146  // Npx is a convenience method to create a Runner running npx --no-install <name> <args.
   147  func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
   148  	arg = append(arg[:0], append([]any{"--no-install", name}, arg[0:]...)...)
   149  	return e.New("npx", arg...)
   150  }
   151  
   152  // Sec returns the security policies this Exec is configured with.
   153  func (e *Exec) Sec() security.Config {
   154  	return e.sc
   155  }
   156  
   157  type NotFoundError struct {
   158  	name string
   159  }
   160  
   161  func (e *NotFoundError) Error() string {
   162  	return fmt.Sprintf("binary with name %q not found", e.name)
   163  }
   164  
   165  // Runner wraps a *os.Cmd.
   166  type Runner interface {
   167  	Run() error
   168  	StdinPipe() (io.WriteCloser, error)
   169  }
   170  
   171  type cmdWrapper struct {
   172  	name string
   173  	c    *exec.Cmd
   174  
   175  	outerr *bytes.Buffer
   176  }
   177  
   178  var notFoundRe = regexp.MustCompile(`(?s)not found:|could not determine executable`)
   179  
   180  func (c *cmdWrapper) Run() error {
   181  	err := c.c.Run()
   182  	if err == nil {
   183  		return nil
   184  	}
   185  	if notFoundRe.MatchString(c.outerr.String()) {
   186  		return &NotFoundError{name: c.name}
   187  	}
   188  	return fmt.Errorf("failed to execute binary %q with args %v: %s", c.name, c.c.Args[1:], c.outerr.String())
   189  }
   190  
   191  func (c *cmdWrapper) StdinPipe() (io.WriteCloser, error) {
   192  	return c.c.StdinPipe()
   193  }
   194  
   195  type commandeer struct {
   196  	stdout io.Writer
   197  	stderr io.Writer
   198  	stdin  io.Reader
   199  	dir    string
   200  	ctx    context.Context
   201  
   202  	name string
   203  	env  []string
   204  }
   205  
   206  func (c *commandeer) command(arg ...any) (*cmdWrapper, error) {
   207  	if c == nil {
   208  		return nil, nil
   209  	}
   210  
   211  	var args []string
   212  	for _, a := range arg {
   213  		switch v := a.(type) {
   214  		case string:
   215  			args = append(args, v)
   216  		case func(*commandeer):
   217  			v(c)
   218  		default:
   219  			return nil, fmt.Errorf("invalid argument to command: %T", a)
   220  		}
   221  	}
   222  
   223  	bin, err := safeexec.LookPath(c.name)
   224  	if err != nil {
   225  		return nil, &NotFoundError{
   226  			name: c.name,
   227  		}
   228  	}
   229  
   230  	outerr := &bytes.Buffer{}
   231  	if c.stderr == nil {
   232  		c.stderr = outerr
   233  	} else {
   234  		c.stderr = io.MultiWriter(c.stderr, outerr)
   235  	}
   236  
   237  	var cmd *exec.Cmd
   238  
   239  	if c.ctx != nil {
   240  		cmd = exec.CommandContext(c.ctx, bin, args...)
   241  	} else {
   242  		cmd = exec.Command(bin, args...)
   243  	}
   244  
   245  	cmd.Stdin = c.stdin
   246  	cmd.Stderr = c.stderr
   247  	cmd.Stdout = c.stdout
   248  	cmd.Env = c.env
   249  	cmd.Dir = c.dir
   250  
   251  	return &cmdWrapper{outerr: outerr, c: cmd, name: c.name}, nil
   252  }
   253  
   254  // InPath reports whether binaryName is in $PATH.
   255  func InPath(binaryName string) bool {
   256  	if strings.Contains(binaryName, "/") {
   257  		panic("binary name should not contain any slash")
   258  	}
   259  	_, err := safeexec.LookPath(binaryName)
   260  	return err == nil
   261  }
   262  
   263  // LookPath finds the path to binaryName in $PATH.
   264  // Returns "" if not found.
   265  func LookPath(binaryName string) string {
   266  	if strings.Contains(binaryName, "/") {
   267  		panic("binary name should not contain any slash")
   268  	}
   269  	s, err := safeexec.LookPath(binaryName)
   270  	if err != nil {
   271  		return ""
   272  	}
   273  	return s
   274  }