Skip to content
Permalink
Browse files

internal/code: implement module proxy protocol

This change adds an initial version of a Go module server
that implements the module proxy protocol, as described
at https://golang.org/cmd/go/#hdr-Module_proxy_protocol.

This enables more efficient serving of Go modules, especially in cases
where only the list, .info and .mod endpoints are requested (not .zip).
That happens often: whenever the module isn't actually used in a build,
rather it just happens to be in the module requirement graph.

The module server isn't fully featured and has some known limitations,
but it's enough to cover my current needs. It serves all current module
versions at dmitri.shuralyov.com/... with identical checksums as the
canonical go mod download algorithm. Its feature set may get expanded
as my needs change.
  • Loading branch information...
dmitshur committed May 6, 2019
1 parent 1c38c56 commit dd5eb37ca0c88b2f7e170ffad635572e3519654d
Showing with 725 additions and 9 deletions.
  1. +2 −1 code.go
  2. +75 −6 httputil/handler.go
  3. +3 −2 internal/code/code_test.go
  4. +325 −0 internal/code/module.go
  5. +148 −0 internal/code/module_test.go
  6. +27 −0 internal/mod/LICENSE
  7. +140 −0 internal/mod/mod.go
  8. +5 −0 main.go
@@ -72,7 +72,8 @@ func (h *codeHandler) ServeCodeMaybe(w http.ResponseWriter, req *http.Request) (
if req.Method == http.MethodGet && req.URL.Query().Get("go-get") == "1" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<meta name="go-import" content="%[1]s git https://%[1]s">
<meta name="go-source" content="%[1]s https://%[1]s https://gotools.org/%[2]s https://gotools.org/%[2]s#{file}-L{line}">`, d.RepoRoot, d.ImportPath)
<meta name="go-import" content="%[1]s mod https://">
<meta name="go-source" content="%[1]s https://%[1]s https://gotools.org/%[2]s https://gotools.org/%[2]s#{file}-L{line}">`, d.RepoRoot, d.ImportPath)
return true
}

@@ -16,7 +16,18 @@ import (
)

// ErrorHandler factors error handling out of the HTTP handler.
func ErrorHandler(users users.Service, handler func(w http.ResponseWriter, req *http.Request) error) http.Handler {
// If users is nil, it treats all requests as made by an unauthenticated user.
func ErrorHandler(
users interface {
// GetAuthenticated fetches the currently authenticated user,
// or User{UserSpec: UserSpec{ID: 0}} if there is no authenticated user.
GetAuthenticated(context.Context) (users.User, error)
},
handler func(w http.ResponseWriter, req *http.Request) error,
) http.Handler {
if users == nil {
users = noUsers{}
}
return &errorHandler{handler: handler, users: users}
}

@@ -30,11 +41,57 @@ type errorHandler struct {
func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
rw := &headerResponseWriter{ResponseWriter: w}
err := h.handler(rw, req)
handleError(w, req, err, h.users, rw.WroteHeader)
}

// ErrorHandleMaybe factors error handling out of the HTTP maybe handler.
// If users is nil, it treats all requests as made by an unauthenticated user.
func ErrorHandleMaybe(
w http.ResponseWriter, req *http.Request,
users interface {
// GetAuthenticated fetches the currently authenticated user,
// or User{UserSpec: UserSpec{ID: 0}} if there is no authenticated user.
GetAuthenticated(context.Context) (users.User, error)
},
// maybeHandler serves an HTTP request, if it matches.
// It returns httperror.NotHandle if the HTTP request was explicitly not handled.
maybeHandler func(w http.ResponseWriter, req *http.Request) error,
) (ok bool) {
if users == nil {
users = noUsers{}
}
rw := &headerResponseWriter{ResponseWriter: w}
err := maybeHandler(rw, req)
if err == httperror.NotHandle {
if rw.WroteHeader {
panic(fmt.Errorf("internal error: maybe handler wrote HTTP header and then returned httperror.NotHandle"))
}
// The request was explicitly not handled by the maybe handler.
// Do nothing, return ok==false.
return false
}
// The request was handled by the maybe handler.
// Handle error and return ok==true.
handleError(w, req, err, users, rw.WroteHeader)
return true
}

// handleError handles error err, which may be nil.
func handleError(
w http.ResponseWriter, req *http.Request,
err error,
users interface {
// GetAuthenticated fetches the currently authenticated user,
// or User{UserSpec: UserSpec{ID: 0}} if there is no authenticated user.
GetAuthenticated(context.Context) (users.User, error)
},
wroteHeader bool,
) {
if err == nil {
// Do nothing.
return
}
if err != nil && rw.WroteHeader {
if err != nil && wroteHeader {
// The header has already been written, so it's too late to send
// a different status code. Just log the error and move on.
log.Println(err)
@@ -55,7 +112,7 @@ func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if err, ok := httperror.IsHTTP(err); ok {
code := err.Code
error := fmt.Sprintf("%d %s", code, http.StatusText(code))
if user, e := h.users.GetAuthenticated(req.Context()); e == nil && user.SiteAdmin {
if user, e := users.GetAuthenticated(req.Context()); e == nil && user.SiteAdmin {
error += "\n\n" + err.Error()
}
http.Error(w, error, code)
@@ -74,7 +131,7 @@ func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if os.IsNotExist(err) {
log.Println(err)
error := "404 Not Found"
if user, e := h.users.GetAuthenticated(req.Context()); e == nil && user.SiteAdmin {
if user, e := users.GetAuthenticated(req.Context()); e == nil && user.SiteAdmin {
error += "\n\n" + err.Error()
}
http.Error(w, error, http.StatusNotFound)
@@ -84,7 +141,7 @@ func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO: Factor in a GetAuthenticatedSpec.ID == 0 check out here. (But this shouldn't apply for APIs.)
log.Println(err)
error := "403 Forbidden"
if user, e := h.users.GetAuthenticated(req.Context()); e == nil && user.SiteAdmin {
if user, e := users.GetAuthenticated(req.Context()); e == nil && user.SiteAdmin {
error += "\n\n" + err.Error()
}
http.Error(w, error, http.StatusForbidden)
@@ -93,7 +150,7 @@ func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {

log.Println(err)
error := "500 Internal Server Error"
if user, e := h.users.GetAuthenticated(req.Context()); e == nil && user.SiteAdmin {
if user, e := users.GetAuthenticated(req.Context()); e == nil && user.SiteAdmin {
error += "\n\n" + err.Error()
}
http.Error(w, error, http.StatusInternalServerError)
@@ -210,3 +267,15 @@ func bodyAllowedForStatus(status int) bool {
return true
}
}

// noUsers implements a subset of the users.Service interface
// relevant to fetching the currently authenticated user.
//
// It does not perform authentication, instead opting to
// always report that there is an unauthenticated user.
type noUsers struct{}

// GetAuthenticated always reports that there is an unauthenticated user.
func (noUsers) GetAuthenticated(context.Context) (users.User, error) {
return users.User{UserSpec: users.UserSpec{ID: 0, Domain: ""}}, nil
}
@@ -19,7 +19,7 @@ import (
"github.com/shurcooL/users"
)

func Test(t *testing.T) {
func TestCode(t *testing.T) {
tempDir, err := ioutil.TempDir("", "code_test_")
if err != nil {
t.Fatal(err)
@@ -42,6 +42,7 @@ func Test(t *testing.T) {
t.Fatal("code.NewService:", err)
}

// Create a real HTTP server so we can git push to it.
gitHandler, err := code.NewGitHandler(service, filepath.Join(tempDir, "repositories"), events, users, nil, func(req *http.Request) *http.Request { return req })
if err != nil {
t.Fatal("code.NewGitHandler:", err)
@@ -50,7 +51,7 @@ func Test(t *testing.T) {
if ok := gitHandler.ServeGitMaybe(w, req); ok {
return
}
t.Error("http server got a non-git request")
t.Error("HTTP server got a non-git request")
http.NotFound(w, req)
}))
defer httpServer.Close()
Oops, something went wrong.

0 comments on commit dd5eb37

Please sign in to comment.
You can’t perform that action at this time.