Source file src/cmd/vendor/golang.org/x/mod/sumdb/server.go

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package sumdb implements the HTTP protocols for serving or accessing a module checksum database.
     6  package sumdb
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"net/http"
    12  	"os"
    13  	"strings"
    14  
    15  	"golang.org/x/mod/internal/lazyregexp"
    16  	"golang.org/x/mod/module"
    17  	"golang.org/x/mod/sumdb/tlog"
    18  )
    19  
    20  // A ServerOps provides the external operations
    21  // (underlying database access and so on) needed by the [Server].
    22  type ServerOps interface {
    23  	// Signed returns the signed hash of the latest tree.
    24  	Signed(ctx context.Context) ([]byte, error)
    25  
    26  	// ReadRecords returns the content for the n records id through id+n-1.
    27  	ReadRecords(ctx context.Context, id, n int64) ([][]byte, error)
    28  
    29  	// Lookup looks up a record for the given module,
    30  	// returning the record ID.
    31  	Lookup(ctx context.Context, m module.Version) (int64, error)
    32  
    33  	// ReadTileData reads the content of tile t.
    34  	// It is only invoked for hash tiles (t.L ≥ 0).
    35  	ReadTileData(ctx context.Context, t tlog.Tile) ([]byte, error)
    36  }
    37  
    38  // A Server is the checksum database HTTP server,
    39  // which implements http.Handler and should be invoked
    40  // to serve the paths listed in [ServerPaths].
    41  type Server struct {
    42  	ops ServerOps
    43  }
    44  
    45  // NewServer returns a new Server using the given operations.
    46  func NewServer(ops ServerOps) *Server {
    47  	return &Server{ops: ops}
    48  }
    49  
    50  // ServerPaths are the URL paths the Server can (and should) serve.
    51  //
    52  // Typically a server will do:
    53  //
    54  //	srv := sumdb.NewServer(ops)
    55  //	for _, path := range sumdb.ServerPaths {
    56  //		http.Handle(path, srv)
    57  //	}
    58  var ServerPaths = []string{
    59  	"/lookup/",
    60  	"/latest",
    61  	"/tile/",
    62  }
    63  
    64  var modVerRE = lazyregexp.New(`^[^@]+@v[0-9]+\.[0-9]+\.[0-9]+(-[^@]*)?(\+incompatible)?$`)
    65  
    66  func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    67  	ctx := r.Context()
    68  
    69  	switch {
    70  	default:
    71  		http.NotFound(w, r)
    72  
    73  	case strings.HasPrefix(r.URL.Path, "/lookup/"):
    74  		mod := strings.TrimPrefix(r.URL.Path, "/lookup/")
    75  		if !modVerRE.MatchString(mod) {
    76  			http.Error(w, "invalid module@version syntax", http.StatusBadRequest)
    77  			return
    78  		}
    79  		i := strings.Index(mod, "@")
    80  		escPath, escVers := mod[:i], mod[i+1:]
    81  		path, err := module.UnescapePath(escPath)
    82  		if err != nil {
    83  			reportError(w, err)
    84  			return
    85  		}
    86  		vers, err := module.UnescapeVersion(escVers)
    87  		if err != nil {
    88  			reportError(w, err)
    89  			return
    90  		}
    91  		id, err := s.ops.Lookup(ctx, module.Version{Path: path, Version: vers})
    92  		if err != nil {
    93  			reportError(w, err)
    94  			return
    95  		}
    96  		records, err := s.ops.ReadRecords(ctx, id, 1)
    97  		if err != nil {
    98  			// This should never happen - the lookup says the record exists.
    99  			http.Error(w, err.Error(), http.StatusInternalServerError)
   100  			return
   101  		}
   102  		if len(records) != 1 {
   103  			http.Error(w, "invalid record count returned by ReadRecords", http.StatusInternalServerError)
   104  			return
   105  		}
   106  		msg, err := tlog.FormatRecord(id, records[0])
   107  		if err != nil {
   108  			http.Error(w, err.Error(), http.StatusInternalServerError)
   109  			return
   110  		}
   111  		signed, err := s.ops.Signed(ctx)
   112  		if err != nil {
   113  			http.Error(w, err.Error(), http.StatusInternalServerError)
   114  			return
   115  		}
   116  		w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
   117  		w.Write(msg)
   118  		w.Write(signed)
   119  
   120  	case r.URL.Path == "/latest":
   121  		data, err := s.ops.Signed(ctx)
   122  		if err != nil {
   123  			http.Error(w, err.Error(), http.StatusInternalServerError)
   124  			return
   125  		}
   126  		w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
   127  		w.Write(data)
   128  
   129  	case strings.HasPrefix(r.URL.Path, "/tile/"):
   130  		t, err := tlog.ParseTilePath(r.URL.Path[1:])
   131  		if err != nil {
   132  			http.Error(w, "invalid tile syntax", http.StatusBadRequest)
   133  			return
   134  		}
   135  		if t.L == -1 {
   136  			// Record data.
   137  			start := t.N << uint(t.H)
   138  			records, err := s.ops.ReadRecords(ctx, start, int64(t.W))
   139  			if err != nil {
   140  				reportError(w, err)
   141  				return
   142  			}
   143  			if len(records) != t.W {
   144  				http.Error(w, "invalid record count returned by ReadRecords", http.StatusInternalServerError)
   145  				return
   146  			}
   147  			var data []byte
   148  			for i, text := range records {
   149  				msg, err := tlog.FormatRecord(start+int64(i), text)
   150  				if err != nil {
   151  					http.Error(w, err.Error(), http.StatusInternalServerError)
   152  					return
   153  				}
   154  				// Data tiles contain formatted records without the first line with record ID.
   155  				_, msg, _ = bytes.Cut(msg, []byte{'\n'})
   156  				data = append(data, msg...)
   157  			}
   158  			w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
   159  			w.Write(data)
   160  			return
   161  		}
   162  
   163  		data, err := s.ops.ReadTileData(ctx, t)
   164  		if err != nil {
   165  			reportError(w, err)
   166  			return
   167  		}
   168  		w.Header().Set("Content-Type", "application/octet-stream")
   169  		w.Write(data)
   170  	}
   171  }
   172  
   173  // reportError reports err to w.
   174  // If it's a not-found, the reported error is 404.
   175  // Otherwise it is an internal server error.
   176  // The caller must only call reportError in contexts where
   177  // a not-found err should be reported as 404.
   178  func reportError(w http.ResponseWriter, err error) {
   179  	if os.IsNotExist(err) {
   180  		http.Error(w, err.Error(), http.StatusNotFound)
   181  		return
   182  	}
   183  	http.Error(w, err.Error(), http.StatusInternalServerError)
   184  }
   185  

View as plain text