11package githttp
22
33import (
4+ "bufio"
5+ "bytes"
46 "context"
7+ "errors"
58 "fmt"
9+ "io"
610 "log/slog"
711 "net/url"
812 "path"
@@ -15,6 +19,7 @@ import (
1519 "gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
1620 "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/accessverifier"
1721 "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/git"
22+ "gitlab.com/gitlab-org/gitlab-shell/v14/internal/pktline"
1823)
1924
2025// CellsPullCommand handles git pull (upload-pack) via SSH-over-HTTP for Cells
@@ -28,7 +33,8 @@ type CellsPullCommand struct {
2833 Response * accessverifier.Response
2934}
3035
31- // Execute runs a Cells SSH-over-HTTP upload-pack request.
36+ // Execute runs a Cells SSH-over-HTTP upload-pack request. Upload-pack is
37+ // client-speaks-first, so pull can use a single request.
3238func (c * CellsPullCommand ) Execute (ctx context.Context ) error {
3339 log .FromContext (ctx ).InfoContext (ctx , "Cells: using SSH-over-HTTP upload-pack" ,
3440 slog .String ("cell_address" , c .Response .CellAddress ))
@@ -62,7 +68,8 @@ type CellsPushCommand struct {
6268 Response * accessverifier.Response
6369}
6470
65- // Execute runs a Cells SSH-over-HTTP receive-pack request.
71+ // Execute runs a Cells SSH-over-HTTP receive-pack request. Receive-pack is
72+ // server-speaks-first, so push must advertise before waiting for SSH input.
6673func (c * CellsPushCommand ) Execute (ctx context.Context ) error {
6774 log .FromContext (ctx ).InfoContext (ctx , "Cells: using SSH-over-HTTP receive-pack" ,
6875 slog .String ("cell_address" , c .Response .CellAddress ))
@@ -72,7 +79,93 @@ func (c *CellsPushCommand) Execute(ctx context.Context) error {
7279 return err
7380 }
7481
75- return executeSSHRequest (ctx , gitClient .SSHReceivePack , c .ReadWriter )
82+ if advertisementErr := c .forwardAdvertisement (ctx , gitClient ); advertisementErr != nil {
83+ return fmt .Errorf ("cells routing: receive-pack advertisement: %w" , advertisementErr )
84+ }
85+
86+ return c .forwardPush (ctx , gitClient )
87+ }
88+
89+ func (c * CellsPushCommand ) forwardPush (ctx context.Context , gitClient * git.Client ) error {
90+ clientInput , err := c .waitForPushInput (ctx )
91+ if err != nil {
92+ return err
93+ }
94+ if clientInput == nil {
95+ return nil
96+ }
97+
98+ return c .forwardReceivePack (ctx , gitClient , clientInput )
99+ }
100+
101+ func (c * CellsPushCommand ) waitForPushInput (ctx context.Context ) (* bufio.Reader , error ) {
102+ clientInput := bufio .NewReader (c .ReadWriter .In )
103+ // Open the streaming request once the client begins sending. The Cell's
104+ // receive-pack validates command and pack framing.
105+ _ , err := clientInput .Peek (1 )
106+ if err == nil {
107+ return clientInput , nil
108+ }
109+ if ctx .Err () != nil {
110+ return nil , ctx .Err ()
111+ }
112+ if errors .Is (err , io .EOF ) {
113+ return nil , nil
114+ }
115+
116+ return nil , fmt .Errorf ("cells routing: waiting for receive-pack input: %w" , err )
117+ }
118+
119+ func (c * CellsPushCommand ) forwardReceivePack (ctx context.Context , gitClient * git.Client , clientInput io.Reader ) error {
120+ response , err := gitClient .SSHReceivePack (ctx , clientInput )
121+ if err != nil {
122+ return fmt .Errorf ("cells routing: receive-pack response: %w" , err )
123+ }
124+ defer response .Body .Close () //nolint:errcheck
125+
126+ if err := copyAdvertisement (io .Discard , response .Body ); err != nil {
127+ return fmt .Errorf ("cells routing: receive-pack response advertisement: %w" , err )
128+ }
129+
130+ if _ , err := io .Copy (c .ReadWriter .Out , response .Body ); err != nil {
131+ return fmt .Errorf ("cells routing: receive-pack response: %w" , err )
132+ }
133+
134+ return nil
135+ }
136+
137+ func (c * CellsPushCommand ) forwardAdvertisement (ctx context.Context , gitClient * git.Client ) error {
138+ // A 0000 body makes command-less receive-pack exit cleanly after advertising;
139+ // an empty body appends fatal output and Internal Server Error instead.
140+ response , err := gitClient .SSHReceivePack (ctx , bytes .NewReader (pktline .PktFlush ()))
141+ if err != nil {
142+ return err
143+ }
144+ defer response .Body .Close () //nolint:errcheck
145+
146+ return copyAdvertisement (c .ReadWriter .Out , response .Body )
147+ }
148+
149+ // copyAdvertisement leaves response bytes after the terminating flush unread.
150+ func copyAdvertisement (w io.Writer , r io.Reader ) error {
151+ for {
152+ packet , err := pktline .ReadPacket (r )
153+ if err != nil {
154+ if errors .Is (err , io .EOF ) {
155+ return fmt .Errorf ("advertisement ended before flush: %w" , err )
156+ }
157+
158+ return err
159+ }
160+
161+ if _ , err := w .Write (packet ); err != nil {
162+ return err
163+ }
164+
165+ if pktline .IsFlush (packet ) {
166+ return nil
167+ }
168+ }
76169}
77170
78171// NewCellsPushCommand builds a Cells SSH-over-HTTP receive-pack command.
0 commit comments