github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/vcweb/svn.go (about) 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 5 package vcweb 6 7 import ( 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. 34 type 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. 47 type svnState struct { 48 listener net.Listener 49 listenErr error 50 conns map[net.Conn]struct{} 51 closing bool 52 done chan struct{} 53 } 54 55 func (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. 66 func (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. 140 func (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. 177 func (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 }