| // 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 |
| } |