package main
import (
"log"
"net/http"
"os"
"strings"
"time"
"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
"git.curoverse.com/arvados.git/sdk/go/auth"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
)
var clientPool = arvadosclient.MakeClientPool()
type authHandler struct {
handler http.Handler
}
func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
var statusCode int
var statusText string
var apiToken string
var repoName string
var validApiToken bool
w := httpserver.WrapResponseWriter(wOrig)
defer func() {
if w.WroteStatus() == 0 {
// Nobody has called WriteHeader yet: that
// must be our job.
w.WriteHeader(statusCode)
w.Write([]byte(statusText))
}
// If the given password is a valid token, log the first 10 characters of the token.
// Otherwise: log the string <invalid> if a password is given, else an empty string.
passwordToLog := ""
if !validApiToken {
if len(apiToken) > 0 {
passwordToLog = "<invalid>"
}
} else {
passwordToLog = apiToken[0:10]
}
httpserver.Log(r.RemoteAddr, passwordToLog, w.WroteStatus(), statusText, repoName, r.Method, r.URL.Path)
}()
creds := auth.NewCredentialsFromHTTPRequest(r)
if len(creds.Tokens) == 0 {
statusCode, statusText = http.StatusUnauthorized, "no credentials provided"
w.Header().Add("WWW-Authenticate", "Basic realm=\"git\"")
return
}
apiToken = creds.Tokens[0]
// Access to paths "/foo/bar.git/*" and "/foo/bar/.git/*" are
// protected by the permissions on the repository named
// "foo/bar".
pathParts := strings.SplitN(r.URL.Path[1:], ".git/", 2)
if len(pathParts) != 2 {
statusCode, statusText = http.StatusBadRequest, "bad request"
return
}
repoName = pathParts[0]
repoName = strings.TrimRight(repoName, "/")
arv := clientPool.Get()
if arv == nil {
statusCode, statusText = http.StatusInternalServerError, "connection pool failed: "+clientPool.Err().Error()
return
}
defer clientPool.Put(arv)
// Ask API server whether the repository is readable using
// this token (by trying to read it!)
arv.ApiToken = apiToken
reposFound := arvadosclient.Dict{}
if err := arv.List("repositories", arvadosclient.Dict{
"filters": [][]string{{"name", "=", repoName}},
}, &reposFound); err != nil {
statusCode, statusText = http.StatusInternalServerError, err.Error()
return
}
validApiToken = true
if avail, ok := reposFound["items_available"].(float64); !ok {
statusCode, statusText = http.StatusInternalServerError, "bad list response from API"
return
} else if avail < 1 {
statusCode, statusText = http.StatusNotFound, "not found"
return
} else if avail > 1 {
statusCode, statusText = http.StatusInternalServerError, "name collision"
return
}
repoUUID := reposFound["items"].([]interface{})[0].(map[string]interface{})["uuid"].(string)
isWrite := strings.HasSuffix(r.URL.Path, "/git-receive-pack")
if !isWrite {
statusText = "read"
} else {
err := arv.Update("repositories", repoUUID, arvadosclient.Dict{
"repository": arvadosclient.Dict{
"modified_at": time.Now().String(),
},
}, &arvadosclient.Dict{})
if err != nil {
statusCode, statusText = http.StatusForbidden, err.Error()
return
}
statusText = "write"
}
// Regardless of whether the client asked for "/foo.git" or
// "/foo/.git", we choose whichever variant exists in our repo
// root, and we try {uuid}.git and {uuid}/.git first. If none
// of these exist, we 404 even though the API told us the repo
// _should_ exist (presumably this means the repo was just
// created, and gitolite sync hasn't run yet).
rewrittenPath := ""
tryDirs := []string{
"/" + repoUUID + ".git",
"/" + repoUUID + "/.git",
"/" + repoName + ".git",
"/" + repoName + "/.git",
}
for _, dir := range tryDirs {
if fileInfo, err := os.Stat(theConfig.Root + dir); err != nil {
if !os.IsNotExist(err) {
statusCode, statusText = http.StatusInternalServerError, err.Error()
return
}
} else if fileInfo.IsDir() {
rewrittenPath = dir + "/" + pathParts[1]
break
}
}
if rewrittenPath == "" {
log.Println("WARNING:", repoUUID,
"git directory not found in", theConfig.Root, tryDirs)
// We say "content not found" to disambiguate from the
// earlier "API says that repo does not exist" error.
statusCode, statusText = http.StatusNotFound, "content not found"
return
}
r.URL.Path = rewrittenPath
h.handler.ServeHTTP(&w, r)
}
package main
import (
"log"
"net"
"net/http"
"net/http/cgi"
)
// gitHandler is an http.Handler that invokes git-http-backend (or
// whatever backend is configured) via CGI, with appropriate
// environment variables in place for git-http-backend or
// gitolite-shell.
type gitHandler struct {
cgi.Handler
}
func newGitHandler() http.Handler {
return &gitHandler{
Handler: cgi.Handler{
Path: theConfig.GitCommand,
Dir: theConfig.Root,
Env: []string{
"GIT_PROJECT_ROOT=" + theConfig.Root,
"GIT_HTTP_EXPORT_ALL=",
"SERVER_ADDR=" + theConfig.Addr,
},
InheritEnv: []string{
"PATH",
// Needed if GitCommand is gitolite-shell:
"GITOLITE_HTTP_HOME",
"GL_BYPASS_ACCESS_CHECKS",
},
Args: []string{"http-backend"},
},
}
}
func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
remoteHost, remotePort, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
log.Printf("Internal error: SplitHostPort(r.RemoteAddr==%q): %s", r.RemoteAddr, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// Copy the wrapped cgi.Handler, so these request-specific
// variables don't leak into the next request.
handlerCopy := h.Handler
handlerCopy.Env = append(handlerCopy.Env,
// In Go1.5 we can skip this, net/http/cgi will do it for us:
"REMOTE_HOST="+remoteHost,
"REMOTE_ADDR="+remoteHost,
"REMOTE_PORT="+remotePort,
// Ideally this would be a real username:
"REMOTE_USER="+r.RemoteAddr,
)
handlerCopy.ServeHTTP(w, r)
}
package main
import (
"flag"
"log"
"os"
)
type config struct {
Addr string
GitCommand string
Root string
}
var theConfig *config
func init() {
theConfig = &config{}
flag.StringVar(&theConfig.Addr, "address", "0.0.0.0:80",
"Address to listen on, \"host:port\".")
flag.StringVar(&theConfig.GitCommand, "git-command", "/usr/bin/git",
"Path to git or gitolite-shell executable. Each authenticated request will execute this program with a single argument, \"http-backend\".")
cwd, err := os.Getwd()
if err != nil {
log.Fatalln("Getwd():", err)
}
flag.StringVar(&theConfig.Root, "repo-root", cwd,
"Path to git repositories.")
// MakeArvadosClient returns an error if token is unset (even
// though we don't need to do anything requiring
// authentication yet). We can't do this in newArvadosClient()
// just before calling MakeArvadosClient(), though, because
// that interferes with the env var needed by "run test
// servers".
os.Setenv("ARVADOS_API_TOKEN", "xxx")
}
func main() {
flag.Parse()
srv := &server{}
if err := srv.Start(); err != nil {
log.Fatal(err)
}
log.Println("Listening at", srv.Addr)
log.Println("Repository root", theConfig.Root)
if err := srv.Wait(); err != nil {
log.Fatal(err)
}
}
package main
import (
"net/http"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
)
type server struct {
httpserver.Server
}
func (srv *server) Start() error {
mux := http.NewServeMux()
mux.Handle("/", &authHandler{newGitHandler()})
srv.Handler = mux
srv.Addr = theConfig.Addr
return srv.Server.Start()
}