122 lines
2.9 KiB
Go
122 lines
2.9 KiB
Go
// Package sshserver implements an SSH server for git clone/push/pull operations.
|
|
// It authenticates users via their stored SSH public keys and executes
|
|
// git-upload-pack / git-receive-pack as subprocesses.
|
|
package sshserver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
"xorm.io/xorm"
|
|
|
|
"github.com/forgeo/forgebucket/internal/config"
|
|
)
|
|
|
|
// Server is the SSH git server.
|
|
type Server struct {
|
|
db *xorm.Engine
|
|
cfg *config.Config
|
|
}
|
|
|
|
func New(db *xorm.Engine, cfg *config.Config) *Server {
|
|
return &Server{db: db, cfg: cfg}
|
|
}
|
|
|
|
// ListenAndServe binds to cfg.SSHPort, loads or generates a host key, and accepts
|
|
// connections until ctx is cancelled. Returns nil when the context is done.
|
|
func (s *Server) ListenAndServe(ctx context.Context) error {
|
|
hostKey, err := s.loadOrGenerateHostKey()
|
|
if err != nil {
|
|
return fmt.Errorf("sshserver: host key: %w", err)
|
|
}
|
|
|
|
srvConfig := &ssh.ServerConfig{
|
|
PublicKeyCallback: s.lookupKey,
|
|
}
|
|
srvConfig.AddHostKey(hostKey)
|
|
|
|
addr := ":" + s.cfg.SSHPort
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
log.Printf("sshserver: cannot bind %s — SSH transport disabled: %v", addr, err)
|
|
return nil
|
|
}
|
|
log.Printf("sshserver: listening on %s", addr)
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
ln.Close()
|
|
}()
|
|
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
default:
|
|
log.Printf("sshserver: accept: %v", err)
|
|
continue
|
|
}
|
|
}
|
|
go s.handleConn(conn, srvConfig)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleConn(netConn net.Conn, srvConfig *ssh.ServerConfig) {
|
|
defer netConn.Close()
|
|
|
|
sshConn, chans, reqs, err := ssh.NewServerConn(netConn, srvConfig)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer sshConn.Close()
|
|
|
|
go ssh.DiscardRequests(reqs)
|
|
|
|
username, _ := sshConn.Permissions.Extensions["username"]
|
|
|
|
for newChan := range chans {
|
|
if newChan.ChannelType() != "session" {
|
|
newChan.Reject(ssh.UnknownChannelType, "unknown channel type") //nolint:errcheck
|
|
continue
|
|
}
|
|
ch, requests, err := newChan.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go s.handleSession(ch, requests, username)
|
|
}
|
|
}
|
|
|
|
// loadOrGenerateHostKey loads the host key from SSHHostKeyPath if set,
|
|
// otherwise generates an ephemeral RSA-4096 key (lost on restart).
|
|
func (s *Server) loadOrGenerateHostKey() (ssh.Signer, error) {
|
|
if s.cfg.SSHHostKeyPath != "" {
|
|
data, err := os.ReadFile(s.cfg.SSHHostKeyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read host key %s: %w", s.cfg.SSHHostKeyPath, err)
|
|
}
|
|
return ssh.ParsePrivateKey(data)
|
|
}
|
|
|
|
log.Printf("sshserver: SSH_HOST_KEY_PATH not set — generating ephemeral host key (host key changes on restart)")
|
|
privKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate host key: %w", err)
|
|
}
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
|
|
})
|
|
return ssh.ParsePrivateKey(keyPEM)
|
|
}
|