Skip to content

Commit a9f7a65

Browse files
committed
refactor(main): remove git protocol support
As this is still used by the test logic, I've moved it to the _test.go files. I don't expect to need this in production as the plaintext git protocol is rather insecure. Signed-off-by: Xe Iaso <xe@tigrisdata.com>
1 parent 2b44c87 commit a9f7a65

5 files changed

Lines changed: 132 additions & 142 deletions

File tree

cmd/objgitd/example_hook_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestExampleHookRuns(t *testing.T) {
4949
if err != nil {
5050
t.Fatalf("listen: %v", err)
5151
}
52-
go func() { _ = d.Serve(ctx, ln) }()
52+
go func() { _ = d.ServeGitProtocol(ctx, ln) }()
5353

5454
remote := "git://" + ln.Addr().String() + "/example.git"
5555
work := t.TempDir()

cmd/objgitd/git_protocol.go

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,14 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"io"
87
"log/slog"
9-
"net"
108
"net/url"
11-
"strings"
129
"time"
1310

1411
"github.com/go-git/go-billy/v6"
1512
git "github.com/go-git/go-git/v6"
1613
"github.com/go-git/go-git/v6/plumbing"
1714
"github.com/go-git/go-git/v6/plumbing/cache"
18-
"github.com/go-git/go-git/v6/plumbing/format/pktline"
19-
"github.com/go-git/go-git/v6/plumbing/protocol/packp"
2015
"github.com/go-git/go-git/v6/plumbing/transport"
2116
"github.com/go-git/go-git/v6/storage"
2217
"github.com/go-git/go-git/v6/storage/filesystem"
@@ -59,124 +54,6 @@ type daemon struct {
5954
hookTimeout time.Duration
6055
}
6156

62-
// Serve accepts connections on l until ctx is cancelled or Accept fails.
63-
func (d *daemon) Serve(ctx context.Context, l net.Listener) error {
64-
go func() {
65-
<-ctx.Done()
66-
_ = l.Close()
67-
}()
68-
69-
for {
70-
conn, err := l.Accept()
71-
if err != nil {
72-
if ctx.Err() != nil {
73-
return nil
74-
}
75-
return fmt.Errorf("objgitd: accept: %w", err)
76-
}
77-
78-
go func() {
79-
if err := d.handle(ctx, conn); err != nil {
80-
slog.Error("connection failed",
81-
"remote", conn.RemoteAddr().String(),
82-
"err", err,
83-
)
84-
}
85-
}()
86-
}
87-
}
88-
89-
// handle services a single git:// connection: decode the request line, resolve
90-
// the repository, and hand the socket to the matching server command.
91-
func (d *daemon) handle(ctx context.Context, conn net.Conn) error {
92-
defer conn.Close()
93-
94-
// A silent client must not be able to pin a goroutine forever.
95-
_ = conn.SetReadDeadline(time.Now().Add(handshakeTimeout))
96-
97-
var req packp.GitProtoRequest
98-
if err := req.Decode(conn); err != nil {
99-
return fmt.Errorf("decoding git-proto-request: %w", err)
100-
}
101-
102-
// The transfer that follows can take a while; drop the handshake deadline.
103-
_ = conn.SetReadDeadline(time.Time{})
104-
105-
slog.Info("serving request",
106-
"service", req.RequestCommand,
107-
"path", req.Pathname,
108-
"remote", conn.RemoteAddr().String(),
109-
)
110-
111-
// ExtraParams carries e.g. "version=2"; transport.ProtocolVersion splits on ":".
112-
gitProtocol := strings.Join(req.ExtraParams, ":")
113-
114-
// UploadPack/ReceivePack call r.Close() between negotiation rounds, so the
115-
// reader must be a no-op closer or the socket dies mid-conversation. The
116-
// writer is the raw conn: its final Close() ends the connection.
117-
r := io.NopCloser(conn)
118-
119-
defer metrics.TrackInFlight("git")()
120-
start := time.Now()
121-
122-
if d.authorize(ctx, auth.Request{
123-
Repo: req.Pathname,
124-
Operation: operationFor(req.RequestCommand),
125-
Cred: auth.Anonymous{},
126-
Transport: "git",
127-
}) != auth.Allow {
128-
metrics.ObserveGitOp("git", req.RequestCommand, "denied", start)
129-
_, _ = pktline.WriteError(conn, fmt.Errorf("access denied"))
130-
return fmt.Errorf("access denied for %q (%s)", req.Pathname, req.RequestCommand)
131-
}
132-
133-
err := d.serveGit(ctx, conn, r, req, gitProtocol)
134-
status := "ok"
135-
if err != nil {
136-
status = "error"
137-
}
138-
metrics.ObserveGitOp("git", req.RequestCommand, status, start)
139-
return err
140-
}
141-
142-
// serveGit dispatches a parsed, authorized git:// request to the matching
143-
// go-git transport command.
144-
func (d *daemon) serveGit(ctx context.Context, conn net.Conn, r io.ReadCloser, req packp.GitProtoRequest, gitProtocol string) error {
145-
switch req.RequestCommand {
146-
case transport.UploadPackService:
147-
st, err := d.load(req.Pathname)
148-
if err != nil {
149-
_, _ = pktline.WriteError(conn, fmt.Errorf("repository %q not found", req.Pathname))
150-
return fmt.Errorf("loading %q: %w", req.Pathname, err)
151-
}
152-
return transport.UploadPack(ctx, st, r, conn, &transport.UploadPackRequest{
153-
GitProtocol: gitProtocol,
154-
})
155-
156-
case transport.UploadArchiveService:
157-
st, err := d.load(req.Pathname)
158-
if err != nil {
159-
_, _ = pktline.WriteError(conn, fmt.Errorf("repository %q not found", req.Pathname))
160-
return fmt.Errorf("loading %q: %w", req.Pathname, err)
161-
}
162-
return transport.UploadArchive(ctx, st, r, conn, &transport.UploadArchiveRequest{})
163-
164-
case transport.ReceivePackService:
165-
st, err := d.loadOrInit(req.Pathname)
166-
if err != nil {
167-
_, _ = pktline.WriteError(conn, fmt.Errorf("cannot open repository %q", req.Pathname))
168-
return fmt.Errorf("opening %q for push: %w", req.Pathname, err)
169-
}
170-
return d.receivePack(ctx, st, req.Pathname, r, conn, &transport.ReceivePackRequest{
171-
GitProtocol: gitProtocol,
172-
})
173-
174-
default:
175-
_, _ = pktline.WriteError(conn, fmt.Errorf("unsupported service %q", req.RequestCommand))
176-
return fmt.Errorf("unsupported service: %s", req.RequestCommand)
177-
}
178-
}
179-
18057
// load opens the storer for repoPath and heals a dangling HEAD before returning
18158
// it (see ensureHEAD). It preserves the loader's error verbatim — notably
18259
// transport.ErrRepositoryNotFound, which callers map to a 404 — and treats a

cmd/objgitd/git_protocol_test.go

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package main
22

33
import (
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

cmd/objgitd/hooks_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func TestReceivePackHook(t *testing.T) {
120120
if err != nil {
121121
t.Fatalf("listen: %v", err)
122122
}
123-
go func() { _ = d.Serve(ctx, ln) }()
123+
go func() { _ = d.ServeGitProtocol(ctx, ln) }()
124124

125125
remote := "git://" + ln.Addr().String() + "/hooked.git"
126126

@@ -200,7 +200,7 @@ func TestReceivePackHookAbsent(t *testing.T) {
200200
if err != nil {
201201
t.Fatalf("listen: %v", err)
202202
}
203-
go func() { _ = d.Serve(ctx, ln) }()
203+
go func() { _ = d.ServeGitProtocol(ctx, ln) }()
204204

205205
remote := "git://" + ln.Addr().String() + "/plain.git"
206206
work := t.TempDir()

cmd/objgitd/main.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import (
3232
)
3333

3434
var (
35-
gitBind = flag.String("git-bind", ":9418", "TCP address to listen on for the git:// protocol; empty disables it")
3635
httpBind = flag.String("http-bind", ":8080", "TCP address to listen on for the git smart-HTTP protocol; empty disables it")
3736
sshBind = flag.String("ssh-bind", "", "TCP address to listen on for the git-over-SSH protocol; empty disables it")
3837
metricsBind = flag.String("metrics-bind", ":9090", "TCP address to serve the Prometheus /metrics endpoint; empty disables it")
@@ -70,8 +69,8 @@ func main() {
7069
os.Exit(1)
7170
}
7271

73-
if *gitBind == "" && *httpBind == "" && *sshBind == "" {
74-
slog.Error("at least one of -git-bind, -http-bind, or -ssh-bind must be set")
72+
if *httpBind == "" && *sshBind == "" {
73+
slog.Error("at least one of -http-bind or -ssh-bind must be set")
7574
os.Exit(1)
7675
}
7776

@@ -143,7 +142,6 @@ func main() {
143142

144143
slog.Info("objgitd listening",
145144
"version", objgit.Version,
146-
"git_bind", *gitBind,
147145
"http_bind", *httpBind,
148146
"ssh_bind", *sshBind,
149147
"metrics_bind", *metricsBind,
@@ -193,15 +191,6 @@ func main() {
193191
})
194192
}
195193

196-
if *gitBind != "" {
197-
ln, err := net.Listen("tcp", *gitBind)
198-
if err != nil {
199-
slog.Error("can't listen", "git_bind", *gitBind, "err", err)
200-
os.Exit(1)
201-
}
202-
g.Go(func() error { return d.Serve(gCtx, ln) })
203-
}
204-
205194
if *httpBind != "" {
206195
ln, err := net.Listen("tcp", *httpBind)
207196
if err != nil {

0 commit comments

Comments
 (0)