1// Copyright 2022 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package vcweb
6
7import (
8	"io"
9	"log"
10	"net"
11	"net/http"
12	"os/exec"
13	"strings"
14	"sync"
15)
16
17// An svnHandler serves requests for Subversion repos.
18//
19// Unlike the other vcweb handlers, svnHandler does not serve the Subversion
20// protocol directly over the HTTP connection. Instead, it opens a separate port
21// that serves the (non-HTTP) 'svn' protocol. The test binary can retrieve the
22// URL for that port by sending an HTTP request with the query parameter
23// "vcwebsvn=1".
24//
25// We take this approach because the 'svn' protocol is implemented by a
26// lightweight 'svnserve' binary that is usually packaged along with the 'svn'
27// client binary, whereas only known implementation of the Subversion HTTP
28// protocol is the mod_dav_svn apache2 module. Apache2 has a lot of dependencies
29// and also seems to rely on global configuration via well-known file paths, so
30// implementing a hermetic test using apache2 would require the test to run in a
31// complicated container environment, which wouldn't be nearly as
32// straightforward for Go contributors to set up and test against on their local
33// machine.
34type svnHandler struct {
35	svnRoot string // a directory containing all svn repos to be served
36	logger  *log.Logger
37
38	pathOnce     sync.Once
39	svnservePath string // the path to the 'svnserve' executable
40	svnserveErr  error
41
42	listenOnce sync.Once
43	s          chan *svnState // 1-buffered
44}
45
46// An svnState describes the state of a port serving the 'svn://' protocol.
47type svnState struct {
48	listener  net.Listener
49	listenErr error
50	conns     map[net.Conn]struct{}
51	closing   bool
52	done      chan struct{}
53}
54
55func (h *svnHandler) Available() bool {
56	h.pathOnce.Do(func() {
57		h.svnservePath, h.svnserveErr = exec.LookPath("svnserve")
58	})
59	return h.svnserveErr == nil
60}
61
62// Handler returns an http.Handler that checks for the "vcwebsvn" query
63// parameter and then serves the 'svn://' URL for the repository at the
64// requested path.
65// The HTTP client is expected to read that URL and pass it to the 'svn' client.
66func (h *svnHandler) Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) {
67	if !h.Available() {
68		return nil, ServerNotInstalledError{name: "svn"}
69	}
70
71	// Go ahead and start the listener now, so that if it fails (for example, due
72	// to port exhaustion) we can return an error from the Handler method instead
73	// of serving an error for each individual HTTP request.
74	h.listenOnce.Do(func() {
75		h.s = make(chan *svnState, 1)
76		l, err := net.Listen("tcp", "localhost:0")
77		done := make(chan struct{})
78
79		h.s <- &svnState{
80			listener:  l,
81			listenErr: err,
82			conns:     map[net.Conn]struct{}{},
83			done:      done,
84		}
85		if err != nil {
86			close(done)
87			return
88		}
89
90		h.logger.Printf("serving svn on svn://%v", l.Addr())
91
92		go func() {
93			for {
94				c, err := l.Accept()
95
96				s := <-h.s
97				if err != nil {
98					s.listenErr = err
99					if len(s.conns) == 0 {
100						close(s.done)
101					}
102					h.s <- s
103					return
104				}
105				if s.closing {
106					c.Close()
107				} else {
108					s.conns[c] = struct{}{}
109					go h.serve(c)
110				}
111				h.s <- s
112			}
113		}()
114	})
115
116	s := <-h.s
117	addr := ""
118	if s.listener != nil {
119		addr = s.listener.Addr().String()
120	}
121	err := s.listenErr
122	h.s <- s
123	if err != nil {
124		return nil, err
125	}
126
127	handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
128		if req.FormValue("vcwebsvn") != "" {
129			w.Header().Add("Content-Type", "text/plain; charset=UTF-8")
130			io.WriteString(w, "svn://"+addr+"\n")
131			return
132		}
133		http.NotFound(w, req)
134	})
135
136	return handler, nil
137}
138
139// serve serves a single 'svn://' connection on c.
140func (h *svnHandler) serve(c net.Conn) {
141	defer func() {
142		c.Close()
143
144		s := <-h.s
145		delete(s.conns, c)
146		if len(s.conns) == 0 && s.listenErr != nil {
147			close(s.done)
148		}
149		h.s <- s
150	}()
151
152	// The "--inetd" flag causes svnserve to speak the 'svn' protocol over its
153	// stdin and stdout streams as if invoked by the Unix "inetd" service.
154	// We aren't using inetd, but we are implementing essentially the same
155	// approach: using a host process to listen for connections and spawn
156	// subprocesses to serve them.
157	cmd := exec.Command(h.svnservePath, "--read-only", "--root="+h.svnRoot, "--inetd")
158	cmd.Stdin = c
159	cmd.Stdout = c
160	stderr := new(strings.Builder)
161	cmd.Stderr = stderr
162	err := cmd.Run()
163
164	var errFrag any = "ok"
165	if err != nil {
166		errFrag = err
167	}
168	stderrFrag := ""
169	if stderr.Len() > 0 {
170		stderrFrag = "\n" + stderr.String()
171	}
172	h.logger.Printf("%v: %s%s", cmd, errFrag, stderrFrag)
173}
174
175// Close stops accepting new svn:// connections and terminates the existing
176// ones, then waits for the 'svnserve' subprocesses to complete.
177func (h *svnHandler) Close() error {
178	h.listenOnce.Do(func() {})
179	if h.s == nil {
180		return nil
181	}
182
183	var err error
184	s := <-h.s
185	s.closing = true
186	if s.listener == nil {
187		err = s.listenErr
188	} else {
189		err = s.listener.Close()
190	}
191	for c := range s.conns {
192		c.Close()
193	}
194	done := s.done
195	h.s <- s
196
197	<-done
198	return err
199}
200