1
2
3
4
5
6 package sumdb
7
8 import (
9 "context"
10 "net/http"
11 "os"
12 "strings"
13
14 "golang.org/x/mod/internal/lazyregexp"
15 "golang.org/x/mod/module"
16 "golang.org/x/mod/sumdb/tlog"
17 )
18
19
20
21 type ServerOps interface {
22
23 Signed(ctx context.Context) ([]byte, error)
24
25
26 ReadRecords(ctx context.Context, id, n int64) ([][]byte, error)
27
28
29
30 Lookup(ctx context.Context, m module.Version) (int64, error)
31
32
33
34 ReadTileData(ctx context.Context, t tlog.Tile) ([]byte, error)
35 }
36
37
38
39
40 type Server struct {
41 ops ServerOps
42 }
43
44
45 func NewServer(ops ServerOps) *Server {
46 return &Server{ops: ops}
47 }
48
49
50
51
52
53
54
55
56
57 var ServerPaths = []string{
58 "/lookup/",
59 "/latest",
60 "/tile/",
61 }
62
63 var modVerRE = lazyregexp.New(`^[^@]+@v[0-9]+\.[0-9]+\.[0-9]+(-[^@]*)?(\+incompatible)?$`)
64
65 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
66 ctx := r.Context()
67
68 switch {
69 default:
70 http.NotFound(w, r)
71
72 case strings.HasPrefix(r.URL.Path, "/lookup/"):
73 mod := strings.TrimPrefix(r.URL.Path, "/lookup/")
74 if !modVerRE.MatchString(mod) {
75 http.Error(w, "invalid module@version syntax", http.StatusBadRequest)
76 return
77 }
78 i := strings.Index(mod, "@")
79 escPath, escVers := mod[:i], mod[i+1:]
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 data = append(data, msg...)
154 }
155 w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
156 w.Write(data)
157 return
158 }
159
160 data, err := s.ops.ReadTileData(ctx, t)
161 if err != nil {
162 reportError(w, err)
163 return
164 }
165 w.Header().Set("Content-Type", "application/octet-stream")
166 w.Write(data)
167 }
168 }
169
170
171
172
173
174
175 func reportError(w http.ResponseWriter, err error) {
176 if os.IsNotExist(err) {
177 http.Error(w, err.Error(), http.StatusNotFound)
178 return
179 }
180 http.Error(w, err.Error(), http.StatusInternalServerError)
181 }
182
View as plain text