diff --git a/assets/config/ssh/Caddyfile b/assets/config/ssh/Caddyfile index 73570a5..d01743e 100644 --- a/assets/config/ssh/Caddyfile +++ b/assets/config/ssh/Caddyfile @@ -8,6 +8,8 @@ repo authp.github.io { base_dir ./tmp/ssh url git@github.com:authp/authp.github.io.git + # auth key ~/.ssh/id_rsa passphrase {env.MY_SSH_KEY_PASSPHRASE} + # auth key ~/.ssh/id_rsa passphrase {env.MY_SSH_KEY_PASSPHRASE} no_strict_host_key_check auth key ~/.ssh/id_rsa branch gh-pages depth 1 diff --git a/caddyfile.go b/caddyfile.go index 5691524..b322b9f 100644 --- a/caddyfile.go +++ b/caddyfile.go @@ -40,7 +40,7 @@ func init() { // repo { // base_dir // url -// auth key [passcode +// auth key [passphrase ] [no_strict_host_key_check] // auth username password // webhook
// branch @@ -61,7 +61,7 @@ const badRepl string = "ERROR_BAD_REPL" var argRules = map[string]argRule{ "base_dir": argRule{Min: 1, Max: 1}, "url": argRule{Min: 1, Max: 1}, - "auth": argRule{Min: 2, Max: 5}, + "auth": argRule{Min: 2, Max: 255}, "branch": argRule{Min: 1, Max: 1}, "depth": argRule{Min: 1, Max: 1}, "update": argRule{Min: 1, Max: 255}, @@ -109,21 +109,26 @@ func parseCaddyfileAppConfig(d *caddyfile.Dispenser, _ interface{}) (interface{} authCfg := &service.AuthConfig{} switch v[0] { case "key": - switch len(v) { - case 2: - authCfg.KeyPath = v[1] - case 4: - authCfg.KeyPath = v[1] - authCfg.KeyPassphrase = v[3] - default: - return nil, d.Errf("malformed %q directive", k) + if len(v) < 2 { + return nil, d.Errf("malformed %q directive: %v", k, v) + } + authCfg.KeyPath = v[1] + if len(v) > 2 { + if v[2] == "passphrase" { + authCfg.KeyPassphrase = v[3] + } } case "username": - if len(v) != 4 { + if len(v) < 4 { return nil, d.Errf("malformed %q directive", k) } authCfg.Username = v[1] - authCfg.Password = v[3] + if v[2] == "password" { + authCfg.Password = v[3] + } + } + if findString(v, "no_strict_host_key_check") { + authCfg.StrictHostKeyCheckingDisabled = true } rc.Auth = authCfg case "webhook": @@ -240,3 +245,12 @@ func findReplace(repl *caddy.Replacer, arr []string) (output []string) { } return output } + +func findString(arr []string, s string) bool { + for _, x := range arr { + if x == s { + return true + } + } + return false +} diff --git a/go.mod b/go.mod index ccefaec..943c493 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,5 @@ require ( github.com/go-git/go-git/v5 v5.3.0 github.com/google/go-cmp v0.5.6 go.uber.org/zap v1.20.0 + golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 ) diff --git a/pkg/service/config.go b/pkg/service/config.go index d46e202..29eb714 100644 --- a/pkg/service/config.go +++ b/pkg/service/config.go @@ -27,10 +27,11 @@ type Config struct { // AuthConfig is authentication configuration in RepositoryConfig. type AuthConfig struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - KeyPath string `json:"key_path,omitempty"` - KeyPassphrase string `json:"key_passphrase,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + KeyPath string `json:"key_path,omitempty"` + KeyPassphrase string `json:"key_passphrase,omitempty"` + StrictHostKeyCheckingDisabled bool `json:"strict_host_key_checking_disabled,omitempty"` } // WebhookConfig is a webhook configuration in RepositoryConfig. @@ -60,6 +61,7 @@ type RepositoryConfig struct { Auth *AuthConfig `json:"auth,omitempty"` Webhooks []*WebhookConfig `json:"webhooks,omitempty"` PostPullExec []*ExecConfig `json:"post_pull_exec,omitempty"` + transport string `json:"transport,omitempty"` } // NewConfig returns an instance of Config. @@ -99,10 +101,15 @@ func (rc *RepositoryConfig) validate() error { if rc.Address == "" { return errors.ErrRepositoryConfigAddressEmpty } + if !strings.HasSuffix(rc.Address, ".git") { + return errors.ErrRepositoryConfigAddressUnsupported.WithArgs(rc.Address) + } + switch { - case strings.HasSuffix(rc.Address, ".git"): + case strings.HasPrefix(rc.Address, "https://"), strings.HasPrefix(rc.Address, "http://"): + rc.transport = "http" default: - return errors.ErrRepositoryConfigAddressUnsupported.WithArgs(rc.Address) + rc.transport = "ssh" } return nil } diff --git a/pkg/service/repo.go b/pkg/service/repo.go index 741d41c..128fb69 100644 --- a/pkg/service/repo.go +++ b/pkg/service/repo.go @@ -17,10 +17,15 @@ package service import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" "go.uber.org/zap" + cryptossh "golang.org/x/crypto/ssh" "os" "path" "path/filepath" + "strings" "sync" "time" ) @@ -45,6 +50,8 @@ func (r *Repository) update() error { r.mu.Lock() defer r.mu.Unlock() + r.Config.BaseDir = expandDir(r.Config.BaseDir) + baseDirExists, err := dirExists(r.Config.BaseDir) if err != nil { return err @@ -62,19 +69,13 @@ func (r *Repository) update() error { } if !repoDirExists { // Clone the repository. - opts := &git.CloneOptions{ - URL: r.Config.Address, - } - if r.Config.Depth > 0 { - opts.Depth = r.Config.Depth - } - if r.Config.Branch != "" { - opts.ReferenceName = plumbing.NewBranchReferenceName(r.Config.Branch) + opts := &git.CloneOptions{} + if err := configureCloneOptions(r.Config, opts); err != nil { + return err } if _, err := git.PlainClone(repoDir, false, opts); err != nil { return err } - // return nil } // Pull the repository. @@ -90,18 +91,11 @@ func (r *Repository) update() error { if err != nil { return err } - opts := &git.PullOptions{ - RemoteName: "origin", - SingleBranch: true, - } - if r.Config.Branch != "" { - opts.ReferenceName = plumbing.NewBranchReferenceName(r.Config.Branch) - } - if r.Config.Depth > 0 { - opts.Depth = r.Config.Depth + opts := &git.PullOptions{} + if err := configurePullOptions(r.Config, opts); err != nil { + return err } - if err := w.Pull(opts); err != nil { if err == git.NoErrAlreadyUpToDate { r.logger.Debug( @@ -140,3 +134,107 @@ func dirExists(s string) (bool, error) { return true, err } + +func configureCloneOptions(cfg *RepositoryConfig, opts *git.CloneOptions) error { + opts.URL = cfg.Address + trAuthMethod, err := configureAuthOptions(cfg) + if err != nil { + return err + } + opts.Auth = trAuthMethod + if cfg.Depth > 0 { + opts.Depth = cfg.Depth + } + if cfg.Branch != "" { + opts.ReferenceName = plumbing.NewBranchReferenceName(cfg.Branch) + } + return nil +} + +func configurePullOptions(cfg *RepositoryConfig, opts *git.PullOptions) error { + opts.RemoteName = "origin" + trAuthMethod, err := configureAuthOptions(cfg) + if err != nil { + return err + } + opts.Auth = trAuthMethod + if cfg.Depth > 0 { + opts.Depth = cfg.Depth + } + if cfg.Branch != "" { + opts.ReferenceName = plumbing.NewBranchReferenceName(cfg.Branch) + opts.SingleBranch = true + } + return nil +} + +func configureAuthOptions(cfg *RepositoryConfig) (transport.AuthMethod, error) { + if cfg.Auth == nil { + return nil, nil + } + cfg.Auth.KeyPath = expandDir(cfg.Auth.KeyPath) + + switch cfg.transport { + case "http": + // Configure authentication for HTTP/S. + switch { + case cfg.Auth.Username != "": + return &http.BasicAuth{ + Username: cfg.Auth.Username, + Password: cfg.Auth.Password, + }, nil + } + case "ssh": + // Configure authentication for SSH. + switch { + case cfg.Auth.KeyPath != "": + var publicKeysUser string + switch { + case strings.Contains(cfg.Address, "@"): + cfgAddressArr := strings.SplitN(cfg.Address, "@", 2) + publicKeysUser = cfgAddressArr[0] + case cfg.Auth.Username != "": + publicKeysUser = cfg.Auth.Username + } + + if publicKeysUser == "" { + publicKeysUser = "git" + } + + publicKeys, err := ssh.NewPublicKeysFromFile(publicKeysUser, cfg.Auth.KeyPath, cfg.Auth.KeyPassphrase) + if err != nil { + return nil, err + } + if cfg.Auth.StrictHostKeyCheckingDisabled { + publicKeys.HostKeyCallbackHelper = ssh.HostKeyCallbackHelper{ + HostKeyCallback: cryptossh.InsecureIgnoreHostKey(), + } + } + return publicKeys, nil + case cfg.Auth.Username != "": + password := &ssh.Password{ + User: cfg.Auth.Username, + Password: cfg.Auth.Password, + } + if cfg.Auth.StrictHostKeyCheckingDisabled { + password.HostKeyCallbackHelper = ssh.HostKeyCallbackHelper{ + HostKeyCallback: cryptossh.InsecureIgnoreHostKey(), + } + } + return password, nil + } + } + return nil, nil +} + +func expandDir(s string) string { + if s == "" || !strings.HasPrefix(s, "~") { + return s + } + hd, err := os.UserHomeDir() + if err != nil { + return s + } + output := hd + s[1:] + return output +}