├── .gitignore ├── go.mod ├── doc.go ├── testing ├── end2end │ ├── params.go │ ├── lsremote_test.go │ ├── end2end_main.go │ ├── fetch_test.go │ └── push_test.go ├── testproxy │ └── testproxy.go ├── cgit_server.go ├── testserver │ └── testserver.go └── http_proxy_server.go ├── CONTRIBUTING.md ├── chunked_writer.go ├── sideband.go ├── README.md ├── v2resp.go ├── v2req.go ├── token.go ├── v1receivepackresp.go ├── v1uploadpackresp.go ├── inforefs.go ├── PROTOCOL.md ├── v1uploadpackreq.go ├── LICENSE └── v1receivepackreq.go /.gitignore: -------------------------------------------------------------------------------- 1 | cover.html 2 | cover.out 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/gitprotocolio 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package gitprotocolio is a Git protocol parser written in Go. 2 | package gitprotocolio 3 | -------------------------------------------------------------------------------- /testing/end2end/params.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package end2end 16 | 17 | func protocolParams() map[string][]string { 18 | return map[string][]string{ 19 | "Protocol V0": []string{}, 20 | "Protocol V1": []string{"-c", "protocol.version=1"}, 21 | "Protocol V2": []string{"-c", "protocol.version=2"}, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /testing/testproxy/testproxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // testproxy is a Git protocol HTTP transport proxy. 16 | package main 17 | 18 | import ( 19 | "flag" 20 | "fmt" 21 | "log" 22 | "net/http" 23 | 24 | "github.com/google/gitprotocolio/testing" 25 | ) 26 | 27 | var ( 28 | port = flag.Int("port", 0, "the proxy port number") 29 | delegateURL = flag.String("delegate_url", "", "the Git repository URL the server delegates to") 30 | ) 31 | 32 | func main() { 33 | flag.Parse() 34 | 35 | if *port == 0 { 36 | log.Fatal("--port is unspecified") 37 | } 38 | if *delegateURL == "" { 39 | log.Fatal("--delegate_url is unspecified") 40 | } 41 | 42 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), testing.HTTPProxyHandler(*delegateURL))) 43 | } 44 | -------------------------------------------------------------------------------- /testing/cgit_server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testing 16 | 17 | import ( 18 | "io/ioutil" 19 | "net/http" 20 | "net/http/cgi" 21 | ) 22 | 23 | // HTTPHandler returns an http.Handler that is backed by git-http-backend. 24 | func HTTPHandler(gitBinary, gitDir string) http.Handler { 25 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 26 | h := &cgi.Handler{ 27 | Path: gitBinary, 28 | Dir: gitDir, 29 | Env: []string{ 30 | "GIT_PROJECT_ROOT=" + gitDir, 31 | "GIT_HTTP_EXPORT_ALL=1", 32 | }, 33 | Args: []string{ 34 | "http-backend", 35 | }, 36 | Stderr: ioutil.Discard, 37 | } 38 | if p := req.Header.Get("Git-Protocol"); p != "" { 39 | h.Env = append(h.Env, "GIT_PROTOCOL="+p) 40 | } 41 | if len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked" { 42 | // Not sure why this restriction is in place in the 43 | // library. 44 | req.TransferEncoding = nil 45 | } 46 | h.ServeHTTP(w, req) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /testing/end2end/lsremote_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package end2end 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestLsRemote_empty(t *testing.T) { 22 | refreshRemote() 23 | r := createLocalGitRepo() 24 | defer r.close() 25 | 26 | for name, args := range protocolParams() { 27 | if _, err := r.run(append(args, "ls-remote", httpProxyURL)...); err != nil { 28 | t.Errorf("%s: %v", name, err) 29 | } 30 | } 31 | } 32 | 33 | func TestLsRemote_nonEmpty(t *testing.T) { 34 | refreshRemote() 35 | r := createLocalGitRepo() 36 | defer r.close() 37 | 38 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 39 | t.Fatal(err) 40 | } 41 | _, err := r.run("rev-parse", "master") 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if _, err := r.run("push", httpServerURL, "master:master"); err != nil { 46 | t.Fatalf("%v", err) 47 | } 48 | 49 | r = createLocalGitRepo() 50 | for name, args := range protocolParams() { 51 | // TODO 52 | if _, err := r.run(append(args, "ls-remote", httpProxyURL)...); err != nil { 53 | t.Errorf("%s: %v", name, err) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /chunked_writer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "bytes" 19 | "io" 20 | ) 21 | 22 | type WriteFlushCloser interface { 23 | io.WriteCloser 24 | Flush() error 25 | } 26 | 27 | type chunkedWriter struct { 28 | buf bytes.Buffer 29 | sz int 30 | ch chan<- []byte 31 | } 32 | 33 | func NewChunkedWriter(sz int) (<-chan []byte, WriteFlushCloser) { 34 | ch := make(chan []byte) 35 | return ch, &chunkedWriter{sz: sz, ch: ch} 36 | } 37 | 38 | func (w *chunkedWriter) Write(p []byte) (int, error) { 39 | n, err := w.buf.Write(p) 40 | if err != nil { 41 | return n, err 42 | } 43 | if w.sz <= w.buf.Len() { 44 | bs := make([]byte, w.sz) 45 | for w.sz <= w.buf.Len() { 46 | rdsz, err := w.buf.Read(bs) 47 | if err != nil { 48 | return n, err 49 | } 50 | w.ch <- bs[:rdsz] 51 | } 52 | } 53 | return n, nil 54 | } 55 | 56 | func (w *chunkedWriter) Flush() error { 57 | bs := make([]byte, w.sz) 58 | for { 59 | rdsz, err := w.buf.Read(bs) 60 | if rdsz != 0 { 61 | w.ch <- bs[:rdsz] 62 | } 63 | if err != nil { 64 | if err == io.EOF { 65 | return nil 66 | } 67 | return err 68 | } 69 | } 70 | } 71 | 72 | func (w *chunkedWriter) Close() error { 73 | if err := w.Flush(); err != nil { 74 | return err 75 | } 76 | close(w.ch) 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /testing/testserver/testserver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // testserver runs cgit servers. 16 | package main 17 | 18 | import ( 19 | "flag" 20 | "fmt" 21 | "io/ioutil" 22 | "log" 23 | "net" 24 | "net/http" 25 | "os" 26 | "os/exec" 27 | 28 | "github.com/google/gitprotocolio/testing" 29 | ) 30 | 31 | var ( 32 | httpPort = flag.Int("http_port", 0, "the HTTP port number") 33 | ) 34 | 35 | func main() { 36 | flag.Parse() 37 | 38 | gitDir, err := ioutil.TempDir("", "gitprotocolio_testserver") 39 | if err != nil { 40 | log.Fatal("cannot create remote dir", err) 41 | } 42 | defer func() { 43 | if err := os.RemoveAll(gitDir); err != nil { 44 | log.Fatal(err) 45 | } 46 | }() 47 | 48 | gitBinary, err := exec.LookPath("git") 49 | if err != nil { 50 | log.Fatal("Cannot find the git binary: ", err) 51 | } 52 | 53 | runGit(gitBinary, gitDir, "init", "--bare") 54 | runGit(gitBinary, gitDir, "config", "http.receivepack", "1") 55 | runGit(gitBinary, gitDir, "config", "uploadpack.allowfilter", "1") 56 | runGit(gitBinary, gitDir, "config", "receive.advertisepushoptions", "1") 57 | runGit(gitBinary, gitDir, "config", "receive.certNonceSeed", "testnonce") 58 | 59 | l, err := net.Listen("tcp", fmt.Sprintf(":%d", *httpPort)) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | httpBackend := &http.Server{ 64 | Handler: testing.HTTPHandler(gitBinary, gitDir), 65 | } 66 | log.Println(gitDir) 67 | log.Println(fmt.Sprintf("http://%s/", l.Addr().String())) 68 | log.Fatal(httpBackend.Serve(l)) 69 | } 70 | 71 | func runGit(gitBinary, gitDir string, arg ...string) { 72 | cmd := exec.Command(gitBinary, arg...) 73 | cmd.Dir = gitDir 74 | if err := cmd.Run(); err != nil { 75 | log.Fatal(err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sideband.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | // BytePayloadPacket is the interface of Packets that the payload is []byte. 22 | type BytePayloadPacket interface { 23 | Packet 24 | Bytes() []byte 25 | } 26 | 27 | // SideBandMainPacket is a sideband packet for the main stream (0x01). 28 | type SideBandMainPacket []byte 29 | 30 | // EncodeToPktLine serializes the packet. 31 | func (p SideBandMainPacket) EncodeToPktLine() []byte { 32 | sz := len(p) 33 | if sz > 0xFFFF-5 { 34 | panic("content too large") 35 | } 36 | return append([]byte(fmt.Sprintf("%04x%c", sz+5, 1)), p...) 37 | } 38 | 39 | // Bytes returns the payload. 40 | func (p SideBandMainPacket) Bytes() []byte { 41 | return p 42 | } 43 | 44 | // SideBandReportPacket is a sideband packet for the report stream (0x02). 45 | type SideBandReportPacket []byte 46 | 47 | // EncodeToPktLine serializes the packet. 48 | func (p SideBandReportPacket) EncodeToPktLine() []byte { 49 | sz := len(p) 50 | if sz > 0xFFFF-5 { 51 | panic("content too large") 52 | } 53 | return append([]byte(fmt.Sprintf("%04x%c", sz+5, 2)), p...) 54 | } 55 | 56 | // Bytes returns the payload. 57 | func (p SideBandReportPacket) Bytes() []byte { 58 | return p 59 | } 60 | 61 | // SideBandErrorPacket is a sideband packet for the error stream (0x03). 62 | type SideBandErrorPacket []byte 63 | 64 | // EncodeToPktLine serializes the packet. 65 | func (p SideBandErrorPacket) EncodeToPktLine() []byte { 66 | sz := len(p) 67 | if sz > 0xFFFF-5 { 68 | panic("content too large") 69 | } 70 | return append([]byte(fmt.Sprintf("%04x%c", sz+5, 3)), p...) 71 | } 72 | 73 | // Bytes returns the payload. 74 | func (p SideBandErrorPacket) Bytes() []byte { 75 | return p 76 | } 77 | 78 | // ParseSideBandPacket parses the BytesPacket as a sideband packet. Returns nil 79 | // if the packet is not a sideband packet. 80 | func ParseSideBandPacket(bp BytesPacket) BytePayloadPacket { 81 | switch bp[0] { 82 | case 1: 83 | return SideBandMainPacket(bp[1:]) 84 | case 2: 85 | return SideBandReportPacket(bp[1:]) 86 | case 3: 87 | return SideBandErrorPacket(bp[1:]) 88 | } 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitprotocolio 2 | 3 | A Git protocol parser written in Go. 4 | 5 | This is more like an experimental project to better understand the protocol. 6 | 7 | This is not an official Google product (i.e. a 20% project). 8 | 9 | ## Background 10 | 11 | Git protocol is defined in 12 | [Documentation/technical/pack-protocol.txt](https://git.kernel.org/pub/scm/git/git.git/tree/Documentation/technical/pack-protocol.txt). 13 | This is not a complete definition. Also, a transport specific spec 14 | [Documentation/technical/http-protocol.txt](https://git.kernel.org/pub/scm/git/git.git/tree/Documentation/technical/http-protocol.txt) 15 | is not complete. This project was started so that these upstream protocol spec 16 | becomes more accurate. To verify the written syntax is accurate, this project 17 | includes a Git protocol parser written in Go, and have end-to-end test suites. 18 | 19 | This makes it easy to write a test case for Git client. Currently the test cases 20 | are run against the canonical Git implementation, but this can be extended to 21 | run against JGit, etc.. Also it makes it easy to test an attack case. With this 22 | library, one can write an attack case like 23 | [git-bomb](https://github.com/Katee/git-bomb) against Git protocol by producing 24 | a request that is usually not produced by a sane Git client. Protocol properties 25 | can also be checked. For example, it's possible to write a test to check valid 26 | request/response's prefixes are not a valid request/response. This property 27 | makes sure a client won't process an incomplete response thinking it's complete. 28 | 29 | ## TODOs 30 | 31 | * Protocol semantics is not defined. 32 | 33 | The syntax is relatively complete. The semantics is not even mentioned. One 34 | idea is to define the semantics by treating the request/response as an 35 | operation to modify Git repositories. This perspective makes it possible to 36 | define a formal protocol semantics in a same way as programming language 37 | formal semantics. 38 | 39 | Defining a simple git-push semantics seems easy. Defining a pack 40 | negotiation semantics for shallow cloned repositories seems difficult. 41 | 42 | * Upstream pack-protocol.txt is not updated. 43 | 44 | The initial purpose, create a complete pack-protocol.txt, is not yet done. 45 | We can start from a minor fix (e.g. capability separator in some places is 46 | space not NUL). Also relationship between Git protocol and network 47 | transports (HTTPS, SSH, Git wire) are good to be mentioned. 48 | 49 | * Bidi-transports are not tested and defined. 50 | 51 | Git's bidi-transports, SSH and Git-wire protocol, are not tested with this 52 | project and the protocol syntax is also not defined. The majority of the 53 | syntax is same, but there's a slight difference. Go has an SSH library, so 54 | it's easy to run a test SSH server. 55 | -------------------------------------------------------------------------------- /v2resp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | ) 21 | 22 | type protocolV2ResponseState int 23 | 24 | const ( 25 | protocolV2ResponseStateBegin protocolV2ResponseState = iota 26 | protocolV2ResponseStateScanResponse 27 | protocolV2ResponseStateEnd 28 | ) 29 | 30 | // ProtocolV2ResponseChunk is a chunk of a protocol v2 response. 31 | type ProtocolV2ResponseChunk struct { 32 | Response []byte 33 | Delimiter bool 34 | EndResponse bool 35 | } 36 | 37 | // EncodeToPktLine serializes the chunk. 38 | func (c *ProtocolV2ResponseChunk) EncodeToPktLine() []byte { 39 | if len(c.Response) != 0 { 40 | return BytesPacket(c.Response).EncodeToPktLine() 41 | } 42 | if c.Delimiter { 43 | return DelimPacket{}.EncodeToPktLine() 44 | } 45 | if c.EndResponse { 46 | return FlushPacket{}.EncodeToPktLine() 47 | } 48 | panic("impossible chunk") 49 | } 50 | 51 | // ProtocolV2Response provides an interface for reading a protocol v2 response. 52 | type ProtocolV2Response struct { 53 | scanner *PacketScanner 54 | state protocolV2ResponseState 55 | err error 56 | curr *ProtocolV2ResponseChunk 57 | } 58 | 59 | // NewProtocolV2Response returns a new ProtocolV2Response to read from rd. 60 | func NewProtocolV2Response(rd io.Reader) *ProtocolV2Response { 61 | return &ProtocolV2Response{scanner: NewPacketScanner(rd)} 62 | } 63 | 64 | // Err returns the first non-EOF error that was encountered by the 65 | // ProtocolV2Response. 66 | func (r *ProtocolV2Response) Err() error { 67 | return r.err 68 | } 69 | 70 | // Chunk returns the most recent request chunk generated by a call to Scan. 71 | // 72 | // The underlying array of Response may point to data that will be overwritten 73 | // by a subsequent call to Scan. It does no allocation. 74 | func (r *ProtocolV2Response) Chunk() *ProtocolV2ResponseChunk { 75 | return r.curr 76 | } 77 | 78 | // Scan advances the scanner to the next packet. It returns false when the scan 79 | // stops, either by reaching the end of the input or an error. After scan 80 | // returns false, the Err method will return any error that occurred during 81 | // scanning, except that if it was io.EOF, Err will return nil. 82 | func (r *ProtocolV2Response) Scan() bool { 83 | if r.err != nil || r.state == protocolV2ResponseStateEnd { 84 | return false 85 | } 86 | if !r.scanner.Scan() { 87 | r.err = r.scanner.Err() 88 | if r.err == nil && r.state != protocolV2ResponseStateBegin { 89 | r.err = SyntaxError("early EOF") 90 | } 91 | return false 92 | } 93 | 94 | switch p := r.scanner.Packet().(type) { 95 | case FlushPacket: 96 | r.state = protocolV2ResponseStateBegin 97 | r.curr = &ProtocolV2ResponseChunk{ 98 | EndResponse: true, 99 | } 100 | return true 101 | case DelimPacket: 102 | r.state = protocolV2ResponseStateScanResponse 103 | r.curr = &ProtocolV2ResponseChunk{ 104 | Delimiter: true, 105 | } 106 | return true 107 | case BytesPacket: 108 | r.state = protocolV2ResponseStateScanResponse 109 | r.curr = &ProtocolV2ResponseChunk{ 110 | Response: p, 111 | } 112 | return true 113 | default: 114 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", r.scanner.Packet())) 115 | return false 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /testing/end2end/end2end_main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package end2end 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io/ioutil" 21 | "log" 22 | "net" 23 | "net/http" 24 | "os" 25 | "os/exec" 26 | "strings" 27 | 28 | "github.com/google/gitprotocolio/testing" 29 | ) 30 | 31 | var ( 32 | gitBinary string 33 | remoteGitRepo gitRepo 34 | gnuPGHome string 35 | 36 | httpServerURL string 37 | httpProxyURL string 38 | ) 39 | 40 | func init() { 41 | dir, err := ioutil.TempDir("", "gitprotocolio_remote") 42 | if err != nil { 43 | log.Fatal("cannot create remote dir", err) 44 | } 45 | remoteGitRepo = gitRepo(dir) 46 | 47 | gitBinary, err = exec.LookPath("git") 48 | if err != nil { 49 | log.Fatal("Cannot find the git binary: ", err) 50 | } 51 | 52 | { 53 | l, err := net.Listen("tcp", ":0") 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | httpBackend := &http.Server{ 58 | Handler: testing.HTTPHandler(gitBinary, string(remoteGitRepo)), 59 | } 60 | go func() { 61 | log.Fatal(httpBackend.Serve(l)) 62 | }() 63 | httpServerURL = fmt.Sprintf("http://%s/", l.Addr().String()) 64 | } 65 | 66 | if os.Getenv("BYPASS_PROXY") == "1" { 67 | httpProxyURL = httpServerURL 68 | } else { 69 | l, err := net.Listen("tcp", ":0") 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | httpProxy := &http.Server{ 74 | Handler: testing.HTTPProxyHandler(httpServerURL), 75 | } 76 | go func() { 77 | log.Fatal(httpProxy.Serve(l)) 78 | }() 79 | httpProxyURL = fmt.Sprintf("http://%s/", l.Addr().String()) 80 | } 81 | 82 | gnuPGHome, err = ioutil.TempDir("", "gitprotocolio_remote") 83 | if err != nil { 84 | log.Fatal("cannot create a GNUPGHOME: ", err) 85 | } 86 | 87 | gpgBinary, err := exec.LookPath("gpg") 88 | if err != nil { 89 | log.Fatal("cannot find the gpg binary: ", err) 90 | } 91 | 92 | cmd := exec.Command(gpgBinary, "--homedir", gnuPGHome, "--no-tty", "--batch", "--gen-key") 93 | cmd.Dir = gnuPGHome 94 | cmd.Stdin = bytes.NewBufferString(` 95 | %no-protection 96 | %transient-key 97 | Key-Type: RSA 98 | Key-Length: 2048 99 | Subkey-Type: RSA 100 | Subkey-Length: 2048 101 | Name-Real: local root 102 | Name-Email: local-root@example.com 103 | Expire-Date: 1d 104 | `) 105 | if bs, err := cmd.CombinedOutput(); err != nil { 106 | log.Fatalf("cannot create a GPG key: %v\n%s", err, string(bs)) 107 | } 108 | 109 | } 110 | 111 | func refreshRemote() { 112 | remoteGitRepo.close() 113 | if err := os.Mkdir(string(remoteGitRepo), 0755); err != nil { 114 | log.Fatal(err) 115 | } 116 | remoteGitRepo.run("init", "--bare") 117 | remoteGitRepo.run("config", "http.receivepack", "1") 118 | remoteGitRepo.run("config", "uploadpack.allowfilter", "1") 119 | remoteGitRepo.run("config", "receive.advertisepushoptions", "1") 120 | remoteGitRepo.run("config", "receive.certNonceSeed", "testnonce") 121 | } 122 | 123 | func createLocalGitRepo() gitRepo { 124 | dir, err := ioutil.TempDir("", "gitprotocolio_local") 125 | if err != nil { 126 | log.Fatal(err) 127 | } 128 | r := gitRepo(dir) 129 | r.run("init") 130 | r.run("config", "user.email", "local-root@example.com") 131 | r.run("config", "user.name", "local root") 132 | return r 133 | } 134 | 135 | type gitRepo string 136 | 137 | func (r gitRepo) run(arg ...string) (string, error) { 138 | cmd := exec.Command(gitBinary, arg...) 139 | cmd.Dir = string(r) 140 | cmd.Env = append(os.Environ(), fmt.Sprintf("GNUPGHOME=%s", gnuPGHome)) 141 | bs, err := cmd.CombinedOutput() 142 | if err != nil { 143 | return "", &commandError{err, cmd.Args, strings.TrimRight(string(bs), "\n")} 144 | } 145 | return string(bs), nil 146 | } 147 | 148 | func (r gitRepo) close() error { 149 | return os.RemoveAll(string(r)) 150 | } 151 | 152 | type commandError struct { 153 | err error 154 | args []string 155 | output string 156 | } 157 | 158 | func (c *commandError) Error() string { 159 | ss := []string{ 160 | "cannot execute a git command", 161 | fmt.Sprintf("Error: %v", c.err), 162 | fmt.Sprintf("Args: %#v", c.args), 163 | } 164 | for _, s := range strings.Split(c.output, "\n") { 165 | ss = append(ss, "Output: "+s) 166 | } 167 | return strings.Join(ss, "\n") 168 | } 169 | -------------------------------------------------------------------------------- /v2req.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "strings" 22 | ) 23 | 24 | type protocolV2RequestState int 25 | 26 | const ( 27 | protocolV2RequestStateBegin protocolV2RequestState = iota 28 | protocolV2RequestStateScanCapabilities 29 | protocolV2RequestStateScanArguments 30 | protocolV2RequestStateEnd 31 | ) 32 | 33 | // ProtocolV2RequestChunk is a chunk of a protocol v2 request. 34 | type ProtocolV2RequestChunk struct { 35 | Command string 36 | Capability string 37 | EndCapability bool 38 | Argument []byte 39 | EndArgument bool 40 | EndRequest bool 41 | } 42 | 43 | // EncodeToPktLine serializes the chunk. 44 | func (c *ProtocolV2RequestChunk) EncodeToPktLine() []byte { 45 | if c.Command != "" { 46 | return BytesPacket([]byte(fmt.Sprintf("command=%s\n", c.Command))).EncodeToPktLine() 47 | } 48 | if c.Capability != "" { 49 | return BytesPacket([]byte(c.Capability + "\n")).EncodeToPktLine() 50 | } 51 | if c.EndCapability { 52 | return DelimPacket{}.EncodeToPktLine() 53 | } 54 | if len(c.Argument) != 0 { 55 | return BytesPacket(c.Argument).EncodeToPktLine() 56 | } 57 | if c.EndArgument || c.EndRequest { 58 | return FlushPacket{}.EncodeToPktLine() 59 | } 60 | panic("impossible chunk") 61 | } 62 | 63 | // ProtocolV2Request provides an interface for reading a protocol v2 request. 64 | type ProtocolV2Request struct { 65 | scanner *PacketScanner 66 | state protocolV2RequestState 67 | err error 68 | curr *ProtocolV2RequestChunk 69 | } 70 | 71 | // NewProtocolV2Request returns a new ProtocolV2Request to read from rd. 72 | func NewProtocolV2Request(rd io.Reader) *ProtocolV2Request { 73 | return &ProtocolV2Request{scanner: NewPacketScanner(rd)} 74 | } 75 | 76 | // Err returns the first non-EOF error that was encountered by the 77 | // ProtocolV2Request. 78 | func (r *ProtocolV2Request) Err() error { 79 | return r.err 80 | } 81 | 82 | // Chunk returns the most recent request chunk generated by a call to Scan. 83 | // 84 | // The underlying array of Argument may point to data that will be overwritten 85 | // by a subsequent call to Scan. It does no allocation. 86 | func (r *ProtocolV2Request) Chunk() *ProtocolV2RequestChunk { 87 | return r.curr 88 | } 89 | 90 | // Scan advances the scanner to the next packet. It returns false when the scan 91 | // stops, either by reaching the end of the input or an error. After scan 92 | // returns false, the Err method will return any error that occurred during 93 | // scanning, except that if it was io.EOF, Err will return nil. 94 | func (r *ProtocolV2Request) Scan() bool { 95 | if r.err != nil || r.state == protocolV2RequestStateEnd { 96 | return false 97 | } 98 | if !r.scanner.Scan() { 99 | r.err = r.scanner.Err() 100 | if r.err == nil && r.state != protocolV2RequestStateBegin { 101 | r.err = SyntaxError("early EOF") 102 | } 103 | return false 104 | } 105 | pkt := r.scanner.Packet() 106 | 107 | switch r.state { 108 | case protocolV2RequestStateBegin: 109 | switch p := pkt.(type) { 110 | case FlushPacket: 111 | r.state = protocolV2RequestStateEnd 112 | r.curr = &ProtocolV2RequestChunk{ 113 | EndRequest: true, 114 | } 115 | return true 116 | case BytesPacket: 117 | if !bytes.HasPrefix(p, []byte("command=")) { 118 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 119 | return false 120 | } 121 | r.state = protocolV2RequestStateScanCapabilities 122 | r.curr = &ProtocolV2RequestChunk{ 123 | Command: strings.TrimSuffix(strings.TrimPrefix(string(p), "command="), "\n"), 124 | } 125 | return true 126 | default: 127 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 128 | return false 129 | } 130 | case protocolV2RequestStateScanCapabilities: 131 | switch p := pkt.(type) { 132 | case DelimPacket: 133 | r.state = protocolV2RequestStateScanArguments 134 | r.curr = &ProtocolV2RequestChunk{ 135 | EndCapability: true, 136 | } 137 | return true 138 | case BytesPacket: 139 | r.curr = &ProtocolV2RequestChunk{ 140 | Capability: strings.TrimSuffix(string(p), "\n"), 141 | } 142 | return true 143 | default: 144 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 145 | return false 146 | } 147 | case protocolV2RequestStateScanArguments: 148 | switch p := pkt.(type) { 149 | case FlushPacket: 150 | r.state = protocolV2RequestStateBegin 151 | r.curr = &ProtocolV2RequestChunk{ 152 | EndArgument: true, 153 | } 154 | return true 155 | case BytesPacket: 156 | r.curr = &ProtocolV2RequestChunk{ 157 | Argument: p, 158 | } 159 | return true 160 | default: 161 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 162 | return false 163 | } 164 | } 165 | panic("impossible state") 166 | } 167 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "fmt" 21 | "io" 22 | "strconv" 23 | ) 24 | 25 | // SyntaxError is an error returned when the parser cannot parse the input. 26 | type SyntaxError string 27 | 28 | func (s SyntaxError) Error() string { return string(s) } 29 | 30 | // Packet is the interface that wraps a packet line. 31 | type Packet interface { 32 | EncodeToPktLine() []byte 33 | } 34 | 35 | // FlushPacket is the flush packet ("0000"). 36 | type FlushPacket struct{} 37 | 38 | // EncodeToPktLine serializes the packet. 39 | func (FlushPacket) EncodeToPktLine() []byte { 40 | return []byte("0000") 41 | } 42 | 43 | // DelimPacket is the delim packet ("0001"). 44 | type DelimPacket struct{} 45 | 46 | // EncodeToPktLine serializes the packet. 47 | func (DelimPacket) EncodeToPktLine() []byte { 48 | return []byte("0001") 49 | } 50 | 51 | // BytesPacket is a packet with a content. 52 | type BytesPacket []byte 53 | 54 | // EncodeToPktLine serializes the packet. 55 | func (b BytesPacket) EncodeToPktLine() []byte { 56 | sz := len(b) 57 | if sz > 0xFFFF-4 { 58 | panic("content too large") 59 | } 60 | return append([]byte(fmt.Sprintf("%04x", sz+4)), b...) 61 | } 62 | 63 | // ErrorPacket is a packet that indicates an error. 64 | type ErrorPacket string 65 | 66 | func (e ErrorPacket) Error() string { return "error: " + string(e) } 67 | 68 | // EncodeToPktLine serializes the packet. 69 | func (e ErrorPacket) EncodeToPktLine() []byte { 70 | bs := []byte("ERR " + e) 71 | sz := len(bs) 72 | if sz > 0xFFFF { 73 | panic("content too large") 74 | } 75 | return append([]byte(fmt.Sprintf("%04X", sz+4)), bs...) 76 | } 77 | 78 | // PackFileIndicatorPacket is the indicator of the beginning of the pack file 79 | // ("PACK"). 80 | type PackFileIndicatorPacket struct{} 81 | 82 | // EncodeToPktLine serializes the packet. 83 | func (PackFileIndicatorPacket) EncodeToPktLine() []byte { 84 | return []byte("PACK") 85 | } 86 | 87 | // PackFilePacket is a chunk of the pack file. 88 | type PackFilePacket []byte 89 | 90 | // EncodeToPktLine serializes the packet. 91 | func (p PackFilePacket) EncodeToPktLine() []byte { 92 | return []byte(p) 93 | } 94 | 95 | // PacketScanner provides an interface for reading packet line data. The usage 96 | // is same as bufio.Scanner. 97 | type PacketScanner struct { 98 | err error 99 | curr Packet 100 | packFileMode bool 101 | scanner *bufio.Scanner 102 | } 103 | 104 | // NewPacketScanner returns a new PacketScanner to read from r. 105 | func NewPacketScanner(r io.Reader) *PacketScanner { 106 | s := &PacketScanner{scanner: bufio.NewScanner(r)} 107 | s.scanner.Split(s.packetSplitFunc) 108 | return s 109 | } 110 | 111 | // Err returns the first non-EOF error that was encountered by the 112 | // PacketScanner. 113 | func (s *PacketScanner) Err() error { 114 | return s.err 115 | } 116 | 117 | // Packet returns the most recent packet generated by a call to Scan. 118 | func (s *PacketScanner) Packet() Packet { 119 | return s.curr 120 | } 121 | 122 | // Scan advances the scanner to the next packet. It returns false when the scan 123 | // stops, either by reaching the end of the input or an error. After scan 124 | // returns false, the Err method will return any error that occurred during 125 | // scanning, except that if it was io.EOF, Err will return nil. 126 | func (s *PacketScanner) Scan() bool { 127 | if s.err != nil { 128 | return false 129 | } 130 | if !s.scanner.Scan() { 131 | s.err = s.scanner.Err() 132 | return false 133 | } 134 | 135 | bs := s.scanner.Bytes() 136 | if s.packFileMode { 137 | if len(bs) == 0 { 138 | // EOF 139 | return false 140 | } 141 | s.curr = PackFilePacket(bs) 142 | return true 143 | } 144 | if bytes.Equal(bs, []byte("0000")) { 145 | s.curr = FlushPacket{} 146 | return true 147 | } 148 | if bytes.Equal(bs, []byte("0001")) { 149 | s.curr = DelimPacket{} 150 | return true 151 | } 152 | if bytes.Equal(bs, []byte("PACK")) { 153 | s.packFileMode = true 154 | s.curr = PackFileIndicatorPacket{} 155 | return true 156 | } 157 | if len(bs) == 4 { 158 | s.err = SyntaxError("unknown special packet: " + string(bs)) 159 | return false 160 | } 161 | if bytes.Equal(bs[4:8], []byte("ERR ")) { 162 | s.err = ErrorPacket(string(bs[8:])) 163 | return false 164 | } 165 | s.curr = BytesPacket(bs[4:]) 166 | return true 167 | } 168 | 169 | func (s *PacketScanner) packetSplitFunc(data []byte, atEOF bool) (int, []byte, error) { 170 | if s.packFileMode { 171 | return len(data), data, nil 172 | } 173 | if len(data) < 4 { 174 | return 0, nil, nil 175 | } 176 | if bytes.HasPrefix(data, []byte("PACK")) { 177 | return 4, data[:4], nil 178 | } 179 | sz, err := strconv.ParseUint(string(data[:4]), 16, 32) 180 | if err != nil { 181 | return 0, nil, err 182 | } 183 | if sz == 0 || sz == 1 { 184 | // Special packet. 185 | return 4, data[:4], nil 186 | } 187 | if len(data) < int(sz) { 188 | return 0, nil, nil 189 | } 190 | return int(sz), data[:int(sz)], nil 191 | } 192 | -------------------------------------------------------------------------------- /v1receivepackresp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "strings" 21 | ) 22 | 23 | type protocolV1ReceivePackResponseState int 24 | 25 | const ( 26 | protocolV1ReceivePackResponseStateBegin protocolV1ReceivePackResponseState = iota 27 | protocolV1ReceivePackResponseStateScanResult 28 | protocolV1ReceivePackResponseStateEnd 29 | ) 30 | 31 | // ProtocolV1ReceivePackResponseChunk is a chunk of a protocol v1 32 | // git-receive-pack response. 33 | type ProtocolV1ReceivePackResponseChunk struct { 34 | UnpackStatus string 35 | RefUpdateStatus string 36 | RefName string 37 | RefUpdateFailMessage string 38 | EndOfResponse bool 39 | } 40 | 41 | // EncodeToPktLine serializes the chunk. 42 | func (c *ProtocolV1ReceivePackResponseChunk) EncodeToPktLine() []byte { 43 | if c.UnpackStatus != "" { 44 | return BytesPacket([]byte(fmt.Sprintf("unpack %s\n", c.UnpackStatus))).EncodeToPktLine() 45 | } 46 | if c.RefUpdateStatus != "" { 47 | if c.RefUpdateFailMessage == "" { 48 | return BytesPacket([]byte(fmt.Sprintf("%s %s\n", c.RefUpdateStatus, c.RefName))).EncodeToPktLine() 49 | } 50 | return BytesPacket([]byte(fmt.Sprintf("%s %s %s\n", c.RefUpdateStatus, c.RefName, c.RefUpdateFailMessage))).EncodeToPktLine() 51 | } 52 | if c.EndOfResponse { 53 | return FlushPacket{}.EncodeToPktLine() 54 | } 55 | panic("impossible chunk") 56 | } 57 | 58 | // ProtocolV1ReceivePackResponse provides an interface for reading a protocol v1 59 | // git-receive-pack response. 60 | type ProtocolV1ReceivePackResponse struct { 61 | scanner *PacketScanner 62 | state protocolV1ReceivePackResponseState 63 | err error 64 | curr *ProtocolV1ReceivePackResponseChunk 65 | } 66 | 67 | // NewProtocolV1ReceivePackResponse returns a new ProtocolV1ReceivePackResponse 68 | // to read from rd. 69 | func NewProtocolV1ReceivePackResponse(rd io.Reader) *ProtocolV1ReceivePackResponse { 70 | return &ProtocolV1ReceivePackResponse{scanner: NewPacketScanner(rd)} 71 | } 72 | 73 | // Err returns the first non-EOF error that was encountered by the 74 | // ProtocolV1ReceivePackResponse. 75 | func (r *ProtocolV1ReceivePackResponse) Err() error { 76 | return r.err 77 | } 78 | 79 | // Chunk returns the most recent response chunk generated by a call to Scan. 80 | func (r *ProtocolV1ReceivePackResponse) Chunk() *ProtocolV1ReceivePackResponseChunk { 81 | return r.curr 82 | } 83 | 84 | // Scan advances the scanner to the next packet. It returns false when the scan 85 | // stops, either by reaching the end of the input or an error. After scan 86 | // returns false, the Err method will return any error that occurred during 87 | // scanning, except that if it was io.EOF, Err will return nil. 88 | func (r *ProtocolV1ReceivePackResponse) Scan() bool { 89 | if r.err != nil || r.state == protocolV1ReceivePackResponseStateEnd { 90 | return false 91 | } 92 | if !r.scanner.Scan() { 93 | r.err = r.scanner.Err() 94 | if r.err == nil && r.state != protocolV1ReceivePackResponseStateBegin { 95 | r.err = SyntaxError("early EOF") 96 | } 97 | return false 98 | } 99 | pkt := r.scanner.Packet() 100 | switch r.state { 101 | case protocolV1ReceivePackResponseStateBegin: 102 | bp, ok := pkt.(BytesPacket) 103 | if !ok { 104 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 105 | return false 106 | } 107 | s := strings.TrimSuffix(string(bp), "\n") 108 | if !strings.HasPrefix(s, "unpack ") { 109 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", s)) 110 | return false 111 | } 112 | r.state = protocolV1ReceivePackResponseStateScanResult 113 | r.curr = &ProtocolV1ReceivePackResponseChunk{ 114 | UnpackStatus: strings.SplitN(s, " ", 2)[1], 115 | } 116 | return true 117 | case protocolV1ReceivePackResponseStateScanResult: 118 | switch p := pkt.(type) { 119 | case FlushPacket: 120 | r.state = protocolV1ReceivePackResponseStateEnd 121 | r.curr = &ProtocolV1ReceivePackResponseChunk{ 122 | EndOfResponse: true, 123 | } 124 | return true 125 | case BytesPacket: 126 | s := strings.TrimSuffix(string(p), "\n") 127 | if strings.HasPrefix(s, "ok ") { 128 | ss := strings.SplitN(s, " ", 2) 129 | r.curr = &ProtocolV1ReceivePackResponseChunk{ 130 | RefUpdateStatus: ss[0], 131 | RefName: ss[1], 132 | } 133 | return true 134 | } 135 | if strings.HasPrefix(s, "ng ") { 136 | ss := strings.SplitN(s, " ", 3) 137 | if len(ss) != 3 { 138 | r.err = SyntaxError("cannot split into three: " + s) 139 | return false 140 | } 141 | r.curr = &ProtocolV1ReceivePackResponseChunk{ 142 | RefUpdateStatus: ss[0], 143 | RefName: ss[1], 144 | RefUpdateFailMessage: ss[2], 145 | } 146 | return true 147 | } 148 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 149 | return false 150 | default: 151 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 152 | return false 153 | } 154 | } 155 | panic("impossible state") 156 | } 157 | -------------------------------------------------------------------------------- /testing/end2end/fetch_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package end2end 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestFetch(t *testing.T) { 22 | refreshRemote() 23 | r := createLocalGitRepo() 24 | defer r.close() 25 | 26 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 27 | t.Fatal(err) 28 | } 29 | want, err := r.run("rev-parse", "master") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | if _, err := r.run("push", httpServerURL, "master:master"); err != nil { 34 | t.Fatalf("%v", err) 35 | } 36 | 37 | for name, args := range protocolParams() { 38 | r = createLocalGitRepo() 39 | defer r.close() 40 | if _, err := r.run(append(args, "fetch", httpProxyURL)...); err != nil { 41 | t.Errorf("%s: %v", name, err) 42 | continue 43 | } 44 | if got, err := r.run("rev-parse", "FETCH_HEAD"); err != nil { 45 | t.Errorf("%s: %v", name, err) 46 | } else if got != want { 47 | t.Errorf("%s: want %s, got %s", name, want, got) 48 | } 49 | } 50 | } 51 | 52 | func TestFetch_multiWants(t *testing.T) { 53 | refreshRemote() 54 | r := createLocalGitRepo() 55 | defer r.close() 56 | 57 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 58 | t.Fatal(err) 59 | } 60 | if _, err := r.run("checkout", "--orphan", "testbranch"); err != nil { 61 | t.Fatal(err) 62 | } 63 | if _, err := r.run("commit", "--allow-empty", "--message=another"); err != nil { 64 | t.Fatal(err) 65 | } 66 | if _, err := r.run("push", httpServerURL, "master:master", "testbranch:testbranch"); err != nil { 67 | t.Fatalf("%v", err) 68 | } 69 | 70 | for name, args := range protocolParams() { 71 | r = createLocalGitRepo() 72 | defer r.close() 73 | if _, err := r.run(append(args, "remote", "add", "origin", httpProxyURL)...); err != nil { 74 | t.Errorf("%s: %v", name, err) 75 | continue 76 | } 77 | if _, err := r.run(append(args, "fetch", "origin")...); err != nil { 78 | t.Errorf("%s: %v", name, err) 79 | continue 80 | } 81 | } 82 | } 83 | 84 | func TestFetch_shallowDepth(t *testing.T) { 85 | refreshRemote() 86 | r := createLocalGitRepo() 87 | defer r.close() 88 | 89 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 90 | t.Fatal(err) 91 | } 92 | want, err := r.run("rev-parse", "master") 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | if _, err := r.run("push", httpServerURL, "master:master"); err != nil { 97 | t.Fatalf("%v", err) 98 | } 99 | 100 | for name, args := range protocolParams() { 101 | r = createLocalGitRepo() 102 | defer r.close() 103 | if _, err := r.run(append(args, "fetch", "--depth=1", httpProxyURL)...); err != nil { 104 | t.Errorf("%s: %v", name, err) 105 | continue 106 | } 107 | if got, err := r.run("rev-parse", "FETCH_HEAD"); err != nil { 108 | t.Errorf("%s: %v", name, err) 109 | } else if got != want { 110 | t.Errorf("%s: want %s, got %s", name, want, got) 111 | } 112 | } 113 | } 114 | 115 | func TestFetch_deepen(t *testing.T) { 116 | refreshRemote() 117 | r := createLocalGitRepo() 118 | defer r.close() 119 | 120 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 121 | t.Fatal(err) 122 | } 123 | if _, err := r.run("push", httpServerURL, "master:master"); err != nil { 124 | t.Fatalf("%v", err) 125 | } 126 | 127 | for name, args := range protocolParams() { 128 | r = createLocalGitRepo() 129 | defer r.close() 130 | if _, err := r.run(append(args, "remote", "add", "origin", httpProxyURL)...); err != nil { 131 | t.Errorf("%s: %v", name, err) 132 | continue 133 | } 134 | if _, err := r.run(append(args, "fetch", "--depth=1", "origin")...); err != nil { 135 | t.Errorf("%s: %v", name, err) 136 | continue 137 | } 138 | if _, err := r.run(append(args, "fetch", "--deepen=1", "origin")...); err != nil { 139 | t.Errorf("%s: %v", name, err) 140 | continue 141 | } 142 | } 143 | } 144 | 145 | func TestFetch_shallowSince(t *testing.T) { 146 | refreshRemote() 147 | r := createLocalGitRepo() 148 | defer r.close() 149 | 150 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 151 | t.Fatal(err) 152 | } 153 | if _, err := r.run("push", httpServerURL, "master:master"); err != nil { 154 | t.Fatalf("%v", err) 155 | } 156 | 157 | for name, args := range protocolParams() { 158 | r = createLocalGitRepo() 159 | defer r.close() 160 | if _, err := r.run(append(args, "fetch", "--shallow-since=2000-01-01", httpProxyURL)...); err != nil { 161 | t.Errorf("%s: %v", name, err) 162 | continue 163 | } 164 | } 165 | } 166 | 167 | func TestFetch_shallowNot(t *testing.T) { 168 | refreshRemote() 169 | r := createLocalGitRepo() 170 | defer r.close() 171 | 172 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 173 | t.Fatal(err) 174 | } 175 | if _, err := r.run("tag", "-a", "should_be_excluded", "-m", "excluded"); err != nil { 176 | t.Fatal(err) 177 | } 178 | if _, err := r.run("commit", "--allow-empty", "--message=second"); err != nil { 179 | t.Fatal(err) 180 | } 181 | if _, err := r.run("push", httpServerURL, "--follow-tags"); err != nil { 182 | t.Fatalf("%v", err) 183 | } 184 | 185 | for name, args := range protocolParams() { 186 | r = createLocalGitRepo() 187 | defer r.close() 188 | if _, err := r.run(append(args, "fetch", "--shallow-exclude=should_be_excluded", httpProxyURL)...); err != nil { 189 | t.Errorf("%s: %v", name, err) 190 | continue 191 | } 192 | } 193 | } 194 | 195 | func TestFetch_filter(t *testing.T) { 196 | refreshRemote() 197 | r := createLocalGitRepo() 198 | defer r.close() 199 | 200 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 201 | t.Fatal(err) 202 | } 203 | if _, err := r.run("push", httpServerURL, "master:master"); err != nil { 204 | t.Fatalf("%v", err) 205 | } 206 | 207 | for name, args := range protocolParams() { 208 | r = createLocalGitRepo() 209 | defer r.close() 210 | if _, err := r.run("config", "extensions.partialClone", httpProxyURL); err != nil { 211 | t.Fatalf("%v", err) 212 | } 213 | if _, err := r.run(append(args, "fetch", "--filter=blob:none", httpProxyURL)...); err != nil { 214 | t.Errorf("%s: %v", name, err) 215 | continue 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /testing/end2end/push_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package end2end 16 | 17 | import ( 18 | "io/ioutil" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | func TestPush(t *testing.T) { 26 | r := createLocalGitRepo() 27 | defer r.close() 28 | 29 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 30 | t.Fatal(err) 31 | } 32 | want, err := r.run("rev-parse", "master") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | for name, args := range protocolParams() { 38 | refreshRemote() 39 | if _, err := r.run(append(args, "push", httpProxyURL, "master:master")...); err != nil { 40 | t.Errorf("%s: %v", name, err) 41 | continue 42 | } 43 | if got, err := remoteGitRepo.run("rev-parse", "master"); err != nil { 44 | t.Errorf("%s: %v", name, err) 45 | } else if got != want { 46 | t.Errorf("%s: want %s, got %s", name, want, got) 47 | } 48 | } 49 | } 50 | 51 | func TestPush_multipleRefs(t *testing.T) { 52 | r := createLocalGitRepo() 53 | defer r.close() 54 | 55 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 56 | t.Fatal(err) 57 | } 58 | if _, err := r.run("checkout", "-b", "another"); err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | for name, args := range protocolParams() { 63 | refreshRemote() 64 | if _, err := r.run(append(args, "push", httpProxyURL, "master:master", "another:another")...); err != nil { 65 | t.Errorf("%s: %v", name, err) 66 | continue 67 | } 68 | } 69 | } 70 | 71 | func TestPush_prereceiveReject(t *testing.T) { 72 | r := createLocalGitRepo() 73 | defer r.close() 74 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 75 | t.Fatal(err) 76 | } 77 | refreshRemote() 78 | if err := os.MkdirAll(filepath.Join(string(remoteGitRepo), "hooks"), 0755); err != nil { 79 | t.Fatal(err) 80 | } 81 | if err := ioutil.WriteFile(filepath.Join(string(remoteGitRepo), "hooks", "pre-receive"), []byte("#!/bin/sh\nfalse\n"), 0755); err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | for name, args := range protocolParams() { 86 | if _, err := r.run(append(args, "push", httpProxyURL, "master:master")...); err == nil { 87 | t.Errorf("%s: want error, got nothing", name) 88 | continue 89 | } else if cmderr, ok := err.(*commandError); !ok { 90 | t.Errorf("%s: want commandError, got %v", name, err) 91 | continue 92 | } else if !strings.Contains(cmderr.output, "pre-receive hook declined") { 93 | t.Errorf("%s: want a hook error, got %v", name, err) 94 | continue 95 | } 96 | } 97 | } 98 | 99 | func TestPush_nonFastForwardReject(t *testing.T) { 100 | r := createLocalGitRepo() 101 | defer r.close() 102 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 103 | t.Fatal(err) 104 | } 105 | refreshRemote() 106 | if _, err := r.run("push", httpServerURL, "master:master"); err != nil { 107 | t.Fatalf("%v", err) 108 | } 109 | 110 | r = createLocalGitRepo() 111 | defer r.close() 112 | if _, err := r.run("commit", "--allow-empty", "--message=another"); err != nil { 113 | t.Fatal(err) 114 | } 115 | if _, err := r.run("fetch", httpServerURL); err != nil { 116 | t.Fatal(err) 117 | } 118 | 119 | for name, args := range protocolParams() { 120 | if _, err := r.run(append(args, "push", httpProxyURL, "master:master")...); err == nil { 121 | t.Errorf("%s: want error, got nothing", name) 122 | continue 123 | } else if cmderr, ok := err.(*commandError); !ok { 124 | t.Errorf("%s: want commandError, got %v", name, err) 125 | continue 126 | } else if !strings.Contains(cmderr.output, "non-fast-forward") { 127 | t.Errorf("%s: want a non-fastforward error, got %v", name, err) 128 | continue 129 | } 130 | } 131 | } 132 | 133 | func TestPush_pushOption(t *testing.T) { 134 | r := createLocalGitRepo() 135 | defer r.close() 136 | 137 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 138 | t.Fatal(err) 139 | } 140 | 141 | for name, args := range protocolParams() { 142 | refreshRemote() 143 | if _, err := r.run(append(args, "push", "-o", "testoption", httpProxyURL, "master:master")...); err != nil { 144 | t.Errorf("%s: %v", name, err) 145 | continue 146 | } 147 | } 148 | } 149 | 150 | func TestPush_multiplePushOptions(t *testing.T) { 151 | r := createLocalGitRepo() 152 | defer r.close() 153 | 154 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 155 | t.Fatal(err) 156 | } 157 | 158 | for name, args := range protocolParams() { 159 | refreshRemote() 160 | if _, err := r.run(append(args, "push", "-o", "testoption1", "-o", "testoption2", httpProxyURL, "master:master")...); err != nil { 161 | t.Errorf("%s: %v", name, err) 162 | continue 163 | } 164 | } 165 | } 166 | 167 | func TestPush_shallowPush(t *testing.T) { 168 | for name, args := range protocolParams() { 169 | r := createLocalGitRepo() 170 | defer r.close() 171 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 172 | t.Fatal(err) 173 | } 174 | if _, err := r.run("commit", "--allow-empty", "--message=second"); err != nil { 175 | t.Fatal(err) 176 | } 177 | refreshRemote() 178 | if _, err := r.run("push", httpServerURL, "master:master"); err != nil { 179 | t.Fatal(err) 180 | } 181 | 182 | r = createLocalGitRepo() 183 | defer r.close() 184 | if _, err := r.run("remote", "add", "origin", httpServerURL); err != nil { 185 | t.Fatal(err) 186 | } 187 | if _, err := r.run("pull", "--depth=1", "origin", "master"); err != nil { 188 | t.Fatal(err) 189 | } 190 | if _, err := r.run("commit", "--allow-empty", "--message=third"); err != nil { 191 | t.Fatal(err) 192 | } 193 | 194 | if _, err := r.run(append(args, "push", httpProxyURL, "master:master")...); err != nil { 195 | t.Errorf("%s: %v", name, err) 196 | continue 197 | } 198 | } 199 | } 200 | 201 | func TestPush_GPGSign(t *testing.T) { 202 | r := createLocalGitRepo() 203 | defer r.close() 204 | 205 | if _, err := r.run("commit", "--allow-empty", "--message=init"); err != nil { 206 | t.Fatal(err) 207 | } 208 | 209 | for name, args := range protocolParams() { 210 | refreshRemote() 211 | if _, err := r.run(append(args, "push", "--signed", httpProxyURL, "master:master")...); err != nil { 212 | t.Errorf("%s: %v", name, err) 213 | continue 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /v1uploadpackresp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "strings" 22 | ) 23 | 24 | type protocolV1UploadPackResponseState int 25 | 26 | const ( 27 | protocolV1UploadPackResponseStateBegin protocolV1UploadPackResponseState = iota 28 | protocolV1UploadPackResponseStateScanShallows 29 | protocolV1UploadPackResponseStateScanUnshallows 30 | protocolV1UploadPackResponseStateBeginAcknowledgements 31 | protocolV1UploadPackResponseStateScanAcknowledgements 32 | protocolV1UploadPackResponseStateScanPacks 33 | protocolV1UploadPackResponseStateEnd 34 | ) 35 | 36 | // ProtocolV1UploadPackResponseChunk is a chunk of a protocol v1 git-upload-pack 37 | // response. 38 | type ProtocolV1UploadPackResponseChunk struct { 39 | ShallowObjectID string 40 | UnshallowObjectID string 41 | EndOfShallows bool 42 | AckObjectID string 43 | AckDetail string 44 | Nak bool 45 | PackStream []byte 46 | EndOfRequest bool 47 | } 48 | 49 | // EncodeToPktLine serializes the chunk. 50 | func (c *ProtocolV1UploadPackResponseChunk) EncodeToPktLine() []byte { 51 | if c.ShallowObjectID != "" { 52 | return BytesPacket([]byte(fmt.Sprintf("shallow %s\n", c.ShallowObjectID))).EncodeToPktLine() 53 | } 54 | if c.UnshallowObjectID != "" { 55 | return BytesPacket([]byte(fmt.Sprintf("unshallow %s\n", c.UnshallowObjectID))).EncodeToPktLine() 56 | } 57 | if c.EndOfShallows { 58 | return FlushPacket{}.EncodeToPktLine() 59 | } 60 | if c.AckObjectID != "" { 61 | if c.AckDetail != "" { 62 | return BytesPacket([]byte(fmt.Sprintf("ACK %s %s\n", c.AckObjectID, c.AckDetail))).EncodeToPktLine() 63 | } 64 | return BytesPacket([]byte(fmt.Sprintf("ACK %s\n", c.AckObjectID))).EncodeToPktLine() 65 | } 66 | if c.Nak { 67 | return BytesPacket([]byte("NAK\n")).EncodeToPktLine() 68 | } 69 | if len(c.PackStream) != 0 { 70 | return BytesPacket(c.PackStream).EncodeToPktLine() 71 | } 72 | if c.EndOfRequest { 73 | return FlushPacket{}.EncodeToPktLine() 74 | } 75 | panic("impossible chunk") 76 | } 77 | 78 | // ProtocolV1UploadPackResponse provides an interface for reading a protocol v1 79 | // git-upload-pack response. 80 | type ProtocolV1UploadPackResponse struct { 81 | scanner *PacketScanner 82 | state protocolV1UploadPackResponseState 83 | err error 84 | curr *ProtocolV1UploadPackResponseChunk 85 | } 86 | 87 | // NewProtocolV1UploadPackResponse returns a new ProtocolV1UploadPackResponse to 88 | // read from rd. 89 | func NewProtocolV1UploadPackResponse(rd io.Reader) *ProtocolV1UploadPackResponse { 90 | return &ProtocolV1UploadPackResponse{scanner: NewPacketScanner(rd)} 91 | } 92 | 93 | // Err returns the first non-EOF error that was encountered by the 94 | // ProtocolV1UploadPackResponse. 95 | func (r *ProtocolV1UploadPackResponse) Err() error { 96 | return r.err 97 | } 98 | 99 | // Chunk returns the most recent chunk generated by a call to Scan. 100 | func (r *ProtocolV1UploadPackResponse) Chunk() *ProtocolV1UploadPackResponseChunk { 101 | return r.curr 102 | } 103 | 104 | // Scan advances the scanner to the next packet. It returns false when the scan 105 | // stops, either by reaching the end of the input or an error. After scan 106 | // returns false, the Err method will return any error that occurred during 107 | // scanning, except that if it was io.EOF, Err will return nil. 108 | func (r *ProtocolV1UploadPackResponse) Scan() bool { 109 | if r.err != nil || r.state == protocolV1UploadPackResponseStateEnd { 110 | return false 111 | } 112 | if !r.scanner.Scan() { 113 | r.err = r.scanner.Err() 114 | if r.err == nil && r.state != protocolV1UploadPackResponseStateBeginAcknowledgements { 115 | r.err = SyntaxError("early EOF") 116 | } 117 | return false 118 | } 119 | pkt := r.scanner.Packet() 120 | 121 | switch r.state { 122 | case protocolV1UploadPackResponseStateBegin, protocolV1UploadPackResponseStateScanShallows: 123 | if bp, ok := pkt.(BytesPacket); ok { 124 | if bytes.HasPrefix(bp, []byte("shallow ")) { 125 | ss := strings.SplitN(strings.TrimSuffix(string(bp), "\n"), " ", 2) 126 | if len(ss) < 2 { 127 | r.err = SyntaxError("cannot split shallow: " + string(bp)) 128 | return false 129 | } 130 | r.state = protocolV1UploadPackResponseStateScanShallows 131 | r.curr = &ProtocolV1UploadPackResponseChunk{ 132 | ShallowObjectID: ss[1], 133 | } 134 | return true 135 | } 136 | } 137 | fallthrough 138 | case protocolV1UploadPackResponseStateScanUnshallows: 139 | if bp, ok := pkt.(BytesPacket); ok { 140 | if bytes.HasPrefix(bp, []byte("unshallow ")) { 141 | ss := strings.SplitN(strings.TrimSuffix(string(bp), "\n"), " ", 2) 142 | if len(ss) < 2 { 143 | r.err = SyntaxError("cannot split unshallow: " + string(bp)) 144 | return false 145 | } 146 | r.state = protocolV1UploadPackResponseStateScanUnshallows 147 | r.curr = &ProtocolV1UploadPackResponseChunk{ 148 | UnshallowObjectID: ss[1], 149 | } 150 | return true 151 | } 152 | } 153 | if _, ok := pkt.(FlushPacket); ok { 154 | r.state = protocolV1UploadPackResponseStateBeginAcknowledgements 155 | r.curr = &ProtocolV1UploadPackResponseChunk{ 156 | EndOfShallows: true, 157 | } 158 | return true 159 | } 160 | fallthrough 161 | case protocolV1UploadPackResponseStateBeginAcknowledgements, protocolV1UploadPackResponseStateScanAcknowledgements: 162 | if bp, ok := pkt.(BytesPacket); ok { 163 | if bytes.HasPrefix(bp, []byte("ACK ")) { 164 | ss := strings.SplitN(strings.TrimSuffix(string(bp), "\n"), " ", 3) 165 | if len(ss) < 2 { 166 | r.err = SyntaxError("cannot split ACK: " + string(bp)) 167 | return false 168 | } 169 | detail := "" 170 | if len(ss) == 3 { 171 | detail = ss[2] 172 | } 173 | r.state = protocolV1UploadPackResponseStateScanAcknowledgements 174 | r.curr = &ProtocolV1UploadPackResponseChunk{ 175 | AckObjectID: ss[1], 176 | AckDetail: detail, 177 | } 178 | return true 179 | } 180 | if bytes.Equal(bp, []byte("NAK\n")) { 181 | r.state = protocolV1UploadPackResponseStateScanPacks 182 | r.curr = &ProtocolV1UploadPackResponseChunk{ 183 | Nak: true, 184 | } 185 | return true 186 | } 187 | } 188 | if r.state == protocolV1UploadPackResponseStateBegin { 189 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 190 | return false 191 | } 192 | fallthrough 193 | case protocolV1UploadPackResponseStateScanPacks: 194 | switch p := pkt.(type) { 195 | case FlushPacket: 196 | r.state = protocolV1UploadPackResponseStateEnd 197 | r.curr = &ProtocolV1UploadPackResponseChunk{ 198 | EndOfRequest: true, 199 | } 200 | return true 201 | case BytesPacket: 202 | r.state = protocolV1UploadPackResponseStateScanPacks 203 | r.curr = &ProtocolV1UploadPackResponseChunk{ 204 | PackStream: p, 205 | } 206 | return true 207 | default: 208 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 209 | return false 210 | } 211 | } 212 | panic("impossible state") 213 | } 214 | -------------------------------------------------------------------------------- /inforefs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | type infoRefsResponseState int 26 | 27 | const ( 28 | infoRefsResponseStateScanServiceHeader infoRefsResponseState = iota 29 | infoRefsResponseStateScanServiceHeaderFlush 30 | infoRefsResponseStateScanOptionalProtocolVersion 31 | infoRefsResponseStateScanCapabilities 32 | infoRefsResponseStateScanRefs 33 | infoRefsResponseStateScanProtocolV2Capabilities 34 | infoRefsResponseStateEnd 35 | ) 36 | 37 | // InfoRefsResponseChunk is a chunk of an /info/refs response. 38 | type InfoRefsResponseChunk struct { 39 | ServiceHeader string 40 | ServiceHeaderFlush bool 41 | ProtocolVersion uint64 42 | Capabilities []string 43 | ObjectID string 44 | Ref string 45 | EndOfRequest bool 46 | } 47 | 48 | // EncodeToPktLine serializes the chunk. 49 | func (c *InfoRefsResponseChunk) EncodeToPktLine() []byte { 50 | if c.ServiceHeader != "" { 51 | return BytesPacket([]byte(fmt.Sprintf("# service=%s\n", c.ServiceHeader))).EncodeToPktLine() 52 | } 53 | if c.ServiceHeaderFlush { 54 | return FlushPacket{}.EncodeToPktLine() 55 | } 56 | if c.ProtocolVersion != 0 { 57 | return BytesPacket([]byte(fmt.Sprintf("version %d\n", c.ProtocolVersion))).EncodeToPktLine() 58 | } 59 | if len(c.Capabilities) > 0 && c.ObjectID != "" && c.Ref != "" { 60 | // V1 packet. 61 | return BytesPacket([]byte(fmt.Sprintf("%s %s\000%s\n", c.ObjectID, c.Ref, strings.Join(c.Capabilities, " ")))).EncodeToPktLine() 62 | } 63 | if len(c.Capabilities) == 1 { 64 | // V2 packet. 65 | return BytesPacket([]byte(c.Capabilities[0] + "\n")).EncodeToPktLine() 66 | } 67 | if c.ObjectID != "" && c.Ref != "" { 68 | return BytesPacket([]byte(fmt.Sprintf("%s %s\n", c.ObjectID, c.Ref))).EncodeToPktLine() 69 | } 70 | if c.EndOfRequest { 71 | return FlushPacket{}.EncodeToPktLine() 72 | } 73 | panic("impossible chunk") 74 | } 75 | 76 | // InfoRefsResponse provides an interface for reading an /info/refs response. 77 | // The usage is same as bufio.Scanner. 78 | type InfoRefsResponse struct { 79 | scanner *PacketScanner 80 | state infoRefsResponseState 81 | err error 82 | curr *InfoRefsResponseChunk 83 | } 84 | 85 | // NewInfoRefsResponse returns a new InfoRefsResponse to read from rd. 86 | func NewInfoRefsResponse(rd io.Reader) (r *InfoRefsResponse) { 87 | return &InfoRefsResponse{scanner: NewPacketScanner(rd)} 88 | } 89 | 90 | // Err returns the first non-EOF error that was encountered by the 91 | // InfoRefsResponse. 92 | func (r *InfoRefsResponse) Err() error { 93 | return r.err 94 | } 95 | 96 | // Chunk returns the most recent response chunk generated by a call to Scan. 97 | func (r *InfoRefsResponse) Chunk() *InfoRefsResponseChunk { 98 | return r.curr 99 | } 100 | 101 | // Scan advances the scanner to the next chunk. It returns false when the scan 102 | // stops, either by reaching the end of the input or an error. After Scan 103 | // returns false, the Err method will return any error that occurred during 104 | // scanning, except that if it was io.EOF, Err will return nil. 105 | func (r *InfoRefsResponse) Scan() bool { 106 | if r.err != nil || r.state == infoRefsResponseStateEnd { 107 | return false 108 | } 109 | if !r.scanner.Scan() { 110 | r.err = r.scanner.Err() 111 | return false 112 | } 113 | pkt := r.scanner.Packet() 114 | 115 | transition: 116 | switch r.state { 117 | case infoRefsResponseStateScanServiceHeader: 118 | bp, ok := pkt.(BytesPacket) 119 | if !ok { 120 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 121 | return false 122 | } 123 | if bytes.HasPrefix(bp, []byte("version ")) { 124 | r.state = infoRefsResponseStateScanOptionalProtocolVersion 125 | goto transition 126 | } 127 | if !bytes.HasPrefix(bp, []byte("# service=")) { 128 | r.err = SyntaxError(fmt.Sprintf("expect the service header, but got: %v", pkt)) 129 | } 130 | r.state = infoRefsResponseStateScanServiceHeaderFlush 131 | r.curr = &InfoRefsResponseChunk{ 132 | ServiceHeader: strings.TrimPrefix(strings.TrimSuffix(string(bp), "\n"), "# service="), 133 | } 134 | return true 135 | case infoRefsResponseStateScanServiceHeaderFlush: 136 | if _, ok := pkt.(FlushPacket); !ok { 137 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 138 | return false 139 | } 140 | r.state = infoRefsResponseStateScanOptionalProtocolVersion 141 | r.curr = &InfoRefsResponseChunk{ 142 | ServiceHeaderFlush: true, 143 | } 144 | return true 145 | case infoRefsResponseStateScanOptionalProtocolVersion: 146 | bp, ok := pkt.(BytesPacket) 147 | if !ok || !bytes.HasPrefix(bp, []byte("version ")) { 148 | r.state = infoRefsResponseStateScanCapabilities 149 | goto transition 150 | } 151 | verStr := strings.TrimSuffix(strings.TrimPrefix(string(bp), "version "), "\n") 152 | ver, err := strconv.ParseUint(verStr, 10, 64) 153 | if err != nil { 154 | r.err = SyntaxError("cannot parse the protocol version: " + verStr) 155 | return false 156 | } 157 | if ver == 2 { 158 | r.state = infoRefsResponseStateScanProtocolV2Capabilities 159 | } else { 160 | r.state = infoRefsResponseStateScanRefs 161 | } 162 | r.curr = &InfoRefsResponseChunk{ 163 | ProtocolVersion: ver, 164 | } 165 | return true 166 | case infoRefsResponseStateScanCapabilities: 167 | switch p := pkt.(type) { 168 | case FlushPacket: 169 | r.state = infoRefsResponseStateEnd 170 | r.curr = &InfoRefsResponseChunk{ 171 | EndOfRequest: true, 172 | } 173 | return true 174 | case BytesPacket: 175 | zss := bytes.SplitN(p, []byte{0}, 2) 176 | if len(zss) != 2 { 177 | r.err = SyntaxError("cannot split into two: " + string(p)) 178 | return false 179 | } 180 | caps := []string{} 181 | if capStr := strings.TrimSuffix(string(zss[1]), "\n"); capStr != "" { 182 | // This is to avoid strings.Split("", " ") => []string{""}. 183 | caps = strings.Split(capStr, " ") 184 | } 185 | ss := strings.SplitN(string(zss[0]), " ", 2) 186 | if len(ss) != 2 { 187 | r.err = SyntaxError("cannot split into two: " + string(zss[0])) 188 | return false 189 | } 190 | r.state = infoRefsResponseStateScanRefs 191 | r.curr = &InfoRefsResponseChunk{ 192 | Capabilities: caps, 193 | ObjectID: ss[0], 194 | Ref: ss[1], 195 | } 196 | return true 197 | default: 198 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 199 | return false 200 | } 201 | case infoRefsResponseStateScanRefs: 202 | switch p := pkt.(type) { 203 | case FlushPacket: 204 | r.state = infoRefsResponseStateEnd 205 | r.curr = &InfoRefsResponseChunk{ 206 | EndOfRequest: true, 207 | } 208 | return true 209 | case BytesPacket: 210 | ss := strings.SplitN(strings.TrimSuffix(string(p), "\n"), " ", 2) 211 | if len(ss) != 2 { 212 | r.err = SyntaxError("cannot split into two: " + string(p)) 213 | return false 214 | } 215 | r.curr = &InfoRefsResponseChunk{ 216 | ObjectID: ss[0], 217 | Ref: strings.TrimSuffix(ss[1], "\n"), 218 | } 219 | return true 220 | default: 221 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 222 | return false 223 | } 224 | case infoRefsResponseStateScanProtocolV2Capabilities: 225 | switch p := pkt.(type) { 226 | case FlushPacket: 227 | r.state = infoRefsResponseStateEnd 228 | r.curr = &InfoRefsResponseChunk{ 229 | EndOfRequest: true, 230 | } 231 | return true 232 | case BytesPacket: 233 | r.curr = &InfoRefsResponseChunk{ 234 | Capabilities: []string{strings.TrimSuffix(string(p), "\n")}, 235 | } 236 | return true 237 | default: 238 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 239 | return false 240 | } 241 | } 242 | panic("impossible state") 243 | } 244 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # Unofficial Git protocol syntax spec 2 | 3 | Git protocol is a line-based text protocol except git-receive-pack's packfile 4 | part. To define the protocol, we define its low-level token structure and its 5 | higher-level syntax structure. 6 | 7 | ## Token 8 | 9 | There are four Git protocol tokens. FlushPacket, DelimPacket, and BytesPacket. 10 | 11 | ### FlushPacket 12 | 13 | FlushPacket is 4-byte array `[0x30, 0x30, 0x30, 0x30]`. This is `"0000"` in 14 | ASCII encoding. 15 | 16 | ### DelimPacket 17 | 18 | DelimPacket is 4-byte array `[0x30, 0x30, 0x30, 0x31]`. This is `"0001"` in 19 | ASCII encoding. 20 | 21 | ### BytesPacket 22 | 23 | BytesPacket is a byte array prefixed by length. This is similar to a Pascal 24 | string. The length prefix is 4-byte, hexadecimal, zero-filled, ASCII-encoding 25 | string. The length includes the length prefix itself. For example, 26 | `"hello, world"` (12 characters) is encoded to `"0010hello, world"`. As the 27 | encoding suggests, the payload size cannot exceed 65531 bytes. 28 | 29 | ### ErrorPacket 30 | 31 | ErrorPacket is a special BytesPacket that the payload starts with "ERR ". 32 | 33 | ## Syntax 34 | 35 | The syntax defines the valid token sequence of the Git protcol. 36 | 37 | ### Basic constructs 38 | 39 | ``` 40 | NUL ::= 0x00 41 | SP ::= 0x20 42 | LF ::= 0x0A 43 | DECIMAL_DIGIT ::= c where isdigit(c) 44 | DECIMAL_NUMBER ::= '0' | c DECIMAL_DIGIT* where '1' <= c && c <= '9' 45 | HEX_DIGIT ::= c where isxdigit(c) 46 | NAME ::= c+ where isgraph(c) 47 | ANY_STR ::= c+ where isprint(c) 48 | ANY_BYTES ::= c+ where 0x00 <= c && c <= 0xFF 49 | 50 | OID_STR ::= HEX_DIGIT{40} 51 | REF_NAME ::= NAME 52 | ``` 53 | 54 | ### Sideband 55 | 56 | Sideband encoding is a packetization method of the Git protocol. For example, 57 | the input `000eunpack ok\n` is sideband encoded to `0013\x01000eunpack ok\n`. As 58 | you can see, the BytesPacket is nested. 59 | 60 | Some parts of the Git protocol optionally need the sideband encoding. If we want 61 | to define a formal grammar of the protocol, we should define two languages. 62 | 63 | We define a special form `MaybeSidebandEncoding` for the syntax language. When 64 | this appears in the syntax, it represents the language it takes as-is, or a 65 | language that is sideband encoded. The sideband encoded languages is defined as 66 | follows. 67 | 68 | ``` 69 | SIDEBAND_STREAM ::= BytePacket((0x01 | 0x02 | 0x03) ANY_BYTES)* 70 | ``` 71 | 72 | The byte stream consists of BytePackets starting from 0x01 is in the input 73 | language. The byte stream of 0x02 is a progress message. This progress 74 | message is usually just shown to the end-user and does not have a meaning in 75 | Git. The byte stream of 0x03 is an error message. This error message is treated 76 | same as ErrorPacket. 77 | 78 | ### Protocol V2 constructs 79 | 80 | ``` 81 | PROTOCOL_V2_HEADER ::= BytesPacket("version 2" LF) 82 | CAPABILITY_LINE* 83 | FlushPacket() 84 | 85 | CAPABILITY_LINE ::= BytesPacket(CAPABILITY_KEY ("=" CAPABILITY_VALUE) LF) 86 | CAPABILITY_KEY ::= c+ where isalpha(c) || isdigit(c) || c == '-' || c == '_' 87 | CAPABILITY_VALUE ::= c+ where isalpha(c) || isdigit(c) || strchr(" -_.,?\/{}[]()<>!@#$%^&*+=:;", c) != NULL 88 | 89 | PROTOCOL_V2_REQ ::= BytesPacket("command=" CAPABILITY_KEY) 90 | CAPABILITY_LINE* 91 | (DelimPacket() BytesPacket(ANY_BYTES)*)? 92 | FlushPacket() 93 | 94 | PROTOCOL_V2_RESP ::= BytesPacket(ANY_BYTES)* 95 | FlushPacket() 96 | ``` 97 | 98 | ### HTTP transport /info/refs 99 | 100 | ``` 101 | HTTP_INFO_REFS ::= INFO_REFS_V0_V1 | PROTOCOL_V2_HEADER 102 | 103 | INFO_REFS_V0_V1 ::= BytesPacket("# service=" SERVICE_NAME LF) 104 | FlushPacket() 105 | (REFS)? 106 | FlushPacket() 107 | REFS ::= BytesPacket(OID_STR SP REF_NAME NUL CAPABILITY_LIST? LF) 108 | REF* 109 | REF ::= BytesPacket(OID_STR SP REF_NAME LF) 110 | SERVICE_NAME ::= NAME 111 | CAPABILITY_LIST ::= CAPABILITY 112 | | CAPABILITY (SP CAPABILITY_LIST)? 113 | CAPABILITY ::= NAME 114 | ``` 115 | 116 | ### HTTP transport /git-upload-pack 117 | 118 | ``` 119 | HTTP_UPLOAD_PACK_REQ ::= UPLOAD_PACK_V0_V1_REQ | PROTOCOL_V2_REQ 120 | 121 | UPLOAD_PACK_V0_V1_REQ ::= BytesPacket("want" SP OID_STR (SP CAPABILITY_LIST)? LF) 122 | CLIENT_WANT* 123 | SHALLOW_REQUEST* 124 | (DEPTH_REQUEST)? 125 | (FILTER_REQUEST)? 126 | FlushPacket() 127 | NEXT_NEGOTIATION 128 | NEXT_NEGOTIATION ::= CLIENT_HAVE* 129 | FlushPacket() NEXT_NEGOTIATION | BytesPacket("done")) 130 | CLIENT_WANT ::= BytesPacket("want" SP OID_STR LF) 131 | SHALLOW_REQUEST ::= BytesPacket("shallow" SP OID_STR LF) 132 | DEPTH_REQUEST ::= BytesPacket("deepen" SP DECIMAL_NUMBER LF) 133 | | BytesPacket("deepen-since" SP DECIMAL_NUMBER LF) 134 | | BytesPacket("deepen-not" SP REF_NAME LF) 135 | FILTER_REQUEST ::= BytesPacket("filter" SP ANY_STR LF) 136 | CLIENT_HAVE ::= BytesPacket("have" SP OID_STR LF) 137 | 138 | HTTP_UPLOAD_PACK_RESP ::= UPLOAD_PACK_V0_V1_RESP | PROTOCOL_V2_RESP 139 | 140 | UPLOAD_PACK_V0_V1_RESP ::= SHALLOW_UPDATE* 141 | FlushPacket? 142 | ACKNOWLEDGEMENT+ 143 | (MaybeSidebandEncoding(PACK_FILE))? 144 | FlushPacket() 145 | SHALLOW_UPDATE ::= BytesPacket("shallow" SP OID_STR LF)* 146 | BytesPacket("unshallow" SP OID_STR LF)* 147 | ACKNOWLEDGEMENT ::= BytesPacket("ACK" SP OID_STR SP ("continue" | "common" | "ready") LF)* 148 | | BytesPacket("ACK" SP OID_STR LF) 149 | | BytesPacket("NAK" LF) 150 | ``` 151 | 152 | ### HTTP transport /git-receive-pack 153 | 154 | ``` 155 | HTTP_RECEIVE_PACK_REQ ::= RECEIVE_PACK_V0_V1_REQ | PROTOCOL_V2_REQ 156 | 157 | RECEIVE_PACK_V0_V1_REQ ::= CLIENT_SHALLOW* 158 | (COMMAND_LIST | PUSH_CERT) 159 | (PUSH_OPTION* FlushPacket())? 160 | (PACK_FILE)? 161 | CLIENT_SHALLOW ::= BytesPacket("shallow" SP OID_STR LF) 162 | COMMAND_LIST ::= BytesPacket(COMMAND NUL SP? CAPABILITY_LIST? LF) 163 | BytesPacket(COMMAND LF)* 164 | FlushPacket() 165 | COMMAND ::= OID_STR SP OID_STR SP REF_NAME 166 | PUSH_CERT ::= BytesPacket("push-cert" NUL SP? CAPABILITIY_LIST? LF) 167 | BytesPacket("certificate version 0.1" LF) 168 | BytesPacket("pusher" SP ANY_STR LF) 169 | (BytesPacket("pushee" SP ANY_STR LF))? 170 | BytesPacket("nonce" SP ANY_STR LF) 171 | BytesPacket("push-option" SP ANY_STR LF)* 172 | BytesPacket(LF) 173 | BytesPacket(COMMAND LF)* 174 | BytesPacket(GPG_SIGNATURE_LINES LF)* 175 | BytesPacket("push-cert-end" LF) 176 | GPG_SIGNATURE_LINES ::= ANY_BYTES 177 | PUSH_OPTION ::= BytesPacket(ANY_STR LF) 178 | PACK_FILE ::= "PACK" ANY_BYTES 179 | 180 | HTTP_RECEIVE_PACK_RESP ::= 181 | | MaybeSidebandEncoding(RECEIVE_PACK_V0_V1_RESP) 182 | FlushPacket() 183 | | PROTOCOL_V2_RESP 184 | 185 | RECEIVE_PACK_V0_V1_RESP ::= 186 | | BytesPacket("unpack" SP ("ok" | ANY_STR) LF) 187 | REF_UPDATE_RESULT+ 188 | REF_UPDATE_RESULT ::= BytesPacket("ok" SP REF_NAME LF) 189 | | BytesPacket("ng" SP REF_NAME SP ANY_STR LF) 190 | ``` 191 | 192 | ## Questions 193 | 194 | ### What's wrong with the capability list 195 | 196 | In case you haven't noticed this, let us extract the rules around capabilities: 197 | 198 | ``` 199 | INFO_REFS_V0_V1 ::= ... 200 | BytesPacket(OID_STR SP REF_NAME NUL CAPABILITY_LIST? LF) 201 | UPLOAD_PACK_V0_V1_REQ ::= BytesPacket("want" SP OID_STR (SP CAPABILITY_LIST)? LF) 202 | COMMAND_LIST ::= BytesPacket(COMMAND NUL SP? CAPABILITY_LIST? LF) 203 | PUSH_CERT ::= BytesPacket("push-cert" NUL CAPABILITIY_LIST? LF) 204 | ``` 205 | 206 | The way the capabilities are sent is inconsistent. They're even not in the first 207 | line in some cases. We have no idea what's going on, but anyway the protocol v2 208 | will get rid of this. 209 | 210 | ### What's wrong with the push-options 211 | 212 | When a client pushes a push certificate, it sends the push options twice. We 213 | have no idea what's going on. 214 | 215 | ### Is push cert's pushee optional? 216 | 217 | This can be a bug. Looking at builtin/send-pack.c, args.url is not set for this 218 | path always. 219 | -------------------------------------------------------------------------------- /v1uploadpackreq.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "strconv" 21 | "strings" 22 | ) 23 | 24 | type protocolV1UploadPackRequestState int 25 | 26 | const ( 27 | protocolV1UploadPackRequestStateBegin protocolV1UploadPackRequestState = iota 28 | protocolV1UploadPackRequestStateScanWants 29 | protocolV1UploadPackRequestStateScanShallows 30 | protocolV1UploadPackRequestStateScanDepth 31 | protocolV1UploadPackRequestStateScanFilter 32 | protocolV1UploadPackRequestStateBeginNegotiationOrDoneOrEnd 33 | protocolV1UploadPackRequestStateNegotiation 34 | protocolV1UploadPackRequestStateScanHaves 35 | protocolV1UploadPackRequestStateEnd 36 | ) 37 | 38 | // ProtocolV1UploadPackRequestChunk is a chunk of a protocol v1 git-upload-pack 39 | // request. 40 | type ProtocolV1UploadPackRequestChunk struct { 41 | Capabilities []string 42 | WantObjectID string 43 | ShallowObjectID string 44 | DeepenDepth int 45 | // Not documented, but seconds from UNIX epoch. 46 | DeepenSince uint64 47 | DeepenNotRef string 48 | FilterSpec string 49 | HaveObjectID string 50 | EndOneRound bool 51 | NoMoreNegotiation bool 52 | } 53 | 54 | // EncodeToPktLine serializes the chunk. 55 | func (c *ProtocolV1UploadPackRequestChunk) EncodeToPktLine() []byte { 56 | if len(c.Capabilities) > 0 && c.WantObjectID != "" { 57 | return BytesPacket([]byte(fmt.Sprintf("want %s %s\n", c.WantObjectID, strings.Join(c.Capabilities, " ")))).EncodeToPktLine() 58 | } 59 | if c.WantObjectID != "" { 60 | return BytesPacket([]byte(fmt.Sprintf("want %s\n", c.WantObjectID))).EncodeToPktLine() 61 | } 62 | if c.ShallowObjectID != "" { 63 | return BytesPacket([]byte(fmt.Sprintf("shallow %s\n", c.ShallowObjectID))).EncodeToPktLine() 64 | } 65 | if c.DeepenDepth != 0 { 66 | return BytesPacket([]byte(fmt.Sprintf("deepen %d\n", c.DeepenDepth))).EncodeToPktLine() 67 | } 68 | if c.DeepenSince != 0 { 69 | return BytesPacket([]byte(fmt.Sprintf("deepen-since %d\n", c.DeepenSince))).EncodeToPktLine() 70 | } 71 | if c.DeepenNotRef != "" { 72 | return BytesPacket([]byte(fmt.Sprintf("deepen-not %s\n", c.DeepenNotRef))).EncodeToPktLine() 73 | } 74 | if c.FilterSpec != "" { 75 | return BytesPacket([]byte(fmt.Sprintf("filter %s\n", c.FilterSpec))).EncodeToPktLine() 76 | } 77 | if c.HaveObjectID != "" { 78 | return BytesPacket([]byte(fmt.Sprintf("have %s\n", c.HaveObjectID))).EncodeToPktLine() 79 | } 80 | if c.EndOneRound { 81 | return FlushPacket{}.EncodeToPktLine() 82 | } 83 | if c.NoMoreNegotiation { 84 | return BytesPacket([]byte("done\n")).EncodeToPktLine() 85 | } 86 | panic("impossible chunk") 87 | } 88 | 89 | // ProtocolV1UploadPackRequest provides an interface for reading a protocol v1 90 | // git-upload-pack request. 91 | type ProtocolV1UploadPackRequest struct { 92 | scanner *PacketScanner 93 | state protocolV1UploadPackRequestState 94 | err error 95 | curr *ProtocolV1UploadPackRequestChunk 96 | } 97 | 98 | // NewProtocolV1UploadPackRequest returns a new ProtocolV1UploadPackRequest to 99 | // read from rd. 100 | func NewProtocolV1UploadPackRequest(rd io.Reader) *ProtocolV1UploadPackRequest { 101 | return &ProtocolV1UploadPackRequest{scanner: NewPacketScanner(rd)} 102 | } 103 | 104 | // Err returns the first non-EOF error that was encountered by the 105 | // ProtocolV1UploadPackRequest. 106 | func (r *ProtocolV1UploadPackRequest) Err() error { 107 | return r.err 108 | } 109 | 110 | // Chunk returns the most recent chunk generated by a call to Scan. 111 | func (r *ProtocolV1UploadPackRequest) Chunk() *ProtocolV1UploadPackRequestChunk { 112 | return r.curr 113 | } 114 | 115 | // Scan advances the scanner to the next packet. It returns false when the scan 116 | // stops, either by reaching the end of the input or an error. After scan 117 | // returns false, the Err method will return any error that occurred during 118 | // scanning, except that if it was io.EOF, Err will return nil. 119 | func (r *ProtocolV1UploadPackRequest) Scan() bool { 120 | if r.err != nil || r.state == protocolV1UploadPackRequestStateEnd { 121 | return false 122 | } 123 | if !r.scanner.Scan() { 124 | r.err = r.scanner.Err() 125 | if r.err == nil && r.state != protocolV1UploadPackRequestStateBeginNegotiationOrDoneOrEnd { 126 | r.err = SyntaxError("early EOF") 127 | } 128 | return false 129 | } 130 | pkt := r.scanner.Packet() 131 | 132 | if r.state == protocolV1UploadPackRequestStateBegin { 133 | bp, ok := pkt.(BytesPacket) 134 | if !ok { 135 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 136 | return false 137 | } 138 | ss := strings.SplitN(string(bp), " ", 3) 139 | if len(ss) < 2 { 140 | r.err = SyntaxError("cannot split wants: " + string(bp)) 141 | return false 142 | } 143 | caps := []string{} 144 | if len(ss) == 3 { 145 | if capStr := strings.TrimSuffix(ss[2], "\n"); capStr != "" { 146 | // This is to avoid strings.Split("", " ") => []string{""}. 147 | caps = strings.Split(capStr, " ") 148 | } 149 | } 150 | if ss[0] != "want" { 151 | r.err = SyntaxError("the first packet is not want: " + string(bp)) 152 | } 153 | r.state = protocolV1UploadPackRequestStateScanWants 154 | r.curr = &ProtocolV1UploadPackRequestChunk{ 155 | Capabilities: caps, 156 | WantObjectID: ss[1], 157 | } 158 | return true 159 | } 160 | 161 | if _, ok := pkt.(FlushPacket); ok { 162 | r.state = protocolV1UploadPackRequestStateBeginNegotiationOrDoneOrEnd 163 | r.curr = &ProtocolV1UploadPackRequestChunk{ 164 | EndOneRound: true, 165 | } 166 | return true 167 | } 168 | 169 | bp, ok := pkt.(BytesPacket) 170 | if !ok { 171 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 172 | return false 173 | } 174 | s := strings.TrimSuffix(string(bp), "\n") 175 | 176 | if s == "done" { 177 | if r.state == protocolV1UploadPackRequestStateNegotiation || r.state == protocolV1UploadPackRequestStateBeginNegotiationOrDoneOrEnd { 178 | r.state = protocolV1UploadPackRequestStateEnd 179 | r.curr = &ProtocolV1UploadPackRequestChunk{ 180 | NoMoreNegotiation: true, 181 | } 182 | return true 183 | } 184 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 185 | return false 186 | } 187 | 188 | ss := strings.SplitN(s, " ", 2) 189 | if len(ss) != 2 { 190 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 191 | return false 192 | } 193 | 194 | switch r.state { 195 | case protocolV1UploadPackRequestStateScanWants: 196 | if ss[0] == "want" { 197 | r.curr = &ProtocolV1UploadPackRequestChunk{ 198 | WantObjectID: ss[1], 199 | } 200 | return true 201 | } 202 | fallthrough 203 | case protocolV1UploadPackRequestStateScanShallows: 204 | if ss[0] == "shallow" { 205 | r.state = protocolV1UploadPackRequestStateScanShallows 206 | r.curr = &ProtocolV1UploadPackRequestChunk{ 207 | ShallowObjectID: ss[1], 208 | } 209 | return true 210 | } 211 | fallthrough 212 | case protocolV1UploadPackRequestStateScanDepth: 213 | if ss[0] == "deepen" { 214 | depth, err := strconv.ParseInt(ss[1], 10, strconv.IntSize) 215 | if err != nil { 216 | r.err = SyntaxError("cannot parse depth") 217 | return false 218 | } 219 | r.state = protocolV1UploadPackRequestStateScanFilter 220 | r.curr = &ProtocolV1UploadPackRequestChunk{ 221 | DeepenDepth: int(depth), 222 | } 223 | return true 224 | } 225 | if ss[0] == "deepen-since" { 226 | since, err := strconv.ParseUint(ss[1], 10, 64) 227 | if err != nil { 228 | r.err = SyntaxError("cannot parse depth") 229 | return false 230 | } 231 | r.state = protocolV1UploadPackRequestStateScanFilter 232 | r.curr = &ProtocolV1UploadPackRequestChunk{ 233 | DeepenSince: since, 234 | } 235 | return true 236 | } 237 | if ss[0] == "deepen-not" { 238 | r.state = protocolV1UploadPackRequestStateScanFilter 239 | r.curr = &ProtocolV1UploadPackRequestChunk{ 240 | DeepenNotRef: ss[1], 241 | } 242 | return true 243 | } 244 | fallthrough 245 | case protocolV1UploadPackRequestStateScanFilter: 246 | if ss[0] != "filter" { 247 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 248 | return false 249 | } 250 | r.state = protocolV1UploadPackRequestStateNegotiation 251 | r.curr = &ProtocolV1UploadPackRequestChunk{ 252 | FilterSpec: ss[1], 253 | } 254 | return true 255 | case protocolV1UploadPackRequestStateNegotiation, protocolV1UploadPackRequestStateBeginNegotiationOrDoneOrEnd: 256 | if ss[0] != "have" { 257 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 258 | return false 259 | } 260 | r.state = protocolV1UploadPackRequestStateNegotiation 261 | r.curr = &ProtocolV1UploadPackRequestChunk{ 262 | HaveObjectID: ss[1], 263 | } 264 | return true 265 | } 266 | panic("impossible state") 267 | } 268 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /testing/http_proxy_server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testing 16 | 17 | import ( 18 | "compress/gzip" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "log" 23 | "net/http" 24 | "net/url" 25 | "path" 26 | "sync" 27 | 28 | "github.com/google/gitprotocolio" 29 | ) 30 | 31 | // HTTPProxyHandler returns an http.handler that delegates requests to the 32 | // provided URL. 33 | func HTTPProxyHandler(delegateURL string) http.Handler { 34 | s := &httpProxyServer{delegateURL} 35 | mux := http.NewServeMux() 36 | mux.HandleFunc("/info/refs", s.infoRefsHandler) 37 | mux.HandleFunc("/git-upload-pack", s.uploadPackHandler) 38 | mux.HandleFunc("/git-receive-pack", s.receivePackHandler) 39 | return mux 40 | } 41 | 42 | type httpProxyServer struct { 43 | delegateURL string 44 | } 45 | 46 | func (s *httpProxyServer) infoRefsHandler(w http.ResponseWriter, r *http.Request) { 47 | u, err := httpURLForLsRemote(s.delegateURL, r.URL.Query().Get("service")) 48 | if err != nil { 49 | http.Error(w, "cannot construct the /info/refs URL", http.StatusInternalServerError) 50 | log.Printf("cannot construct the /info/refs URL: %#v", err) 51 | return 52 | } 53 | req, err := http.NewRequest("GET", u, nil) 54 | if err != nil { 55 | http.Error(w, "cannot construct the request object", http.StatusInternalServerError) 56 | return 57 | } 58 | req.Header.Add("Accept", "*/*") 59 | if proto := r.Header.Get("Git-Protocol"); proto == "version=2" || proto == "version=1" { 60 | req.Header.Add("Git-Protocol", proto) 61 | } 62 | 63 | resp, err := http.DefaultClient.Do(req) 64 | if err != nil { 65 | http.Error(w, "cannot send a request to the delegate", http.StatusInternalServerError) 66 | return 67 | } 68 | if resp.StatusCode != http.StatusOK { 69 | http.Error(w, resp.Status, resp.StatusCode) 70 | return 71 | } 72 | 73 | w.Header().Add("Content-Type", fmt.Sprintf("application/x-%s-advertisement", r.URL.Query().Get("service"))) 74 | infoRefsResp := gitprotocolio.NewInfoRefsResponse(resp.Body) 75 | for infoRefsResp.Scan() { 76 | if err := writePacket(w, infoRefsResp.Chunk()); err != nil { 77 | writePacket(w, gitprotocolio.ErrorPacket("cannot write a packet")) 78 | return 79 | } 80 | } 81 | 82 | if err := infoRefsResp.Err(); err != nil { 83 | if ep, ok := err.(gitprotocolio.ErrorPacket); ok { 84 | writePacket(w, ep) 85 | } else { 86 | writePacket(w, gitprotocolio.ErrorPacket("internal error")) 87 | log.Printf("Parsing error: %#v, parser: %#v", err, infoRefsResp) 88 | } 89 | return 90 | } 91 | } 92 | 93 | func (s *httpProxyServer) uploadPackHandler(w http.ResponseWriter, r *http.Request) { 94 | u, err := httpURLForUploadPack(s.delegateURL) 95 | if err != nil { 96 | http.Error(w, "cannot construct the /git-upload-pack URL", http.StatusInternalServerError) 97 | return 98 | } 99 | 100 | if r.Header.Get("Content-Encoding") == "gzip" { 101 | var err error 102 | if r.Body, err = gzip.NewReader(r.Body); err != nil { 103 | http.Error(w, "cannot ungzip", http.StatusBadRequest) 104 | return 105 | } 106 | } 107 | 108 | if r.Header.Get("Git-Protocol") == "version=2" { 109 | serveProtocolV2(u, w, r) 110 | return 111 | } 112 | uploadPackV1Handler(u, w, r) 113 | } 114 | 115 | func uploadPackV1Handler(delegateURL string, w http.ResponseWriter, r *http.Request) { 116 | pr, pw := io.Pipe() 117 | go func() { 118 | defer pw.Close() 119 | v1Req := gitprotocolio.NewProtocolV1UploadPackRequest(r.Body) 120 | 121 | for v1Req.Scan() { 122 | if err := writePacket(pw, v1Req.Chunk()); err != nil { 123 | writePacket(pw, gitprotocolio.ErrorPacket("cannot write a packet")) 124 | return 125 | } 126 | } 127 | 128 | if err := v1Req.Err(); err != nil { 129 | if ep, ok := err.(gitprotocolio.ErrorPacket); ok { 130 | writePacket(pw, ep) 131 | } else { 132 | writePacket(pw, gitprotocolio.ErrorPacket("internal error")) 133 | log.Printf("Parsing error: %#v, parser: %#v", err, v1Req) 134 | } 135 | return 136 | } 137 | }() 138 | 139 | req, err := http.NewRequest("POST", delegateURL, pr) 140 | if err != nil { 141 | http.Error(w, "cannot construct the request object", http.StatusInternalServerError) 142 | return 143 | } 144 | req.Header.Add("Content-Type", "application/x-git-upload-pack-request") 145 | req.Header.Add("Accept", "application/x-git-upload-pack-result") 146 | 147 | resp, err := http.DefaultClient.Do(req) 148 | if err != nil { 149 | http.Error(w, "cannot send a request to the delegate", http.StatusInternalServerError) 150 | return 151 | } 152 | if resp.StatusCode != http.StatusOK { 153 | http.Error(w, resp.Status, resp.StatusCode) 154 | return 155 | } 156 | 157 | w.Header().Add("Content-Type", "application/x-git-upload-pack-result") 158 | v1Resp := gitprotocolio.NewProtocolV1UploadPackResponse(resp.Body) 159 | for v1Resp.Scan() { 160 | if err := writePacket(w, v1Resp.Chunk()); err != nil { 161 | writePacket(w, gitprotocolio.ErrorPacket("cannot write a packet")) 162 | return 163 | } 164 | } 165 | 166 | if err := v1Resp.Err(); err != nil { 167 | if ep, ok := err.(gitprotocolio.ErrorPacket); ok { 168 | writePacket(w, ep) 169 | } else { 170 | writePacket(w, gitprotocolio.ErrorPacket("internal error")) 171 | log.Printf("Parsing error: %#v, parser: %#v", err, v1Resp) 172 | } 173 | return 174 | } 175 | } 176 | 177 | func (s *httpProxyServer) receivePackHandler(w http.ResponseWriter, r *http.Request) { 178 | u, err := httpURLForReceivePack(s.delegateURL) 179 | if err != nil { 180 | http.Error(w, "cannot construct the /git-receive-pack URL", http.StatusInternalServerError) 181 | return 182 | } 183 | 184 | if r.Header.Get("Content-Encoding") == "gzip" { 185 | var err error 186 | if r.Body, err = gzip.NewReader(r.Body); err != nil { 187 | http.Error(w, "cannot ungzip", http.StatusBadRequest) 188 | return 189 | } 190 | } 191 | 192 | if r.Header.Get("Git-Protocol") == "version=2" { 193 | serveProtocolV2(u, w, r) 194 | return 195 | } 196 | receivePackV1Handler(u, w, r) 197 | } 198 | 199 | func receivePackV1Handler(delegateURL string, w http.ResponseWriter, r *http.Request) { 200 | pr, pw := io.Pipe() 201 | go func() { 202 | defer pw.Close() 203 | v1Req := gitprotocolio.NewProtocolV1ReceivePackRequest(r.Body) 204 | 205 | for v1Req.Scan() { 206 | if err := writePacket(pw, v1Req.Chunk()); err != nil { 207 | writePacket(pw, gitprotocolio.ErrorPacket("cannot write a packet")) 208 | return 209 | } 210 | } 211 | 212 | if err := v1Req.Err(); err != nil { 213 | if ep, ok := err.(gitprotocolio.ErrorPacket); ok { 214 | writePacket(pw, ep) 215 | } else { 216 | writePacket(pw, gitprotocolio.ErrorPacket("internal error")) 217 | log.Printf("Parsing error: %#v, parser: %#v", err, v1Req) 218 | } 219 | return 220 | } 221 | }() 222 | 223 | req, err := http.NewRequest("POST", delegateURL, pr) 224 | if err != nil { 225 | http.Error(w, "cannot construct the request object", http.StatusInternalServerError) 226 | return 227 | } 228 | req.Header.Add("Content-Type", "application/x-git-receive-pack-request") 229 | req.Header.Add("Accept", "application/x-git-receive-pack-result") 230 | 231 | resp, err := http.DefaultClient.Do(req) 232 | if err != nil { 233 | http.Error(w, "cannot send a request to the delegate", http.StatusInternalServerError) 234 | return 235 | } 236 | if resp.StatusCode != http.StatusOK { 237 | http.Error(w, resp.Status, resp.StatusCode) 238 | return 239 | } 240 | 241 | pktWt := synchronizedWriter{w: w} 242 | mainRd, mainWt := io.Pipe() 243 | go func() { 244 | defer mainWt.Close() 245 | sc := gitprotocolio.NewPacketScanner(resp.Body) 246 | scanner: 247 | for sc.Scan() { 248 | switch p := sc.Packet().(type) { 249 | case gitprotocolio.BytesPacket: 250 | sp := gitprotocolio.ParseSideBandPacket(p) 251 | if mp, ok := sp.(gitprotocolio.SideBandMainPacket); ok { 252 | if _, err := mainWt.Write(mp); err != nil { 253 | pktWt.closeWithError(err) 254 | return 255 | } 256 | } 257 | pktWt.writePacket(sp) 258 | case gitprotocolio.FlushPacket: 259 | break scanner 260 | default: 261 | pktWt.closeWithError(fmt.Errorf("unexpected packet: %#v", sc.Packet())) 262 | return 263 | } 264 | } 265 | if err := sc.Err(); err != nil { 266 | pktWt.closeWithError(err) 267 | } 268 | }() 269 | ch, chunkWt := gitprotocolio.NewChunkedWriter(0xFFFF - 5) 270 | go func() { 271 | defer chunkWt.Close() 272 | v1Resp := gitprotocolio.NewProtocolV1ReceivePackResponse(mainRd) 273 | for v1Resp.Scan() { 274 | if err := writePacket(chunkWt, v1Resp.Chunk()); err != nil { 275 | pktWt.closeWithError(err) 276 | return 277 | } 278 | } 279 | if err := v1Resp.Err(); err != nil { 280 | log.Println(err) 281 | pktWt.closeWithError(err) 282 | } 283 | }() 284 | 285 | w.Header().Add("Content-Type", "application/x-git-receive-pack-result") 286 | for bs := range ch { 287 | pktWt.writePacket(gitprotocolio.SideBandMainPacket(bs)) 288 | } 289 | pktWt.writePacket(gitprotocolio.FlushPacket{}) 290 | 291 | } 292 | 293 | func serveProtocolV2(delegateURL string, w http.ResponseWriter, r *http.Request) { 294 | pr, pw := io.Pipe() 295 | go func() { 296 | defer pw.Close() 297 | v2Req := gitprotocolio.NewProtocolV2Request(r.Body) 298 | 299 | for v2Req.Scan() { 300 | if err := writePacket(pw, v2Req.Chunk()); err != nil { 301 | writePacket(pw, gitprotocolio.ErrorPacket("cannot write a packet")) 302 | return 303 | } 304 | } 305 | 306 | if err := v2Req.Err(); err != nil { 307 | if ep, ok := err.(gitprotocolio.ErrorPacket); ok { 308 | writePacket(pw, ep) 309 | } else { 310 | writePacket(pw, gitprotocolio.ErrorPacket("internal error")) 311 | log.Printf("Parsing error: %#v, parser: %#v", err, v2Req) 312 | } 313 | return 314 | } 315 | }() 316 | 317 | req, err := http.NewRequest("POST", delegateURL, pr) 318 | if err != nil { 319 | http.Error(w, "cannot construct the request object", http.StatusInternalServerError) 320 | return 321 | } 322 | req.Header.Add("Content-Type", "application/x-git-upload-pack-request") 323 | req.Header.Add("Accept", "application/x-git-upload-pack-result") 324 | req.Header.Add("Git-Protocol", "version=2") 325 | 326 | resp, err := http.DefaultClient.Do(req) 327 | if err != nil { 328 | http.Error(w, "cannot send a request to the delegate", http.StatusInternalServerError) 329 | return 330 | } 331 | if resp.StatusCode != http.StatusOK { 332 | http.Error(w, resp.Status, resp.StatusCode) 333 | return 334 | } 335 | 336 | w.Header().Add("Content-Type", "application/x-git-upload-pack-result") 337 | v2Resp := gitprotocolio.NewProtocolV2Response(resp.Body) 338 | for v2Resp.Scan() { 339 | if err := writePacket(w, v2Resp.Chunk()); err != nil { 340 | writePacket(w, gitprotocolio.ErrorPacket("cannot write a packet")) 341 | return 342 | } 343 | } 344 | 345 | if err := v2Resp.Err(); err != nil { 346 | if ep, ok := err.(gitprotocolio.ErrorPacket); ok { 347 | writePacket(w, ep) 348 | } else { 349 | writePacket(w, gitprotocolio.ErrorPacket("internal error")) 350 | log.Printf("Parsing error: %#v, parser: %#v", err, v2Resp) 351 | } 352 | } 353 | } 354 | 355 | func httpURLForLsRemote(base, service string) (string, error) { 356 | u, err := url.Parse(base) 357 | if err != nil { 358 | return "", err 359 | } 360 | query := u.Query() 361 | query.Add("service", service) 362 | u.Path = path.Join(u.Path, "/info/refs") 363 | u.RawQuery = query.Encode() 364 | return u.String(), nil 365 | } 366 | 367 | func httpURLForUploadPack(base string) (string, error) { 368 | u, err := url.Parse(base) 369 | if err != nil { 370 | return "", err 371 | } 372 | u.Path = path.Join(u.Path, "/git-upload-pack") 373 | return u.String(), nil 374 | } 375 | 376 | func httpURLForReceivePack(base string) (string, error) { 377 | u, err := url.Parse(base) 378 | if err != nil { 379 | return "", err 380 | } 381 | u.Path = path.Join(u.Path, "/git-receive-pack") 382 | return u.String(), nil 383 | } 384 | 385 | func writePacket(w io.Writer, p gitprotocolio.Packet) error { 386 | _, err := w.Write(p.EncodeToPktLine()) 387 | return err 388 | } 389 | 390 | type synchronizedWriter struct { 391 | w io.Writer 392 | m sync.Mutex 393 | closed bool 394 | } 395 | 396 | func (s *synchronizedWriter) writePacket(p gitprotocolio.Packet) error { 397 | s.m.Lock() 398 | defer s.m.Unlock() 399 | if s.closed { 400 | return errors.New("already closed") 401 | } 402 | _, err := s.w.Write(p.EncodeToPktLine()) 403 | return err 404 | } 405 | 406 | func (s *synchronizedWriter) closeWithError(err error) { 407 | s.m.Lock() 408 | defer s.m.Unlock() 409 | if s.closed { 410 | return 411 | } 412 | s.closed = true 413 | s.w.Write(gitprotocolio.SideBandErrorPacket(err.Error()).EncodeToPktLine()) 414 | } 415 | -------------------------------------------------------------------------------- /v1receivepackreq.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitprotocolio 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "strings" 22 | ) 23 | 24 | type protocolV1ReceivePackRequestState int 25 | 26 | const ( 27 | protocolV1ReceivePackRequestStateBegin protocolV1ReceivePackRequestState = iota 28 | 29 | protocolV1ReceivePackRequestStateScanCommandAndCapabilities 30 | protocolV1ReceivePackRequestStateScanCommand 31 | 32 | protocolV1ReceivePackRequestStateScanCert 33 | protocolV1ReceivePackRequestStateScanCertVersion 34 | protocolV1ReceivePackRequestStateScanCertPusher 35 | protocolV1ReceivePackRequestStateScanCertPusheeOrNonce 36 | protocolV1ReceivePackRequestStateScanCertNonce 37 | protocolV1ReceivePackRequestStateScanOptionalCertPushOptions 38 | protocolV1ReceivePackRequestStateScanCertCommand 39 | protocolV1ReceivePackRequestStateScanCertGPGLine 40 | 41 | protocolV1ReceivePackRequestStateScanOptionalPushOptions 42 | protocolV1ReceivePackRequestStateScanPushOptions 43 | protocolV1ReceivePackRequestStateScanPackFile 44 | ) 45 | 46 | // ProtocolV1ReceivePackRequestChunk is a chunk of a protocol v1 47 | // git-receive-pack request. 48 | type ProtocolV1ReceivePackRequestChunk struct { 49 | ClientShallow string 50 | 51 | Capabilities []string 52 | OldObjectID string 53 | NewObjectID string 54 | RefName string 55 | EndOfCommands bool 56 | 57 | StartOfPushCert bool 58 | PushCertHeader bool 59 | Pusher string 60 | Pushee string 61 | Nonce string 62 | CertPushOption string 63 | EndOfCertPushOptions bool 64 | GPGSignaturePart []byte 65 | EndOfPushCert bool 66 | 67 | PushOption string 68 | EndOfPushOptions bool 69 | 70 | PackStream []byte 71 | } 72 | 73 | // EncodeToPktLine serializes the chunk. 74 | func (c *ProtocolV1ReceivePackRequestChunk) EncodeToPktLine() []byte { 75 | if c.ClientShallow != "" { 76 | return BytesPacket([]byte(fmt.Sprintf("shallow %s\n", c.ClientShallow))).EncodeToPktLine() 77 | } 78 | if c.OldObjectID != "" && c.NewObjectID != "" && c.RefName != "" { 79 | if len(c.Capabilities) != 0 { 80 | return BytesPacket([]byte(fmt.Sprintf("%s %s %s\x00%s\n", c.OldObjectID, c.NewObjectID, c.RefName, strings.Join(c.Capabilities, " ")))).EncodeToPktLine() 81 | } 82 | return BytesPacket([]byte(fmt.Sprintf("%s %s %s\n", c.OldObjectID, c.NewObjectID, c.RefName))).EncodeToPktLine() 83 | } 84 | if c.EndOfCommands { 85 | return FlushPacket{}.EncodeToPktLine() 86 | } 87 | if c.PushOption != "" { 88 | return BytesPacket([]byte(fmt.Sprintf("%s\n", c.PushOption))).EncodeToPktLine() 89 | } 90 | if c.EndOfPushOptions { 91 | return FlushPacket{}.EncodeToPktLine() 92 | } 93 | if c.StartOfPushCert { 94 | return BytesPacket([]byte(fmt.Sprintf("push-cert\x00%s\n", strings.Join(c.Capabilities, " ")))).EncodeToPktLine() 95 | } 96 | if c.PushCertHeader { 97 | return BytesPacket([]byte("certificate version 0.1\n")).EncodeToPktLine() 98 | } 99 | if c.Pusher != "" { 100 | return BytesPacket([]byte(fmt.Sprintf("pusher %s\n", c.Pusher))).EncodeToPktLine() 101 | } 102 | if c.Pushee != "" { 103 | return BytesPacket([]byte(fmt.Sprintf("pushee %s\n", c.Pushee))).EncodeToPktLine() 104 | } 105 | if c.Nonce != "" { 106 | return BytesPacket([]byte(fmt.Sprintf("nonce %s\n", c.Nonce))).EncodeToPktLine() 107 | } 108 | if c.CertPushOption != "" { 109 | return BytesPacket([]byte(fmt.Sprintf("push-option %s\n", c.CertPushOption))).EncodeToPktLine() 110 | } 111 | if c.EndOfCertPushOptions { 112 | return BytesPacket([]byte("\n")).EncodeToPktLine() 113 | } 114 | if len(c.GPGSignaturePart) != 0 { 115 | return BytesPacket(c.GPGSignaturePart).EncodeToPktLine() 116 | } 117 | if c.EndOfPushCert { 118 | return BytesPacket([]byte("push-cert-end\n")).EncodeToPktLine() 119 | } 120 | // TODO 121 | if len(c.PackStream) != 0 { 122 | return c.PackStream 123 | } 124 | panic("impossible chunk") 125 | } 126 | 127 | // ProtocolV1ReceivePackRequest provides an interface for reading a protocol v1 128 | // git-receive-pack request. 129 | type ProtocolV1ReceivePackRequest struct { 130 | scanner *PacketScanner 131 | state protocolV1ReceivePackRequestState 132 | err error 133 | curr *ProtocolV1ReceivePackRequestChunk 134 | } 135 | 136 | // NewProtocolV1ReceivePackRequest returns a new ProtocolV1ReceivePackRequest to 137 | // read from rd. 138 | func NewProtocolV1ReceivePackRequest(rd io.Reader) *ProtocolV1ReceivePackRequest { 139 | return &ProtocolV1ReceivePackRequest{scanner: NewPacketScanner(rd)} 140 | } 141 | 142 | // Err returns the first non-EOF error that was encountered by the 143 | // ProtocolV1ReceivePackRequest. 144 | func (r *ProtocolV1ReceivePackRequest) Err() error { 145 | return r.err 146 | } 147 | 148 | // Chunk returns the most recent chunk generated by a call to Scan. 149 | func (r *ProtocolV1ReceivePackRequest) Chunk() *ProtocolV1ReceivePackRequestChunk { 150 | return r.curr 151 | } 152 | 153 | // Scan advances the scanner to the next packet. It returns false when the scan 154 | // stops, either by reaching the end of the input or an error. After scan 155 | // returns false, the Err method will return any error that occurred during 156 | // scanning, except that if it was io.EOF, Err will return nil. 157 | func (r *ProtocolV1ReceivePackRequest) Scan() bool { 158 | if r.err != nil { 159 | return false 160 | } 161 | if !r.scanner.Scan() { 162 | r.err = r.scanner.Err() 163 | if r.err == nil && r.state != protocolV1ReceivePackRequestStateScanOptionalPushOptions && r.state != protocolV1ReceivePackRequestStateScanPackFile { 164 | r.err = SyntaxError("early EOF") 165 | } 166 | return false 167 | } 168 | pkt := r.scanner.Packet() 169 | 170 | transition: 171 | switch r.state { 172 | case protocolV1ReceivePackRequestStateBegin: 173 | bp, ok := pkt.(BytesPacket) 174 | if !ok { 175 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 176 | return false 177 | } 178 | if bytes.HasPrefix(bp, []byte("shallow ")) { 179 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 180 | ClientShallow: strings.TrimPrefix(strings.TrimSuffix(string(bp), "\n"), "shallow "), 181 | } 182 | return true 183 | } 184 | if bytes.HasPrefix(bp, []byte("push-cert\x00")) { 185 | r.state = protocolV1ReceivePackRequestStateScanCert 186 | goto transition 187 | } 188 | r.state = protocolV1ReceivePackRequestStateScanCommandAndCapabilities 189 | goto transition 190 | case protocolV1ReceivePackRequestStateScanCommandAndCapabilities: 191 | bp, ok := pkt.(BytesPacket) 192 | if !ok { 193 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 194 | return false 195 | } 196 | zss := bytes.SplitN(bp, []byte{0}, 2) 197 | if len(zss) != 2 { 198 | r.err = SyntaxError("cannot split into two: " + string(bp)) 199 | return false 200 | } 201 | caps := []string{} 202 | if capStr := strings.TrimPrefix(strings.TrimSuffix(string(zss[1]), "\n"), " "); capStr != "" { 203 | // This is to avoid strings.Split("", " ") => []string{""}. 204 | caps = strings.Split(capStr, " ") 205 | } 206 | ss := strings.SplitN(string(zss[0]), " ", 3) 207 | if len(ss) != 3 { 208 | r.err = SyntaxError("cannot split into three: " + string(zss[0])) 209 | return false 210 | } 211 | r.state = protocolV1ReceivePackRequestStateScanCommand 212 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 213 | Capabilities: caps, 214 | OldObjectID: ss[0], 215 | NewObjectID: ss[1], 216 | RefName: ss[2], 217 | } 218 | return true 219 | case protocolV1ReceivePackRequestStateScanCommand: 220 | switch p := pkt.(type) { 221 | case FlushPacket: 222 | r.state = protocolV1ReceivePackRequestStateScanOptionalPushOptions 223 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 224 | EndOfCommands: true, 225 | } 226 | return true 227 | case BytesPacket: 228 | ss := strings.SplitN(strings.TrimSuffix(string(p), "\n"), " ", 3) 229 | if len(ss) != 3 { 230 | r.err = SyntaxError("cannot split into three: " + string(p)) 231 | return false 232 | } 233 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 234 | OldObjectID: ss[0], 235 | NewObjectID: ss[1], 236 | RefName: ss[2], 237 | } 238 | return true 239 | default: 240 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 241 | return false 242 | } 243 | case protocolV1ReceivePackRequestStateScanCert: 244 | bp, ok := pkt.(BytesPacket) 245 | if !ok { 246 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 247 | return false 248 | } 249 | zss := bytes.SplitN(bp, []byte{0}, 2) 250 | if len(zss) != 2 { 251 | r.err = SyntaxError("cannot split into two: " + string(bp)) 252 | return false 253 | } 254 | caps := []string{} 255 | if capStr := strings.TrimPrefix(strings.TrimSuffix(string(zss[1]), "\n"), " "); capStr != "" { 256 | // This is to avoid strings.Split("", " ") => []string{""}. 257 | caps = strings.Split(capStr, " ") 258 | } 259 | r.state = protocolV1ReceivePackRequestStateScanCertVersion 260 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 261 | Capabilities: caps, 262 | StartOfPushCert: true, 263 | } 264 | return true 265 | case protocolV1ReceivePackRequestStateScanCertVersion: 266 | bp, ok := pkt.(BytesPacket) 267 | if !ok { 268 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 269 | return false 270 | } 271 | if string(bp) != "certificate version 0.1\n" { 272 | r.err = SyntaxError(fmt.Sprintf("unexpected certificate version: %#q", string(bp))) 273 | return false 274 | } 275 | r.state = protocolV1ReceivePackRequestStateScanCertPusher 276 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 277 | PushCertHeader: true, 278 | } 279 | return true 280 | case protocolV1ReceivePackRequestStateScanCertPusher: 281 | bp, ok := pkt.(BytesPacket) 282 | if !ok { 283 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 284 | return false 285 | } 286 | ss := strings.SplitN(strings.TrimSuffix(string(bp), "\n"), " ", 2) 287 | if len(ss) != 2 { 288 | r.err = SyntaxError("cannot split into two: " + string(bp)) 289 | return false 290 | } 291 | if ss[0] != "pusher" { 292 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", string(bp))) 293 | return false 294 | } 295 | r.state = protocolV1ReceivePackRequestStateScanCertPusheeOrNonce 296 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 297 | Pusher: ss[1], 298 | } 299 | return true 300 | case protocolV1ReceivePackRequestStateScanCertPusheeOrNonce: 301 | bp, ok := pkt.(BytesPacket) 302 | if !ok { 303 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 304 | return false 305 | } 306 | ss := strings.SplitN(strings.TrimSuffix(string(bp), "\n"), " ", 2) 307 | if len(ss) != 2 { 308 | r.err = SyntaxError("cannot split into two: " + string(bp)) 309 | return false 310 | } 311 | if ss[0] == "nonce" { 312 | r.state = protocolV1ReceivePackRequestStateScanCertNonce 313 | goto transition 314 | } 315 | if ss[0] != "pushee" { 316 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", string(bp))) 317 | return false 318 | } 319 | r.state = protocolV1ReceivePackRequestStateScanCertNonce 320 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 321 | Pushee: ss[1], 322 | } 323 | return true 324 | case protocolV1ReceivePackRequestStateScanCertNonce: 325 | bp, ok := pkt.(BytesPacket) 326 | if !ok { 327 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 328 | return false 329 | } 330 | ss := strings.SplitN(strings.TrimSuffix(string(bp), "\n"), " ", 2) 331 | if len(ss) != 2 { 332 | r.err = SyntaxError("cannot split into two: " + string(bp)) 333 | return false 334 | } 335 | if ss[0] != "nonce" { 336 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", string(bp))) 337 | return false 338 | } 339 | r.state = protocolV1ReceivePackRequestStateScanOptionalCertPushOptions 340 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 341 | Nonce: ss[1], 342 | } 343 | return true 344 | case protocolV1ReceivePackRequestStateScanOptionalCertPushOptions: 345 | bp, ok := pkt.(BytesPacket) 346 | if !ok { 347 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 348 | return false 349 | } 350 | if string(bp) == "\n" { 351 | r.state = protocolV1ReceivePackRequestStateScanCertCommand 352 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 353 | EndOfCertPushOptions: true, 354 | } 355 | return true 356 | } 357 | ss := strings.SplitN(strings.TrimSuffix(string(bp), "\n"), " ", 2) 358 | if len(ss) != 2 { 359 | r.err = SyntaxError("cannot split into two: " + string(bp)) 360 | return false 361 | } 362 | if ss[0] != "push-option" { 363 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", string(bp))) 364 | return false 365 | } 366 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 367 | CertPushOption: ss[1], 368 | } 369 | return true 370 | case protocolV1ReceivePackRequestStateScanCertCommand: 371 | bp, ok := pkt.(BytesPacket) 372 | if !ok { 373 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 374 | return false 375 | } 376 | if string(bp) == "-----BEGIN PGP SIGNATURE-----\n" { 377 | r.state = protocolV1ReceivePackRequestStateScanCertGPGLine 378 | goto transition 379 | } 380 | ss := strings.SplitN(strings.TrimSuffix(string(bp), "\n"), " ", 3) 381 | if len(ss) != 3 { 382 | r.err = SyntaxError("cannot split into three: " + string(bp)) 383 | return false 384 | } 385 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 386 | OldObjectID: ss[0], 387 | NewObjectID: ss[1], 388 | RefName: ss[2], 389 | } 390 | return true 391 | case protocolV1ReceivePackRequestStateScanCertGPGLine: 392 | bp, ok := pkt.(BytesPacket) 393 | if !ok { 394 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 395 | return false 396 | } 397 | if string(bp) == "push-cert-end\n" { 398 | r.state = protocolV1ReceivePackRequestStateScanPushOptions 399 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 400 | EndOfPushCert: true, 401 | } 402 | return true 403 | } 404 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 405 | GPGSignaturePart: bp, 406 | } 407 | return true 408 | case protocolV1ReceivePackRequestStateScanOptionalPushOptions: 409 | if _, ok := pkt.(PackFileIndicatorPacket); ok { 410 | r.state = protocolV1ReceivePackRequestStateScanPackFile 411 | goto transition 412 | } 413 | bp, ok := pkt.(BytesPacket) 414 | if !ok { 415 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", pkt)) 416 | return false 417 | } 418 | r.state = protocolV1ReceivePackRequestStateScanPushOptions 419 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 420 | PushOption: strings.TrimSuffix(string(bp), "\n"), 421 | } 422 | return true 423 | case protocolV1ReceivePackRequestStateScanPushOptions: 424 | switch p := pkt.(type) { 425 | case FlushPacket: 426 | r.state = protocolV1ReceivePackRequestStateScanPackFile 427 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 428 | EndOfPushOptions: true, 429 | } 430 | return true 431 | case BytesPacket: 432 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 433 | PushOption: strings.TrimSuffix(string(p), "\n"), 434 | } 435 | return true 436 | default: 437 | r.err = SyntaxError(fmt.Sprintf("unexpected packet: %#v", p)) 438 | return false 439 | } 440 | case protocolV1ReceivePackRequestStateScanPackFile: 441 | r.curr = &ProtocolV1ReceivePackRequestChunk{ 442 | PackStream: pkt.EncodeToPktLine(), 443 | } 444 | return true 445 | } 446 | panic("impossible state") 447 | } 448 | --------------------------------------------------------------------------------