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, ¬FoundErr) 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 }