vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
refactor
c5902451905acdaa478f8476844ccd4a510d9ca9 · Keenan Nemetz · 16d ago · parent ec2aba4b
internal/sshserver/sshserver.go → server/ssh/server.go +24−25
| @@ -1,11 +1,11 @@ | |||
| 1 | // Package sshserver is Vole's write transport. Identity comes from the SSH key | ||
| 1 | // Package ssh is Vole's write transport. Identity comes from the SSH key | ||
| 2 | 2 | // presented at handshake; the only thing a session may do is run git's own | |
| 3 | 3 | // upload-pack or receive-pack against a repository that exists in the store. | |
| 4 | 4 | // | |
| 5 | 5 | // The exec surface is a remote shell until it is made not one, so it is | |
| 6 | 6 | // whitelisted by exact command name and validated path, and everything else is | |
| 7 | 7 | // refused. A test asserts that `sh -c` cannot get through. | |
| 8 | package sshserver | ||
| 8 | package ssh | ||
| 9 | 9 | ||
| 10 | 10 | import ( | |
| 11 | 11 | "crypto/ed25519" | |
| @@ -24,13 +24,12 @@ import ( | |||
| 24 | 24 | "sync" | |
| 25 | 25 | "time" | |
| 26 | 26 | ||
| 27 | "golang.org/x/crypto/ssh" | ||
| 27 | xssh "golang.org/x/crypto/ssh" | ||
| 28 | 28 | ||
| 29 | "github.com/rodent-software/vole/internal/identity" | ||
| 30 | "github.com/rodent-software/vole/internal/lfs" | ||
| 31 | "github.com/rodent-software/vole/internal/locks" | ||
| 32 | "github.com/rodent-software/vole/internal/repo" | ||
| 33 | "github.com/rodent-software/vole/internal/runs" | ||
| 29 | "github.com/rodent-software/vole/ci/runs" | ||
| 30 | "github.com/rodent-software/vole/core/lfs" | ||
| 31 | "github.com/rodent-software/vole/forge/identity" | ||
| 32 | "github.com/rodent-software/vole/forge/repo" | ||
| 34 | 33 | ) | |
| 35 | 34 | ||
| 36 | 35 | const serverVersion = "SSH-2.0-vole" | |
| @@ -55,7 +54,7 @@ type Config struct { | |||
| 55 | 54 | LFSWarnBytes int64 | |
| 56 | 55 | // Locks is the Git LFS lock table, keyed by account id. Nil means this server | |
| 57 | 56 | // offers no locking, and `git-lfs-transfer` does not advertise the capability. | |
| 58 | Locks *locks.Store | ||
| 57 | Locks *lfs.LockStore | ||
| 59 | 58 | // Lease is how long a runner may hold a job without a heartbeat. | |
| 60 | 59 | Lease time.Duration | |
| 61 | 60 | // MaxJobsPerRepo bounds how many jobs one repository may have running at once. | |
| @@ -71,7 +70,7 @@ type Config struct { | |||
| 71 | 70 | ||
| 72 | 71 | type Server struct { | |
| 73 | 72 | cfg Config | |
| 74 | sshCfg *ssh.ServerConfig | ||
| 73 | sshCfg *xssh.ServerConfig | ||
| 75 | 74 | mu sync.Mutex | |
| 76 | 75 | ln net.Listener | |
| 77 | 76 | } | |
| @@ -86,17 +85,17 @@ func New(cfg Config) (*Server, error) { | |||
| 86 | 85 | return nil, err | |
| 87 | 86 | } | |
| 88 | 87 | s := &Server{cfg: cfg} | |
| 89 | s.sshCfg = &ssh.ServerConfig{ | ||
| 88 | s.sshCfg = &xssh.ServerConfig{ | ||
| 90 | 89 | ServerVersion: serverVersion, | |
| 91 | 90 | MaxAuthTries: 6, | |
| 92 | PublicKeyCallback: func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { | ||
| 93 | fp := ssh.FingerprintSHA256(key) | ||
| 91 | PublicKeyCallback: func(c xssh.ConnMetadata, key xssh.PublicKey) (*xssh.Permissions, error) { | ||
| 92 | fp := xssh.FingerprintSHA256(key) | ||
| 94 | 93 | accountID, name, ok := cfg.Identity.Lookup(fp) | |
| 95 | 94 | if !ok { | |
| 96 | 95 | cfg.Log.Warn("ssh: unknown key", "fingerprint", fp, "remote", c.RemoteAddr().String()) | |
| 97 | 96 | return nil, fmt.Errorf("key not registered") | |
| 98 | 97 | } | |
| 99 | return &ssh.Permissions{Extensions: map[string]string{ | ||
| 98 | return &xssh.Permissions{Extensions: map[string]string{ | ||
| 100 | 99 | "user": name, | |
| 101 | 100 | "account": accountID, | |
| 102 | 101 | "fingerprint": fp, | |
| @@ -142,13 +141,13 @@ func (s *Server) Close() error { | |||
| 142 | 141 | } | |
| 143 | 142 | ||
| 144 | 143 | func (s *Server) handleConn(c net.Conn) { | |
| 145 | sconn, chans, reqs, err := ssh.NewServerConn(c, s.sshCfg) | ||
| 144 | sconn, chans, reqs, err := xssh.NewServerConn(c, s.sshCfg) | ||
| 146 | 145 | if err != nil { | |
| 147 | 146 | c.Close() | |
| 148 | 147 | return | |
| 149 | 148 | } | |
| 150 | 149 | defer sconn.Close() | |
| 151 | go ssh.DiscardRequests(reqs) | ||
| 150 | go xssh.DiscardRequests(reqs) | ||
| 152 | 151 | ||
| 153 | 152 | user := sconn.Permissions.Extensions["user"] | |
| 154 | 153 | // The account id is the authorization identity. The name is only ever used | |
| @@ -159,7 +158,7 @@ func (s *Server) handleConn(c net.Conn) { | |||
| 159 | 158 | ||
| 160 | 159 | for newCh := range chans { | |
| 161 | 160 | if newCh.ChannelType() != "session" { | |
| 162 | newCh.Reject(ssh.UnknownChannelType, "only session channels are supported") | ||
| 161 | newCh.Reject(xssh.UnknownChannelType, "only session channels are supported") | ||
| 163 | 162 | continue | |
| 164 | 163 | } | |
| 165 | 164 | ch, chReqs, err := newCh.Accept() | |
| @@ -170,13 +169,13 @@ func (s *Server) handleConn(c net.Conn) { | |||
| 170 | 169 | } | |
| 171 | 170 | } | |
| 172 | 171 | ||
| 173 | func (s *Server) handleSession(ch ssh.Channel, reqs <-chan *ssh.Request, user, accountID, fp string) { | ||
| 172 | func (s *Server) handleSession(ch xssh.Channel, reqs <-chan *xssh.Request, user, accountID, fp string) { | ||
| 174 | 173 | defer ch.Close() | |
| 175 | 174 | for req := range reqs { | |
| 176 | 175 | switch req.Type { | |
| 177 | 176 | case "exec": | |
| 178 | 177 | var payload struct{ Command string } | |
| 179 | ssh.Unmarshal(req.Payload, &payload) | ||
| 178 | xssh.Unmarshal(req.Payload, &payload) | ||
| 180 | 179 | s.exec(ch, req, payload.Command, user, accountID, fp) | |
| 181 | 180 | return | |
| 182 | 181 | case "shell", "pty-req", "subsystem": | |
| @@ -193,7 +192,7 @@ func (s *Server) handleSession(ch ssh.Channel, reqs <-chan *ssh.Request, user, a | |||
| 193 | 192 | } | |
| 194 | 193 | } | |
| 195 | 194 | ||
| 196 | func (s *Server) exec(ch ssh.Channel, req *ssh.Request, command, user, accountID, fp string) { | ||
| 195 | func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, accountID, fp string) { | ||
| 197 | 196 | // A refusal is still answered on the channel rather than at the request | |
| 198 | 197 | // layer: replying false makes the client print "exec request failed on | |
| 199 | 198 | // channel 0" and the reason is lost. Nothing is ever executed on these | |
| @@ -382,11 +381,11 @@ func splitCommand(s string) ([]string, error) { | |||
| 382 | 381 | return out, nil | |
| 383 | 382 | } | |
| 384 | 383 | ||
| 385 | func exit(ch ssh.Channel, code int) { | ||
| 386 | ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{uint32(code)})) | ||
| 384 | func exit(ch xssh.Channel, code int) { | ||
| 385 | ch.SendRequest("exit-status", false, xssh.Marshal(struct{ Status uint32 }{uint32(code)})) | ||
| 387 | 386 | } | |
| 388 | 387 | ||
| 389 | func loadOrCreateHostKey(path string) (ssh.Signer, error) { | ||
| 388 | func loadOrCreateHostKey(path string) (xssh.Signer, error) { | ||
| 390 | 389 | if data, err := os.ReadFile(path); err == nil { | |
| 391 | 390 | blk, _ := pem.Decode(data) | |
| 392 | 391 | if blk == nil { | |
| @@ -396,7 +395,7 @@ func loadOrCreateHostKey(path string) (ssh.Signer, error) { | |||
| 396 | 395 | if err != nil { | |
| 397 | 396 | return nil, fmt.Errorf("%s: %w", path, err) | |
| 398 | 397 | } | |
| 399 | return ssh.NewSignerFromKey(key) | ||
| 398 | return xssh.NewSignerFromKey(key) | ||
| 400 | 399 | } else if !os.IsNotExist(err) { | |
| 401 | 400 | return nil, err | |
| 402 | 401 | } | |
| @@ -416,5 +415,5 @@ func loadOrCreateHostKey(path string) (ssh.Signer, error) { | |||
| 416 | 415 | if err := os.WriteFile(path, blk, 0o600); err != nil { | |
| 417 | 416 | return nil, err | |
| 418 | 417 | } | |
| 419 | return ssh.NewSignerFromKey(priv) | ||
| 418 | return xssh.NewSignerFromKey(priv) | ||
| 420 | 419 | } | |