1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 package vcweb
31
32 import (
33 "bufio"
34 "cmd/internal/script"
35 "context"
36 "crypto/sha256"
37 "errors"
38 "fmt"
39 "io"
40 "io/fs"
41 "log"
42 "net/http"
43 "os"
44 "os/exec"
45 "path"
46 "path/filepath"
47 "runtime/debug"
48 "strings"
49 "sync"
50 "text/tabwriter"
51 "time"
52 )
53
54
55 type Server struct {
56 env []string
57 logger *log.Logger
58
59 scriptDir string
60 workDir string
61 homeDir string
62 engine *script.Engine
63
64 scriptCache sync.Map
65
66 vcsHandlers map[string]vcsHandler
67 }
68
69
70 type vcsHandler interface {
71 Available() bool
72 Handler(dir string, env []string, logger *log.Logger) (http.Handler, error)
73 }
74
75
76 type scriptResult struct {
77 mu sync.RWMutex
78
79 hash [sha256.Size]byte
80 hashTime time.Time
81
82 handler http.Handler
83 err error
84 }
85
86
87
88
89
90
91
92 func NewServer(scriptDir, workDir string, logger *log.Logger) (*Server, error) {
93 if scriptDir == "" {
94 panic("vcweb.NewServer: scriptDir is required")
95 }
96 var err error
97 scriptDir, err = filepath.Abs(scriptDir)
98 if err != nil {
99 return nil, err
100 }
101
102 if workDir == "" {
103 workDir, err = os.MkdirTemp("", "vcweb-*")
104 if err != nil {
105 return nil, err
106 }
107 logger.Printf("vcweb work directory: %s", workDir)
108 } else {
109 workDir, err = filepath.Abs(workDir)
110 if err != nil {
111 return nil, err
112 }
113 }
114
115 homeDir := filepath.Join(workDir, "home")
116 if err := os.MkdirAll(homeDir, 0755); err != nil {
117 return nil, err
118 }
119
120 env := scriptEnviron(homeDir)
121
122 s := &Server{
123 env: env,
124 logger: logger,
125 scriptDir: scriptDir,
126 workDir: workDir,
127 homeDir: homeDir,
128 engine: newScriptEngine(),
129 vcsHandlers: map[string]vcsHandler{
130 "auth": new(authHandler),
131 "dir": new(dirHandler),
132 "bzr": new(bzrHandler),
133 "fossil": new(fossilHandler),
134 "git": new(gitHandler),
135 "hg": new(hgHandler),
136 "insecure": new(insecureHandler),
137 "svn": &svnHandler{svnRoot: workDir, logger: logger},
138 },
139 }
140
141 if err := os.WriteFile(filepath.Join(s.homeDir, ".gitconfig"), []byte(gitConfig), 0644); err != nil {
142 return nil, err
143 }
144 gitConfigDir := filepath.Join(s.homeDir, ".config", "git")
145 if err := os.MkdirAll(gitConfigDir, 0755); err != nil {
146 return nil, err
147 }
148 if err := os.WriteFile(filepath.Join(gitConfigDir, "ignore"), []byte(""), 0644); err != nil {
149 return nil, err
150 }
151
152 if err := os.WriteFile(filepath.Join(s.homeDir, ".hgrc"), []byte(hgrc), 0644); err != nil {
153 return nil, err
154 }
155
156 return s, nil
157 }
158
159 func (s *Server) Close() error {
160 var firstErr error
161 for _, h := range s.vcsHandlers {
162 if c, ok := h.(io.Closer); ok {
163 if closeErr := c.Close(); firstErr == nil {
164 firstErr = closeErr
165 }
166 }
167 }
168 return firstErr
169 }
170
171
172
173 var gitConfig = `
174 [user]
175 name = Go Gopher
176 email = gopher@golang.org
177 [init]
178 defaultBranch = main
179 [core]
180 eol = lf
181 [gui]
182 encoding = utf-8
183 `[1:]
184
185
186
187 var hgrc = `
188 [ui]
189 username=Go Gopher <gopher@golang.org>
190 [phases]
191 new-commit=public
192 [extensions]
193 convert=
194 `[1:]
195
196
197 func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
198 s.logger.Printf("serving %s", req.URL)
199
200 defer func() {
201 if v := recover(); v != nil {
202 if v == http.ErrAbortHandler {
203 panic(v)
204 }
205 s.logger.Fatalf("panic serving %s: %v\n%s", req.URL, v, debug.Stack())
206 }
207 }()
208
209 urlPath := req.URL.Path
210 if !strings.HasPrefix(urlPath, "/") {
211 urlPath = "/" + urlPath
212 }
213 clean := path.Clean(urlPath)[1:]
214 if clean == "" {
215 s.overview(w, req)
216 return
217 }
218 if clean == "help" {
219 s.help(w, req)
220 return
221 }
222
223
224
225
226
227
228 scriptPath := "."
229 for part := range strings.SplitSeq(clean, "/") {
230 scriptPath = filepath.Join(scriptPath, part)
231 dir := filepath.Join(s.scriptDir, scriptPath)
232 if _, err := os.Stat(dir); err != nil {
233 if !os.IsNotExist(err) {
234 http.Error(w, err.Error(), http.StatusInternalServerError)
235 return
236 }
237
238
239 break
240 }
241 }
242 scriptPath += ".txt"
243
244 err := s.HandleScript(scriptPath, s.logger, func(handler http.Handler) {
245 handler.ServeHTTP(w, req)
246 })
247 if err != nil {
248 s.logger.Print(err)
249 if _, ok := errors.AsType[ScriptNotFoundError](err); ok {
250 http.NotFound(w, req)
251 } else if _, ok := errors.AsType[ServerNotInstalledError](err); ok || errors.Is(err, exec.ErrNotFound) {
252 http.Error(w, err.Error(), http.StatusNotImplemented)
253 } else {
254 http.Error(w, err.Error(), http.StatusInternalServerError)
255 }
256 }
257 }
258
259
260
261 type ScriptNotFoundError struct{ err error }
262
263 func (e ScriptNotFoundError) Error() string { return e.err.Error() }
264 func (e ScriptNotFoundError) Unwrap() error { return e.err }
265
266
267
268 type ServerNotInstalledError struct{ name string }
269
270 func (v ServerNotInstalledError) Error() string {
271 return fmt.Sprintf("server for %#q VCS is not installed", v.name)
272 }
273
274
275
276
277
278
279
280
281
282 func (s *Server) HandleScript(scriptRelPath string, logger *log.Logger, f func(http.Handler)) error {
283 ri, ok := s.scriptCache.Load(scriptRelPath)
284 if !ok {
285 ri, _ = s.scriptCache.LoadOrStore(scriptRelPath, new(scriptResult))
286 }
287 r := ri.(*scriptResult)
288
289 relDir := strings.TrimSuffix(scriptRelPath, filepath.Ext(scriptRelPath))
290 workDir := filepath.Join(s.workDir, relDir)
291 prefix := path.Join("/", filepath.ToSlash(relDir))
292
293 r.mu.RLock()
294 defer r.mu.RUnlock()
295 for {
296
297
298
299
300
301
302 content, err := os.ReadFile(filepath.Join(s.scriptDir, scriptRelPath))
303 if err != nil {
304 if !os.IsNotExist(err) {
305 return err
306 }
307 return ScriptNotFoundError{err}
308 }
309
310 hash := sha256.Sum256(content)
311 if prevHash := r.hash; prevHash != hash {
312
313 func() {
314 r.mu.RUnlock()
315 r.mu.Lock()
316 defer func() {
317 r.mu.Unlock()
318 r.mu.RLock()
319 }()
320 if r.hash != prevHash {
321
322
323
324 return
325 }
326
327 r.hash = hash
328 r.hashTime = time.Now()
329 r.handler, r.err = nil, nil
330
331 if err := os.RemoveAll(workDir); err != nil {
332 r.err = err
333 return
334 }
335
336
337
338
339 scriptHandler, err := s.loadScript(context.Background(), logger, scriptRelPath, content, workDir)
340 if err != nil {
341 r.err = err
342 return
343 }
344 r.handler = http.StripPrefix(prefix, scriptHandler)
345 }()
346 }
347
348 if r.hash != hash {
349 continue
350 }
351
352 if r.err != nil {
353 return r.err
354 }
355 f(r.handler)
356 return nil
357 }
358 }
359
360
361
362 func (s *Server) overview(w http.ResponseWriter, r *http.Request) {
363 fmt.Fprintf(w, "<html>\n")
364 fmt.Fprintf(w, "<title>vcweb</title>\n<pre>\n")
365 fmt.Fprintf(w, "<b>vcweb</b>\n\n")
366 fmt.Fprintf(w, "This server serves various version control repos for testing the go command.\n\n")
367 fmt.Fprintf(w, "For an overview of the script language, see <a href=\"/help\">/help</a>.\n\n")
368
369 fmt.Fprintf(w, "<b>cache</b>\n")
370
371 tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
372 err := filepath.WalkDir(s.scriptDir, func(path string, d fs.DirEntry, err error) error {
373 if err != nil {
374 return err
375 }
376 if filepath.Ext(path) != ".txt" {
377 return nil
378 }
379
380 rel, err := filepath.Rel(s.scriptDir, path)
381 if err != nil {
382 return err
383 }
384 hashTime := "(not loaded)"
385 status := ""
386 if ri, ok := s.scriptCache.Load(rel); ok {
387 r := ri.(*scriptResult)
388 r.mu.RLock()
389 defer r.mu.RUnlock()
390
391 if !r.hashTime.IsZero() {
392 hashTime = r.hashTime.Format(time.RFC3339)
393 }
394 if r.err == nil {
395 status = "ok"
396 } else {
397 status = r.err.Error()
398 }
399 }
400 fmt.Fprintf(tw, "%s\t%s\t%s\n", rel, hashTime, status)
401 return nil
402 })
403 tw.Flush()
404
405 if err != nil {
406 fmt.Fprintln(w, err)
407 }
408 }
409
410
411 func (s *Server) help(w http.ResponseWriter, req *http.Request) {
412 st, err := s.newState(req.Context(), s.workDir)
413 if err != nil {
414 http.Error(w, err.Error(), http.StatusInternalServerError)
415 return
416 }
417
418 scriptLog := new(strings.Builder)
419 err = s.engine.Execute(st, "help", bufio.NewReader(strings.NewReader("help")), scriptLog)
420 if err != nil {
421 http.Error(w, err.Error(), http.StatusInternalServerError)
422 return
423 }
424
425 w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
426 io.WriteString(w, scriptLog.String())
427 }
428
View as plain text