| // 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 sumweb implements the HTTP protocols for serving or accessing a go.sum database. |
| package sumweb |
| |
| import ( |
| "context" |
| "net/http" |
| "os" |
| "regexp" |
| "strings" |
| |
| "golang.org/x/exp/sumdb/internal/tlog" |
| ) |
| |
| // A Server provides the external operations |
| // (underlying database access and so on) |
| // needed to implement the HTTP server Handler. |
| type Server interface { |
| // NewContext returns the context to use for the request r. |
| NewContext(r *http.Request) (context.Context, error) |
| |
| // Signed returns the signed hash of the latest tree. |
| Signed(ctx context.Context) ([]byte, error) |
| |
| // ReadRecords returns the content for the n records id through id+n-1. |
| ReadRecords(ctx context.Context, id, n int64) ([][]byte, error) |
| |
| // Lookup looks up a record by its associated key ("module@version"), |
| // returning the record ID. |
| Lookup(ctx context.Context, key string) (int64, error) |
| |
| // ReadTileData reads the content of tile t. |
| // It is only invoked for hash tiles (t.L ≥ 0). |
| ReadTileData(ctx context.Context, t tlog.Tile) ([]byte, error) |
| } |
| |
| // A Handler is the go.sum database server handler, |
| // which should be invoked to serve the paths listed in Paths. |
| // The calling code is responsible for initializing Server. |
| type Handler struct { |
| Server Server |
| } |
| |
| // Paths are the URL paths for which Handler should be invoked. |
| // |
| // Typically a server will do: |
| // |
| // handler := &sumweb.Handler{Server: srv} |
| // for _, path := range sumweb.Paths { |
| // http.HandleFunc(path, handler) |
| // } |
| var Paths = []string{ |
| "/lookup/", |
| "/latest", |
| "/tile/", |
| } |
| |
| var modVerRE = regexp.MustCompile(`^[^@]+@v[0-9]+\.[0-9]+\.[0-9]+(-[^@]*)?(\+incompatible)?$`) |
| |
| func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| ctx, err := h.Server.NewContext(r) |
| if err != nil { |
| http.Error(w, err.Error(), 500) |
| return |
| } |
| |
| switch { |
| default: |
| http.NotFound(w, r) |
| |
| case strings.HasPrefix(r.URL.Path, "/lookup/"): |
| mod := strings.TrimPrefix(r.URL.Path, "/lookup/") |
| if !modVerRE.MatchString(mod) { |
| http.Error(w, "invalid module@version syntax", http.StatusBadRequest) |
| return |
| } |
| i := strings.Index(mod, "@") |
| encPath, encVers := mod[:i], mod[i+1:] |
| path, err := decodePath(encPath) |
| if err != nil { |
| reportError(w, r, err) |
| return |
| } |
| vers, err := decodeVersion(encVers) |
| if err != nil { |
| reportError(w, r, err) |
| return |
| } |
| id, err := h.Server.Lookup(ctx, path+"@"+vers) |
| if err != nil { |
| reportError(w, r, err) |
| return |
| } |
| records, err := h.Server.ReadRecords(ctx, id, 1) |
| if err != nil { |
| // This should never happen - the lookup says the record exists. |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| if len(records) != 1 { |
| http.Error(w, "invalid record count returned by ReadRecords", http.StatusInternalServerError) |
| return |
| } |
| msg, err := tlog.FormatRecord(id, records[0]) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| signed, err := h.Server.Signed(ctx) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| w.Header().Set("Content-Type", "text/plain; charset=UTF-8") |
| w.Write(msg) |
| w.Write(signed) |
| |
| case r.URL.Path == "/latest": |
| data, err := h.Server.Signed(ctx) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| w.Header().Set("Content-Type", "text/plain; charset=UTF-8") |
| w.Write(data) |
| |
| case strings.HasPrefix(r.URL.Path, "/tile/"): |
| t, err := tlog.ParseTilePath(r.URL.Path[1:]) |
| if err != nil { |
| http.Error(w, "invalid tile syntax", http.StatusBadRequest) |
| return |
| } |
| if t.L == -1 { |
| // Record data. |
| start := t.N << uint(t.H) |
| records, err := h.Server.ReadRecords(ctx, start, int64(t.W)) |
| if err != nil { |
| reportError(w, r, err) |
| return |
| } |
| if len(records) != t.W { |
| http.Error(w, "invalid record count returned by ReadRecords", http.StatusInternalServerError) |
| return |
| } |
| var data []byte |
| for i, text := range records { |
| msg, err := tlog.FormatRecord(start+int64(i), text) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| } |
| data = append(data, msg...) |
| } |
| w.Header().Set("Content-Type", "text/plain; charset=UTF-8") |
| w.Write(data) |
| return |
| } |
| |
| data, err := h.Server.ReadTileData(ctx, t) |
| if err != nil { |
| reportError(w, r, err) |
| return |
| } |
| w.Header().Set("Content-Type", "application/octet-stream") |
| w.Write(data) |
| } |
| } |
| |
| // reportError reports err to w. |
| // If it's a not-found, the reported error is 404. |
| // Otherwise it is an internal server error. |
| // The caller must only call reportError in contexts where |
| // a not-found err should be reported as 404. |
| func reportError(w http.ResponseWriter, r *http.Request, err error) { |
| if os.IsNotExist(err) { |
| http.Error(w, err.Error(), http.StatusNotFound) |
| return |
| } |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| } |