@@ -2,6 +2,9 @@ package main
22
33import (
44 "context"
5+ "fmt"
6+ "io"
7+ "log/slog"
58 "net"
69 "os"
710 "os/exec"
@@ -12,10 +15,131 @@ import (
1215
1316 "github.com/go-git/go-billy/v6"
1417 "github.com/go-git/go-billy/v6/memfs"
18+ "github.com/go-git/go-git/v6/plumbing/format/pktline"
19+ "github.com/go-git/go-git/v6/plumbing/protocol/packp"
1520 "github.com/go-git/go-git/v6/plumbing/transport"
1621 "github.com/tigrisdata/objgit/internal/auth"
22+ "github.com/tigrisdata/objgit/internal/metrics"
1723)
1824
25+ // ServeGitProtocol accepts connections on l until ctx is cancelled or Accept fails.
26+ func (d * daemon ) ServeGitProtocol (ctx context.Context , l net.Listener ) error {
27+ go func () {
28+ <- ctx .Done ()
29+ _ = l .Close ()
30+ }()
31+
32+ for {
33+ conn , err := l .Accept ()
34+ if err != nil {
35+ if ctx .Err () != nil {
36+ return nil
37+ }
38+ return fmt .Errorf ("objgitd: accept: %w" , err )
39+ }
40+
41+ go func () {
42+ if err := d .handleGitProtocol (ctx , conn ); err != nil {
43+ slog .Error ("connection failed" ,
44+ "remote" , conn .RemoteAddr ().String (),
45+ "err" , err ,
46+ )
47+ }
48+ }()
49+ }
50+ }
51+
52+ // handleGitProtocol services a single git:// connection: decode the request line, resolve
53+ // the repository, and hand the socket to the matching server command.
54+ func (d * daemon ) handleGitProtocol (ctx context.Context , conn net.Conn ) error {
55+ defer conn .Close ()
56+
57+ // A silent client must not be able to pin a goroutine forever.
58+ _ = conn .SetReadDeadline (time .Now ().Add (handshakeTimeout ))
59+
60+ var req packp.GitProtoRequest
61+ if err := req .Decode (conn ); err != nil {
62+ return fmt .Errorf ("decoding git-proto-request: %w" , err )
63+ }
64+
65+ // The transfer that follows can take a while; drop the handshake deadline.
66+ _ = conn .SetReadDeadline (time.Time {})
67+
68+ slog .Info ("serving request" ,
69+ "service" , req .RequestCommand ,
70+ "path" , req .Pathname ,
71+ "remote" , conn .RemoteAddr ().String (),
72+ )
73+
74+ // ExtraParams carries e.g. "version=2"; transport.ProtocolVersion splits on ":".
75+ gitProtocol := strings .Join (req .ExtraParams , ":" )
76+
77+ // UploadPack/ReceivePack call r.Close() between negotiation rounds, so the
78+ // reader must be a no-op closer or the socket dies mid-conversation. The
79+ // writer is the raw conn: its final Close() ends the connection.
80+ r := io .NopCloser (conn )
81+
82+ defer metrics .TrackInFlight ("git" )()
83+ start := time .Now ()
84+
85+ if d .authorize (ctx , auth.Request {
86+ Repo : req .Pathname ,
87+ Operation : operationFor (req .RequestCommand ),
88+ Cred : auth.Anonymous {},
89+ Transport : "git" ,
90+ }) != auth .Allow {
91+ metrics .ObserveGitOp ("git" , req .RequestCommand , "denied" , start )
92+ _ , _ = pktline .WriteError (conn , fmt .Errorf ("access denied" ))
93+ return fmt .Errorf ("access denied for %q (%s)" , req .Pathname , req .RequestCommand )
94+ }
95+
96+ err := d .serveGit (ctx , conn , r , req , gitProtocol )
97+ status := "ok"
98+ if err != nil {
99+ status = "error"
100+ }
101+ metrics .ObserveGitOp ("git" , req .RequestCommand , status , start )
102+ return err
103+ }
104+
105+ // serveGit dispatches a parsed, authorized git:// request to the matching
106+ // go-git transport command.
107+ func (d * daemon ) serveGit (ctx context.Context , conn net.Conn , r io.ReadCloser , req packp.GitProtoRequest , gitProtocol string ) error {
108+ switch req .RequestCommand {
109+ case transport .UploadPackService :
110+ st , err := d .load (req .Pathname )
111+ if err != nil {
112+ _ , _ = pktline .WriteError (conn , fmt .Errorf ("repository %q not found" , req .Pathname ))
113+ return fmt .Errorf ("loading %q: %w" , req .Pathname , err )
114+ }
115+ return transport .UploadPack (ctx , st , r , conn , & transport.UploadPackRequest {
116+ GitProtocol : gitProtocol ,
117+ })
118+
119+ case transport .UploadArchiveService :
120+ st , err := d .load (req .Pathname )
121+ if err != nil {
122+ _ , _ = pktline .WriteError (conn , fmt .Errorf ("repository %q not found" , req .Pathname ))
123+ return fmt .Errorf ("loading %q: %w" , req .Pathname , err )
124+ }
125+ return transport .UploadArchive (ctx , st , r , conn , & transport.UploadArchiveRequest {})
126+
127+ case transport .ReceivePackService :
128+ st , err := d .loadOrInit (req .Pathname )
129+ if err != nil {
130+ _ , _ = pktline .WriteError (conn , fmt .Errorf ("cannot open repository %q" , req .Pathname ))
131+ return fmt .Errorf ("opening %q for push: %w" , req .Pathname , err )
132+ }
133+ return d .receivePack (ctx , st , req .Pathname , r , conn , & transport.ReceivePackRequest {
134+ GitProtocol : gitProtocol ,
135+ })
136+
137+ default :
138+ _ , _ = pktline .WriteError (conn , fmt .Errorf ("unsupported service %q" , req .RequestCommand ))
139+ return fmt .Errorf ("unsupported service: %s" , req .RequestCommand )
140+ }
141+ }
142+
19143// TestDaemonPushCreatesRepo reproduces "git push git://host/new.git" against a
20144// path that does not exist yet. The daemon must create the bare repository on
21145// demand and the result must clone back cleanly.
@@ -40,7 +164,7 @@ func TestDaemonPushCreatesRepo(t *testing.T) {
40164 }
41165
42166 srvErr := make (chan error , 1 )
43- go func () { srvErr <- d .Serve (ctx , ln ) }()
167+ go func () { srvErr <- d .ServeGitProtocol (ctx , ln ) }()
44168
45169 remote := "git://" + ln .Addr ().String () + "/test.git"
46170
@@ -95,7 +219,7 @@ func TestDaemonPushDisabled(t *testing.T) {
95219 if err != nil {
96220 t .Fatalf ("listen: %v" , err )
97221 }
98- go func () { _ = d .Serve (ctx , ln ) }()
222+ go func () { _ = d .ServeGitProtocol (ctx , ln ) }()
99223
100224 remote := "git://" + ln .Addr ().String () + "/test.git"
101225
@@ -139,7 +263,7 @@ func TestDaemonPushKeepsPack(t *testing.T) {
139263 if err != nil {
140264 t .Fatalf ("listen: %v" , err )
141265 }
142- go func () { _ = d .Serve (ctx , ln ) }()
266+ go func () { _ = d .ServeGitProtocol (ctx , ln ) }()
143267
144268 remote := "git://" + ln .Addr ().String () + "/test.git"
145269
0 commit comments