├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── auth ├── auth.go ├── auth_test.go ├── basicauth.go └── basicauth_test.go ├── errors.go ├── events.go ├── git_reader.go ├── githttp.go ├── pktparser.go ├── pktparser_test.go ├── routing.go ├── rpc_reader.go ├── rpc_reader_test.go ├── testdata ├── receive-pack.0 ├── receive-pack.1 ├── receive-pack.2 ├── receive-pack.3 ├── upload-pack.0 └── upload-pack.1 ├── utils.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - tip 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-git-http 2 | =========== 3 | 4 | [![Build Status](https://travis-ci.org/AaronO/go-git-http.svg)](https://travis-ci.org/AaronO/go-git-http) 5 | 6 | A Smart Git Http server library in Go (golang) 7 | 8 | ### Example 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | "log" 15 | "net/http" 16 | 17 | "github.com/AaronO/go-git-http" 18 | ) 19 | 20 | func main() { 21 | // Get git handler to serve a directory of repos 22 | git := githttp.New("/Users/aaron/git") 23 | 24 | // Attach handler to http server 25 | http.Handle("/", git) 26 | 27 | // Start HTTP server 28 | err := http.ListenAndServe(":8080", nil) 29 | if err != nil { 30 | log.Fatal("ListenAndServe: ", err) 31 | } 32 | } 33 | ``` 34 | 35 | ### Authentication example 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "log" 42 | "net/http" 43 | 44 | "github.com/AaronO/go-git-http" 45 | "github.com/AaronO/go-git-http/auth" 46 | ) 47 | 48 | 49 | func main() { 50 | // Get git handler to serve a directory of repos 51 | git := githttp.New("/Users/aaron/git") 52 | 53 | // Build an authentication middleware based on a function 54 | authenticator := auth.Authenticator(func(info auth.AuthInfo) (bool, error) { 55 | // Disallow Pushes (making git server pull only) 56 | if info.Push { 57 | return false, nil 58 | } 59 | 60 | // Typically this would be a database lookup 61 | if info.Username == "admin" && info.Password == "password" { 62 | return true, nil 63 | } 64 | 65 | return false, nil 66 | }) 67 | 68 | // Attach handler to http server 69 | // wrap authenticator around git handler 70 | http.Handle("/", authenticator(git)) 71 | 72 | // Start HTTP server 73 | err := http.ListenAndServe(":8080", nil) 74 | if err != nil { 75 | log.Fatal("ListenAndServe: ", err) 76 | } 77 | } 78 | ``` 79 | 80 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type AuthInfo struct { 10 | // Usernane or email 11 | Username string 12 | // Plaintext password or token 13 | Password string 14 | 15 | // repo component of URL 16 | // Usually: "username/repo_name" 17 | // But could also be: "some_repo.git" 18 | Repo string 19 | 20 | // Are we pushing or fetching ? 21 | Push bool 22 | Fetch bool 23 | } 24 | 25 | var ( 26 | repoNameRegex = regexp.MustCompile("^/?(.*?)/(HEAD|git-upload-pack|git-receive-pack|info/refs|objects/.*)$") 27 | ) 28 | 29 | func Authenticator(authf func(AuthInfo) (bool, error)) func(http.Handler) http.Handler { 30 | return func(handler http.Handler) http.Handler { 31 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 32 | auth, err := parseAuthHeader(req.Header.Get("Authorization")) 33 | if err != nil { 34 | w.Header().Set("WWW-Authenticate", `Basic realm="git server"`) 35 | http.Error(w, err.Error(), 401) 36 | return 37 | } 38 | 39 | // Build up info from request headers and URL 40 | info := AuthInfo{ 41 | Username: auth.Name, 42 | Password: auth.Pass, 43 | Repo: repoName(req.URL.Path), 44 | Push: isPush(req), 45 | Fetch: isFetch(req), 46 | } 47 | 48 | // Call authentication function 49 | authenticated, err := authf(info) 50 | if err != nil { 51 | code := 500 52 | msg := err.Error() 53 | if se, ok := err.(StatusError); ok { 54 | code = se.StatusCode() 55 | } 56 | http.Error(w, msg, code) 57 | return 58 | } 59 | 60 | // Deny access to repo 61 | if !authenticated { 62 | http.Error(w, "Forbidden", 403) 63 | return 64 | } 65 | 66 | // Access granted 67 | handler.ServeHTTP(w, req) 68 | }) 69 | } 70 | } 71 | 72 | func isFetch(req *http.Request) bool { 73 | return isService("upload-pack", req) 74 | } 75 | 76 | func isPush(req *http.Request) bool { 77 | return isService("receive-pack", req) 78 | } 79 | 80 | func isService(service string, req *http.Request) bool { 81 | return getServiceType(req) == service || strings.HasSuffix(req.URL.Path, service) 82 | } 83 | 84 | func repoName(urlPath string) string { 85 | matches := repoNameRegex.FindStringSubmatch(urlPath) 86 | if matches == nil { 87 | return "" 88 | } 89 | return matches[1] 90 | } 91 | 92 | func getServiceType(r *http.Request) string { 93 | service_type := r.FormValue("service") 94 | 95 | if s := strings.HasPrefix(service_type, "git-"); !s { 96 | return "" 97 | } 98 | 99 | return strings.Replace(service_type, "git-", "", 1) 100 | } 101 | 102 | // StatusCode is an interface allowing authenticators 103 | // to pass down error's with an http error code 104 | type StatusError interface { 105 | StatusCode() int 106 | } 107 | -------------------------------------------------------------------------------- /auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRepoName(t *testing.T) { 8 | if x := repoName("/yapp.ss.git/HEAD"); x != "yapp.ss.git" { 9 | t.Errorf("Should have been 'yapp.js.git' is '%s'", x) 10 | } 11 | 12 | if x := repoName("aarono/gogo-proxy/HEAD"); x != "aarono/gogo-proxy" { 13 | t.Errorf("Should have been 'aarono/gogo-proxy' is '%s'", x) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /auth/basicauth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // Parse http basic header 11 | type BasicAuth struct { 12 | Name string 13 | Pass string 14 | } 15 | 16 | var ( 17 | basicAuthRegex = regexp.MustCompile("^([^:]*):(.*)$") 18 | ) 19 | 20 | func parseAuthHeader(header string) (*BasicAuth, error) { 21 | parts := strings.SplitN(header, " ", 2) 22 | if len(parts) < 2 { 23 | return nil, fmt.Errorf("Invalid authorization header, not enought parts") 24 | } 25 | 26 | authType := parts[0] 27 | authData := parts[1] 28 | 29 | if strings.ToLower(authType) != "basic" { 30 | return nil, fmt.Errorf("Authentication '%s' was not of 'Basic' type", authType) 31 | } 32 | 33 | data, err := base64.StdEncoding.DecodeString(authData) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | matches := basicAuthRegex.FindStringSubmatch(string(data)) 39 | if matches == nil { 40 | return nil, fmt.Errorf("Authorization data '%s' did not match auth regexp", data) 41 | } 42 | 43 | return &BasicAuth{ 44 | Name: matches[1], 45 | Pass: matches[2], 46 | }, nil 47 | } 48 | -------------------------------------------------------------------------------- /auth/basicauth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHeaderParsing(t *testing.T) { 8 | // Basic admin:password 9 | authorization := "Basic YWRtaW46cGFzc3dvcmQ=" 10 | 11 | auth, err := parseAuthHeader(authorization) 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | 16 | if auth.Name != "admin" { 17 | t.Errorf("Detected name does not match: '%s'", auth.Name) 18 | } 19 | if auth.Pass != "password" { 20 | t.Errorf("Detected password does not match: '%s'", auth.Pass) 21 | } 22 | } 23 | 24 | func TestEmptyHeader(t *testing.T) { 25 | if _, err := parseAuthHeader(""); err == nil { 26 | t.Errorf("Empty headers should generate errors") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ErrorNoAccess struct { 8 | // Path to directory of repo accessed 9 | Dir string 10 | } 11 | 12 | func (e *ErrorNoAccess) Error() string { 13 | return fmt.Sprintf("Could not access repo at '%s'", e.Dir) 14 | } 15 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // An event (triggered on push/pull) 9 | type Event struct { 10 | // One of tag/push/fetch 11 | Type EventType `json:"type"` 12 | 13 | //// 14 | // Set for pushes and pulls 15 | //// 16 | 17 | // SHA of commit 18 | Commit string `json:"commit"` 19 | 20 | // Path to bare repo 21 | Dir string 22 | 23 | //// 24 | // Set for pushes or tagging 25 | //// 26 | Tag string `json:"tag,omitempty"` 27 | Last string `json:"last,omitempty"` 28 | Branch string `json:"branch,omitempty"` 29 | 30 | // Error contains the error that happened (if any) 31 | // during this action/event 32 | Error error 33 | 34 | // Http stuff 35 | Request *http.Request 36 | } 37 | 38 | type EventType int 39 | 40 | // Possible event types 41 | const ( 42 | TAG = iota + 1 43 | PUSH 44 | FETCH 45 | PUSH_FORCE 46 | ) 47 | 48 | func (e EventType) String() string { 49 | switch e { 50 | case TAG: 51 | return "tag" 52 | case PUSH: 53 | return "push" 54 | case PUSH_FORCE: 55 | return "push-force" 56 | case FETCH: 57 | return "fetch" 58 | } 59 | return "unknown" 60 | } 61 | 62 | func (e EventType) MarshalJSON() ([]byte, error) { 63 | return []byte(fmt.Sprintf(`"%s"`, e)), nil 64 | } 65 | 66 | func (e EventType) UnmarshalJSON(data []byte) error { 67 | str := string(data[:]) 68 | switch str { 69 | case "tag": 70 | e = TAG 71 | case "push": 72 | e = PUSH 73 | case "push-force": 74 | e = PUSH_FORCE 75 | case "fetch": 76 | e = FETCH 77 | default: 78 | return fmt.Errorf("'%s' is not a known git event type") 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /git_reader.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "regexp" 7 | ) 8 | 9 | // GitReader scans for errors in the output of a git command 10 | type GitReader struct { 11 | // Underlying reader (to relay calls to) 12 | io.Reader 13 | 14 | // Error 15 | GitError error 16 | } 17 | 18 | // Regex to detect errors 19 | var ( 20 | gitErrorRegex = regexp.MustCompile("error: (.*)") 21 | ) 22 | 23 | // Implement the io.Reader interface 24 | func (g *GitReader) Read(p []byte) (n int, err error) { 25 | // Relay call 26 | n, err = g.Reader.Read(p) 27 | 28 | // Scan for errors 29 | g.scan(p) 30 | 31 | return n, err 32 | } 33 | 34 | func (g *GitReader) scan(data []byte) { 35 | // Already got an error 36 | // the main error will be the first error line 37 | if g.GitError != nil { 38 | return 39 | } 40 | 41 | matches := gitErrorRegex.FindSubmatch(data) 42 | 43 | // Skip, no matches found 44 | if matches == nil { 45 | return 46 | } 47 | 48 | g.GitError = errors.New(string(matches[1][:])) 49 | } 50 | -------------------------------------------------------------------------------- /githttp.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "strings" 11 | ) 12 | 13 | type GitHttp struct { 14 | // Root directory to serve repos from 15 | ProjectRoot string 16 | 17 | // Path to git binary 18 | GitBinPath string 19 | 20 | // Access rules 21 | UploadPack bool 22 | ReceivePack bool 23 | 24 | // Event handling functions 25 | EventHandler func(ev Event) 26 | } 27 | 28 | // Implement the http.Handler interface 29 | func (g *GitHttp) ServeHTTP(w http.ResponseWriter, r *http.Request) { 30 | g.requestHandler(w, r) 31 | return 32 | } 33 | 34 | // Shorthand constructor for most common scenario 35 | func New(root string) *GitHttp { 36 | return &GitHttp{ 37 | ProjectRoot: root, 38 | GitBinPath: "/usr/bin/git", 39 | UploadPack: true, 40 | ReceivePack: true, 41 | } 42 | } 43 | 44 | // Build root directory if doesn't exist 45 | func (g *GitHttp) Init() (*GitHttp, error) { 46 | if err := os.MkdirAll(g.ProjectRoot, os.ModePerm); err != nil { 47 | return nil, err 48 | } 49 | return g, nil 50 | } 51 | 52 | // Publish event if EventHandler is set 53 | func (g *GitHttp) event(e Event) { 54 | if g.EventHandler != nil { 55 | g.EventHandler(e) 56 | } else { 57 | fmt.Printf("EVENT: %q\n", e) 58 | } 59 | } 60 | 61 | // Actual command handling functions 62 | 63 | func (g *GitHttp) serviceRpc(hr HandlerReq) error { 64 | w, r, rpc, dir := hr.w, hr.r, hr.Rpc, hr.Dir 65 | 66 | access, err := g.hasAccess(r, dir, rpc, true) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | if access == false { 72 | return &ErrorNoAccess{hr.Dir} 73 | } 74 | 75 | // Reader that decompresses if necessary 76 | reader, err := requestReader(r) 77 | if err != nil { 78 | return err 79 | } 80 | defer reader.Close() 81 | 82 | // Reader that scans for events 83 | rpcReader := &RpcReader{ 84 | Reader: reader, 85 | Rpc: rpc, 86 | } 87 | 88 | // Set content type 89 | w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", rpc)) 90 | 91 | args := []string{rpc, "--stateless-rpc", "."} 92 | cmd := exec.Command(g.GitBinPath, args...) 93 | cmd.Dir = dir 94 | stdin, err := cmd.StdinPipe() 95 | if err != nil { 96 | return err 97 | } 98 | 99 | stdout, err := cmd.StdoutPipe() 100 | if err != nil { 101 | return err 102 | } 103 | defer stdout.Close() 104 | 105 | err = cmd.Start() 106 | if err != nil { 107 | return err 108 | } 109 | 110 | // Scan's git command's output for errors 111 | gitReader := &GitReader{ 112 | Reader: stdout, 113 | } 114 | 115 | // Copy input to git binary 116 | io.Copy(stdin, rpcReader) 117 | stdin.Close() 118 | 119 | // Write git binary's output to http response 120 | io.Copy(w, gitReader) 121 | 122 | // Wait till command has completed 123 | mainError := cmd.Wait() 124 | 125 | if mainError == nil { 126 | mainError = gitReader.GitError 127 | } 128 | 129 | // Fire events 130 | for _, e := range rpcReader.Events { 131 | // Set directory to current repo 132 | e.Dir = dir 133 | e.Request = hr.r 134 | e.Error = mainError 135 | 136 | // Fire event 137 | g.event(e) 138 | } 139 | 140 | // Because a response was already written, 141 | // the header cannot be changed 142 | return nil 143 | } 144 | 145 | func (g *GitHttp) getInfoRefs(hr HandlerReq) error { 146 | w, r, dir := hr.w, hr.r, hr.Dir 147 | service_name := getServiceType(r) 148 | access, err := g.hasAccess(r, dir, service_name, false) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | if !access { 154 | g.updateServerInfo(dir) 155 | hdrNocache(w) 156 | return sendFile("text/plain; charset=utf-8", hr) 157 | } 158 | 159 | args := []string{service_name, "--stateless-rpc", "--advertise-refs", "."} 160 | refs, err := g.gitCommand(dir, args...) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | hdrNocache(w) 166 | w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service_name)) 167 | w.WriteHeader(http.StatusOK) 168 | w.Write(packetWrite("# service=git-" + service_name + "\n")) 169 | w.Write(packetFlush()) 170 | w.Write(refs) 171 | 172 | return nil 173 | } 174 | 175 | func (g *GitHttp) getInfoPacks(hr HandlerReq) error { 176 | hdrCacheForever(hr.w) 177 | return sendFile("text/plain; charset=utf-8", hr) 178 | } 179 | 180 | func (g *GitHttp) getLooseObject(hr HandlerReq) error { 181 | hdrCacheForever(hr.w) 182 | return sendFile("application/x-git-loose-object", hr) 183 | } 184 | 185 | func (g *GitHttp) getPackFile(hr HandlerReq) error { 186 | hdrCacheForever(hr.w) 187 | return sendFile("application/x-git-packed-objects", hr) 188 | } 189 | 190 | func (g *GitHttp) getIdxFile(hr HandlerReq) error { 191 | hdrCacheForever(hr.w) 192 | return sendFile("application/x-git-packed-objects-toc", hr) 193 | } 194 | 195 | func (g *GitHttp) getTextFile(hr HandlerReq) error { 196 | hdrNocache(hr.w) 197 | return sendFile("text/plain", hr) 198 | } 199 | 200 | // Logic helping functions 201 | 202 | func sendFile(content_type string, hr HandlerReq) error { 203 | w, r := hr.w, hr.r 204 | req_file := path.Join(hr.Dir, hr.File) 205 | 206 | f, err := os.Stat(req_file) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | w.Header().Set("Content-Type", content_type) 212 | w.Header().Set("Content-Length", fmt.Sprintf("%d", f.Size())) 213 | w.Header().Set("Last-Modified", f.ModTime().Format(http.TimeFormat)) 214 | http.ServeFile(w, r, req_file) 215 | 216 | return nil 217 | } 218 | 219 | func (g *GitHttp) getGitDir(file_path string) (string, error) { 220 | root := g.ProjectRoot 221 | 222 | if root == "" { 223 | cwd, err := os.Getwd() 224 | 225 | if err != nil { 226 | return "", err 227 | } 228 | 229 | root = cwd 230 | } 231 | 232 | f := path.Join(root, file_path) 233 | if _, err := os.Stat(f); os.IsNotExist(err) { 234 | return "", err 235 | } 236 | 237 | return f, nil 238 | } 239 | 240 | func (g *GitHttp) hasAccess(r *http.Request, dir string, rpc string, check_content_type bool) (bool, error) { 241 | if check_content_type { 242 | if r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", rpc) { 243 | return false, nil 244 | } 245 | } 246 | 247 | if !(rpc == "upload-pack" || rpc == "receive-pack") { 248 | return false, nil 249 | } 250 | if rpc == "receive-pack" { 251 | return g.ReceivePack, nil 252 | } 253 | if rpc == "upload-pack" { 254 | return g.UploadPack, nil 255 | } 256 | 257 | return g.getConfigSetting(rpc, dir) 258 | } 259 | 260 | func (g *GitHttp) getConfigSetting(service_name string, dir string) (bool, error) { 261 | service_name = strings.Replace(service_name, "-", "", -1) 262 | setting, err := g.getGitConfig("http."+service_name, dir) 263 | if err != nil { 264 | return false, nil 265 | } 266 | 267 | if service_name == "uploadpack" { 268 | return setting != "false", nil 269 | } 270 | 271 | return setting == "true", nil 272 | } 273 | 274 | func (g *GitHttp) getGitConfig(config_name string, dir string) (string, error) { 275 | args := []string{"config", config_name} 276 | out, err := g.gitCommand(dir, args...) 277 | if err != nil { 278 | return "", err 279 | } 280 | return string(out)[0 : len(out)-1], nil 281 | } 282 | 283 | func (g *GitHttp) updateServerInfo(dir string) ([]byte, error) { 284 | args := []string{"update-server-info"} 285 | return g.gitCommand(dir, args...) 286 | } 287 | 288 | func (g *GitHttp) gitCommand(dir string, args ...string) ([]byte, error) { 289 | command := exec.Command(g.GitBinPath, args...) 290 | command.Dir = dir 291 | 292 | return command.Output() 293 | } 294 | -------------------------------------------------------------------------------- /pktparser.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // pktLineParser is a parser for git pkt-line Format, 10 | // as documented in https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt. 11 | // A zero value of pktLineParser is valid to use as a parser in ready state. 12 | // Output should be read from Lines and Error after Step returns finished true. 13 | // pktLineParser reads until a terminating "0000" flush-pkt. It's good for a single use only. 14 | type pktLineParser struct { 15 | // Lines contains all pkt-lines. 16 | Lines []string 17 | 18 | // Error contains the first error encountered while parsing, or nil otherwise. 19 | Error error 20 | 21 | // Internal state machine. 22 | state state 23 | next int // next is the number of bytes that need to be written to buf before its contents should be processed by the state machine. 24 | buf []byte 25 | } 26 | 27 | // Feed accumulates and parses data. 28 | // It will return early if it reaches end of pkt-line data (indicated by a flush-pkt "0000"), 29 | // or if it encounters a parsing error. 30 | // It must not be called when state is done. 31 | // When done, all of pkt-lines will be available in Lines, and Error will be set if any error occurred. 32 | func (p *pktLineParser) Feed(data []byte) { 33 | for { 34 | // If not enough data to reach next state, append it to buf and return. 35 | if len(data) < p.next { 36 | p.buf = append(p.buf, data...) 37 | p.next -= len(data) 38 | return 39 | } 40 | 41 | // There's enough data to reach next state. Take from data only what's needed. 42 | b := data[:p.next] 43 | data = data[p.next:] 44 | p.buf = append(p.buf, b...) 45 | p.next = 0 46 | 47 | // Take a step to next state. 48 | err := p.step() 49 | if err != nil { 50 | p.state = done 51 | p.Error = err 52 | return 53 | } 54 | 55 | // Break out once reached done state. 56 | if p.state == done { 57 | return 58 | } 59 | } 60 | } 61 | 62 | const ( 63 | // pkt-len = 4*(HEXDIG) 64 | pktLenSize = 4 65 | ) 66 | 67 | type state uint8 68 | 69 | const ( 70 | ready state = iota 71 | readingLen 72 | readingPayload 73 | done 74 | ) 75 | 76 | // step moves the state machine to the next state. 77 | // buf must contain all the data ready for consumption for current state. 78 | // It must not be called when state is done. 79 | func (p *pktLineParser) step() error { 80 | switch p.state { 81 | case ready: 82 | p.state = readingLen 83 | p.next = pktLenSize 84 | return nil 85 | case readingLen: 86 | // len(p.buf) is 4. 87 | pktLen, err := parsePktLen(p.buf) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | switch { 93 | case pktLen == 0: 94 | p.state = done 95 | p.next = 0 96 | p.buf = nil 97 | return nil 98 | default: 99 | p.state = readingPayload 100 | p.next = pktLen - pktLenSize // (pkt-len - 4)*(OCTET) 101 | p.buf = p.buf[:0] 102 | return nil 103 | } 104 | case readingPayload: 105 | p.state = readingLen 106 | p.next = pktLenSize 107 | p.Lines = append(p.Lines, string(p.buf)) 108 | p.buf = p.buf[:0] 109 | return nil 110 | default: 111 | panic(fmt.Errorf("unreachable: %v", p.state)) 112 | } 113 | } 114 | 115 | // parsePktLen parses a pkt-len segment. 116 | // len(b) must be 4. 117 | func parsePktLen(b []byte) (int, error) { 118 | pktLen, err := parseHex(b) 119 | switch { 120 | case err != nil: 121 | return 0, err 122 | case 1 <= pktLen && pktLen < pktLenSize: 123 | return 0, fmt.Errorf("invalid pkt-len: %v", pktLen) 124 | case pktLen > 65524: 125 | // The maximum length of a pkt-line is 65524 bytes (65520 bytes of payload + 4 bytes of length data). 126 | return 0, fmt.Errorf("invalid pkt-len: %v", pktLen) 127 | } 128 | return int(pktLen), nil 129 | } 130 | 131 | // parseHex parses a 4-byte hex number. 132 | // len(h) must be 4. 133 | func parseHex(h []byte) (uint16, error) { 134 | var b [2]uint8 135 | n, err := hex.Decode(b[:], h) 136 | switch { 137 | case err != nil: 138 | return 0, err 139 | case n != 2: 140 | return 0, errors.New("short output") 141 | } 142 | return uint16(b[0])<<8 | uint16(b[1]), nil 143 | } 144 | -------------------------------------------------------------------------------- /pktparser_test.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestParsePktLen(t *testing.T) { 11 | tests := []struct { 12 | in string 13 | 14 | wantLen int 15 | wantErr error 16 | }{ 17 | // Valid pkt-len. 18 | {"00a5", 165, nil}, 19 | {"01a5", 421, nil}, 20 | {"0032", 50, nil}, 21 | {"000b", 11, nil}, 22 | {"000B", 11, nil}, 23 | 24 | // Valud flush-pkt. 25 | {"0000", 0, nil}, 26 | 27 | {"0001", 0, errors.New("invalid pkt-len: 1")}, 28 | {"0003", 0, errors.New("invalid pkt-len: 3")}, 29 | {"abyz", 0, hex.InvalidByteError('y')}, 30 | {"-<%^", 0, hex.InvalidByteError('-')}, 31 | 32 | // Maximum length. 33 | {"fff4", 65524, nil}, 34 | {"fff5", 0, errors.New("invalid pkt-len: 65525")}, 35 | {"ffff", 0, errors.New("invalid pkt-len: 65535")}, 36 | } 37 | 38 | for _, tt := range tests { 39 | gotLen, gotErr := parsePktLen([]byte(tt.in)) 40 | if gotLen != tt.wantLen || !reflect.DeepEqual(gotErr, tt.wantErr) { 41 | t.Errorf("test %q:\n got: %#v, %#v\nwant: %#v, %#v\n", tt.in, gotLen, gotErr, tt.wantLen, tt.wantErr) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /routing.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type Service struct { 11 | Method string 12 | Handler func(HandlerReq) error 13 | Rpc string 14 | } 15 | 16 | type HandlerReq struct { 17 | w http.ResponseWriter 18 | r *http.Request 19 | Rpc string 20 | Dir string 21 | File string 22 | } 23 | 24 | // Routing regexes 25 | var ( 26 | _serviceRpcUpload = regexp.MustCompile("(.*?)/git-upload-pack$") 27 | _serviceRpcReceive = regexp.MustCompile("(.*?)/git-receive-pack$") 28 | _getInfoRefs = regexp.MustCompile("(.*?)/info/refs$") 29 | _getHead = regexp.MustCompile("(.*?)/HEAD$") 30 | _getAlternates = regexp.MustCompile("(.*?)/objects/info/alternates$") 31 | _getHttpAlternates = regexp.MustCompile("(.*?)/objects/info/http-alternates$") 32 | _getInfoPacks = regexp.MustCompile("(.*?)/objects/info/packs$") 33 | _getInfoFile = regexp.MustCompile("(.*?)/objects/info/[^/]*$") 34 | _getLooseObject = regexp.MustCompile("(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$") 35 | _getPackFile = regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$") 36 | _getIdxFile = regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$") 37 | ) 38 | 39 | func (g *GitHttp) services() map[*regexp.Regexp]Service { 40 | return map[*regexp.Regexp]Service{ 41 | _serviceRpcUpload: Service{"POST", g.serviceRpc, "upload-pack"}, 42 | _serviceRpcReceive: Service{"POST", g.serviceRpc, "receive-pack"}, 43 | _getInfoRefs: Service{"GET", g.getInfoRefs, ""}, 44 | _getHead: Service{"GET", g.getTextFile, ""}, 45 | _getAlternates: Service{"GET", g.getTextFile, ""}, 46 | _getHttpAlternates: Service{"GET", g.getTextFile, ""}, 47 | _getInfoPacks: Service{"GET", g.getInfoPacks, ""}, 48 | _getInfoFile: Service{"GET", g.getTextFile, ""}, 49 | _getLooseObject: Service{"GET", g.getLooseObject, ""}, 50 | _getPackFile: Service{"GET", g.getPackFile, ""}, 51 | _getIdxFile: Service{"GET", g.getIdxFile, ""}, 52 | } 53 | } 54 | 55 | // getService return's the service corresponding to the 56 | // current http.Request's URL 57 | // as well as the name of the repo 58 | func (g *GitHttp) getService(path string) (string, *Service) { 59 | for re, service := range g.services() { 60 | if m := re.FindStringSubmatch(path); m != nil { 61 | return m[1], &service 62 | } 63 | } 64 | 65 | // No match 66 | return "", nil 67 | } 68 | 69 | // Request handling function 70 | func (g *GitHttp) requestHandler(w http.ResponseWriter, r *http.Request) { 71 | // Get service for URL 72 | repo, service := g.getService(r.URL.Path) 73 | 74 | // No url match 75 | if service == nil { 76 | renderNotFound(w) 77 | return 78 | } 79 | 80 | // Bad method 81 | if service.Method != r.Method { 82 | renderMethodNotAllowed(w, r) 83 | return 84 | } 85 | 86 | // Rpc type 87 | rpc := service.Rpc 88 | 89 | // Get specific file 90 | file := strings.Replace(r.URL.Path, repo+"/", "", 1) 91 | 92 | // Resolve directory 93 | dir, err := g.getGitDir(repo) 94 | 95 | // Repo not found on disk 96 | if err != nil { 97 | renderNotFound(w) 98 | return 99 | } 100 | 101 | // Build request info for handler 102 | hr := HandlerReq{w, r, rpc, dir, file} 103 | 104 | // Call handler 105 | if err := service.Handler(hr); err != nil { 106 | if os.IsNotExist(err) { 107 | renderNotFound(w) 108 | return 109 | } 110 | switch err.(type) { 111 | case *ErrorNoAccess: 112 | renderNoAccess(w) 113 | return 114 | } 115 | http.Error(w, err.Error(), 500) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /rpc_reader.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | import ( 4 | "io" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // RpcReader scans for events in the incoming rpc request data. 10 | type RpcReader struct { 11 | // Underlying reader (to relay calls to). 12 | io.Reader 13 | 14 | // Rpc type (receive-pack or upload-pack). 15 | Rpc string 16 | 17 | // List of events RpcReader has picked up through scanning. 18 | // These events do not have the Dir field set. 19 | Events []Event 20 | 21 | pktLineParser pktLineParser 22 | } 23 | 24 | // Read implements the io.Reader interface. 25 | func (r *RpcReader) Read(p []byte) (n int, err error) { 26 | // Relay call 27 | n, err = r.Reader.Read(p) 28 | 29 | // Scan for events 30 | if n > 0 { 31 | r.scan(p[:n]) 32 | } 33 | 34 | return n, err 35 | } 36 | 37 | func (r *RpcReader) scan(data []byte) { 38 | if r.pktLineParser.state == done { 39 | return 40 | } 41 | 42 | r.pktLineParser.Feed(data) 43 | 44 | // If parsing has just finished, process its output once. 45 | if r.pktLineParser.state == done { 46 | if r.pktLineParser.Error != nil { 47 | return 48 | } 49 | 50 | // When we get here, we're done collecting all pkt-lines successfully 51 | // and can now extract relevant events. 52 | switch r.Rpc { 53 | case "receive-pack": 54 | for _, line := range r.pktLineParser.Lines { 55 | events := scanPush(line) 56 | r.Events = append(r.Events, events...) 57 | } 58 | case "upload-pack": 59 | total := strings.Join(r.pktLineParser.Lines, "") 60 | events := scanFetch(total) 61 | r.Events = append(r.Events, events...) 62 | } 63 | } 64 | } 65 | 66 | // TODO: Avoid using regexp to parse a well documented binary protocol with an open source 67 | // implementation. There should not be a need for regexp. 68 | 69 | // receivePackRegex is used once per pkt-line. 70 | var receivePackRegex = regexp.MustCompile("([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) refs\\/(heads|tags)\\/(.+?)(\x00|$)") 71 | 72 | func scanPush(line string) []Event { 73 | matches := receivePackRegex.FindAllStringSubmatch(line, -1) 74 | 75 | if matches == nil { 76 | return nil 77 | } 78 | 79 | var events []Event 80 | for _, m := range matches { 81 | e := Event{ 82 | Last: m[1], 83 | Commit: m[2], 84 | } 85 | 86 | // Handle pushes to branches and tags differently 87 | if m[3] == "heads" { 88 | e.Type = PUSH 89 | e.Branch = m[4] 90 | } else { 91 | e.Type = TAG 92 | e.Tag = m[4] 93 | } 94 | 95 | events = append(events, e) 96 | } 97 | 98 | return events 99 | } 100 | 101 | // uploadPackRegex is used once on the entire header data. 102 | var uploadPackRegex = regexp.MustCompile(`^want ([0-9a-fA-F]{40})`) 103 | 104 | func scanFetch(total string) []Event { 105 | matches := uploadPackRegex.FindAllStringSubmatch(total, -1) 106 | 107 | if matches == nil { 108 | return nil 109 | } 110 | 111 | var events []Event 112 | for _, m := range matches { 113 | events = append(events, Event{ 114 | Type: FETCH, 115 | Commit: m[1], 116 | }) 117 | } 118 | 119 | return events 120 | } 121 | -------------------------------------------------------------------------------- /rpc_reader_test.go: -------------------------------------------------------------------------------- 1 | package githttp_test 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/AaronO/go-git-http" 13 | ) 14 | 15 | func TestRpcReader(t *testing.T) { 16 | tests := []struct { 17 | rpc string 18 | file string 19 | 20 | want []githttp.Event 21 | }{ 22 | { 23 | rpc: "receive-pack", 24 | file: "receive-pack.0", 25 | 26 | want: []githttp.Event{ 27 | (githttp.Event)(githttp.Event{ 28 | Type: (githttp.EventType)(githttp.PUSH), 29 | Commit: (string)("92eef6dcb9cc198bc3ac6010c108fa482773f116"), 30 | Dir: (string)(""), 31 | Tag: (string)(""), 32 | Last: (string)("0000000000000000000000000000000000000000"), 33 | Branch: (string)("master"), 34 | Error: (error)(nil), 35 | Request: (*http.Request)(nil), 36 | }), 37 | }, 38 | }, 39 | 40 | // A tag using letters only. 41 | { 42 | rpc: "receive-pack", 43 | file: "receive-pack.1", 44 | 45 | want: []githttp.Event{ 46 | (githttp.Event)(githttp.Event{ 47 | Type: (githttp.EventType)(githttp.TAG), 48 | Commit: (string)("3da295397738f395c2ca5fd5570f01a9fcea3be3"), 49 | Dir: (string)(""), 50 | Tag: (string)("sometextualtag"), 51 | Last: (string)("0000000000000000000000000000000000000000"), 52 | Branch: (string)(""), 53 | Error: (error)(nil), 54 | Request: (*http.Request)(nil), 55 | }), 56 | }, 57 | }, 58 | 59 | // A tag containing the string "00". 60 | { 61 | rpc: "receive-pack", 62 | file: "receive-pack.2", 63 | 64 | want: []githttp.Event{ 65 | (githttp.Event)(githttp.Event{ 66 | Type: (githttp.EventType)(githttp.TAG), 67 | Commit: (string)("3da295397738f395c2ca5fd5570f01a9fcea3be3"), 68 | Dir: (string)(""), 69 | Tag: (string)("1.000.1"), 70 | Last: (string)("0000000000000000000000000000000000000000"), 71 | Branch: (string)(""), 72 | Error: (error)(nil), 73 | Request: (*http.Request)(nil), 74 | }), 75 | }, 76 | }, 77 | 78 | // Multiple tags containing string "00" in one git push operation. 79 | { 80 | rpc: "receive-pack", 81 | file: "receive-pack.3", 82 | 83 | want: []githttp.Event{ 84 | (githttp.Event)(githttp.Event{ 85 | Type: (githttp.EventType)(githttp.TAG), 86 | Commit: (string)("3da295397738f395c2ca5fd5570f01a9fcea3be3"), 87 | Dir: (string)(""), 88 | Tag: (string)("1.000.2"), 89 | Last: (string)("0000000000000000000000000000000000000000"), 90 | Branch: (string)(""), 91 | Error: (error)(nil), 92 | Request: (*http.Request)(nil), 93 | }), 94 | (githttp.Event)(githttp.Event{ 95 | Type: (githttp.EventType)(githttp.TAG), 96 | Commit: (string)("3da295397738f395c2ca5fd5570f01a9fcea3be3"), 97 | Dir: (string)(""), 98 | Tag: (string)("1.000.3"), 99 | Last: (string)("0000000000000000000000000000000000000000"), 100 | Branch: (string)(""), 101 | Error: (error)(nil), 102 | Request: (*http.Request)(nil), 103 | }), 104 | (githttp.Event)(githttp.Event{ 105 | Type: (githttp.EventType)(githttp.TAG), 106 | Commit: (string)("3da295397738f395c2ca5fd5570f01a9fcea3be3"), 107 | Dir: (string)(""), 108 | Tag: (string)("1.000.4"), 109 | Last: (string)("0000000000000000000000000000000000000000"), 110 | Branch: (string)(""), 111 | Error: (error)(nil), 112 | Request: (*http.Request)(nil), 113 | }), 114 | }, 115 | }, 116 | 117 | { 118 | rpc: "upload-pack", 119 | file: "upload-pack.0", 120 | 121 | want: []githttp.Event{ 122 | (githttp.Event)(githttp.Event{ 123 | Type: (githttp.EventType)(githttp.FETCH), 124 | Commit: (string)("a647ec2ea40ee9ca35d32232dc28de22b1537e00"), 125 | Dir: (string)(""), 126 | Tag: (string)(""), 127 | Last: (string)(""), 128 | Branch: (string)(""), 129 | Error: (error)(nil), 130 | Request: (*http.Request)(nil), 131 | }), 132 | }, 133 | }, 134 | 135 | { 136 | rpc: "upload-pack", 137 | file: "upload-pack.1", 138 | 139 | want: []githttp.Event{ 140 | (githttp.Event)(githttp.Event{ 141 | Type: (githttp.EventType)(githttp.FETCH), 142 | Commit: (string)("92eef6dcb9cc198bc3ac6010c108fa482773f116"), 143 | Dir: (string)(""), 144 | Tag: (string)(""), 145 | Last: (string)(""), 146 | Branch: (string)(""), 147 | Error: (error)(nil), 148 | Request: (*http.Request)(nil), 149 | }), 150 | }, 151 | }, 152 | } 153 | 154 | for _, tt := range tests { 155 | f, err := os.Open(filepath.Join("testdata", tt.file)) 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | 160 | r := fragmentedReader{f} 161 | 162 | rr := &githttp.RpcReader{ 163 | Reader: r, 164 | Rpc: tt.rpc, 165 | } 166 | 167 | _, err = io.Copy(ioutil.Discard, rr) 168 | if err != nil { 169 | t.Errorf("io.Copy: %v", err) 170 | } 171 | 172 | f.Close() 173 | 174 | if got := rr.Events; !reflect.DeepEqual(got, tt.want) { 175 | t.Errorf("test %q/%q:\n got: %#v\nwant: %#v\n", tt.rpc, tt.file, got, tt.want) 176 | } 177 | } 178 | } 179 | 180 | // fragmentedReader reads from R, with each Read call returning at most fragmentLen bytes even 181 | // if len(p) is greater than fragmentLen. 182 | // It purposefully adds a layer of inefficiency around R, and exists for testing purposes only. 183 | type fragmentedReader struct { 184 | R io.Reader // Underlying reader. 185 | } 186 | 187 | func (r fragmentedReader) Read(p []byte) (n int, err error) { 188 | const fragmentLen = 1 189 | if len(p) <= fragmentLen { 190 | return r.R.Read(p) 191 | } 192 | return r.R.Read(p[:fragmentLen]) 193 | } 194 | -------------------------------------------------------------------------------- /testdata/receive-pack.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AaronO/go-git-http/HEAD/testdata/receive-pack.0 -------------------------------------------------------------------------------- /testdata/receive-pack.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AaronO/go-git-http/HEAD/testdata/receive-pack.1 -------------------------------------------------------------------------------- /testdata/receive-pack.2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AaronO/go-git-http/HEAD/testdata/receive-pack.2 -------------------------------------------------------------------------------- /testdata/receive-pack.3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AaronO/go-git-http/HEAD/testdata/receive-pack.3 -------------------------------------------------------------------------------- /testdata/upload-pack.0: -------------------------------------------------------------------------------- 1 | 0092want a647ec2ea40ee9ca35d32232dc28de22b1537e00 multi_ack_detailed side-band-64k thin-pack include-tag ofs-delta agent=git/2.5.4.(Apple.Git-61) 2 | 00000032have 92eef6dcb9cc198bc3ac6010c108fa482773f116 3 | 0032have 0e2a2938f51c984c4f6fe68400a77b2748e3aac7 4 | 0032have 9b40a23d8105a4d8f5babf9756742b353bc0bf86 5 | 0032have 6dcded2bb74142bd97b346c70e34a7a117dc359d 6 | 0032have 5e247562e60140946b2996ab428ea4ebfd8f7aef 7 | 0032have e399f2b29d0efe66a9597239abdbec5e2960e172 8 | 0032have 6779fd7460bf02ffa67fd7c6fa412fedc22eea02 9 | 0032have 30d62cac4ee3185b5670e9ca366a409a2dade471 10 | 0032have 088a47a7b0d7141b71dbf0fabfbad66c61ffb99f 11 | 0032have 2c960968453207a2a66309e2e752759888345900 12 | 0032have 4f0b8f6c5df1a903b204cdc9ff20a7e00873d73d 13 | 0032have d403d8b126c2d566eb105102972df356f7824406 14 | 0032have 926801b90aa8180679eccb0ee3231de10903df9b 15 | 0032have accbc2b1a251e2cd6dd0c3fba74c2f7789a5addd 16 | 0032have f6eb2af801c0722b3112346da3d1fd68e164cb74 17 | 0032have 5e3817ddb991f9530c9cfc7c4a5cf5203257fbd8 18 | 00000032have 5ee7e39b927366c74181b199e0da78467699dd3d 19 | 0032have 5d1d1ba532f180d55a6fc7b23bda8162693447d7 20 | 0032have 5dc05dfd1827333036a03b455267432157d8315a 21 | 0032have 3378b0e3808ce82185bdf57d97c5d5c7655f14d3 22 | 0032have 1aa139246412abf51c7f6402deb3d9ee84cfba6b 23 | 0032have e94b8b4c1c9103d4aa8656f55c54ce91f4941237 24 | 0032have 2edfc6f95708194e23c92bf80da115fd76953cdc 25 | 0032have 64b4dcae2edfdd6201efe622f6394694281c1fab 26 | 0032have 6224112203b656464ee55f42eca4b80a9d8ae854 27 | 0032have 230517d50257e5f8d2706976c7ed7d333e2b9916 28 | 0032have 6b8d66508e23b76ecf8236b45218c77d2e66c7df 29 | 0032have 622958e856b24f771218aad8d26264d403df0021 30 | 0032have 1c82218b4749a5eec2750b876a7544105d357db5 31 | 0032have 861ceef44614479fecb6c5118773afc73c22fc31 32 | 0032have 74b86980e2e8e3d47b58a719e854819cab1ffb8b 33 | 0032have bb5f1a2dbd16acb79584beb4021053c3718b07ce 34 | 00000009done 35 | -------------------------------------------------------------------------------- /testdata/upload-pack.1: -------------------------------------------------------------------------------- 1 | 0086want 92eef6dcb9cc198bc3ac6010c108fa482773f116 multi_ack_detailed side-band-64k thin-pack ofs-delta agent=git/2.5.4.(Apple.Git-61) 2 | 0032want 92eef6dcb9cc198bc3ac6010c108fa482773f116 3 | 00000009done 4 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | import ( 4 | "compress/flate" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // requestReader returns an io.ReadCloser 15 | // that will decode data if needed, depending on the 16 | // "content-encoding" header 17 | func requestReader(req *http.Request) (io.ReadCloser, error) { 18 | switch req.Header.Get("content-encoding") { 19 | case "gzip": 20 | return gzip.NewReader(req.Body) 21 | case "deflate": 22 | return flate.NewReader(req.Body), nil 23 | } 24 | 25 | // If no encoding, use raw body 26 | return req.Body, nil 27 | } 28 | 29 | // HTTP parsing utility functions 30 | 31 | func getServiceType(r *http.Request) string { 32 | service_type := r.FormValue("service") 33 | 34 | if s := strings.HasPrefix(service_type, "git-"); !s { 35 | return "" 36 | } 37 | 38 | return strings.Replace(service_type, "git-", "", 1) 39 | } 40 | 41 | // HTTP error response handling functions 42 | 43 | func renderMethodNotAllowed(w http.ResponseWriter, r *http.Request) { 44 | if r.Proto == "HTTP/1.1" { 45 | w.WriteHeader(http.StatusMethodNotAllowed) 46 | w.Write([]byte("Method Not Allowed")) 47 | } else { 48 | w.WriteHeader(http.StatusBadRequest) 49 | w.Write([]byte("Bad Request")) 50 | } 51 | } 52 | 53 | func renderNotFound(w http.ResponseWriter) { 54 | w.WriteHeader(http.StatusNotFound) 55 | w.Write([]byte("Not Found")) 56 | } 57 | 58 | func renderNoAccess(w http.ResponseWriter) { 59 | w.WriteHeader(http.StatusForbidden) 60 | w.Write([]byte("Forbidden")) 61 | } 62 | 63 | // Packet-line handling function 64 | 65 | func packetFlush() []byte { 66 | return []byte("0000") 67 | } 68 | 69 | func packetWrite(str string) []byte { 70 | s := strconv.FormatInt(int64(len(str)+4), 16) 71 | 72 | if len(s)%4 != 0 { 73 | s = strings.Repeat("0", 4-len(s)%4) + s 74 | } 75 | 76 | return []byte(s + str) 77 | } 78 | 79 | // Header writing functions 80 | 81 | func hdrNocache(w http.ResponseWriter) { 82 | w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT") 83 | w.Header().Set("Pragma", "no-cache") 84 | w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") 85 | } 86 | 87 | func hdrCacheForever(w http.ResponseWriter) { 88 | now := time.Now().Unix() 89 | expires := now + 31536000 90 | w.Header().Set("Date", fmt.Sprintf("%d", now)) 91 | w.Header().Set("Expires", fmt.Sprintf("%d", expires)) 92 | w.Header().Set("Cache-Control", "public, max-age=31536000") 93 | } 94 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package githttp 2 | 3 | const VERSION = "1.0.0" 4 | --------------------------------------------------------------------------------