Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Commit e25b29c

Browse files
alcortesmmcuadros
authored andcommitted
Fix ssh workflow (#96)
* Fix #80 We were sending an additional `\n` to the server when sending the upload request, see clients/ssh/git_upload_pack.go:174: fmt.Fprintln(si, r.String()) The reason for this was to flush the input stream, otherwise, the message was not send to the server. Also, we were (and still are) not checking remote execution errors, so we were unaware of this error, reading the first portion of the packfile as if nothing were wrong. On the few ocasions when the server was quick enough to fail before sending the full packfile, one of our tests (the one that checks the received packfile size) failed. We were also escaping the repository name in the remote command execution string incorrectly. Now we are: - using ssh.Run to run the remote command, instead of start and wait, which is the same but simpler. - using io.Copy instead of fmt.Fprintln, so we avoid adding and extra EOL and also we don't use a line buffered stream. and we no longer have to flush it. - we are closing the input stream as soon as possible, so the remote command can exit also as soon as possible. - the remote command escape character (') is used correctly * WIP * ssh: return remote command exit value when closing the packfile stream
1 parent b78cd3e commit e25b29c

File tree

2 files changed

+91
-52
lines changed

2 files changed

+91
-52
lines changed

clients/ssh/git_upload_pack.go

Lines changed: 78 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
package ssh
33

44
import (
5-
"bufio"
65
"bytes"
76
"errors"
87
"fmt"
98
"io"
10-
"io/ioutil"
119
"strings"
1210

1311
"gopkg.in/src-d/go-git.v4/clients/common"
12+
"gopkg.in/src-d/go-git.v4/formats/packp/advrefs"
1413

1514
"golang.org/x/crypto/ssh"
1615
)
@@ -24,6 +23,8 @@ var (
2423
ErrUploadPackAnswerFormat = errors.New("git-upload-pack bad answer format")
2524
ErrUnsupportedVCS = errors.New("only git is supported")
2625
ErrUnsupportedRepo = errors.New("only github.com is supported")
26+
27+
nak = []byte("0008NAK\n")
2728
)
2829

2930
// GitUploadPackService holds the service information.
@@ -134,81 +135,108 @@ func (s *GitUploadPackService) Disconnect() (err error) {
134135
return s.client.Close()
135136
}
136137

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.
142142
func (s *GitUploadPackService) Fetch(r *common.GitUploadPackRequest) (rc io.ReadCloser, err error) {
143143
if !s.connected {
144144
return nil, ErrNotConnected
145145
}
146146

147147
session, err := s.client.NewSession()
148148
if err != nil {
149-
return nil, err
149+
return nil, fmt.Errorf("cannot open SSH session: %s", err)
150150
}
151151

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-
158152
si, err := session.StdinPipe()
159153
if err != nil {
160-
return nil, err
154+
return nil, fmt.Errorf("cannot pipe remote stdin: %s", err)
161155
}
162156

163157
so, err := session.StdoutPipe()
164158
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)
170160
}
171161

162+
done := make(chan error)
172163
go func() {
173-
fmt.Fprintln(si, r.String())
174-
err = si.Close()
164+
done <- session.Run(s.getCommand())
175165
}()
176166

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())
201173
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)
203179
}
204180

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
207235
}
208236

209237
func (s *GitUploadPackService) getCommand() string {
210238
directory := s.endpoint.Path
211239
directory = directory[1:len(directory)]
212240

213-
return fmt.Sprintf("git-upload-pack %s", directory)
241+
return fmt.Sprintf("git-upload-pack '%s'", directory)
214242
}

clients/ssh/git_upload_pack_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,21 @@ func (s *RemoteSuite) TestFetch(c *C) {
121121
req.Want(core.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"))
122122
reader, err := r.Fetch(req)
123123
c.Assert(err, IsNil)
124+
defer func() { c.Assert(reader.Close(), IsNil) }()
124125

125126
b, err := ioutil.ReadAll(reader)
126127
c.Assert(err, IsNil)
127-
128-
//this is failling randomly, should be fixed ASAP
129128
c.Check(len(b), Equals, 85585)
130129
}
130+
131+
func (s *RemoteSuite) TestFetchError(c *C) {
132+
r := NewGitUploadPackService(s.Endpoint)
133+
c.Assert(r.Connect(), IsNil)
134+
defer func() { c.Assert(r.Disconnect(), IsNil) }()
135+
136+
req := &common.GitUploadPackRequest{}
137+
req.Want(core.NewHash("1111111111111111111111111111111111111111"))
138+
139+
_, err := r.Fetch(req)
140+
c.Assert(err, Not(IsNil))
141+
}

0 commit comments

Comments
 (0)