diff options
Diffstat (limited to 'src/cmd/go/internal/vcweb/svn.go')
-rw-r--r-- | src/cmd/go/internal/vcweb/svn.go | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/src/cmd/go/internal/vcweb/svn.go b/src/cmd/go/internal/vcweb/svn.go new file mode 100644 index 0000000..60222f1 --- /dev/null +++ b/src/cmd/go/internal/vcweb/svn.go @@ -0,0 +1,199 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vcweb + +import ( + "io" + "log" + "net" + "net/http" + "os/exec" + "strings" + "sync" +) + +// An svnHandler serves requests for Subversion repos. +// +// Unlike the other vcweb handlers, svnHandler does not serve the Subversion +// protocol directly over the HTTP connection. Instead, it opens a separate port +// that serves the (non-HTTP) 'svn' protocol. The test binary can retrieve the +// URL for that port by sending an HTTP request with the query parameter +// "vcwebsvn=1". +// +// We take this approach because the 'svn' protocol is implemented by a +// lightweight 'svnserve' binary that is usually packaged along with the 'svn' +// client binary, whereas only known implementation of the Subversion HTTP +// protocol is the mod_dav_svn apache2 module. Apache2 has a lot of dependencies +// and also seems to rely on global configuration via well-known file paths, so +// implementing a hermetic test using apache2 would require the test to run in a +// complicated container environment, which wouldn't be nearly as +// straightforward for Go contributors to set up and test against on their local +// machine. +type svnHandler struct { + svnRoot string // a directory containing all svn repos to be served + logger *log.Logger + + pathOnce sync.Once + svnservePath string // the path to the 'svnserve' executable + svnserveErr error + + listenOnce sync.Once + s chan *svnState // 1-buffered +} + +// An svnState describes the state of a port serving the 'svn://' protocol. +type svnState struct { + listener net.Listener + listenErr error + conns map[net.Conn]struct{} + closing bool + done chan struct{} +} + +func (h *svnHandler) Available() bool { + h.pathOnce.Do(func() { + h.svnservePath, h.svnserveErr = exec.LookPath("svnserve") + }) + return h.svnserveErr == nil +} + +// Handler returns an http.Handler that checks for the "vcwebsvn" query +// parameter and then serves the 'svn://' URL for the repository at the +// requested path. +// The HTTP client is expected to read that URL and pass it to the 'svn' client. +func (h *svnHandler) Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) { + if !h.Available() { + return nil, ServerNotInstalledError{name: "svn"} + } + + // Go ahead and start the listener now, so that if it fails (for example, due + // to port exhaustion) we can return an error from the Handler method instead + // of serving an error for each individual HTTP request. + h.listenOnce.Do(func() { + h.s = make(chan *svnState, 1) + l, err := net.Listen("tcp", "localhost:0") + done := make(chan struct{}) + + h.s <- &svnState{ + listener: l, + listenErr: err, + conns: map[net.Conn]struct{}{}, + done: done, + } + if err != nil { + close(done) + return + } + + h.logger.Printf("serving svn on svn://%v", l.Addr()) + + go func() { + for { + c, err := l.Accept() + + s := <-h.s + if err != nil { + s.listenErr = err + if len(s.conns) == 0 { + close(s.done) + } + h.s <- s + return + } + if s.closing { + c.Close() + } else { + s.conns[c] = struct{}{} + go h.serve(c) + } + h.s <- s + } + }() + }) + + s := <-h.s + addr := "" + if s.listener != nil { + addr = s.listener.Addr().String() + } + err := s.listenErr + h.s <- s + if err != nil { + return nil, err + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.FormValue("vcwebsvn") != "" { + w.Header().Add("Content-Type", "text/plain; charset=UTF-8") + io.WriteString(w, "svn://"+addr+"\n") + return + } + http.NotFound(w, req) + }) + + return handler, nil +} + +// serve serves a single 'svn://' connection on c. +func (h *svnHandler) serve(c net.Conn) { + defer func() { + c.Close() + + s := <-h.s + delete(s.conns, c) + if len(s.conns) == 0 && s.listenErr != nil { + close(s.done) + } + h.s <- s + }() + + // The "--inetd" flag causes svnserve to speak the 'svn' protocol over its + // stdin and stdout streams as if invoked by the Unix "inetd" service. + // We aren't using inetd, but we are implementing essentially the same + // approach: using a host process to listen for connections and spawn + // subprocesses to serve them. + cmd := exec.Command(h.svnservePath, "--read-only", "--root="+h.svnRoot, "--inetd") + cmd.Stdin = c + cmd.Stdout = c + stderr := new(strings.Builder) + cmd.Stderr = stderr + err := cmd.Run() + + var errFrag any = "ok" + if err != nil { + errFrag = err + } + stderrFrag := "" + if stderr.Len() > 0 { + stderrFrag = "\n" + stderr.String() + } + h.logger.Printf("%v: %s%s", cmd, errFrag, stderrFrag) +} + +// Close stops accepting new svn:// connections and terminates the existing +// ones, then waits for the 'svnserve' subprocesses to complete. +func (h *svnHandler) Close() error { + h.listenOnce.Do(func() {}) + if h.s == nil { + return nil + } + + var err error + s := <-h.s + s.closing = true + if s.listener == nil { + err = s.listenErr + } else { + err = s.listener.Close() + } + for c := range s.conns { + c.Close() + } + done := s.done + h.s <- s + + <-done + return err +} |