notary/internal/noteweb: basic notary web frontend
This is part of a design sketch for a Go module notary.
Eventually the code will live outside golang.org/x/exp.
Everything here is subject to change! Don't depend on it!
This package defines the basic web handlers for
the notary URL endpoints.
Change-Id: I66b8acf99ae08a3959cb8e0192651c01920d1ae4
Reviewed-on: https://go-review.googlesource.com/c/exp/+/162898
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
diff --git a/notary/internal/noteweb/web.go b/notary/internal/noteweb/web.go
new file mode 100644
index 0000000..ada39d6
--- /dev/null
+++ b/notary/internal/noteweb/web.go
@@ -0,0 +1,169 @@
+// 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 noteweb serves the notary web endpoints from a notary database.
+package noteweb
+
+import (
+ "context"
+ "net/http"
+ "os"
+ "regexp"
+ "strings"
+
+ "golang.org/x/exp/notary/internal/tlog"
+)
+
+// Server is a connection to a notary server.
+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)
+
+ // FindKey looks up a record by its associated key ("module@version"),
+ // returning the record ID.
+ FindKey(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)
+}
+
+// Handler is the notary endpoint handler,
+// which should be used for the paths listed in Paths.
+// The client is responsible for initializing Server.
+type Handler struct {
+ Server Server
+}
+
+// Paths are the URL paths for which Handler should be invoked.
+//
+// Typically a client will do:
+//
+// handler := ¬eweb.Handler{Server: srv}
+// for _, path := range noteweb.Paths {
+// http.HandleFunc(path, handler)
+// }
+//
+var Paths = []string{
+ "/lookup/",
+ "/latest",
+ "/tile/",
+}
+
+var modVerRE = regexp.MustCompile(`^[^@]+@v[0-9]+\.[0-9]+\.[0-9]+(-[^@]*)?$`)
+
+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
+ }
+ id, err := h.Server.FindKey(ctx, mod)
+ 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)
+}