1// Package common implements the git pack protocol with a pluggable transport. 2// This is a low-level package to implement new transports. Use a concrete 3// implementation instead (e.g. http, file, ssh). 4// 5// A simple example of usage can be found in the file package. 6package common 7 8import ( 9 "bufio" 10 "context" 11 "errors" 12 "fmt" 13 "io" 14 stdioutil "io/ioutil" 15 "strings" 16 "time" 17 18 "github.com/go-git/go-git/v5/plumbing/format/pktline" 19 "github.com/go-git/go-git/v5/plumbing/protocol/packp" 20 "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" 21 "github.com/go-git/go-git/v5/plumbing/protocol/packp/sideband" 22 "github.com/go-git/go-git/v5/plumbing/transport" 23 "github.com/go-git/go-git/v5/utils/ioutil" 24) 25 26const ( 27 readErrorSecondsTimeout = 10 28) 29 30var ( 31 ErrTimeoutExceeded = errors.New("timeout exceeded") 32) 33 34// Commander creates Command instances. This is the main entry point for 35// transport implementations. 36type Commander interface { 37 // Command creates a new Command for the given git command and 38 // endpoint. cmd can be git-upload-pack or git-receive-pack. An 39 // error should be returned if the endpoint is not supported or the 40 // command cannot be created (e.g. binary does not exist, connection 41 // cannot be established). 42 Command(cmd string, ep *transport.Endpoint, auth transport.AuthMethod) (Command, error) 43} 44 45// Command is used for a single command execution. 46// This interface is modeled after exec.Cmd and ssh.Session in the standard 47// library. 48type Command interface { 49 // StderrPipe returns a pipe that will be connected to the command's 50 // standard error when the command starts. It should not be called after 51 // Start. 52 StderrPipe() (io.Reader, error) 53 // StdinPipe returns a pipe that will be connected to the command's 54 // standard input when the command starts. It should not be called after 55 // Start. The pipe should be closed when no more input is expected. 56 StdinPipe() (io.WriteCloser, error) 57 // StdoutPipe returns a pipe that will be connected to the command's 58 // standard output when the command starts. It should not be called after 59 // Start. 60 StdoutPipe() (io.Reader, error) 61 // Start starts the specified command. It does not wait for it to 62 // complete. 63 Start() error 64 // Close closes the command and releases any resources used by it. It 65 // will block until the command exits. 66 Close() error 67} 68 69// CommandKiller expands the Command interface, enabling it for being killed. 70type CommandKiller interface { 71 // Kill and close the session whatever the state it is. It will block until 72 // the command is terminated. 73 Kill() error 74} 75 76type client struct { 77 cmdr Commander 78} 79 80// NewClient creates a new client using the given Commander. 81func NewClient(runner Commander) transport.Transport { 82 return &client{runner} 83} 84 85// NewUploadPackSession creates a new UploadPackSession. 86func (c *client) NewUploadPackSession(ep *transport.Endpoint, auth transport.AuthMethod) ( 87 transport.UploadPackSession, error) { 88 89 return c.newSession(transport.UploadPackServiceName, ep, auth) 90} 91 92// NewReceivePackSession creates a new ReceivePackSession. 93func (c *client) NewReceivePackSession(ep *transport.Endpoint, auth transport.AuthMethod) ( 94 transport.ReceivePackSession, error) { 95 96 return c.newSession(transport.ReceivePackServiceName, ep, auth) 97} 98 99type session struct { 100 Stdin io.WriteCloser 101 Stdout io.Reader 102 Command Command 103 104 isReceivePack bool 105 advRefs *packp.AdvRefs 106 packRun bool 107 finished bool 108 firstErrLine chan string 109} 110 111func (c *client) newSession(s string, ep *transport.Endpoint, auth transport.AuthMethod) (*session, error) { 112 cmd, err := c.cmdr.Command(s, ep, auth) 113 if err != nil { 114 return nil, err 115 } 116 117 stdin, err := cmd.StdinPipe() 118 if err != nil { 119 return nil, err 120 } 121 122 stdout, err := cmd.StdoutPipe() 123 if err != nil { 124 return nil, err 125 } 126 127 stderr, err := cmd.StderrPipe() 128 if err != nil { 129 return nil, err 130 } 131 132 if err := cmd.Start(); err != nil { 133 return nil, err 134 } 135 136 return &session{ 137 Stdin: stdin, 138 Stdout: stdout, 139 Command: cmd, 140 firstErrLine: c.listenFirstError(stderr), 141 isReceivePack: s == transport.ReceivePackServiceName, 142 }, nil 143} 144 145func (c *client) listenFirstError(r io.Reader) chan string { 146 if r == nil { 147 return nil 148 } 149 150 errLine := make(chan string, 1) 151 go func() { 152 s := bufio.NewScanner(r) 153 if s.Scan() { 154 errLine <- s.Text() 155 } else { 156 close(errLine) 157 } 158 159 _, _ = io.Copy(stdioutil.Discard, r) 160 }() 161 162 return errLine 163} 164 165// AdvertisedReferences retrieves the advertised references from the server. 166func (s *session) AdvertisedReferences() (*packp.AdvRefs, error) { 167 if s.advRefs != nil { 168 return s.advRefs, nil 169 } 170 171 ar := packp.NewAdvRefs() 172 if err := ar.Decode(s.Stdout); err != nil { 173 if err := s.handleAdvRefDecodeError(err); err != nil { 174 return nil, err 175 } 176 } 177 178 // Some servers like jGit, announce capabilities instead of returning an 179 // packp message with a flush. This verifies that we received a empty 180 // adv-refs, even it contains capabilities. 181 if !s.isReceivePack && ar.IsEmpty() { 182 return nil, transport.ErrEmptyRemoteRepository 183 } 184 185 transport.FilterUnsupportedCapabilities(ar.Capabilities) 186 s.advRefs = ar 187 return ar, nil 188} 189 190func (s *session) handleAdvRefDecodeError(err error) error { 191 // If repository is not found, we get empty stdout and server writes an 192 // error to stderr. 193 if err == packp.ErrEmptyInput { 194 s.finished = true 195 if err := s.checkNotFoundError(); err != nil { 196 return err 197 } 198 199 return io.ErrUnexpectedEOF 200 } 201 202 // For empty (but existing) repositories, we get empty advertised-references 203 // message. But valid. That is, it includes at least a flush. 204 if err == packp.ErrEmptyAdvRefs { 205 // Empty repositories are valid for git-receive-pack. 206 if s.isReceivePack { 207 return nil 208 } 209 210 if err := s.finish(); err != nil { 211 return err 212 } 213 214 return transport.ErrEmptyRemoteRepository 215 } 216 217 // Some server sends the errors as normal content (git protocol), so when 218 // we try to decode it fails, we need to check the content of it, to detect 219 // not found errors 220 if uerr, ok := err.(*packp.ErrUnexpectedData); ok { 221 if isRepoNotFoundError(string(uerr.Data)) { 222 return transport.ErrRepositoryNotFound 223 } 224 } 225 226 return err 227} 228 229// UploadPack performs a request to the server to fetch a packfile. A reader is 230// returned with the packfile content. The reader must be closed after reading. 231func (s *session) UploadPack(ctx context.Context, req *packp.UploadPackRequest) (*packp.UploadPackResponse, error) { 232 if req.IsEmpty() { 233 return nil, transport.ErrEmptyUploadPackRequest 234 } 235 236 if err := req.Validate(); err != nil { 237 return nil, err 238 } 239 240 if _, err := s.AdvertisedReferences(); err != nil { 241 return nil, err 242 } 243 244 s.packRun = true 245 246 in := s.StdinContext(ctx) 247 out := s.StdoutContext(ctx) 248 249 if err := uploadPack(in, out, req); err != nil { 250 return nil, err 251 } 252 253 r, err := ioutil.NonEmptyReader(out) 254 if err == ioutil.ErrEmptyReader { 255 if c, ok := s.Stdout.(io.Closer); ok { 256 _ = c.Close() 257 } 258 259 return nil, transport.ErrEmptyUploadPackRequest 260 } 261 262 if err != nil { 263 return nil, err 264 } 265 266 rc := ioutil.NewReadCloser(r, s) 267 return DecodeUploadPackResponse(rc, req) 268} 269 270func (s *session) StdinContext(ctx context.Context) io.WriteCloser { 271 return ioutil.NewWriteCloserOnError( 272 ioutil.NewContextWriteCloser(ctx, s.Stdin), 273 s.onError, 274 ) 275} 276 277func (s *session) StdoutContext(ctx context.Context) io.Reader { 278 return ioutil.NewReaderOnError( 279 ioutil.NewContextReader(ctx, s.Stdout), 280 s.onError, 281 ) 282} 283 284func (s *session) onError(err error) { 285 if k, ok := s.Command.(CommandKiller); ok { 286 _ = k.Kill() 287 } 288 289 _ = s.Close() 290} 291 292func (s *session) ReceivePack(ctx context.Context, req *packp.ReferenceUpdateRequest) (*packp.ReportStatus, error) { 293 if _, err := s.AdvertisedReferences(); err != nil { 294 return nil, err 295 } 296 297 s.packRun = true 298 299 w := s.StdinContext(ctx) 300 if err := req.Encode(w); err != nil { 301 return nil, err 302 } 303 304 if err := w.Close(); err != nil { 305 return nil, err 306 } 307 308 if !req.Capabilities.Supports(capability.ReportStatus) { 309 // If we don't have report-status, we can only 310 // check return value error. 311 return nil, s.Command.Close() 312 } 313 314 r := s.StdoutContext(ctx) 315 316 var d *sideband.Demuxer 317 if req.Capabilities.Supports(capability.Sideband64k) { 318 d = sideband.NewDemuxer(sideband.Sideband64k, r) 319 } else if req.Capabilities.Supports(capability.Sideband) { 320 d = sideband.NewDemuxer(sideband.Sideband, r) 321 } 322 if d != nil { 323 d.Progress = req.Progress 324 r = d 325 } 326 327 report := packp.NewReportStatus() 328 if err := report.Decode(r); err != nil { 329 return nil, err 330 } 331 332 if err := report.Error(); err != nil { 333 defer s.Close() 334 return report, err 335 } 336 337 return report, s.Command.Close() 338} 339 340func (s *session) finish() error { 341 if s.finished { 342 return nil 343 } 344 345 s.finished = true 346 347 // If we did not run a upload/receive-pack, we close the connection 348 // gracefully by sending a flush packet to the server. If the server 349 // operates correctly, it will exit with status 0. 350 if !s.packRun { 351 _, err := s.Stdin.Write(pktline.FlushPkt) 352 return err 353 } 354 355 return nil 356} 357 358func (s *session) Close() (err error) { 359 err = s.finish() 360 361 defer ioutil.CheckClose(s.Command, &err) 362 return 363} 364 365func (s *session) checkNotFoundError() error { 366 t := time.NewTicker(time.Second * readErrorSecondsTimeout) 367 defer t.Stop() 368 369 select { 370 case <-t.C: 371 return ErrTimeoutExceeded 372 case line, ok := <-s.firstErrLine: 373 if !ok { 374 return nil 375 } 376 377 if isRepoNotFoundError(line) { 378 return transport.ErrRepositoryNotFound 379 } 380 381 return fmt.Errorf("unknown error: %s", line) 382 } 383} 384 385var ( 386 githubRepoNotFoundErr = "ERROR: Repository not found." 387 bitbucketRepoNotFoundErr = "conq: repository does not exist." 388 localRepoNotFoundErr = "does not appear to be a git repository" 389 gitProtocolNotFoundErr = "ERR \n Repository not found." 390 gitProtocolNoSuchErr = "ERR no such repository" 391 gitProtocolAccessDeniedErr = "ERR access denied" 392 gogsAccessDeniedErr = "Gogs: Repository does not exist or you do not have access" 393) 394 395func isRepoNotFoundError(s string) bool { 396 if strings.HasPrefix(s, githubRepoNotFoundErr) { 397 return true 398 } 399 400 if strings.HasPrefix(s, bitbucketRepoNotFoundErr) { 401 return true 402 } 403 404 if strings.HasSuffix(s, localRepoNotFoundErr) { 405 return true 406 } 407 408 if strings.HasPrefix(s, gitProtocolNotFoundErr) { 409 return true 410 } 411 412 if strings.HasPrefix(s, gitProtocolNoSuchErr) { 413 return true 414 } 415 416 if strings.HasPrefix(s, gitProtocolAccessDeniedErr) { 417 return true 418 } 419 420 if strings.HasPrefix(s, gogsAccessDeniedErr) { 421 return true 422 } 423 424 return false 425} 426 427var ( 428 nak = []byte("NAK") 429 eol = []byte("\n") 430) 431 432// uploadPack implements the git-upload-pack protocol. 433func uploadPack(w io.WriteCloser, r io.Reader, req *packp.UploadPackRequest) error { 434 // TODO support multi_ack mode 435 // TODO support multi_ack_detailed mode 436 // TODO support acks for common objects 437 // TODO build a proper state machine for all these processing options 438 439 if err := req.UploadRequest.Encode(w); err != nil { 440 return fmt.Errorf("sending upload-req message: %s", err) 441 } 442 443 if err := req.UploadHaves.Encode(w, true); err != nil { 444 return fmt.Errorf("sending haves message: %s", err) 445 } 446 447 if err := sendDone(w); err != nil { 448 return fmt.Errorf("sending done message: %s", err) 449 } 450 451 if err := w.Close(); err != nil { 452 return fmt.Errorf("closing input: %s", err) 453 } 454 455 return nil 456} 457 458func sendDone(w io.Writer) error { 459 e := pktline.NewEncoder(w) 460 461 return e.Encodef("done\n") 462} 463 464// DecodeUploadPackResponse decodes r into a new packp.UploadPackResponse 465func DecodeUploadPackResponse(r io.ReadCloser, req *packp.UploadPackRequest) ( 466 *packp.UploadPackResponse, error, 467) { 468 res := packp.NewUploadPackResponse(req) 469 if err := res.Decode(r); err != nil { 470 return nil, fmt.Errorf("error decoding upload-pack response: %s", err) 471 } 472 473 return res, nil 474} 475