Add support for Git HTTPS clones
186f39b9a369e33bc75bdfbf7cdba9eb83452779
1 parent
0f9899cc
git_http.go
added
+63 -0
| 1 | + | package main |
|
| 2 | + | ||
| 3 | + | import ( |
|
| 4 | + | "net/http" |
|
| 5 | + | "net/http/cgi" |
|
| 6 | + | "os/exec" |
|
| 7 | + | "strings" |
|
| 8 | + | ) |
|
| 9 | + | ||
| 10 | + | // handleGitHTTP serves Git smart HTTP protocol requests by proxying to git-http-backend. |
|
| 11 | + | // This enables `git clone` over HTTPS. |
|
| 12 | + | func (s *server) handleGitHTTP(w http.ResponseWriter, r *http.Request, repo *RepoInfo) { |
|
| 13 | + | gitHTTPBackend, err := exec.LookPath("git-http-backend") |
|
| 14 | + | if err != nil { |
|
| 15 | + | gitExecPath, err2 := exec.Command("git", "--exec-path").Output() |
|
| 16 | + | if err2 != nil { |
|
| 17 | + | http.Error(w, "git-http-backend not found", http.StatusInternalServerError) |
|
| 18 | + | return |
|
| 19 | + | } |
|
| 20 | + | gitHTTPBackend = strings.TrimSpace(string(gitExecPath)) + "/git-http-backend" |
|
| 21 | + | } |
|
| 22 | + | ||
| 23 | + | // Strip the base URL prefix and repo name to get the path info that git-http-backend expects. |
|
| 24 | + | // e.g. /baseurl/repo/info/refs -> /repo.git/info/refs |
|
| 25 | + | pathInfo := r.URL.Path |
|
| 26 | + | if s.baseURL != "" { |
|
| 27 | + | pathInfo = strings.TrimPrefix(pathInfo, s.baseURL) |
|
| 28 | + | } |
|
| 29 | + | // Replace /<reponame>/ with /<reponame>.git/ |
|
| 30 | + | pathInfo = strings.Replace(pathInfo, "/"+repo.Name+"/", "/"+repo.Name+".git/", 1) |
|
| 31 | + | ||
| 32 | + | handler := &cgi.Handler{ |
|
| 33 | + | Path: gitHTTPBackend, |
|
| 34 | + | Env: []string{ |
|
| 35 | + | "GIT_PROJECT_ROOT=" + s.scanPath, |
|
| 36 | + | "GIT_HTTP_EXPORT_ALL=1", |
|
| 37 | + | }, |
|
| 38 | + | InheritEnv: []string{"PATH"}, |
|
| 39 | + | } |
|
| 40 | + | ||
| 41 | + | // Override PATH_INFO for the CGI handler. |
|
| 42 | + | r2 := r.Clone(r.Context()) |
|
| 43 | + | r2.URL.Path = pathInfo |
|
| 44 | + | ||
| 45 | + | handler.ServeHTTP(w, r2) |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | // isGitHTTPRequest returns true if the request is a Git smart HTTP protocol request. |
|
| 49 | + | func isGitHTTPRequest(r *http.Request) bool { |
|
| 50 | + | query := r.URL.RawQuery |
|
| 51 | + | ||
| 52 | + | if strings.HasSuffix(r.URL.Path, "/info/refs") && |
|
| 53 | + | (strings.Contains(query, "service=git-upload-pack") || strings.Contains(query, "service=git-receive-pack")) { |
|
| 54 | + | return true |
|
| 55 | + | } |
|
| 56 | + | if strings.HasSuffix(r.URL.Path, "/git-upload-pack") || strings.HasSuffix(r.URL.Path, "/git-receive-pack") { |
|
| 57 | + | return true |
|
| 58 | + | } |
|
| 59 | + | if strings.HasSuffix(r.URL.Path, "/HEAD") || strings.Contains(r.URL.Path, "/objects/") { |
|
| 60 | + | return true |
|
| 61 | + | } |
|
| 62 | + | return false |
|
| 63 | + | } |
handler.go
+23 -6
| 90 | 90 | if !ok { |
|
| 91 | 91 | s.renderError(w, r, http.StatusNotFound, "Repository not found") |
|
| 92 | 92 | return |
|
| 93 | 93 | } |
|
| 94 | 94 | ||
| 95 | + | // Handle Git smart HTTP protocol requests (git clone over HTTPS). |
|
| 96 | + | if isGitHTTPRequest(r) { |
|
| 97 | + | s.handleGitHTTP(w, r, repo) |
|
| 98 | + | return |
|
| 99 | + | } |
|
| 100 | + | ||
| 95 | 101 | if len(segments) == 1 { |
|
| 96 | 102 | s.handleSummary(w, r, repo) |
|
| 97 | 103 | return |
|
| 98 | 104 | } |
|
| 99 | 105 |
| 140 | 146 | Readme *BlobInfo |
|
| 141 | 147 | LastCommit *CommitInfo |
|
| 142 | 148 | ActiveBlob *BlobInfo |
|
| 143 | 149 | ActivePath string |
|
| 144 | 150 | IsEmpty bool |
|
| 145 | - | CloneURL string |
|
| 151 | + | CloneSSH string |
|
| 152 | + | CloneHTTPS string |
|
| 146 | 153 | } |
|
| 147 | 154 | ||
| 148 | 155 | func (s *server) handleSummary(w http.ResponseWriter, r *http.Request, repo *RepoInfo) { |
|
| 149 | - | s.renderHome(w, repo, "", nil, "") |
|
| 156 | + | s.renderHome(w, r, repo, "", nil, "") |
|
| 150 | 157 | } |
|
| 151 | 158 | ||
| 152 | - | func (s *server) renderHome(w http.ResponseWriter, repo *RepoInfo, ref string, blob *BlobInfo, activePath string) { |
|
| 159 | + | func (s *server) renderHome(w http.ResponseWriter, r *http.Request, repo *RepoInfo, ref string, blob *BlobInfo, activePath string) { |
|
| 153 | 160 | git := repo.Git |
|
| 154 | 161 | ||
| 155 | 162 | if ref == "" { |
|
| 156 | 163 | ref = git.getDefaultBranch() |
|
| 157 | 164 | } |
| 177 | 184 | readmeDir = activePath |
|
| 178 | 185 | } |
|
| 179 | 186 | readme := git.getReadme(hash, readmeDir) |
|
| 180 | 187 | ||
| 181 | 188 | pd := s.newPageData(repo, "home", ref) |
|
| 189 | + | scheme := "https" |
|
| 190 | + | if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") == "" { |
|
| 191 | + | scheme = "http" |
|
| 192 | + | } |
|
| 193 | + | if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { |
|
| 194 | + | scheme = proto |
|
| 195 | + | } |
|
| 196 | + | cloneHTTPS := scheme + "://" + r.Host + s.baseURL + "/" + repo.Name |
|
| 197 | + | ||
| 182 | 198 | pd.Data = homeData{ |
|
| 183 | 199 | DefaultRef: ref, |
|
| 184 | 200 | Branches: branches, |
|
| 185 | 201 | Tree: tree, |
|
| 186 | 202 | Readme: readme, |
|
| 187 | 203 | LastCommit: lastCommit, |
|
| 188 | 204 | ActiveBlob: blob, |
|
| 189 | 205 | ActivePath: activePath, |
|
| 190 | - | CloneURL: "git@radiant.computer:radiant/" + repo.Name + ".git", |
|
| 206 | + | CloneSSH: "git@radiant.computer:radiant/" + repo.Name + ".git", |
|
| 207 | + | CloneHTTPS: cloneHTTPS, |
|
| 191 | 208 | } |
|
| 192 | 209 | s.tmpl.render(w, "home", pd) |
|
| 193 | 210 | } |
|
| 194 | 211 | ||
| 195 | 212 | func (s *server) handleLog(w http.ResponseWriter, r *http.Request, repo *RepoInfo, rest string) { |
| 271 | 288 | blob, err := git.getBlob(hash, path) |
|
| 272 | 289 | if err != nil { |
|
| 273 | 290 | s.renderError(w, r, http.StatusNotFound, "File not found") |
|
| 274 | 291 | return |
|
| 275 | 292 | } |
|
| 276 | - | s.renderHome(w, repo, ref, blob, path) |
|
| 293 | + | s.renderHome(w, r, repo, ref, blob, path) |
|
| 277 | 294 | return |
|
| 278 | 295 | } |
|
| 279 | 296 | ||
| 280 | 297 | // Directory: expand tree to this path |
|
| 281 | - | s.renderHome(w, repo, ref, nil, path) |
|
| 298 | + | s.renderHome(w, r, repo, ref, nil, path) |
|
| 282 | 299 | } |
|
| 283 | 300 | ||
| 284 | 301 | func (s *server) handleCommit(w http.ResponseWriter, r *http.Request, repo *RepoInfo, rest string) { |
|
| 285 | 302 | type commitData struct { |
|
| 286 | 303 | Commit *CommitInfo |
static/style.css
+8 -2
| 638 | 638 | z-index: 10; |
|
| 639 | 639 | background: var(--bg); |
|
| 640 | 640 | border: 1px solid var(--border); |
|
| 641 | 641 | border-radius: 4px; |
|
| 642 | 642 | margin-top: 0.25rem; |
|
| 643 | - | padding: 0.5rem; |
|
| 643 | + | padding: 0.75rem 1rem 1rem; |
|
| 644 | 644 | } |
|
| 645 | 645 | .clone-dropdown label { |
|
| 646 | 646 | display: block; |
|
| 647 | 647 | font-size: 0.875rem; |
|
| 648 | 648 | color: var(--fg-dim); |
|
| 649 | - | margin: 0.25rem 0 0.5rem; |
|
| 649 | + | margin: 0 0 0.25rem; |
|
| 650 | + | } |
|
| 651 | + | .clone-dropdown label + input { |
|
| 652 | + | margin-bottom: 1rem; |
|
| 653 | + | } |
|
| 654 | + | .clone-dropdown label + input:last-child { |
|
| 655 | + | margin-bottom: 0; |
|
| 650 | 656 | } |
|
| 651 | 657 | .clone-dropdown input { |
|
| 652 | 658 | font-family: var(--mono); |
|
| 653 | 659 | font-size: 0.875rem; |
|
| 654 | 660 | padding: 0.25rem 0.5rem; |
templates/home.html
+4 -2
| 21 | 21 | </div> |
|
| 22 | 22 | {{end}} |
|
| 23 | 23 | <details class="clone-selector"> |
|
| 24 | 24 | <summary>Clone</summary> |
|
| 25 | 25 | <div class="clone-dropdown"> |
|
| 26 | - | <label>SSH</label> |
|
| 27 | - | <input type="text" value="{{.Data.CloneURL}}" readonly> |
|
| 26 | + | <label>Git HTTPS URL</label> |
|
| 27 | + | <input type="text" value="{{.Data.CloneHTTPS}}" readonly> |
|
| 28 | + | <label>Git SSH URL</label> |
|
| 29 | + | <input type="text" value="{{.Data.CloneSSH}}" readonly> |
|
| 28 | 30 | </div> |
|
| 29 | 31 | </details> |
|
| 30 | 32 | </nav> |
|
| 31 | 33 | ||
| 32 | 34 | <div class="repo-home"> |