goproxy/proxy/server.go

270 lines
8.4 KiB
Go

// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package proxy implements the HTTP protocols for serving a Go module proxy.
package proxy
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/goproxyio/goproxy/v2/sumdb"
"golang.org/x/mod/module"
)
// A ServerOps provides the external operations
// (accessing module information and so on) needed by the Server.
type ServerOps interface {
// NewContext returns the context to use for the request r.
NewContext(r *http.Request) (context.Context, error)
// List, Latest, Info, GoMod, and Zip all return a File to be sent to a client.
// The File will be closed after its contents are sent.
// In the case of an error, if the error satisfies errors.Is(err, os.ErrNotFound),
// the server responds with an HTTP 404 error;
// otherwise it responds with an HTTP 500 error.
// List returns a list of tagged versions of the module identified by path.
// The versions should all be canonical semantic versions
// and formatted in a text listing, one per line.
// Pseudo-versions derived from untagged commits should be omitted.
// The go command exposes this list in 'go list -m -versions' output
// and also uses it to resolve wildcards like 'go get m@v1.2'.
List(ctx context.Context, path string) (File, error)
// Latest returns an info file for the latest known version of the module identified by path.
// The go command uses this for 'go get m' or 'go get m@latest'
// but only after finding no suitable version among the ones returned by List.
// Typically, Latest should return a pseudo-version for the latest known commit.
Latest(ctx context.Context, path string) (File, error)
// Info opens and returns the module version's info file.
// The requested version can be a canonical semantic version
// but can also be an arbitrary version reference, like "master".
//
// The metadata in the returned file should be a JSON object corresponding
// to the Go type
//
// type Info struct {
// Version string
// Time time.Time
// }
//
// where the version is the resolved canonical semantic version
// and the time is the commit or publication time of that version
// (for use with go list -m).
// The NewInfo function can be used to construct an info File.
//
// Proxies should obtain the module version information by
// executing 'go mod download -json' and caching the file
// listed in the Info field.
Info(ctx context.Context, m module.Version) (File, error)
// GoMod opens and returns the module's go.mod file.
// The requested version is a canonical semantic version.
//
// Proxies should obtain the module version information by
// executing 'go mod download -json' and caching the file
// listed in the GoMod field.
GoMod(ctx context.Context, m module.Version) (File, error)
// Zip opens and returns the module's zip file.
// The requested version is a canonical semantic version.
//
// Proxies should obtain the module version information by
// executing 'go mod download -json' and caching the file
// listed in the Zip field.
Zip(ctx context.Context, m module.Version) (File, error)
}
// A File is a file to be served, typically an *os.File or the result of calling MemFile or NewInfo.
// The modification time is the only necessary field in the Stat result.
type File interface {
io.Reader
io.Seeker
io.Closer
Stat() (os.FileInfo, error)
}
// A Server is the proxy HTTP server,
// which implements http.Handler and should be invoked
// to serve the paths listed in ServerPaths.
//
// The server assumes that the requests are made to the root of the URL space,
// so it should typically be registered using:
//
// srv := proxy.NewServer(ops)
// http.Handle("/", srv)
//
// To register a server at a subdirectory of the URL space, wrap the server in http.StripPrefix:
//
// srv := proxy.NewServer(ops)
// http.Handle("/proxy/", http.StripPrefix("/proxy", srv))
//
// All recognized requests to the server contain the substring "/@v/" in the URL.
// The server will respond with an http.StatusBadRequest (400) error to unrecognized requests.
type Server struct {
ops ServerOps
}
// NewServer returns a new Server using the given operations.
func NewServer(ops ServerOps) *Server {
return &Server{ops: ops}
}
// ServeHTTP is the server's implementation of http.Handler.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, err := s.ops.NewContext(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// sumdb handler
if strings.HasPrefix(r.URL.Path, "/sumdb/") {
sumdb.Handler(w, r)
return
}
i := strings.Index(r.URL.Path, "/@")
if i < 0 {
http.Error(w, "no such path", http.StatusNotFound)
return
}
modPath, err := module.UnescapePath(strings.TrimPrefix(r.URL.Path[:i], "/"))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
what := r.URL.Path[i+len("/@"):]
const (
contentTypeJSON = "application/json"
contentTypeText = "text/plain; charset=UTF-8"
contentTypeBinary = "application/octet-stream"
)
var ctype string
var f File
var openErr error
switch what {
case "latest":
ctype = contentTypeJSON
f, openErr = s.ops.Latest(ctx, modPath)
case "v/list":
ctype = contentTypeText
f, openErr = s.ops.List(ctx, modPath)
default:
what = strings.TrimPrefix(what, "v/")
ext := path.Ext(what)
vers, err := module.UnescapeVersion(strings.TrimSuffix(what, ext))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
m := module.Version{Path: modPath, Version: vers}
if vers == "latest" {
// The go command handles "go get m@latest" by fetching /m/@v/latest, not latest.info.
// We should never see requests for "latest.info" and so on, so avoid confusion
// by disallowing it early.
http.Error(w, "version latest is disallowed", http.StatusNotFound)
return
}
// All requests require canonical versions except for info,
// which accepts any revision identifier known to the underlying storage.
if ext != ".info" && vers != module.CanonicalVersion(vers) {
http.Error(w, "version "+vers+" is not in canonical form", http.StatusNotFound)
return
}
switch ext {
case ".info":
ctype = "application/json"
f, openErr = s.ops.Info(ctx, m)
case ".mod":
ctype = "text/plain; charset=UTF-8"
f, openErr = s.ops.GoMod(ctx, m)
case ".zip":
ctype = "application/octet-stream"
f, openErr = s.ops.Zip(ctx, m)
default:
http.Error(w, "request not recognized", http.StatusNotFound)
return
}
}
if openErr != nil {
code := http.StatusNotFound
http.Error(w, openErr.Error(), code)
return
}
defer f.Close()
info, err := f.Stat()
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if info.IsDir() {
http.Error(w, "unexpected directory", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", ctype)
http.ServeContent(w, r, what, info.ModTime(), f)
}
// MemFile returns an File containing the given in-memory content and modification time.
func MemFile(data []byte, t time.Time) File {
return &memFile{bytes.NewReader(data), memStat{t, int64(len(data))}}
}
type memFile struct {
*bytes.Reader
stat memStat
}
// Close closes file.
func (f *memFile) Close() error { return nil }
// Stat stats file.
func (f *memFile) Stat() (os.FileInfo, error) { return &f.stat, nil }
// Readdir read dir.
func (f *memFile) Readdir(count int) ([]os.FileInfo, error) { return nil, os.ErrInvalid }
type memStat struct {
t time.Time
size int64
}
// Name returns file name.
func (s *memStat) Name() string { return "memfile" }
// Size returns file size.
func (s *memStat) Size() int64 { return s.size }
// Mode returns file mode.
func (s *memStat) Mode() os.FileMode { return 0444 }
// ModTime returns file modtime.
func (s *memStat) ModTime() time.Time { return s.t }
// IsDir returns if file is a dir.
func (s *memStat) IsDir() bool { return false }
// Sys return nil.
func (s *memStat) Sys() interface{} { return nil }
// NewInfo returns a formatted info file for the given version, time pair.
// The version should be a canonical semantic version.
func NewInfo(version string, t time.Time) File {
var info = struct {
Version string
Time time.Time
}{version, t}
js, err := json.Marshal(info)
if err != nil {
// json.Marshal only fails for bad types; there are no bad types in info.
panic("unexpected json.Marshal failure")
}
return MemFile(js, t)
}