7 files changed,
205 insertions(+),
19 deletions(-)
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
jump to
| M | config.yml |
| M | go.mod |
| M | go.sum |
| M | internal/config/config.go |
| M | internal/handlers/git.go |
| A | internal/ssh/server.go |
| M | main.go |
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