Add support for Git HTTPS clones

186f39b9a369e33bc75bdfbf7cdba9eb83452779
Alexis Sellier committed ago 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">