1
2
3
4
5
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
21
22 type ServerOps interface {
23
24 Signed(ctx context.Context) ([]byte, error)
25
26
27 ReadRecords(ctx context.Context, id, n int64) ([][]byte, error)
28
29
30
31 Lookup(ctx context.Context, m module.Version) (int64, error)
32
33
34
35 ReadTileData(ctx context.Context, t tlog.Tile) ([]byte, error)
36 }
37
38
39
40
41 type Server struct {
42 ops ServerOps
43 }
44
45
46 func NewServer(ops ServerOps) *Server {
47 return &Server{ops: ops}
48 }
49
50
51
52
53
54
55
56
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
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
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
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
173
174
175
176
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