Commit eeb0410d authored by Jacob Vosmaer's avatar Jacob Vosmaer

First steps for "git archive" download support

parent c846662d
...@@ -34,7 +34,8 @@ type gitService struct { ...@@ -34,7 +34,8 @@ type gitService struct {
} }
type gitEnv struct { type gitEnv struct {
GL_ID string GL_ID string
ArchivePath string
} }
// Routing table // Routing table
...@@ -42,6 +43,10 @@ var gitServices = [...]gitService{ ...@@ -42,6 +43,10 @@ var gitServices = [...]gitService{
gitService{"GET", "/info/refs", handleGetInfoRefs, ""}, gitService{"GET", "/info/refs", handleGetInfoRefs, ""},
gitService{"POST", "/git-upload-pack", handlePostRPC, "git-upload-pack"}, gitService{"POST", "/git-upload-pack", handlePostRPC, "git-upload-pack"},
gitService{"POST", "/git-receive-pack", handlePostRPC, "git-receive-pack"}, gitService{"POST", "/git-receive-pack", handlePostRPC, "git-receive-pack"},
gitService{"GET", "/repository/archive.zip", handleGetArchive, "zip"},
gitService{"GET", "/repository/archive.tar", handleGetArchive, "tar"},
gitService{"GET", "/repository/archive.tar.gz", handleGetArchive, "tar.gz"},
gitService{"GET", "/repository/archive.tar.bz2", handleGetArchive, "tar.bz2"},
} }
func newGitHandler(repoRoot, authBackend string) *gitHandler { func newGitHandler(repoRoot, authBackend string) *gitHandler {
...@@ -111,10 +116,6 @@ func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -111,10 +116,6 @@ func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// r.URL.Path does not contain '/../', so there is no possibility // r.URL.Path does not contain '/../', so there is no possibility
// of path traversal here. // of path traversal here.
repoPath := path.Join(h.repoRoot, strings.TrimSuffix(r.URL.Path, g.suffix)) repoPath := path.Join(h.repoRoot, strings.TrimSuffix(r.URL.Path, g.suffix))
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
g.handleFunc(env, g.rpc, repoPath, w, r) g.handleFunc(env, g.rpc, repoPath, w, r)
} }
...@@ -143,7 +144,12 @@ func (h *gitHandler) doAuthRequest(r *http.Request) (result *http.Response, err ...@@ -143,7 +144,12 @@ func (h *gitHandler) doAuthRequest(r *http.Request) (result *http.Response, err
return h.httpClient.Do(authReq) return h.httpClient.Do(authReq)
} }
func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter, r *http.Request) { func handleGetInfoRefs(env gitEnv, _ string, repoPath string, w http.ResponseWriter, r *http.Request) {
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
rpc := r.URL.Query().Get("service") rpc := r.URL.Query().Get("service")
if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") { if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") {
// The 'dumb' Git HTTP protocol is not supported // The 'dumb' Git HTTP protocol is not supported
...@@ -152,7 +158,7 @@ func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter, ...@@ -152,7 +158,7 @@ func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter,
} }
// Prepare our Git subprocess // Prepare our Git subprocess
cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", "--advertise-refs", path) cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", "--advertise-refs", repoPath)
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
fail500(w, "handleGetInfoRefs", err) fail500(w, "handleGetInfoRefs", err)
...@@ -187,10 +193,105 @@ func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter, ...@@ -187,10 +193,105 @@ func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter,
} }
} }
func handlePostRPC(env gitEnv, rpc string, path string, w http.ResponseWriter, r *http.Request) { func handleGetArchive(env gitEnv, format string, almostPath string, w http.ResponseWriter, r *http.Request) {
ref := r.URL.Query().Get("ref")
if ref == "" {
ref = "HEAD"
}
repoPath := almostPath + ".git"
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
var compressCmd *exec.Cmd
var archiveFormat string
switch format {
case "tar":
archiveFormat = "tar"
compressCmd = nil
case "tar.gz":
archiveFormat = "tar"
compressCmd = exec.Command("gzip", "-c")
case "tar.bz2":
archiveFormat = "tar"
compressCmd = exec.Command("bzip2", "-c")
case "zip":
archiveFormat = "zip"
compressCmd = nil
}
archiveCmd := gitCommand(env, "git", "--git-dir="+repoPath, "archive", "--format="+archiveFormat, ref)
archiveStdout, err := archiveCmd.StdoutPipe()
if err != nil {
fail500(w, "handleGetArchive", err)
return
}
defer archiveStdout.Close()
if err := archiveCmd.Start(); err != nil {
fail500(w, "handleGetArchive", err)
return
}
defer cleanUpProcessGroup(archiveCmd) // Ensure brute force subprocess clean-up
var stdout io.ReadCloser
if compressCmd == nil {
stdout = archiveStdout
} else {
compressCmd.Stdin = archiveStdout
stdout, err = compressCmd.StdoutPipe()
if err != nil {
fail500(w, "handleGetArchive compressCmd stdout pipe", err)
return
}
defer stdout.Close()
if err := compressCmd.Start(); err != nil {
fail500(w, "handleGetArchive start compressCmd process", err)
return
}
defer compressCmd.Wait()
archiveStdout.Close()
}
// Start writing the response
if format == "zip" {
w.Header().Add("Content-Type", "application/zip")
} else {
w.Header().Add("Content-Type", "application/octet-stream")
}
w.Header().Add("Content-Transfer-Encoding", "binary")
w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, path.Base(env.ArchivePath)))
w.Header().Add("Cache-Control", "private")
w.WriteHeader(200) // Don't bother with HTTP 500 from this point on, just return
if _, err := io.Copy(w, stdout); err != nil {
logContext("handleGetArchive read from subprocess", err)
return
}
if err := archiveCmd.Wait(); err != nil {
logContext("handleGetArchive wait for archiveCmd", err)
return
}
if compressCmd != nil {
if err := compressCmd.Wait(); err != nil {
logContext("handleGetArchive wait for compressCmd", err)
return
}
}
}
func handlePostRPC(env gitEnv, rpc string, repoPath string, w http.ResponseWriter, r *http.Request) {
var body io.Reader var body io.Reader
var err error var err error
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
// The client request body may have been gzipped. // The client request body may have been gzipped.
if r.Header.Get("Content-Encoding") == "gzip" { if r.Header.Get("Content-Encoding") == "gzip" {
body, err = gzip.NewReader(r.Body) body, err = gzip.NewReader(r.Body)
...@@ -203,7 +304,7 @@ func handlePostRPC(env gitEnv, rpc string, path string, w http.ResponseWriter, r ...@@ -203,7 +304,7 @@ func handlePostRPC(env gitEnv, rpc string, path string, w http.ResponseWriter, r
} }
// Prepare our Git subprocess // Prepare our Git subprocess
cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", path) cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", repoPath)
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
fail500(w, "handlePostRPC", err) fail500(w, "handlePostRPC", err)
......
...@@ -19,6 +19,7 @@ const servWaitSleep = 100 // milliseconds sleep interval ...@@ -19,6 +19,7 @@ const servWaitSleep = 100 // milliseconds sleep interval
const scratchDir = "test/scratch" const scratchDir = "test/scratch"
const testRepoRoot = "test/data" const testRepoRoot = "test/data"
const testRepo = "test.git" const testRepo = "test.git"
const testProject = "test"
var remote = fmt.Sprintf("http://%s/%s", servAddr, testRepo) var remote = fmt.Sprintf("http://%s/%s", servAddr, testRepo)
var checkoutDir = path.Join(scratchDir, "test") var checkoutDir = path.Join(scratchDir, "test")
...@@ -96,6 +97,87 @@ func TestDeniedPush(t *testing.T) { ...@@ -96,6 +97,87 @@ func TestDeniedPush(t *testing.T) {
} }
} }
func TestAllowedDownloadZip(t *testing.T) {
prepareDownloadDir(t)
// Prepare test server and backend
archiveName := "foobar.zip"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/%s/repository/archive.zip", servAddr, testProject))
downloadCmd.Dir = scratchDir
runOrFail(t, downloadCmd)
extractCmd := exec.Command("unzip", archiveName)
extractCmd.Dir = scratchDir
runOrFail(t, extractCmd)
}
func TestAllowedDownloadTar(t *testing.T) {
prepareDownloadDir(t)
// Prepare test server and backend
archiveName := "foobar.tar"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/%s/repository/archive.tar", servAddr, testProject))
downloadCmd.Dir = scratchDir
runOrFail(t, downloadCmd)
extractCmd := exec.Command("tar", "xf", archiveName)
extractCmd.Dir = scratchDir
runOrFail(t, extractCmd)
}
func TestAllowedDownloadTarGz(t *testing.T) {
prepareDownloadDir(t)
// Prepare test server and backend
archiveName := "foobar.tar.gz"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/%s/repository/archive.tar.gz", servAddr, testProject))
downloadCmd.Dir = scratchDir
runOrFail(t, downloadCmd)
extractCmd := exec.Command("tar", "zxf", archiveName)
extractCmd.Dir = scratchDir
runOrFail(t, extractCmd)
}
func TestAllowedDownloadTarBz2(t *testing.T) {
prepareDownloadDir(t)
// Prepare test server and backend
archiveName := "foobar.tar.bz2"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/%s/repository/archive.tar.bz2", servAddr, testProject))
downloadCmd.Dir = scratchDir
runOrFail(t, downloadCmd)
extractCmd := exec.Command("tar", "jxf", archiveName)
extractCmd.Dir = scratchDir
runOrFail(t, extractCmd)
}
func prepareDownloadDir(t *testing.T) {
if err := os.RemoveAll(scratchDir); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(scratchDir, 0755); err != nil {
t.Fatal(err)
}
}
func preparePushRepo(t *testing.T) { func preparePushRepo(t *testing.T) {
if err := os.RemoveAll(scratchDir); err != nil { if err := os.RemoveAll(scratchDir); err != nil {
t.Fatal(err) t.Fatal(err)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment