added git ssh support and ablity to download repo via zip, tar.gz, and bundle
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user