Commit 238e32b9 authored by Kirill Smelkov's avatar Kirill Smelkov

.

parent 81b9a523
...@@ -5,10 +5,9 @@ import ( ...@@ -5,10 +5,9 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
// "os" "log"
"net/http" "net/http"
"strings" "strings"
"log"
"time" "time"
) )
......
// Handler for raw blob downloads // Handler for raw blob downloads
// //
// Blobs are read via `git cat-file ...` with first querying authentication // Blobs are read via `git cat-file ...` with first querying authentication
// backend about download-access for containing repository. Replies from // backend about download-access premission for containing repository.
// authentication backend are cached for 30 seconds to keep access-to-blobs // Replies from authentication backend are cached for 30 seconds to keep
// latency to minimum. // access-to-blobs latency to minimum.
package main package main
import ( import (
"bufio"
"fmt"
"io" "io"
"log" "log"
"fmt"
"bufio"
"time"
"strings"
"bytes"
"regexp"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"regexp"
"strings"
"time"
) )
// Reply from auth backend for "download from repo" authorization request // Reply from auth backend for "download from repo" authorization request
type AuthReply struct { type AuthReply struct {
w *httptest.ResponseRecorder // output of backend/preAuthorizeHandler // raw reply from auth backend & preAuthorizeHandler().
authorizationResponse // parsed auth response from preAuthorizeHandler // recorded so we can replay it from auth cache to each client in full
// if access is rejected.
RawReply *httptest.ResponseRecorder
// decoded auth reply
authorizationResponse
} }
// Entry in authorization reply cache // Entry in authorization reply cache
type AuthCacheEntry struct { type AuthCacheEntry struct {
AuthReply AuthReply
Tauth int64 // in seconds XXX do we strictly need this? Tauth int64 // in seconds XXX needed?
// how many times this entry was hit when querying 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
} }
// Authorization reply cache // Authorization reply cache
...@@ -45,8 +48,7 @@ type AuthCacheEntry struct { ...@@ -45,8 +48,7 @@ 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 const authCacheRefresh = 30 * time.Second
// 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 refreshing. // if the entry is detected to be not used - remove it from cache and stop refreshing.
...@@ -54,19 +56,18 @@ func authRefreshEntry(u *upstream, project string) { ...@@ -54,19 +56,18 @@ 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 ?
for ;; { for {
//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)
break // need to further refresh XXX ok? break // no need to further refresh
} }
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)
...@@ -84,63 +85,61 @@ func authRefreshEntry(u *upstream, project string) { ...@@ -84,63 +85,61 @@ func authRefreshEntry(u *upstream, project string) {
} }
// 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
} }
} }
// 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, error) { func askAuthBackend(u *upstream, project string) (AuthReply, error) {
authReply := AuthReply{ authReply := AuthReply{
w: httptest.NewRecorder(), RawReply: httptest.NewRecorder(),
} }
// Request to auth backend to verify whether download is possible via // Request to auth backend to verify whether download is possible via
// asking as git fetch would do. // asking as `git fetch` would do.
// XXX privateToken not propagated, etc ... // XXX privateToken not propagated, etc ...
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.RawReply, "GET git-upload-pack", err)
return authReply, err return authReply, err
} }
// prepare everything and go through preAuthorizeHandler that will send // prepare everything and go through preAuthorizeHandler() that will send
// request to auth backend and analyze/parse the reply into r.authorizationResponse // request to auth backend and analyze/parse the reply into r.authorizationResponse
r := &gitRequest{ r := &gitRequest{
Request: reqDownloadAccess, Request: reqDownloadAccess,
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
// access and thus it is ok to download // access and thus it is ok to download
// downloadOk = true // downloadOk = true XXX
// NOTE we can use authorizationResponse.RepoPath != "" as test for this // NOTE we can use authorizationResponse.RepoPath != "" as test for this
}, "") (authReply.w, r) }, "")(authReply.RawReply, r)
// propagate authorizationResponse back and we are done // propagate authorizationResponse back and we are done
authReply.authorizationResponse = r.authorizationResponse authReply.authorizationResponse = r.authorizationResponse
return authReply, nil return authReply, nil
} }
// Verify that download access is ok or not. // Verify that download access is ok or not.
// first we try to see authCache; if information is not there -> ask auth backend // first we try to see authCache; if information is not there -> ask auth backend
// XXX return -> *AuthReply ?
func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) (AuthReply, error) { func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) (AuthReply, error) {
// XXX lock authCache // XXX lock authCache
auth, ok := authCache[project] auth, ok := authCache[project]
if ok { if ok {
auth.Nhit++ auth.Nhit++
log.Printf("authReply for %v cached ago: %v (hits: %v)", log.Printf("authReply for %v cached ago: %v (hits: %v)",
project, project,
time.Since(time.Unix(auth.Tauth, 0)), time.Since(time.Unix(auth.Tauth, 0)),
auth.Nhit) auth.Nhit)
return auth.AuthReply, nil // XXX make pointer? return auth.AuthReply, nil
} }
authReply, err := askAuthBackend(u, project) authReply, err := askAuthBackend(u, project)
...@@ -148,7 +147,7 @@ func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) (A ...@@ -148,7 +147,7 @@ func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) (A
return authReply, err return authReply, err
} }
// XXX do we need to lock authCache ? // XXX lock
// 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)
...@@ -156,8 +155,6 @@ func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) (A ...@@ -156,8 +155,6 @@ func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) (A
return authReply, nil 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\.-]+/`)
...@@ -166,16 +163,16 @@ func handleGetBlobRaw(w http.ResponseWriter, r *gitRequest) { ...@@ -166,16 +163,16 @@ func handleGetBlobRaw(w http.ResponseWriter, r *gitRequest) {
// <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)
if project == "" || project[len(project)-1] != '/' { if project == "" || project[len(project)-1] != '/' {
fail500(w, "extract project name", nil) // XXX err=nil fail500(w, "extract project name", nil) // XXX err=nil
return return
} }
refpath := r.Request.URL.Path[len(project):] refpath := r.Request.URL.Path[len(project):]
if refpath[:4] != "raw/" { if refpath[:4] != "raw/" {
fail500(w, "refpath != raw/...", nil) // XXX err=nil fail500(w, "refpath != raw/...", nil) // XXX err=nil
return return
} }
project = project[:len(project)-1] project = project[:len(project)-1] // strip '.../'
refpath = refpath[4:] refpath = refpath[4:] // strip 'raw/...'
// Query download access auth for this project // Query download access auth for this project
authReply, err := verifyDownloadAccess(w, r.u, project) authReply, err := verifyDownloadAccess(w, r.u, project)
...@@ -187,11 +184,11 @@ func handleGetBlobRaw(w http.ResponseWriter, r *gitRequest) { ...@@ -187,11 +184,11 @@ func handleGetBlobRaw(w http.ResponseWriter, r *gitRequest) {
// 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
// about why access was denied. // about why access was denied.
for k, v := range authReply.w.HeaderMap { for k, v := range authReply.RawReply.HeaderMap {
w.Header()[k] = v w.Header()[k] = v
} }
w.WriteHeader(authReply.w.Code) w.WriteHeader(authReply.RawReply.Code)
io.Copy(w, authReply.w.Body) io.Copy(w, authReply.RawReply.Body)
return return
} }
...@@ -210,7 +207,6 @@ Content-Transfer-Encoding: binary ...@@ -210,7 +207,6 @@ 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 // 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
...@@ -237,18 +233,18 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) { ...@@ -237,18 +233,18 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
return return
} }
defer cleanUpProcessGroup(queryCmd) defer cleanUpProcessGroup(queryCmd)
// XXX also set communication timeout ?
// refpath components as vector // refpath components as vector
refpathv := strings.Split(refpath, "/") refpathv := strings.Split(refpath, "/")
// 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 git object name. If it does - we read object
// which follows. // content which follows.
// TODO handle communication timeout ?
var sha1, type_ string var sha1, 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], "/")
path := strings.Join(refpathv[i:], "/") path := strings.Join(refpathv[i:], "/")
log.Printf("Trying %v %v", ref, path) log.Printf("Trying %v %v", ref, path)
_, err := fmt.Fprintf(queryStdin, "%s:%s\n", ref, path) _, err := fmt.Fprintf(queryStdin, "%s:%s\n", ref, path)
...@@ -266,7 +262,7 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) { ...@@ -266,7 +262,7 @@ 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, " missing\n") { if strings.HasSuffix(reply, " missing\n") {
continue continue
} }
...@@ -274,31 +270,28 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) { ...@@ -274,31 +270,28 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
_, err = fmt.Sscanf(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
} }
if type_ != "blob" { if type_ != "blob" {
// XXX -> 404 log.Printf("git cat-file --batch-check; %v is not blob (is %v)", sha1, type_)
fail500(w, fmt.Sprintf("git cat-file --batch-check; %v is not blob (is %v)", sha1, type_), nil) sha1 = "" // so it will return 404
return
} }
// so we found this blob object // so we found this blob object
break break
} }
// was the blob found? // Blob not found -> 404
if sha1 == "" { if sha1 == "" {
// XXX -> 404 // XXX -> 404
fail400(w, "Blob not found", nil) fail400(w, fmt.Sprintf("Blob for %v not found", refpath), nil)
return return
} }
log.Printf("blob found, size: %d", size) // Blob found - start writing response
//setRawHeaders(...) // TODO
//setRawHeaders(...) w.WriteHeader(200) // Don't bother with HTTP 500 from this point on, just return
w.WriteHeader(200) // XXX too early
log.Printf("111")
// XXX better use queryStdout instead of queryReader, but we could be // XXX better use queryStdout instead of queryReader, but we could be
// holding some tail bytes in queryReader after chat phase // holding some tail bytes in queryReader after chat phase
_, err = io.CopyN(w, queryReader, size) _, err = io.CopyN(w, queryReader, size)
...@@ -306,14 +299,14 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) { ...@@ -306,14 +299,14 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
logContext("io.CopyN", err) logContext("io.CopyN", err)
return return
} }
log.Printf("222")
// close git stdin explicitly, so it exits cleanly
err = queryStdin.Close() err = queryStdin.Close()
if err != nil { if err != nil {
fail500(w, "queryStdin.Close", nil) logContext("queryStdin.Close", err)
return return
} }
log.Printf("333")
err = queryCmd.Wait() err = queryCmd.Wait()
if err != nil { if err != nil {
logContext("wait", err) logContext("wait", err)
......
...@@ -7,10 +7,10 @@ package main ...@@ -7,10 +7,10 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings" "strings"
"log"
) )
func handleGetInfoRefs(w http.ResponseWriter, r *gitRequest) { func handleGetInfoRefs(w http.ResponseWriter, r *gitRequest) {
......
...@@ -90,6 +90,7 @@ var gitServices = [...]gitService{ ...@@ -90,6 +90,7 @@ var gitServices = [...]gitService{
func newUpstream(authBackend string, authTransport http.RoundTripper) *upstream { func newUpstream(authBackend string, authTransport http.RoundTripper) *upstream {
return &upstream{&http.Client{Transport: authTransport}, authBackend} return &upstream{&http.Client{Transport: authTransport}, authBackend}
// XXX Timeout: ... ?
} }
func (u *upstream) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (u *upstream) ServeHTTP(w http.ResponseWriter, r *http.Request) {
......
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