all repos

mugit @ 70e03536539f9d6f9399e9b1bd0a54e942435d68

馃惍 git server that your cow will love
7 files changed, 205 insertions(+), 19 deletions(-)
add ssh
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-01-21 01:49:14 +0200
Authored at: 2026-01-20 00:50:39 +0200
Change ID: tmqrwyvzvpsvtryusmuqlrvsluvtkwrn
Parent: 012e7df
M config.yml
路路路
        3
        3
           port: 8008

      
        4
        4
         

      
        5
        5
         meta:

      
        6
        
        -  title: i like git

      
        7
        6
           description: hey kid, come get your free software

      
        
        7
        +  title: git.olexsmir.xyz

      
        8
        8
           host: git.olexsmir.xyz

      
        
        9
        +

      
        
        10
        +ssh:

      
        
        11
        +  enable: true

      
        
        12
        +  port: 2222

      
        
        13
        +  host_key: /home/olex/.ssh/mugit

      
        
        14
        +  keys:

      
        
        15
        +    - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPLLJdkVYKZgsayw+sHanKPKZbI0RMS2CakqBCEi5Trz

      
        
        16
        +    - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMPQ0Qz0DFB+rGrD8ScUqbUTZ1/O8FHrOBF5bIAGQgMj olex@rachlinux

      
        9
        17
         

      
        10
        18
         repo:

      
        11
        19
           dir: /home/olex/mugit-test/

      
M go.mod
路路路
        4
        4
         

      
        5
        5
         require (

      
        6
        6
         	github.com/bluekeyes/go-gitdiff v0.8.1

      
        
        7
        +	github.com/gliderlabs/ssh v0.3.8

      
        7
        8
         	github.com/go-git/go-git/v5 v5.16.4

      
        8
        9
         	github.com/yuin/goldmark v1.7.16

      
        
        10
        +	golang.org/x/crypto v0.47.0

      
        9
        11
         	gopkg.in/yaml.v2 v2.4.0

      
        10
        12
         )

      
        11
        13
         

      路路路
        13
        15
         	dario.cat/mergo v1.0.0 // indirect

      
        14
        16
         	github.com/Microsoft/go-winio v0.6.2 // indirect

      
        15
        17
         	github.com/ProtonMail/go-crypto v1.1.6 // indirect

      
        
        18
        +	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect

      
        16
        19
         	github.com/cloudflare/circl v1.6.1 // indirect

      
        17
        20
         	github.com/cyphar/filepath-securejoin v0.4.1 // indirect

      
        18
        21
         	github.com/emirpasic/gods v1.18.1 // indirect

      路路路
        25
        28
         	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect

      
        26
        29
         	github.com/skeema/knownhosts v1.3.1 // indirect

      
        27
        30
         	github.com/xanzy/ssh-agent v0.3.3 // indirect

      
        28
        
        -	golang.org/x/crypto v0.37.0 // indirect

      
        29
        
        -	golang.org/x/net v0.39.0 // indirect

      
        30
        
        -	golang.org/x/sys v0.32.0 // indirect

      
        
        31
        +	golang.org/x/net v0.48.0 // indirect

      
        
        32
        +	golang.org/x/sys v0.40.0 // indirect

      
        31
        33
         	gopkg.in/warnings.v0 v0.1.2 // indirect

      
        32
        34
         )

      
M go.sum
路路路
        72
        72
         github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=

      
        73
        73
         github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=

      
        74
        74
         golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=

      
        75
        
        -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=

      
        76
        
        -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=

      
        
        75
        +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=

      
        
        76
        +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=

      
        77
        77
         golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=

      
        78
        78
         golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=

      
        79
        79
         golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=

      
        80
        
        -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=

      
        81
        
        -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=

      
        
        80
        +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=

      
        
        81
        +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=

      
        82
        82
         golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

      
        83
        83
         golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

      
        84
        84
         golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

      
        85
        85
         golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

      
        86
        86
         golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

      
        87
        87
         golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

      
        88
        
        -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=

      
        89
        
        -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

      
        
        88
        +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=

      
        
        89
        +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

      
        90
        90
         golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

      
        91
        
        -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=

      
        92
        
        -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=

      
        
        91
        +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=

      
        
        92
        +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=

      
        93
        93
         golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

      
        94
        
        -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=

      
        95
        
        -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=

      
        
        94
        +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=

      
        
        95
        +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=

      
        96
        96
         golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

      
        97
        97
         gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

      
        98
        98
         gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

      
M internal/config/config.go
路路路
        18
        18
         		Description string `yaml:"description"`

      
        19
        19
         		Host        string `yaml:"host"`

      
        20
        20
         	} `yaml:"meta"`

      
        
        21
        +	SSH struct {

      
        
        22
        +		Enable  bool     `yaml:"enable"`

      
        
        23
        +		Port    int      `yaml:"port"`

      
        
        24
        +		HostKey string   `yaml:"host_key"`

      
        
        25
        +		Keys    []string `yaml:"keys"`

      
        
        26
        +	} `yaml:"ssh"`

      
        21
        27
         	Repo struct {

      
        22
        28
         		Dir     string   `yaml:"dir"`

      
        23
        29
         		Readmes []string `yaml:"readmes"`

      
M internal/handlers/git.go
路路路
        20
        20
         

      
        21
        21
         	if err := gitservice.InfoRefs(

      
        22
        22
         		filepath.Join(h.c.Repo.Dir, name), // FIXME: use securejoin

      
        
        23
        +		true,

      
        23
        24
         		w,

      
        24
        25
         	); err != nil {

      
        25
        26
         		slog.Error("git: info/refs", "err", err)

      路路路
        50
        51
         

      
        51
        52
         	if err := gitservice.UploadPack(

      
        52
        53
         		filepath.Join(h.c.Repo.Dir, name),

      
        
        54
        +		true,

      
        53
        55
         		reader,

      
        54
        56
         		newFlushWriter(w),

      
        55
        57
         	); err != nil {

      
A internal/ssh/server.go
路路路
        
        1
        +package ssh

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +	"log/slog"

      
        
        6
        +	"path/filepath"

      
        
        7
        +	"strconv"

      
        
        8
        +

      
        
        9
        +	"github.com/gliderlabs/ssh"

      
        
        10
        +	"olexsmir.xyz/mugit/internal/config"

      
        
        11
        +	"olexsmir.xyz/mugit/internal/git"

      
        
        12
        +	"olexsmir.xyz/mugit/internal/git/gitservice"

      
        
        13
        +

      
        
        14
        +	gossh "golang.org/x/crypto/ssh"

      
        
        15
        +)

      
        
        16
        +

      
        
        17
        +type authorizedKeyType string

      
        
        18
        +

      
        
        19
        +const authorizedKey authorizedKeyType = "authorized"

      
        
        20
        +

      
        
        21
        +type Server struct {

      
        
        22
        +	c        *config.Config

      
        
        23
        +	authKeys []gossh.PublicKey

      
        
        24
        +}

      
        
        25
        +

      
        
        26
        +func NewServer(cfg *config.Config) *Server {

      
        
        27
        +	return &Server{

      
        
        28
        +		c:        cfg,

      
        
        29
        +		authKeys: []gossh.PublicKey{},

      
        
        30
        +	}

      
        
        31
        +}

      
        
        32
        +

      
        
        33
        +func (s *Server) Start() error {

      
        
        34
        +	if err := s.parseAuthKeys(); err != nil {

      
        
        35
        +		return err

      
        
        36
        +	}

      
        
        37
        +

      
        
        38
        +	srv := &ssh.Server{

      
        
        39
        +		Addr:             ":" + strconv.Itoa(s.c.SSH.Port),

      
        
        40
        +		Handler:          s.handler,

      
        
        41
        +		PublicKeyHandler: s.authhandler,

      
        
        42
        +	}

      
        
        43
        +	srv.SetOption(ssh.HostKeyFile(s.c.SSH.HostKey)) // TODO: validate `gossh.ParsePrivateKey`

      
        
        44
        +	return srv.ListenAndServe()

      
        
        45
        +}

      
        
        46
        +

      
        
        47
        +func (s *Server) authhandler(ctx ssh.Context, key ssh.PublicKey) bool {

      
        
        48
        +	fingerprint := gossh.FingerprintSHA256(key)

      
        
        49
        +	if ctx.User() != "git" {

      
        
        50
        +		slog.Info("non git ssh request", "user", ctx.User(), "fingerprint", fingerprint)

      
        
        51
        +		return false

      
        
        52
        +	}

      
        
        53
        +

      
        
        54
        +	slog.Info("ssh request", "fingerprint", fingerprint)

      
        
        55
        +

      
        
        56
        +	authorized := false

      
        
        57
        +	for _, authKey := range s.authKeys {

      
        
        58
        +		if ssh.KeysEqual(key, authKey) {

      
        
        59
        +			authorized = true

      
        
        60
        +			break

      
        
        61
        +		}

      
        
        62
        +	}

      
        
        63
        +

      
        
        64
        +	ctx.SetValue(authorizedKey, authorized)

      
        
        65
        +	return true

      
        
        66
        +}

      
        
        67
        +

      
        
        68
        +func (s *Server) handler(sess ssh.Session) {

      
        
        69
        +	authorized := sess.Context().Value(authorizedKey).(bool)

      
        
        70
        +

      
        
        71
        +	cmd := sess.Command()

      
        
        72
        +	if len(cmd) < 2 {

      
        
        73
        +		fmt.Fprintln(sess, "No command provided")

      
        
        74
        +		sess.Exit(1)

      
        
        75
        +		return

      
        
        76
        +	}

      
        
        77
        +

      
        
        78
        +	gitCmd := cmd[0]

      
        
        79
        +	repoPath := cmd[1]

      
        
        80
        +

      
        
        81
        +	repoPath = filepath.Join(s.c.Repo.Dir, filepath.Clean(repoPath))

      
        
        82
        +	_, err := git.Open(repoPath, "")

      
        
        83
        +	if err != nil {

      
        
        84
        +		s.error(sess, err)

      
        
        85
        +		return

      
        
        86
        +	}

      
        
        87
        +

      
        
        88
        +	fmt.Println(repoPath)

      
        
        89
        +

      
        
        90
        +	switch gitCmd {

      
        
        91
        +	case "git-upload-pack":

      
        
        92
        +		if err := gitservice.UploadPack(repoPath, false, sess, sess); err != nil {

      
        
        93
        +			s.error(sess, err)

      
        
        94
        +			return

      
        
        95
        +		}

      
        
        96
        +		sess.Exit(0)

      
        
        97
        +	case "git-receive-pack":

      
        
        98
        +		if !authorized {

      
        
        99
        +			s.repoNotFound(sess)

      
        
        100
        +			return

      
        
        101
        +		}

      
        
        102
        +

      
        
        103
        +		if err := gitservice.ReceivePack(repoPath, sess, sess); err != nil {

      
        
        104
        +			s.error(sess, err)

      
        
        105
        +			return

      
        
        106
        +		}

      
        
        107
        +		sess.Exit(0)

      
        
        108
        +

      
        
        109
        +	default:

      
        
        110
        +		slog.Error("ssh unsupported command", "cmd", cmd)

      
        
        111
        +		fmt.Fprintln(sess, "Unsupported command")

      
        
        112
        +		sess.Exit(1)

      
        
        113
        +	}

      
        
        114
        +}

      
        
        115
        +

      
        
        116
        +func (s *Server) parseAuthKeys() error {

      
        
        117
        +	parsedKeys := make([]gossh.PublicKey, len(s.c.SSH.Keys))

      
        
        118
        +	for i, key := range s.c.SSH.Keys {

      
        
        119
        +		pkey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(key))

      
        
        120
        +		if err != nil {

      
        
        121
        +			return err

      
        
        122
        +		}

      
        
        123
        +		parsedKeys[i] = pkey

      
        
        124
        +	}

      
        
        125
        +	s.authKeys = parsedKeys

      
        
        126
        +	return nil

      
        
        127
        +}

      
        
        128
        +

      
        
        129
        +func (s *Server) repoNotFound(sess ssh.Session) {

      
        
        130
        +	fmt.Fprintln(sess, "Sorry but repo you're looking for is not found.")

      
        
        131
        +	sess.Exit(1)

      
        
        132
        +}

      
        
        133
        +

      
        
        134
        +func (s *Server) error(sess ssh.Session, err error) {

      
        
        135
        +	fmt.Fprintln(sess, "unexpected server error")

      
        
        136
        +	sess.Exit(1)

      
        
        137
        +	slog.Error("error on ssh side", "err", err)

      
        
        138
        +}

      
M main.go
路路路
        1
        1
         package main

      
        2
        2
         

      
        3
        3
         import (

      
        
        4
        +	"context"

      
        4
        5
         	"log"

      
        5
        6
         	"log/slog"

      
        6
        7
         	"net"

      
        7
        8
         	"net/http"

      
        8
        9
         	"os"

      
        
        10
        +	"os/signal"

      
        9
        11
         	"strconv"

      
        
        12
        +	"syscall"

      
        10
        13
         

      
        11
        14
         	"olexsmir.xyz/mugit/internal/config"

      
        12
        15
         	"olexsmir.xyz/mugit/internal/handlers"

      
        
        16
        +	"olexsmir.xyz/mugit/internal/ssh"

      
        13
        17
         )

      
        14
        18
         

      
        15
        19
         func main() {

      路路路
        26
        30
         		return err

      
        27
        31
         	}

      
        28
        32
         

      
        29
        
        -	mux := handlers.InitRoutes(cfg)

      
        
        33
        +	httpServer := &http.Server{

      
        
        34
        +		Addr:    net.JoinHostPort(cfg.Server.Host, strconv.Itoa(cfg.Server.Port)),

      
        
        35
        +		Handler: handlers.InitRoutes(cfg),

      
        
        36
        +	}

      
        30
        37
         

      
        31
        
        -	port := strconv.Itoa(cfg.Server.Port)

      
        32
        
        -	slog.Info("starting server", "host", cfg.Server.Host, "port", port)

      
        33
        
        -	if err = http.ListenAndServe(net.JoinHostPort(cfg.Server.Host, port), mux); err != nil {

      
        34
        
        -		slog.Error("server error", "err", err)

      
        
        38
        +	go func() {

      
        
        39
        +		slog.Info("starting http server", "host", cfg.Server.Host, "port", cfg.Server.Port)

      
        
        40
        +

      
        
        41
        +		if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {

      
        
        42
        +			slog.Error("HTTP server error", "err", err)

      
        
        43
        +		}

      
        
        44
        +	}()

      
        
        45
        +

      
        
        46
        +	sshServer := ssh.NewServer(cfg)

      
        
        47
        +	if cfg.SSH.Enable {

      
        
        48
        +		slog.Info("starting ssh server", "port", cfg.SSH.Port)

      
        
        49
        +		if err := sshServer.Start(); err != nil {

      
        
        50
        +			slog.Error("ssh server error", "err", err)

      
        
        51
        +		}

      
        
        52
        +	}

      
        
        53
        +

      
        
        54
        +	// Wait for interrupt signal

      
        
        55
        +	sigChan := make(chan os.Signal, 1)

      
        
        56
        +	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

      
        
        57
        +

      
        
        58
        +	sig := <-sigChan

      
        
        59
        +	slog.Info("received signal, starting graceful shutdown", "signal", sig)

      
        
        60
        +

      
        
        61
        +	if err := httpServer.Shutdown(context.TODO()); err != nil {

      
        
        62
        +		slog.Error("HTTP server shutdown error", "err", err)

      
        
        63
        +	} else {

      
        
        64
        +		slog.Info("HTTP server shutdown complete")

      
        35
        65
         	}

      
        36
        66
         

      
        37
        67
         	return nil