Git backed by object storage because you can't stop me
git object-storage kefka
10

Configure Feed

Select the types of activity you want to include in your feed.

feat(ssh): add git-over-SSH server and command dispatch

Adds newSSHServer, handleSSH, and gitServiceFor to ssh.go, wiring
gliderlabs/ssh into the daemon so git clone/push over SSH mirrors
the git:// handler: persistent-stream no-op closers, streamingStorer
for receive-pack, and auth.Authorizer gating via pubKeyContextKey.
Adds TestGitServiceFor covering all valid service names and invalid
inputs.

Xe Iaso (May 28, 2026, 11:18 PM EDT) 553532b4 a626c13c

+185
+131
cmd/objgitd/ssh.go
··· 8 8 "fmt" 9 9 "io" 10 10 "log/slog" 11 + "net/url" 11 12 "os" 12 13 "path/filepath" 14 + "strings" 13 15 16 + ssh "github.com/gliderlabs/ssh" 14 17 "github.com/go-git/go-billy/v6" 18 + "github.com/go-git/go-git/v6/plumbing/transport" 19 + "github.com/go-git/go-git/v6/utils/ioutil" 15 20 gossh "golang.org/x/crypto/ssh" 21 + "tangled.org/xeiaso.net/objgit/internal/auth" 16 22 ) 17 23 18 24 const hostKeyPath = ".objgit/ssh_host_ed25519_key" ··· 74 80 slog.Info("created ssh host key", "path", hostKeyPath) 75 81 return signer, nil 76 82 } 83 + 84 + // gitServiceFor maps an SSH exec command to the go-git service it selects. The 85 + // bool is false for anything that is not a git transport command. 86 + func gitServiceFor(command string) (string, bool) { 87 + switch command { 88 + case "git-upload-pack": 89 + return transport.UploadPackService, true 90 + case "git-upload-archive": 91 + return transport.UploadArchiveService, true 92 + case "git-receive-pack": 93 + return transport.ReceivePackService, true 94 + default: 95 + return "", false 96 + } 97 + } 98 + 99 + // pubKeyContextKey keys the authenticated public key stashed on the SSH context 100 + // by PublicKeyHandler, for handleSSH to read when authorizing. 101 + type pubKeyContextKey struct{} 102 + 103 + // newSSHServer builds the git-over-SSH server. It accepts every public key at 104 + // connect time and defers real authorization to handleSSH via daemon.authz. 105 + func newSSHServer(d *daemon, addr string) (*ssh.Server, error) { 106 + signer, err := loadOrCreateHostKey(d.fs) 107 + if err != nil { 108 + return nil, fmt.Errorf("ssh host key: %w", err) 109 + } 110 + srv := &ssh.Server{ 111 + Addr: addr, 112 + Handler: d.handleSSH, 113 + PublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool { 114 + ctx.SetValue(pubKeyContextKey{}, key) 115 + return true 116 + }, 117 + } 118 + srv.AddHostKey(signer) 119 + return srv, nil 120 + } 121 + 122 + // handleSSH services one git-over-SSH exec request: parse the command, authorize, 123 + // resolve the repository, and hand the session to the matching go-git transport 124 + // command. The session is the protocol stream (reader and writer). 125 + func (d *daemon) handleSSH(s ssh.Session) { 126 + cmd := s.Command() 127 + if len(cmd) != 2 { 128 + fmt.Fprintln(s.Stderr(), "objgitd: this is a git SSH endpoint; interactive shells are not supported") 129 + _ = s.Exit(1) 130 + return 131 + } 132 + 133 + service, ok := gitServiceFor(cmd[0]) 134 + if !ok { 135 + fmt.Fprintf(s.Stderr(), "objgitd: unsupported command %q\n", cmd[0]) 136 + _ = s.Exit(1) 137 + return 138 + } 139 + 140 + // ssh://host/foo.git sends "/foo.git"; scp-style host:foo.git sends "foo.git". 141 + repoPath := strings.TrimPrefix(cmd[1], "/") 142 + 143 + var cred auth.Credential = auth.Anonymous{} 144 + if key, ok := s.Context().Value(pubKeyContextKey{}).(ssh.PublicKey); ok && key != nil { 145 + cred = auth.PublicKey{Key: key} 146 + } 147 + if d.authz.Authorize(s.Context(), auth.Request{ 148 + Repo: repoPath, 149 + Operation: operationFor(service), 150 + Cred: cred, 151 + Transport: "ssh", 152 + }) != auth.Allow { 153 + fmt.Fprintln(s.Stderr(), "objgitd: access denied") 154 + _ = s.Exit(1) 155 + return 156 + } 157 + 158 + slog.Info("serving ssh request", 159 + "service", service, 160 + "path", repoPath, 161 + "remote", s.RemoteAddr().String(), 162 + ) 163 + 164 + // SSH is a persistent stream like git://: the transport commands call Close 165 + // between negotiation rounds, which would tear down the channel, so wrap the 166 + // session in no-op closers. 167 + r := io.NopCloser(s) 168 + w := ioutil.WriteNopCloser(s) 169 + ctx := s.Context() 170 + 171 + switch service { 172 + case transport.UploadPackService: 173 + st, err := d.loader.Load(&url.URL{Path: repoPath}) 174 + if err != nil { 175 + fmt.Fprintf(s.Stderr(), "objgitd: repository %q not found\n", repoPath) 176 + _ = s.Exit(1) 177 + return 178 + } 179 + if err := transport.UploadPack(ctx, st, r, w, &transport.UploadPackRequest{}); err != nil { 180 + slog.Error("ssh upload-pack failed", "path", repoPath, "err", err) 181 + } 182 + 183 + case transport.UploadArchiveService: 184 + st, err := d.loader.Load(&url.URL{Path: repoPath}) 185 + if err != nil { 186 + fmt.Fprintf(s.Stderr(), "objgitd: repository %q not found\n", repoPath) 187 + _ = s.Exit(1) 188 + return 189 + } 190 + if err := transport.UploadArchive(ctx, st, r, w, &transport.UploadArchiveRequest{}); err != nil { 191 + slog.Error("ssh upload-archive failed", "path", repoPath, "err", err) 192 + } 193 + 194 + case transport.ReceivePackService: 195 + st, err := d.loadOrInit(repoPath) 196 + if err != nil { 197 + fmt.Fprintf(s.Stderr(), "objgitd: cannot open repository %q\n", repoPath) 198 + _ = s.Exit(1) 199 + return 200 + } 201 + // streamingStorer hides PackfileWriter (the io.CopyBuffer-until-EOF path 202 + // deadlocks on a live socket); d.receivePack runs push hooks afterward. 203 + if err := d.receivePack(ctx, streamingStorer{Storer: st}, st, repoPath, r, w, &transport.ReceivePackRequest{}); err != nil { 204 + slog.Error("ssh receive-pack failed", "path", repoPath, "err", err) 205 + } 206 + } 207 + }
+52
cmd/objgitd/ssh_test.go
··· 6 6 "testing" 7 7 8 8 "github.com/go-git/go-billy/v6/memfs" 9 + "github.com/go-git/go-git/v6/plumbing/transport" 9 10 ) 11 + 12 + func TestGitServiceFor(t *testing.T) { 13 + tt := []struct { 14 + name string 15 + command string 16 + service string 17 + ok bool 18 + }{ 19 + { 20 + name: "upload-pack", 21 + command: "git-upload-pack", 22 + service: transport.UploadPackService, 23 + ok: true, 24 + }, 25 + { 26 + name: "upload-archive", 27 + command: "git-upload-archive", 28 + service: transport.UploadArchiveService, 29 + ok: true, 30 + }, 31 + { 32 + name: "receive-pack", 33 + command: "git-receive-pack", 34 + service: transport.ReceivePackService, 35 + ok: true, 36 + }, 37 + { 38 + name: "git-shell is unsupported", 39 + command: "git-shell", 40 + service: "", 41 + ok: false, 42 + }, 43 + { 44 + name: "empty string is unsupported", 45 + command: "", 46 + service: "", 47 + ok: false, 48 + }, 49 + } 50 + for _, tc := range tt { 51 + t.Run(tc.name, func(t *testing.T) { 52 + got, ok := gitServiceFor(tc.command) 53 + if ok != tc.ok { 54 + t.Errorf("gitServiceFor(%q) ok=%v, want %v", tc.command, ok, tc.ok) 55 + } 56 + if got != tc.service { 57 + t.Errorf("gitServiceFor(%q) service=%q, want %q", tc.command, got, tc.service) 58 + } 59 + }) 60 + } 61 + } 10 62 11 63 func TestLoadOrCreateHostKey(t *testing.T) { 12 64 fs := memfs.New()
+2
go.mod
··· 7 7 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 8 8 github.com/aws/smithy-go v1.26.0 9 9 github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 10 + github.com/gliderlabs/ssh v0.3.8 10 11 github.com/go-git/go-billy/v6 v6.0.0-alpha.1 11 12 github.com/go-git/go-git/v6 v6.0.0-alpha.4 12 13 github.com/joho/godotenv v1.5.1 ··· 21 22 require ( 22 23 github.com/Microsoft/go-winio v0.6.2 // indirect 23 24 github.com/ProtonMail/go-crypto v1.4.1 // indirect 25 + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 24 26 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect 25 27 github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect 26 28 github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect