Commit 9dedc0ce authored by Kirill Smelkov's avatar Kirill Smelkov

.

parent 1a864ebd
// Handler for raw blob downloads
//
// Blobs are read via `git cat-file ...` with first querying authentication
// backend about download-access premission for containing repository.
// backend about download-access permission for containing repository.
// Replies from authentication backend are cached for 30 seconds to keep
// access-to-blobs latency to minimum.
......@@ -39,7 +39,7 @@ type AuthCacheEntry struct {
// read side, but we can tolerate some looses in Nhit and update it
// without mutex or atomic. Only -race complains...
// ( we could use atomic.Value for atomic whole cache entry updates from
// refresher withot need for locks on readers side, but the need to do
// refresher without need for locks on readers side, but the need to do
// .Nhit++ on readers side ruins that )
sync.Mutex
......@@ -54,11 +54,11 @@ type AuthCacheEntry struct {
type AuthCacheKey struct {
project string
query string // e.g. with passing in private_token=...
// TODO headers
header string // request header url-encoded, e.g. PRIVATE-TOKEN=...
}
// Authorization reply cache
// {} project -> AuthCacheEntry
// {} AuthCacheKey -> *AuthCacheEntry
type AuthCache struct {
u *upstream // for which upstream we cache auth
......@@ -113,14 +113,14 @@ have_entry:
c.cached[key] = auth
c.mu.Unlock()
// this goroutine becomes responsible for quering auth backend
auth.AuthReply = askAuthBackend(c.u, key.project, key.query)
// this goroutine becomes responsible for querying auth backend
auth.AuthReply = c.askAuthBackend(key)
auth.Tauth = time.Now().Unix()
auth.Nhit = 0
authReply = auth.AuthReply
// broadcast to other goroutines this entry is ready
// broadcast to other goroutines that this entry is ready
close(auth.ready)
// launch entry refresher
......@@ -131,7 +131,7 @@ have_entry:
}
// Time period for refreshing / removing unused entires in authCache
const authCacheRefresh = 5 * time.Second
const authCacheRefresh = 30 * time.Second
// 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 refreshing.
......@@ -144,9 +144,9 @@ func (c *AuthCache) refreshEntry(auth *AuthCacheEntry, key AuthCacheKey) {
auth.Unlock()
// clear cache entry if it is not used
log.Printf("AUTH refresh - %v #hit: %v", key, nhit)
//log.Printf("AUTH refresh - %v #hit: %v", key, nhit)
if nhit == 0 { // not used - we can remove and stop refreshing
log.Printf("AUTH - removing %v", key)
//log.Printf("AUTH - removing %v", key)
// NOTE it is ok even if someone gets this auth in this time window
// and use it for some time
c.mu.Lock()
......@@ -155,8 +155,8 @@ func (c *AuthCache) refreshEntry(auth *AuthCacheEntry, key AuthCacheKey) {
break
}
log.Printf("AUTH - refreshing %v", key)
authReply := askAuthBackend(c.u, key.project, key.query)
//log.Printf("AUTH - refreshing %v", key)
authReply := c.askAuthBackend(key)
auth.Lock()
auth.AuthReply = authReply
......@@ -166,10 +166,25 @@ func (c *AuthCache) refreshEntry(auth *AuthCacheEntry, key AuthCacheKey) {
}
}
func (c *AuthCache) askAuthBackend(key AuthCacheKey) AuthReply {
// key.header -> url.Values -> http.Header
hv, err := url.ParseQuery(key.header)
if err != nil {
// we prepared key.header ourselves via url-encoding in XXX. It must be ok
panic(err)
}
header := make(http.Header)
for k,v := range hv {
header[k] = v
}
return askAuthBackend(c.u, key.project, key.query, &header)
}
// Ask auth backend about whether download is ok for a project.
// Authorization is approved if AuthReply.RepoPath != "" on return
// Raw auth backend response is emitted to AuthReply.RawReply
func askAuthBackend(u *upstream, project, query string) AuthReply {
func askAuthBackend(u *upstream, project, query string, header *http.Header) AuthReply {
authReply := AuthReply{
RawReply: httptest.NewRecorder(),
}
......@@ -178,8 +193,7 @@ func askAuthBackend(u *upstream, project, query string) AuthReply {
// - first option is via asking as `git fetch` would do, but on Rails
// side this supports only basic auth, not private token.
// - that's why we auth backend to authenticate as if it was request to
// get repo archive.
// XXX PRIVATE-TOKEN (in header)
// get repo archive and propagate request query and header.
// url := project+".git/info/refs?service=git-upload-pack"
url := project+"/repository/archive.zip"
if query != "" {
......@@ -190,6 +204,9 @@ func askAuthBackend(u *upstream, project, query string) AuthReply {
fail500(authReply.RawReply, fmt.Errorf("GET git-upload-pack: %v", err))
return authReply
}
for k,v := range *header {
reqDownloadAccess.Header[k] = v
}
// Prepare everything and go through preAuthorizeHandler() that will send
// request to auth backend and analyze/parse the reply into r.authorizationResponse.
......@@ -209,8 +226,8 @@ func askAuthBackend(u *upstream, project, query string) AuthReply {
return authReply
}
func verifyDownloadAccess(u *upstream, project, query string) AuthReply {
return u.authCache.VerifyDownloadAccess(AuthCacheKey{project, query})
func verifyDownloadAccess(u *upstream, project, query string, header string) AuthReply {
return u.authCache.VerifyDownloadAccess(AuthCacheKey{project, query, header})
}
// HTTP handler for `.../raw/<ref>/path`
......@@ -228,6 +245,7 @@ func handleGetBlobRaw(w http.ResponseWriter, r *gitRequest) {
project := u.Path[:rawLoc[0]]
refpath := u.Path[rawLoc[1]:]
// XXX should be in cache
// Extract only tokens from query
query := url.Values{}
for k, v := range u.Query() {
......@@ -236,16 +254,17 @@ func handleGetBlobRaw(w http.ResponseWriter, r *gitRequest) {
}
}
// Extract only tokens from headers
h := url.Values{}
// XXX should be in cache
// Extract only token headers
header := url.Values{}
for k, v := range r.Request.Header {
if strings.HasSuffix(strings.ToUpper(k), "-TOKEN") {
h[k] = v
header[k] = v
}
}
// Query download access auth for this project
authReply := verifyDownloadAccess(r.u, project, query.Encode(), h)
authReply := verifyDownloadAccess(r.u, project, query.Encode(), header.Encode())
if authReply.RepoPath == "" {
// access denied - copy auth reply to client in full -
// there are HTTP code and other headers / body relevant for
......
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