hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

exec.go (6073B)

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