summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/vcweb/svn.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/go/internal/vcweb/svn.go')
-rw-r--r--src/cmd/go/internal/vcweb/svn.go199
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
+}