|
2 | 2 | package ssh
|
3 | 3 |
|
4 | 4 | import (
|
5 |
| - "bufio" |
6 | 5 | "bytes"
|
7 | 6 | "errors"
|
8 | 7 | "fmt"
|
9 | 8 | "io"
|
10 |
| - "io/ioutil" |
11 | 9 | "strings"
|
12 | 10 |
|
13 | 11 | "gopkg.in/src-d/go-git.v4/clients/common"
|
| 12 | + "gopkg.in/src-d/go-git.v4/formats/packp/advrefs" |
14 | 13 |
|
15 | 14 | "golang.org/x/crypto/ssh"
|
16 | 15 | )
|
|
24 | 23 | ErrUploadPackAnswerFormat = errors.New("git-upload-pack bad answer format")
|
25 | 24 | ErrUnsupportedVCS = errors.New("only git is supported")
|
26 | 25 | ErrUnsupportedRepo = errors.New("only github.com is supported")
|
| 26 | + |
| 27 | + nak = []byte("0008NAK\n") |
27 | 28 | )
|
28 | 29 |
|
29 | 30 | // GitUploadPackService holds the service information.
|
@@ -134,81 +135,108 @@ func (s *GitUploadPackService) Disconnect() (err error) {
|
134 | 135 | return s.client.Close()
|
135 | 136 | }
|
136 | 137 |
|
137 |
| -// Fetch retrieves the GitUploadPack form the repository. |
138 |
| -// You must be connected to the repository before using this method |
139 |
| -// (using the ConnectWithAuth() method). |
140 |
| -// TODO: fetch should really reuse the info session instead of openning a new |
141 |
| -// one |
| 138 | +// Fetch returns a packfile for a given upload request. It opens a new |
| 139 | +// SSH session on a connected GitUploadPackService, sends the given |
| 140 | +// upload request to the server and returns a reader for the received |
| 141 | +// packfile. Closing the returned reader will close the SSH session. |
142 | 142 | func (s *GitUploadPackService) Fetch(r *common.GitUploadPackRequest) (rc io.ReadCloser, err error) {
|
143 | 143 | if !s.connected {
|
144 | 144 | return nil, ErrNotConnected
|
145 | 145 | }
|
146 | 146 |
|
147 | 147 | session, err := s.client.NewSession()
|
148 | 148 | if err != nil {
|
149 |
| - return nil, err |
| 149 | + return nil, fmt.Errorf("cannot open SSH session: %s", err) |
150 | 150 | }
|
151 | 151 |
|
152 |
| - defer func() { |
153 |
| - // the session can be closed by the other endpoint, |
154 |
| - // therefore we must ignore a close error. |
155 |
| - _ = session.Close() |
156 |
| - }() |
157 |
| - |
158 | 152 | si, err := session.StdinPipe()
|
159 | 153 | if err != nil {
|
160 |
| - return nil, err |
| 154 | + return nil, fmt.Errorf("cannot pipe remote stdin: %s", err) |
161 | 155 | }
|
162 | 156 |
|
163 | 157 | so, err := session.StdoutPipe()
|
164 | 158 | if err != nil {
|
165 |
| - return nil, err |
166 |
| - } |
167 |
| - |
168 |
| - if err := session.Start(s.getCommand()); err != nil { |
169 |
| - return nil, err |
| 159 | + return nil, fmt.Errorf("cannot pipe remote stdout: %s", err) |
170 | 160 | }
|
171 | 161 |
|
| 162 | + done := make(chan error) |
172 | 163 | go func() {
|
173 |
| - fmt.Fprintln(si, r.String()) |
174 |
| - err = si.Close() |
| 164 | + done <- session.Run(s.getCommand()) |
175 | 165 | }()
|
176 | 166 |
|
177 |
| - // TODO: investigate this *ExitError type (command fails or |
178 |
| - // doesn't complete successfully), as it is happenning all |
179 |
| - // the time, but everything seems to work fine. |
180 |
| - if err := session.Wait(); err != nil { |
181 |
| - if _, ok := err.(*ssh.ExitError); !ok { |
182 |
| - return nil, err |
183 |
| - } |
184 |
| - } |
185 |
| - |
186 |
| - // read until the header of the second answer |
187 |
| - soBuf := bufio.NewReader(so) |
188 |
| - token := "0000" |
189 |
| - for { |
190 |
| - var line string |
191 |
| - line, err = soBuf.ReadString('\n') |
192 |
| - if err == io.EOF { |
193 |
| - return nil, ErrUploadPackAnswerFormat |
194 |
| - } |
195 |
| - if line[0:len(token)] == token { |
196 |
| - break |
197 |
| - } |
198 |
| - } |
199 |
| - |
200 |
| - data, err := ioutil.ReadAll(soBuf) |
| 167 | + if err := skipAdvRef(so); err != nil { |
| 168 | + return nil, fmt.Errorf("skipping advertised-refs: %s", err) |
| 169 | + } |
| 170 | + |
| 171 | + // send the upload request |
| 172 | + _, err = io.Copy(si, r.Reader()) |
201 | 173 | if err != nil {
|
202 |
| - return nil, err |
| 174 | + return nil, fmt.Errorf("sending upload-req message: %s", err) |
| 175 | + } |
| 176 | + |
| 177 | + if err := si.Close(); err != nil { |
| 178 | + return nil, fmt.Errorf("closing input: %s", err) |
203 | 179 | }
|
204 | 180 |
|
205 |
| - buf := bytes.NewBuffer(data) |
206 |
| - return ioutil.NopCloser(buf), nil |
| 181 | + // TODO support multi_ack mode |
| 182 | + // TODO support multi_ack_detailed mode |
| 183 | + // TODO support acks for common objects |
| 184 | + // TODO build a proper state machine for all these processing options |
| 185 | + buf := make([]byte, len(nak)) |
| 186 | + if _, err := io.ReadFull(so, buf); err != nil { |
| 187 | + return nil, fmt.Errorf("looking for NAK: %s", err) |
| 188 | + } |
| 189 | + if !bytes.Equal(buf, nak) { |
| 190 | + return nil, fmt.Errorf("NAK answer not found") |
| 191 | + } |
| 192 | + |
| 193 | + return &fetchSession{ |
| 194 | + Reader: so, |
| 195 | + session: session, |
| 196 | + done: done, |
| 197 | + }, nil |
| 198 | +} |
| 199 | + |
| 200 | +func skipAdvRef(so io.Reader) error { |
| 201 | + d := advrefs.NewDecoder(so) |
| 202 | + ar := advrefs.New() |
| 203 | + |
| 204 | + return d.Decode(ar) |
| 205 | +} |
| 206 | + |
| 207 | +type fetchSession struct { |
| 208 | + io.Reader |
| 209 | + session *ssh.Session |
| 210 | + done chan error |
| 211 | +} |
| 212 | + |
| 213 | +// Close closes the session and collects the output state of the remote |
| 214 | +// SSH command. |
| 215 | +// |
| 216 | +// If both the remote command and the closing of the session completes |
| 217 | +// susccessfully it returns nil. |
| 218 | +// |
| 219 | +// If the remote command completes unsuccessfully or is interrupted by a |
| 220 | +// signal, it returns the corresponding *ExitError. |
| 221 | +// |
| 222 | +// Otherwise, if clossing the SSH session fails it returns the close |
| 223 | +// error. Closing the session when the other has already close it is |
| 224 | +// not cosidered an error. |
| 225 | +func (f *fetchSession) Close() (err error) { |
| 226 | + if err := <-f.done; err != nil { |
| 227 | + return err |
| 228 | + } |
| 229 | + |
| 230 | + if err := f.session.Close(); err != nil && err != io.EOF { |
| 231 | + return err |
| 232 | + } |
| 233 | + |
| 234 | + return nil |
207 | 235 | }
|
208 | 236 |
|
209 | 237 | func (s *GitUploadPackService) getCommand() string {
|
210 | 238 | directory := s.endpoint.Path
|
211 | 239 | directory = directory[1:len(directory)]
|
212 | 240 |
|
213 |
| - return fmt.Sprintf("git-upload-pack %s", directory) |
| 241 | + return fmt.Sprintf("git-upload-pack '%s'", directory) |
214 | 242 | }
|
0 commit comments