Skip to content

Commit

Permalink
internal/code: implement module proxy protocol
Browse files Browse the repository at this point in the history
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 7, 2019
1 parent 1c38c56 commit df9d62c
Show file tree
Hide file tree
Showing 8 changed files with 729 additions and 9 deletions.
3 changes: 2 additions & 1 deletion code.go
Expand Up @@ -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" { if req.Method == http.MethodGet && req.URL.Query().Get("go-get") == "1" {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<meta name="go-import" content="%[1]s git https://%[1]s"> 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 return true
} }


Expand Down
81 changes: 75 additions & 6 deletions httputil/handler.go
Expand Up @@ -16,7 +16,18 @@ import (
) )


// ErrorHandler factors error handling out of the HTTP handler. // 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} return &errorHandler{handler: handler, users: users}
} }


Expand All @@ -30,11 +41,57 @@ type errorHandler struct {
func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
rw := &headerResponseWriter{ResponseWriter: w} rw := &headerResponseWriter{ResponseWriter: w}
err := h.handler(rw, req) 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 { if err == nil {
// Do nothing. // Do nothing.
return return
} }
if err != nil && rw.WroteHeader { if err != nil && wroteHeader {
// The header has already been written, so it's too late to send // The header has already been written, so it's too late to send
// a different status code. Just log the error and move on. // a different status code. Just log the error and move on.
log.Println(err) log.Println(err)
Expand All @@ -55,7 +112,7 @@ func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if err, ok := httperror.IsHTTP(err); ok { if err, ok := httperror.IsHTTP(err); ok {
code := err.Code code := err.Code
error := fmt.Sprintf("%d %s", code, http.StatusText(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() error += "\n\n" + err.Error()
} }
http.Error(w, error, code) http.Error(w, error, code)
Expand All @@ -74,7 +131,7 @@ func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Println(err) log.Println(err)
error := "404 Not Found" 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() error += "\n\n" + err.Error()
} }
http.Error(w, error, http.StatusNotFound) http.Error(w, error, http.StatusNotFound)
Expand All @@ -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.) // TODO: Factor in a GetAuthenticatedSpec.ID == 0 check out here. (But this shouldn't apply for APIs.)
log.Println(err) log.Println(err)
error := "403 Forbidden" 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() error += "\n\n" + err.Error()
} }
http.Error(w, error, http.StatusForbidden) http.Error(w, error, http.StatusForbidden)
Expand All @@ -93,7 +150,7 @@ func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {


log.Println(err) log.Println(err)
error := "500 Internal Server Error" 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() error += "\n\n" + err.Error()
} }
http.Error(w, error, http.StatusInternalServerError) http.Error(w, error, http.StatusInternalServerError)
Expand Down Expand Up @@ -210,3 +267,15 @@ func bodyAllowedForStatus(status int) bool {
return true 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
}
5 changes: 3 additions & 2 deletions internal/code/code_test.go
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/shurcooL/users" "github.com/shurcooL/users"
) )


func Test(t *testing.T) { func TestCode(t *testing.T) {
tempDir, err := ioutil.TempDir("", "code_test_") tempDir, err := ioutil.TempDir("", "code_test_")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
Expand All @@ -42,6 +42,7 @@ func Test(t *testing.T) {
t.Fatal("code.NewService:", err) 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 }) gitHandler, err := code.NewGitHandler(service, filepath.Join(tempDir, "repositories"), events, users, nil, func(req *http.Request) *http.Request { return req })
if err != nil { if err != nil {
t.Fatal("code.NewGitHandler:", err) t.Fatal("code.NewGitHandler:", err)
Expand All @@ -50,7 +51,7 @@ func Test(t *testing.T) {
if ok := gitHandler.ServeGitMaybe(w, req); ok { if ok := gitHandler.ServeGitMaybe(w, req); ok {
return return
} }
t.Error("http server got a non-git request") t.Error("HTTP server got a non-git request")
http.NotFound(w, req) http.NotFound(w, req)
})) }))
defer httpServer.Close() defer httpServer.Close()
Expand Down

0 comments on commit df9d62c

Please sign in to comment.