Commit 81b9a523 authored by Kirill Smelkov's avatar Kirill Smelkov

.

parent c99926dc
Pipeline #120 failed with stage
/* // Handler for raw blob downloads
Handler for raw blob downloads //
*/ // Blobs are read via `git cat-file ...` with first querying authentication
// backend about download-access for containing repository. Replies from
// authentication backend are cached for 30 seconds to keep access-to-blobs
// latency to minimum.
package main package main
...@@ -30,7 +33,7 @@ type AuthCacheEntry struct { ...@@ -30,7 +33,7 @@ type AuthCacheEntry struct {
Tauth int64 // in seconds XXX do we strictly need this? Tauth int64 // in seconds XXX do we strictly need this?
// how many times this entry was hit when quiering auth cache during // how many times this entry was hit when querying auth cache during
// the last refresh period. // the last refresh period.
Nhit int64 Nhit int64
} }
...@@ -42,10 +45,11 @@ type AuthCacheEntry struct { ...@@ -42,10 +45,11 @@ type AuthCacheEntry struct {
var authCache = make(map[string]*AuthCacheEntry) var authCache = make(map[string]*AuthCacheEntry)
// Time period for refreshing / removing unused entires in authCache // Time period for refreshing / removing unused entires in authCache
const authCacheRefresh = 5 * time.Second // XXX -> 30 or 60 const authCacheRefresh = 5 * time.Second // XXX -> 30
// Goroutine to refresh auth cache entry periodically while it is used. // Goroutine to refresh auth cache entry periodically while it is used.
// if the entry is detected to be not used - remove it from cache and stop rereshing. // if the entry is detected to be not used - remove it from cache and stop refreshing.
func authRefreshEntry(u *upstream, project string) { func authRefreshEntry(u *upstream, project string) {
// XXX auth := authCache[project] // XXX auth := authCache[project]
// and then use auth without authCache lookup ? // and then use auth without authCache lookup ?
...@@ -54,7 +58,7 @@ func authRefreshEntry(u *upstream, project string) { ...@@ -54,7 +58,7 @@ func authRefreshEntry(u *upstream, project string) {
//log.Printf("AUTH refresh sleep ...") //log.Printf("AUTH refresh sleep ...")
time.Sleep(authCacheRefresh) time.Sleep(authCacheRefresh)
// XXX lock? // XXX lock
auth, ok := authCache[project] auth, ok := authCache[project]
if !ok { // someone removed the entry from cache - no if !ok { // someone removed the entry from cache - no
log.Printf("AUTH refresh - %v entry removed", project) log.Printf("AUTH refresh - %v entry removed", project)
...@@ -64,15 +68,22 @@ func authRefreshEntry(u *upstream, project string) { ...@@ -64,15 +68,22 @@ func authRefreshEntry(u *upstream, project string) {
log.Printf("AUTH refresh - %v #hit: %v", project, auth.Nhit) log.Printf("AUTH refresh - %v #hit: %v", project, auth.Nhit)
if auth.Nhit == 0 { // not used - we can remove and stop refreshing if auth.Nhit == 0 { // not used - we can remove and stop refreshing
log.Printf("AUTH - removing %v", project) log.Printf("AUTH - removing %v", project)
// XXX lock? // XXX lock
delete(authCache, project) delete(authCache, project)
break break
} }
log.Printf("AUTH - refreshing %v", project) log.Printf("AUTH - refreshing %v", project)
authReply := askAuthBackend(u, project) // XXX what if it stucks?
authReply, err := askAuthBackend(u, project)
if err != nil {
// an error -> delete entry from cache and be done with
// refreshing XXX lock, unify with ^^^
delete(authCache, project)
break
}
// XXX lock ? // XXX lock
auth.AuthReply = authReply auth.AuthReply = authReply
auth.Tauth = time.Now().Unix() auth.Tauth = time.Now().Unix()
auth.Nhit = 0 auth.Nhit = 0
...@@ -82,7 +93,7 @@ func authRefreshEntry(u *upstream, project string) { ...@@ -82,7 +93,7 @@ func authRefreshEntry(u *upstream, project string) {
// Ask auth backend about whether download is ok for a project // Ask auth backend about whether download is ok for a project
func askAuthBackend(u *upstream, project string) AuthReply { func askAuthBackend(u *upstream, project string) (AuthReply, error) {
authReply := AuthReply{ authReply := AuthReply{
w: httptest.NewRecorder(), w: httptest.NewRecorder(),
} }
...@@ -93,7 +104,7 @@ func askAuthBackend(u *upstream, project string) AuthReply { ...@@ -93,7 +104,7 @@ func askAuthBackend(u *upstream, project string) AuthReply {
reqDownloadAccess, err := http.NewRequest("GET", project + ".git/info/refs?service=git-upload-pack", nil) reqDownloadAccess, err := http.NewRequest("GET", project + ".git/info/refs?service=git-upload-pack", nil)
if err != nil { if err != nil {
fail500(authReply.w, "GET git-upload-pack", err) fail500(authReply.w, "GET git-upload-pack", err)
return authReply return authReply, err
} }
// prepare everything and go through preAuthorizeHandler that will send // prepare everything and go through preAuthorizeHandler that will send
...@@ -103,6 +114,7 @@ func askAuthBackend(u *upstream, project string) AuthReply { ...@@ -103,6 +114,7 @@ func askAuthBackend(u *upstream, project string) AuthReply {
u: u, u: u,
} }
// XXX what if it gets stuck?
preAuthorizeHandler( preAuthorizeHandler(
func(w http.ResponseWriter, r *gitRequest) { func(w http.ResponseWriter, r *gitRequest) {
// if we ever get to this point - auth handler approved // if we ever get to this point - auth handler approved
...@@ -113,13 +125,14 @@ func askAuthBackend(u *upstream, project string) AuthReply { ...@@ -113,13 +125,14 @@ func askAuthBackend(u *upstream, project string) AuthReply {
// propagate authorizationResponse back and we are done // propagate authorizationResponse back and we are done
authReply.authorizationResponse = r.authorizationResponse authReply.authorizationResponse = r.authorizationResponse
return authReply return authReply, nil
} }
// Verify that download access is authorized by auth backend // Verify that download access is ok or not.
func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) AuthReply { // first we try to see authCache; if information is not there -> ask auth backend
// XXX do we need mutex to lock authCache ? func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) (AuthReply, error) {
// XXX lock authCache
auth, ok := authCache[project] auth, ok := authCache[project]
if ok { if ok {
auth.Nhit++ auth.Nhit++
...@@ -127,44 +140,49 @@ func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) Au ...@@ -127,44 +140,49 @@ func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) Au
project, project,
time.Since(time.Unix(auth.Tauth, 0)), time.Since(time.Unix(auth.Tauth, 0)),
auth.Nhit) auth.Nhit)
return auth.AuthReply // XXX make pointer? return auth.AuthReply, nil // XXX make pointer?
} }
authReply := askAuthBackend(u, project) authReply, err := askAuthBackend(u, project)
if err != nil {
return authReply, err
}
// XXX do we need to lock authCache ? // XXX do we need to lock authCache ?
// store in cache and start cache entry refresher // store in cache and start cache entry refresher
authCache[project] = &AuthCacheEntry{authReply, time.Now().Unix(), 0} authCache[project] = &AuthCacheEntry{authReply, time.Now().Unix(), 0}
go authRefreshEntry(u, project) go authRefreshEntry(u, project)
return authReply return authReply, nil
} }
// HTTP handler for .../raw/<ref>/path // HTTP handler for `.../raw/<ref>/path`
var projectRe = regexp.MustCompile(`^/[\w\.-]+/[\w\.-]+/`) var projectRe = regexp.MustCompile(`^/[\w\.-]+/[\w\.-]+/`)
func handleGetBlobRaw(w http.ResponseWriter, r *gitRequest) { func handleGetBlobRaw(w http.ResponseWriter, r *gitRequest) {
// Extract project & refpath // Extract project & refpath
// <project>/raw/branch/file -> <project>, branch/file // <project>/raw/branch/file -> <project>, branch/file
project := projectRe.FindString(r.Request.URL.Path) project := projectRe.FindString(r.Request.URL.Path)
refpath := r.Request.URL.Path[len(project):] if project == "" || project[len(project)-1] != '/' {
if project == "" { fail500(w, "extract project name", nil) // XXX err=nil
fail500(w, "extract project name", nil)
return return
} }
// assert project[-1] == "/" refpath := r.Request.URL.Path[len(project):]
project = project[:len(project)-1]
if refpath[:4] != "raw/" { if refpath[:4] != "raw/" {
fail500(w, "refpath != raw/...", nil) fail500(w, "refpath != raw/...", nil) // XXX err=nil
return return
} }
project = project[:len(project)-1]
refpath = refpath[4:] refpath = refpath[4:]
// Query download access auth for this project // Query download access auth for this project
authReply := verifyDownloadAccess(w, r.u, project) authReply, err := verifyDownloadAccess(w, r.u, project)
if err != nil {
fail500(w, "verifyDownloadAccess", err)
return
}
if authReply.RepoPath == "" { if authReply.RepoPath == "" {
// access denied - copy auth reply to client in full - // access denied - copy auth reply to client in full -
// there are HTTP code and other headers / body relevant for // there are HTTP code and other headers / body relevant for
...@@ -192,10 +210,12 @@ Content-Transfer-Encoding: binary ...@@ -192,10 +210,12 @@ Content-Transfer-Encoding: binary
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
*/ */
// Emit content of blob located at <ref>/path (jointly denoted as 'refpath') to output
func emitBlob(w http.ResponseWriter, repopath string, refpath string) { func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
// Communicate with `git cat-file --batch` trying refs from longest // Communicate with `git cat-file --batch` trying refs from longest
// to shortest prefix in refpath. This way we find longest-match for // to shortest prefix in refpath. This way we find longest-match for
// ref and get object content in the end. // ref and get blob sha1 and content in the end.
queryCmd := gitCommand("", "git", "--git-dir="+repopath, "cat-file", "--batch") queryCmd := gitCommand("", "git", "--git-dir="+repopath, "cat-file", "--batch")
queryStdin, err := queryCmd.StdinPipe() queryStdin, err := queryCmd.StdinPipe()
if err != nil { if err != nil {
...@@ -216,7 +236,7 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) { ...@@ -216,7 +236,7 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
fail500(w, "git cat-file --batch; start", err) fail500(w, "git cat-file --batch; start", err)
return return
} }
defer cleanUpProcessGroup(queryCmd) // XXX do we really need this? defer cleanUpProcessGroup(queryCmd)
// refpath components as vector // refpath components as vector
refpathv := strings.Split(refpath, "/") refpathv := strings.Split(refpath, "/")
...@@ -224,9 +244,8 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) { ...@@ -224,9 +244,8 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
// scan from right to left and try to change '/' -> ':' and see if it // scan from right to left and try to change '/' -> ':' and see if it
// creates a correct object name. If it does - we read object content // creates a correct object name. If it does - we read object content
// which follows. // which follows.
// TODO handle communication timeout // TODO handle communication timeout ?
var sha1 string var sha1, type_ string
var type_ string
var size int64 var size int64
for i := len(refpathv); i > 0; i-- { for i := len(refpathv); i > 0; i-- {
ref := strings.Join(refpathv[:i], "/") ref := strings.Join(refpathv[:i], "/")
...@@ -238,7 +257,7 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) { ...@@ -238,7 +257,7 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
return return
} }
reply, err := queryReader.ReadBytes('\n') reply, err := queryReader.ReadString('\n')
if err != nil { if err != nil {
fail500(w, "git cat-file --batch; read", err) fail500(w, "git cat-file --batch; read", err)
return return
...@@ -247,12 +266,12 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) { ...@@ -247,12 +266,12 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
log.Printf("<- %s", reply) log.Printf("<- %s", reply)
// <object> SP missing LF // <object> SP missing LF
if bytes.HasSuffix(reply, []byte(" missing\n")) { // XXX byte literal? if bytes.HasSuffix(reply, " missing\n") {
continue continue
} }
// <sha1> SP <type> SP <size> LF // <sha1> SP <type> SP <size> LF
_, err = fmt.Sscanf(string(reply), "%s %s %d\n", &sha1, &type_, &size) _, err = fmt.Sscanf(reply, "%s %s %d\n", &sha1, &type_, &size)
if err != nil { if err != nil {
fail500(w, "git cat-file --batch; reply parse", err) fail500(w, "git cat-file --batch; reply parse", err)
return; return;
......
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