code.gitea.io/gitea@v1.19.3/modules/git/command.go (about) 1 // Copyright 2015 The Gogs Authors. All rights reserved. 2 // Copyright 2016 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package git 6 7 import ( 8 "bytes" 9 "context" 10 "errors" 11 "fmt" 12 "io" 13 "os" 14 "os/exec" 15 "strings" 16 "time" 17 "unsafe" 18 19 "code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions 20 "code.gitea.io/gitea/modules/log" 21 "code.gitea.io/gitea/modules/process" 22 "code.gitea.io/gitea/modules/util" 23 ) 24 25 // TrustedCmdArgs returns the trusted arguments for git command. 26 // It's mainly for passing user-provided and trusted arguments to git command 27 // In most cases, it shouldn't be used. Use AddXxx function instead 28 type TrustedCmdArgs []internal.CmdArg 29 30 var ( 31 // globalCommandArgs global command args for external package setting 32 globalCommandArgs TrustedCmdArgs 33 34 // defaultCommandExecutionTimeout default command execution timeout duration 35 defaultCommandExecutionTimeout = 360 * time.Second 36 ) 37 38 // DefaultLocale is the default LC_ALL to run git commands in. 39 const DefaultLocale = "C" 40 41 // Command represents a command with its subcommands or arguments. 42 type Command struct { 43 name string 44 args []string 45 parentContext context.Context 46 desc string 47 globalArgsLength int 48 brokenArgs []string 49 } 50 51 func (c *Command) String() string { 52 if len(c.args) == 0 { 53 return c.name 54 } 55 return fmt.Sprintf("%s %s", c.name, strings.Join(c.args, " ")) 56 } 57 58 // NewCommand creates and returns a new Git Command based on given command and arguments. 59 // Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead. 60 func NewCommand(ctx context.Context, args ...internal.CmdArg) *Command { 61 // Make an explicit copy of globalCommandArgs, otherwise append might overwrite it 62 cargs := make([]string, 0, len(globalCommandArgs)+len(args)) 63 for _, arg := range globalCommandArgs { 64 cargs = append(cargs, string(arg)) 65 } 66 for _, arg := range args { 67 cargs = append(cargs, string(arg)) 68 } 69 return &Command{ 70 name: GitExecutable, 71 args: cargs, 72 parentContext: ctx, 73 globalArgsLength: len(globalCommandArgs), 74 } 75 } 76 77 // NewCommandContextNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args 78 // Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead. 79 func NewCommandContextNoGlobals(ctx context.Context, args ...internal.CmdArg) *Command { 80 cargs := make([]string, 0, len(args)) 81 for _, arg := range args { 82 cargs = append(cargs, string(arg)) 83 } 84 return &Command{ 85 name: GitExecutable, 86 args: cargs, 87 parentContext: ctx, 88 } 89 } 90 91 // SetParentContext sets the parent context for this command 92 func (c *Command) SetParentContext(ctx context.Context) *Command { 93 c.parentContext = ctx 94 return c 95 } 96 97 // SetDescription sets the description for this command which be returned on c.String() 98 func (c *Command) SetDescription(desc string) *Command { 99 c.desc = desc 100 return c 101 } 102 103 // isSafeArgumentValue checks if the argument is safe to be used as a value (not an option) 104 func isSafeArgumentValue(s string) bool { 105 return s == "" || s[0] != '-' 106 } 107 108 // isValidArgumentOption checks if the argument is a valid option (starting with '-'). 109 // It doesn't check whether the option is supported or not 110 func isValidArgumentOption(s string) bool { 111 return s != "" && s[0] == '-' 112 } 113 114 // AddArguments adds new git arguments (option/value) to the command. It only accepts string literals, or trusted CmdArg. 115 // Type CmdArg is in the internal package, so it can not be used outside of this package directly, 116 // it makes sure that user-provided arguments won't cause RCE risks. 117 // User-provided arguments should be passed by other AddXxx functions 118 func (c *Command) AddArguments(args ...internal.CmdArg) *Command { 119 for _, arg := range args { 120 c.args = append(c.args, string(arg)) 121 } 122 return c 123 } 124 125 // AddOptionValues adds a new option with a list of non-option values 126 // For example: AddOptionValues("--opt", val) means 2 arguments: {"--opt", val}. 127 // The values are treated as dynamic argument values. It equals to: AddArguments("--opt") then AddDynamicArguments(val). 128 func (c *Command) AddOptionValues(opt internal.CmdArg, args ...string) *Command { 129 if !isValidArgumentOption(string(opt)) { 130 c.brokenArgs = append(c.brokenArgs, string(opt)) 131 return c 132 } 133 c.args = append(c.args, string(opt)) 134 c.AddDynamicArguments(args...) 135 return c 136 } 137 138 // AddOptionFormat adds a new option with a format string and arguments 139 // For example: AddOptionFormat("--opt=%s %s", val1, val2) means 1 argument: {"--opt=val1 val2"}. 140 func (c *Command) AddOptionFormat(opt string, args ...any) *Command { 141 if !isValidArgumentOption(opt) { 142 c.brokenArgs = append(c.brokenArgs, opt) 143 return c 144 } 145 // a quick check to make sure the format string matches the number of arguments, to find low-level mistakes ASAP 146 if strings.Count(strings.ReplaceAll(opt, "%%", ""), "%") != len(args) { 147 c.brokenArgs = append(c.brokenArgs, opt) 148 return c 149 } 150 s := fmt.Sprintf(opt, args...) 151 c.args = append(c.args, s) 152 return c 153 } 154 155 // AddDynamicArguments adds new dynamic argument values to the command. 156 // The arguments may come from user input and can not be trusted, so no leading '-' is allowed to avoid passing options. 157 // TODO: in the future, this function can be renamed to AddArgumentValues 158 func (c *Command) AddDynamicArguments(args ...string) *Command { 159 for _, arg := range args { 160 if !isSafeArgumentValue(arg) { 161 c.brokenArgs = append(c.brokenArgs, arg) 162 } 163 } 164 if len(c.brokenArgs) != 0 { 165 return c 166 } 167 c.args = append(c.args, args...) 168 return c 169 } 170 171 // AddDashesAndList adds the "--" and then add the list as arguments, it's usually for adding file list 172 // At the moment, this function can be only called once, maybe in future it can be refactored to support multiple calls (if necessary) 173 func (c *Command) AddDashesAndList(list ...string) *Command { 174 c.args = append(c.args, "--") 175 // Some old code also checks `arg != ""`, IMO it's not necessary. 176 // If the check is needed, the list should be prepared before the call to this function 177 c.args = append(c.args, list...) 178 return c 179 } 180 181 // ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs 182 // In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead 183 func ToTrustedCmdArgs(args []string) TrustedCmdArgs { 184 ret := make(TrustedCmdArgs, len(args)) 185 for i, arg := range args { 186 ret[i] = internal.CmdArg(arg) 187 } 188 return ret 189 } 190 191 // RunOpts represents parameters to run the command. If UseContextTimeout is specified, then Timeout is ignored. 192 type RunOpts struct { 193 Env []string 194 Timeout time.Duration 195 UseContextTimeout bool 196 Dir string 197 Stdout, Stderr io.Writer 198 Stdin io.Reader 199 PipelineFunc func(context.Context, context.CancelFunc) error 200 } 201 202 func commonBaseEnvs() []string { 203 // at the moment, do not set "GIT_CONFIG_NOSYSTEM", users may have put some configs like "receive.certNonceSeed" in it 204 envs := []string{ 205 "HOME=" + HomeDir(), // make Gitea use internal git config only, to prevent conflicts with user's git config 206 "GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace) 207 } 208 209 // some environment variables should be passed to git command 210 passThroughEnvKeys := []string{ 211 "GNUPGHOME", // git may call gnupg to do commit signing 212 } 213 for _, key := range passThroughEnvKeys { 214 if val, ok := os.LookupEnv(key); ok { 215 envs = append(envs, key+"="+val) 216 } 217 } 218 return envs 219 } 220 221 // CommonGitCmdEnvs returns the common environment variables for a "git" command. 222 func CommonGitCmdEnvs() []string { 223 return append(commonBaseEnvs(), []string{ 224 "LC_ALL=" + DefaultLocale, 225 "GIT_TERMINAL_PROMPT=0", // avoid prompting for credentials interactively, supported since git v2.3 226 }...) 227 } 228 229 // CommonCmdServEnvs is like CommonGitCmdEnvs, but it only returns minimal required environment variables for the "gitea serv" command 230 func CommonCmdServEnvs() []string { 231 return commonBaseEnvs() 232 } 233 234 var ErrBrokenCommand = errors.New("git command is broken") 235 236 // Run runs the command with the RunOpts 237 func (c *Command) Run(opts *RunOpts) error { 238 if len(c.brokenArgs) != 0 { 239 log.Error("git command is broken: %s, broken args: %s", c.String(), strings.Join(c.brokenArgs, " ")) 240 return ErrBrokenCommand 241 } 242 if opts == nil { 243 opts = &RunOpts{} 244 } 245 246 // We must not change the provided options 247 timeout := opts.Timeout 248 if timeout <= 0 { 249 timeout = defaultCommandExecutionTimeout 250 } 251 252 if len(opts.Dir) == 0 { 253 log.Debug("%s", c) 254 } else { 255 log.Debug("%s: %v", opts.Dir, c) 256 } 257 258 desc := c.desc 259 if desc == "" { 260 args := c.args[c.globalArgsLength:] 261 var argSensitiveURLIndexes []int 262 for i, arg := range c.args { 263 if strings.Contains(arg, "://") && strings.Contains(arg, "@") { 264 argSensitiveURLIndexes = append(argSensitiveURLIndexes, i) 265 } 266 } 267 if len(argSensitiveURLIndexes) > 0 { 268 args = make([]string, len(c.args)) 269 copy(args, c.args) 270 for _, urlArgIndex := range argSensitiveURLIndexes { 271 args[urlArgIndex] = util.SanitizeCredentialURLs(args[urlArgIndex]) 272 } 273 } 274 desc = fmt.Sprintf("%s %s [repo_path: %s]", c.name, strings.Join(args, " "), opts.Dir) 275 } 276 277 var ctx context.Context 278 var cancel context.CancelFunc 279 var finished context.CancelFunc 280 281 if opts.UseContextTimeout { 282 ctx, cancel, finished = process.GetManager().AddContext(c.parentContext, desc) 283 } else { 284 ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, timeout, desc) 285 } 286 defer finished() 287 288 cmd := exec.CommandContext(ctx, c.name, c.args...) 289 if opts.Env == nil { 290 cmd.Env = os.Environ() 291 } else { 292 cmd.Env = opts.Env 293 } 294 295 process.SetSysProcAttribute(cmd) 296 cmd.Env = append(cmd.Env, CommonGitCmdEnvs()...) 297 cmd.Dir = opts.Dir 298 cmd.Stdout = opts.Stdout 299 cmd.Stderr = opts.Stderr 300 cmd.Stdin = opts.Stdin 301 if err := cmd.Start(); err != nil { 302 return err 303 } 304 305 if opts.PipelineFunc != nil { 306 err := opts.PipelineFunc(ctx, cancel) 307 if err != nil { 308 cancel() 309 _ = cmd.Wait() 310 return err 311 } 312 } 313 314 if err := cmd.Wait(); err != nil && ctx.Err() != context.DeadlineExceeded { 315 return err 316 } 317 318 return ctx.Err() 319 } 320 321 type RunStdError interface { 322 error 323 Unwrap() error 324 Stderr() string 325 IsExitCode(code int) bool 326 } 327 328 type runStdError struct { 329 err error 330 stderr string 331 errMsg string 332 } 333 334 func (r *runStdError) Error() string { 335 // the stderr must be in the returned error text, some code only checks `strings.Contains(err.Error(), "git error")` 336 if r.errMsg == "" { 337 r.errMsg = ConcatenateError(r.err, r.stderr).Error() 338 } 339 return r.errMsg 340 } 341 342 func (r *runStdError) Unwrap() error { 343 return r.err 344 } 345 346 func (r *runStdError) Stderr() string { 347 return r.stderr 348 } 349 350 func (r *runStdError) IsExitCode(code int) bool { 351 var exitError *exec.ExitError 352 if errors.As(r.err, &exitError) { 353 return exitError.ExitCode() == code 354 } 355 return false 356 } 357 358 func bytesToString(b []byte) string { 359 return *(*string)(unsafe.Pointer(&b)) // that's what Golang's strings.Builder.String() does (go/src/strings/builder.go) 360 } 361 362 // RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr). 363 func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) { 364 stdoutBytes, stderrBytes, err := c.RunStdBytes(opts) 365 stdout = bytesToString(stdoutBytes) 366 stderr = bytesToString(stderrBytes) 367 if err != nil { 368 return stdout, stderr, &runStdError{err: err, stderr: stderr} 369 } 370 // even if there is no err, there could still be some stderr output, so we just return stdout/stderr as they are 371 return stdout, stderr, nil 372 } 373 374 // RunStdBytes runs the command with options and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr). 375 func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { 376 if opts == nil { 377 opts = &RunOpts{} 378 } 379 if opts.Stdout != nil || opts.Stderr != nil { 380 // we must panic here, otherwise there would be bugs if developers set Stdin/Stderr by mistake, and it would be very difficult to debug 381 panic("stdout and stderr field must be nil when using RunStdBytes") 382 } 383 stdoutBuf := &bytes.Buffer{} 384 stderrBuf := &bytes.Buffer{} 385 386 // We must not change the provided options as it could break future calls - therefore make a copy. 387 newOpts := &RunOpts{ 388 Env: opts.Env, 389 Timeout: opts.Timeout, 390 UseContextTimeout: opts.UseContextTimeout, 391 Dir: opts.Dir, 392 Stdout: stdoutBuf, 393 Stderr: stderrBuf, 394 Stdin: opts.Stdin, 395 PipelineFunc: opts.PipelineFunc, 396 } 397 398 err := c.Run(newOpts) 399 stderr = stderrBuf.Bytes() 400 if err != nil { 401 return nil, stderr, &runStdError{err: err, stderr: bytesToString(stderr)} 402 } 403 // even if there is no err, there could still be some stderr output 404 return stdoutBuf.Bytes(), stderr, nil 405 } 406 407 // AllowLFSFiltersArgs return globalCommandArgs with lfs filter, it should only be used for tests 408 func AllowLFSFiltersArgs() TrustedCmdArgs { 409 // Now here we should explicitly allow lfs filters to run 410 filteredLFSGlobalArgs := make(TrustedCmdArgs, len(globalCommandArgs)) 411 j := 0 412 for _, arg := range globalCommandArgs { 413 if strings.Contains(string(arg), "lfs") { 414 j-- 415 } else { 416 filteredLFSGlobalArgs[j] = arg 417 j++ 418 } 419 } 420 return filteredLFSGlobalArgs[:j] 421 }