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  		escPath, escVers, _ := strings.Cut(mod, "@")
    80  		path, err := module.UnescapePath(escPath)
    81  		if err != nil {
    82  			reportError(w, err)
    83  			return
    84  		}
    85  		vers, err := module.UnescapeVersion(escVers)
    86  		if err != nil {
    87  			reportError(w, err)
    88  			return
    89  		}
    90  		id, err := s.ops.Lookup(ctx, module.Version{Path: path, Version: vers})
    91  		if err != nil {
    92  			reportError(w, err)
    93  			return
    94  		}
    95  		records, err := s.ops.ReadRecords(ctx, id, 1)
    96  		if err != nil {
    97  			// This should never happen - the lookup says the record exists.
    98  			http.Error(w, err.Error(), http.StatusInternalServerError)
    99  			return
   100  		}
   101  		if len(records) != 1 {
   102  			http.Error(w, "invalid record count returned by ReadRecords", http.StatusInternalServerError)
   103  			return
   104  		}
   105  		msg, err := tlog.FormatRecord(id, records[0])
   106  		if err != nil {
   107  			http.Error(w, err.Error(), http.StatusInternalServerError)
   108  			return
   109  		}
   110  		signed, err := s.ops.Signed(ctx)
   111  		if err != nil {
   112  			http.Error(w, err.Error(), http.StatusInternalServerError)
   113  			return
   114  		}
   115  		w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
   116  		w.Write(msg)
   117  		w.Write(signed)
   118  
   119  	case r.URL.Path == "/latest":
   120  		data, err := s.ops.Signed(ctx)
   121  		if err != nil {
   122  			http.Error(w, err.Error(), http.StatusInternalServerError)
   123  			return
   124  		}
   125  		w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
   126  		w.Write(data)
   127  
   128  	case strings.HasPrefix(r.URL.Path, "/tile/"):
   129  		t, err := tlog.ParseTilePath(r.URL.Path[1:])
   130  		if err != nil {
   131  			http.Error(w, "invalid tile syntax", http.StatusBadRequest)
   132  			return
   133  		}
   134  		if t.L == -1 {
   135  			// Record data.
   136  			start := t.N << uint(t.H)
   137  			records, err := s.ops.ReadRecords(ctx, start, int64(t.W))
   138  			if err != nil {
   139  				reportError(w, err)
   140  				return
   141  			}
   142  			if len(records) != t.W {
   143  				http.Error(w, "invalid record count returned by ReadRecords", http.StatusInternalServerError)
   144  				return
   145  			}
   146  			var data []byte
   147  			for i, text := range records {
   148  				msg, err := tlog.FormatRecord(start+int64(i), text)
   149  				if err != nil {
   150  					http.Error(w, err.Error(), http.StatusInternalServerError)
   151  					return
   152  				}
   153  				// Data tiles contain formatted records without the first line with record ID.
   154  				_, msg, _ = bytes.Cut(msg, []byte{'\n'})
   155  				data = append(data, msg...)
   156  			}
   157  			w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
   158  			w.Write(data)
   159  			return
   160  		}
   161  
   162  		data, err := s.ops.ReadTileData(ctx, t)
   163  		if err != nil {
   164  			reportError(w, err)
   165  			return
   166  		}
   167  		w.Header().Set("Content-Type", "application/octet-stream")
   168  		w.Write(data)
   169  	}
   170  }
   171  
   172  // reportError reports err to w.
   173  // If it's a not-found, the reported error is 404.
   174  // Otherwise it is an internal server error.
   175  // The caller must only call reportError in contexts where
   176  // a not-found err should be reported as 404.
   177  func reportError(w http.ResponseWriter, err error) {
   178  	if os.IsNotExist(err) {
   179  		http.Error(w, err.Error(), http.StatusNotFound)
   180  		return
   181  	}
   182  	http.Error(w, err.Error(), http.StatusInternalServerError)
   183  }
   184  

View as plain text