Initial import
This commit is contained in:
		
							
								
								
									
										6
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
    "gopls": {
 | 
			
		||||
        "formatting.local": "git.maze.io/maze/conduit"
 | 
			
		||||
    },
 | 
			
		||||
    "CodeGPT.apiKey": "Ollama"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										96
									
								
								auth/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								auth/auth.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ErrUnauthorized = errors.New("unauthorized")
 | 
			
		||||
 | 
			
		||||
// Provider implements zero or more of the [Password], [PublicKey] interfaces.
 | 
			
		||||
type Provider any
 | 
			
		||||
 | 
			
		||||
// Password authenticator.
 | 
			
		||||
type Password interface {
 | 
			
		||||
	VerifyPassword(meta ssh.ConnMetadata, password string) (Principal, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Token authenticator.
 | 
			
		||||
type Token interface {
 | 
			
		||||
	Instruction() string
 | 
			
		||||
 | 
			
		||||
	Prompt() string
 | 
			
		||||
 | 
			
		||||
	// Multiline accepts multiline input and wait for the user to supply
 | 
			
		||||
	// an empty line.
 | 
			
		||||
	Multiline() bool
 | 
			
		||||
 | 
			
		||||
	VerifyToken(meta ssh.ConnMetadata, token string) (Principal, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PublicKey authenticator.
 | 
			
		||||
type PublicKey interface {
 | 
			
		||||
	VerifyPublicKey(meta ssh.ConnMetadata, publicKey ssh.PublicKey) (Principal, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CertificateAuthority struct {
 | 
			
		||||
	checker *ssh.CertChecker
 | 
			
		||||
	keys    []ssh.PublicKey
 | 
			
		||||
	blob    [][]byte
 | 
			
		||||
	user    bool
 | 
			
		||||
	host    bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUserCertificateAuthority(keys ...ssh.PublicKey) *CertificateAuthority {
 | 
			
		||||
	var blob [][]byte
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		blob = append(blob, key.Marshal())
 | 
			
		||||
	}
 | 
			
		||||
	auth := &CertificateAuthority{
 | 
			
		||||
		checker: &ssh.CertChecker{
 | 
			
		||||
			Clock: time.Now,
 | 
			
		||||
		},
 | 
			
		||||
		keys: keys,
 | 
			
		||||
		blob: blob,
 | 
			
		||||
		user: true,
 | 
			
		||||
	}
 | 
			
		||||
	auth.checker.IsUserAuthority = auth.isUserAuthority
 | 
			
		||||
	return auth
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *CertificateAuthority) isUserAuthority(key ssh.PublicKey) bool {
 | 
			
		||||
	if !auth.user {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	blob := key.Marshal()
 | 
			
		||||
	for _, other := range auth.blob {
 | 
			
		||||
		if bytes.Equal(blob, other) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *CertificateAuthority) VerifyPublicKey(meta ssh.ConnMetadata, publicKey ssh.PublicKey) (Principal, error) {
 | 
			
		||||
	log := logger.StandardLog.Values(logger.Values{
 | 
			
		||||
		"client":  meta.RemoteAddr().String(),
 | 
			
		||||
		"key":     strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey))),
 | 
			
		||||
		"user":    meta.User(),
 | 
			
		||||
		"version": string(meta.ClientVersion()),
 | 
			
		||||
	})
 | 
			
		||||
	log.Debug("Verifying user certificate")
 | 
			
		||||
 | 
			
		||||
	_, err := auth.checker.Authenticate(meta, publicKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cert := publicKey.(*ssh.Certificate)
 | 
			
		||||
	return UserCertificatePrincipal{cert}, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										104
									
								
								auth/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								auth/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-crypt/crypt"
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
	"git.maze.io/maze/conduit/ssh/sshutil"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type passwordFile map[string]string
 | 
			
		||||
 | 
			
		||||
func PasswordFile(name string) (Password, error) {
 | 
			
		||||
	f, err := os.Open(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	s := bufio.NewScanner(f)
 | 
			
		||||
	p := make(passwordFile)
 | 
			
		||||
	for s.Scan() {
 | 
			
		||||
		line := s.Text()
 | 
			
		||||
		if len(line) == 0 || line[0] == '#' || strings.HasPrefix(line, "//") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if i := strings.IndexByte(line, ':'); i > 0 {
 | 
			
		||||
			p[line[:i]] = line[i+1:]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return p, s.Err()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth passwordFile) VerifyPassword(meta ssh.ConnMetadata, password string) (Principal, error) {
 | 
			
		||||
	encodedDigest := auth[meta.User()]
 | 
			
		||||
	if ok, err := crypt.CheckPasswordWithPlainText(password, encodedDigest); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else if !ok {
 | 
			
		||||
		return nil, ErrUnauthorized
 | 
			
		||||
	}
 | 
			
		||||
	return MakePasswordPrincipal(meta), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type publibKeyFile map[string][]ssh.PublicKey
 | 
			
		||||
 | 
			
		||||
func PublicKeyFile(name string) (PublicKey, error) {
 | 
			
		||||
	log := logger.StandardLog.Value("path", name)
 | 
			
		||||
	log.Trace("Parsing public keys file")
 | 
			
		||||
 | 
			
		||||
	f, err := os.Open(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		s      = bufio.NewScanner(f)
 | 
			
		||||
		p      = make(publibKeyFile)
 | 
			
		||||
		lineno int
 | 
			
		||||
	)
 | 
			
		||||
	for s.Scan() {
 | 
			
		||||
		line := s.Text()
 | 
			
		||||
		lineno++
 | 
			
		||||
		if len(line) == 0 || line[0] == '#' || strings.HasPrefix(line, "//") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if i := strings.IndexByte(line, ':'); i > 0 {
 | 
			
		||||
			//k, err := ssh.ParsePublicKey([]byte(line[i+1:]))
 | 
			
		||||
			k, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line[i+1:]))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, fmt.Errorf("auth: invalid public key in %s:%d: %w", name, lineno, err)
 | 
			
		||||
			}
 | 
			
		||||
			log.Values(logger.Values{
 | 
			
		||||
				"user": line[:i],
 | 
			
		||||
				"key":  k.Type(),
 | 
			
		||||
			}).Trace("Parsed authorized public key")
 | 
			
		||||
			p[line[:i]] = append(p[line[:i]], k)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return p, s.Err()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth publibKeyFile) VerifyPublicKey(meta ssh.ConnMetadata, key ssh.PublicKey) (Principal, error) {
 | 
			
		||||
	log := logger.StandardLog.Values(logger.Values{
 | 
			
		||||
		"client":   meta.RemoteAddr().String(),
 | 
			
		||||
		"key_type": sshutil.KeyType(key),
 | 
			
		||||
		"key_bits": sshutil.KeyBits(key),
 | 
			
		||||
		"user":     meta.User(),
 | 
			
		||||
		"version":  string(meta.ClientVersion()),
 | 
			
		||||
	})
 | 
			
		||||
	log.Debug("Verifying user public key")
 | 
			
		||||
 | 
			
		||||
	blob := key.Marshal()
 | 
			
		||||
	keys := auth[meta.User()]
 | 
			
		||||
	for _, other := range keys {
 | 
			
		||||
		if bytes.Equal(other.Marshal(), blob) {
 | 
			
		||||
			return MakePublicKeyPrincipal(meta, key), nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil, ErrUnauthorized
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										134
									
								
								auth/file_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								auth/file_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var testAddr = &net.TCPAddr{
 | 
			
		||||
	IP:   net.ParseIP("127.1.2.3"),
 | 
			
		||||
	Port: 22,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type testConnMetadata struct {
 | 
			
		||||
	user          string
 | 
			
		||||
	sessionID     []byte
 | 
			
		||||
	clientVersion string
 | 
			
		||||
	serverVersion string
 | 
			
		||||
	laddr, raddr  net.Addr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t testConnMetadata) User() string          { return t.user }
 | 
			
		||||
func (t testConnMetadata) SessionID() []byte     { return t.sessionID }
 | 
			
		||||
func (t testConnMetadata) ClientVersion() []byte { return []byte(t.clientVersion) }
 | 
			
		||||
func (t testConnMetadata) ServerVersion() []byte { return []byte(t.serverVersion) }
 | 
			
		||||
func (t testConnMetadata) RemoteAddr() net.Addr  { return t.raddr }
 | 
			
		||||
func (t testConnMetadata) LocalAddr() net.Addr   { return t.laddr }
 | 
			
		||||
 | 
			
		||||
var _ ssh.ConnMetadata = (*testConnMetadata)(nil)
 | 
			
		||||
 | 
			
		||||
func TestPasswordFile(t *testing.T) {
 | 
			
		||||
	a, err := PasswordFile(filepath.Join("testdata", "passwd"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		Username string
 | 
			
		||||
		Password string
 | 
			
		||||
	}{
 | 
			
		||||
		{"example", "example"},
 | 
			
		||||
		{"bcrypt", "example"},
 | 
			
		||||
	}
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.Username, func(it *testing.T) {
 | 
			
		||||
			p, err := a.VerifyPassword(testConnMetadata{user: test.Username}, test.Password)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				it.Error(err)
 | 
			
		||||
			} else {
 | 
			
		||||
				it.Logf("%s: %s (%T)", p.Type(), p.Identity(), p)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPublicKeyFile(t *testing.T) {
 | 
			
		||||
	a, err := PublicKeyFile(filepath.Join("testdata", "pubkey"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		Name      string
 | 
			
		||||
		Username  string
 | 
			
		||||
		PublicKey string
 | 
			
		||||
	}{
 | 
			
		||||
		{"single/ed25519", "test_a", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFo1lt6lEk+1VUrMbhlaVpkI0p1TFUGujHaKKn7+VoGb"},
 | 
			
		||||
		{"dual/ed25519", "test_b", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICA9dQjNeX3eBvkOXJN+nJm1C2W9UtRiLbK9O87Mjkir"},
 | 
			
		||||
		{"dual/rsa", "test_b", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFq82Pfsg7KjTU5LN4jikxITDQhCWB3TFxQdXTgYtKt40+gv88hZkemM1MYTzR30bUX/zcRsioUSwr3u7/2La7ti+BoilsHjrEx4w+nxNGCCe8D3M6K5Xi8MPL2AqbXFqkPSEpX+psrs+qILfNhs1lWAsN7GLP0cTIxPynFNECwJnUlleN0hsn8N8bQCoUInZQGmHwIHq62H+3IPbv7Vko3J0Zrqqo4OqfeV5BA0By7ZP+2Jd9ZsLJ2efaiALcs6oTk0v95wVQ36wp605x9ePYg6zHzIZDfpA400RqeuiZF5jpiG7q3eb0+CysfMbU0BpfeHmCq15PFYqre8HKAJZ3"},
 | 
			
		||||
	}
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.Username, func(it *testing.T) {
 | 
			
		||||
			k, _, _, _, err := ssh.ParseAuthorizedKey([]byte(test.PublicKey))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				it.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			p, err := a.VerifyPublicKey(testConnMetadata{
 | 
			
		||||
				user:  test.Username,
 | 
			
		||||
				laddr: testAddr,
 | 
			
		||||
				raddr: testAddr,
 | 
			
		||||
			}, k)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				it.Error(err)
 | 
			
		||||
			} else {
 | 
			
		||||
				it.Logf("%s: %s (%T)", p.Type(), p.Identity(), p)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BenchmarkPasswordFileHits(b *testing.B) {
 | 
			
		||||
	a, err := PasswordFile(filepath.Join("testdata", "passwd"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c := testConnMetadata{user: "example"}
 | 
			
		||||
 | 
			
		||||
	b.ResetTimer()
 | 
			
		||||
	for b.Loop() {
 | 
			
		||||
		a.VerifyPassword(c, "example")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BenchmarkPasswordFileMissPassword(b *testing.B) {
 | 
			
		||||
	a, err := PasswordFile(filepath.Join("testdata", "passwd"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c := testConnMetadata{user: "example"}
 | 
			
		||||
 | 
			
		||||
	b.ResetTimer()
 | 
			
		||||
	for b.Loop() {
 | 
			
		||||
		a.VerifyPassword(c, "invalid")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BenchmarkPasswordFileMissPrincipal(b *testing.B) {
 | 
			
		||||
	a, err := PasswordFile(filepath.Join("testdata", "passwd"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c := testConnMetadata{user: "invalid"}
 | 
			
		||||
 | 
			
		||||
	b.ResetTimer()
 | 
			
		||||
	for b.Loop() {
 | 
			
		||||
		a.VerifyPassword(c, "example")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										329
									
								
								auth/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										329
									
								
								auth/handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,329 @@
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
	"git.maze.io/maze/conduit/policy"
 | 
			
		||||
	"git.maze.io/maze/conduit/policy/input"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// None accepts no authentication, this is only useful for debugging/testing.
 | 
			
		||||
type None struct{}
 | 
			
		||||
 | 
			
		||||
func (None) HandleAccept(conn net.Conn, serverConfig *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
 | 
			
		||||
	config := new(ssh.ServerConfig)
 | 
			
		||||
	if serverConfig != nil {
 | 
			
		||||
		*config = *serverConfig
 | 
			
		||||
	}
 | 
			
		||||
	config.NoClientAuth = true
 | 
			
		||||
	return ssh.NewServerConn(conn, config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UserCertificate offers user certificate based authentication.
 | 
			
		||||
type UserCertificate struct {
 | 
			
		||||
	// Loader is the policy loader.
 | 
			
		||||
	Loader policy.Loader
 | 
			
		||||
 | 
			
		||||
	// CA is our trusted user certificate authority public keys.
 | 
			
		||||
	CA []ssh.PublicKey
 | 
			
		||||
 | 
			
		||||
	// VerifyCallback can optionally be used to perform additional checks on the certificate.
 | 
			
		||||
	VerifyCallback func(*ssh.Certificate) bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth UserCertificate) HandleAccept(conn net.Conn, serverConfig *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
 | 
			
		||||
	log := logger.StandardLog.Value("client", conn.RemoteAddr().String())
 | 
			
		||||
 | 
			
		||||
	policy, err := auth.Loader.LoadPolicy("conduit/auth/user_certificate")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	_ = policy
 | 
			
		||||
 | 
			
		||||
	checker := &ssh.CertChecker{
 | 
			
		||||
		Clock: time.Now,
 | 
			
		||||
	}
 | 
			
		||||
	checker.IsUserAuthority = func(key ssh.PublicKey) bool {
 | 
			
		||||
		blob := key.Marshal()
 | 
			
		||||
		for _, ca := range auth.CA {
 | 
			
		||||
			if bytes.Equal(ca.Marshal(), blob) {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config := new(ssh.ServerConfig)
 | 
			
		||||
	if serverConfig != nil {
 | 
			
		||||
		*config = *serverConfig
 | 
			
		||||
	}
 | 
			
		||||
	config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
 | 
			
		||||
		if _, err := checker.Authenticate(conn, key); err != nil {
 | 
			
		||||
			log.Err(err).Debug("Certificate check failed")
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cert := key.(*ssh.Certificate)
 | 
			
		||||
		principal := UserCertificatePrincipal{Certificate: cert}
 | 
			
		||||
		var (
 | 
			
		||||
			query = "data." + policy.Package()
 | 
			
		||||
			input = struct {
 | 
			
		||||
				Conn      *input.ConnMetadata `json:"conn"`
 | 
			
		||||
				Principal Principal           `json:"principal"`
 | 
			
		||||
			}{input.NewConnMetadata(conn), principal}
 | 
			
		||||
			result struct {
 | 
			
		||||
				Permit bool `mapstructure:"permit"`
 | 
			
		||||
			}
 | 
			
		||||
		)
 | 
			
		||||
		if err = policy.Query(query, input, &result); err != nil {
 | 
			
		||||
			log.Err(err).Warn("Policy query returned error")
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if !result.Permit {
 | 
			
		||||
			log.Debug("Policy rejected certificate")
 | 
			
		||||
			return nil, ErrUnauthorized
 | 
			
		||||
		}
 | 
			
		||||
		return &cert.Permissions, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ssh.NewServerConn(conn, config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MultiFactor offers policy-based MFA (multi factor authentication).
 | 
			
		||||
type MultiFactor struct {
 | 
			
		||||
	// Loader for the policy rules.
 | 
			
		||||
	Loader policy.Loader
 | 
			
		||||
 | 
			
		||||
	// PublicKey enables public key authentication.
 | 
			
		||||
	PublicKey PublicKey
 | 
			
		||||
 | 
			
		||||
	// UserCA enables user certificate based authentication.
 | 
			
		||||
	UserCA []ssh.PublicKey
 | 
			
		||||
 | 
			
		||||
	// Password enables password authentication.
 | 
			
		||||
	Password Password
 | 
			
		||||
 | 
			
		||||
	// Token enabled token authentication.
 | 
			
		||||
	Token Token
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mfa *MultiFactor) HandleAccept(conn net.Conn, serverConfig *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
 | 
			
		||||
	log := logger.StandardLog.Value("client", conn.RemoteAddr().String())
 | 
			
		||||
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			if err, ok := r.(error); ok {
 | 
			
		||||
				log.Err(err).Error("Recovered from panic in accept handler! This is a bug!")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	rules, err := mfa.Loader.LoadPolicy("conduit/auth/mfa")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config := new(ssh.ServerConfig)
 | 
			
		||||
	if serverConfig != nil {
 | 
			
		||||
		*config = *serverConfig
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		meta            ssh.ConnMetadata
 | 
			
		||||
		principals      []Principal
 | 
			
		||||
		tokenAuth       = mfa.Token
 | 
			
		||||
		passwordAuth    = mfa.Password
 | 
			
		||||
		publicKeyAuth   = mfa.PublicKey
 | 
			
		||||
		certificateAuth []ssh.PublicKey
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	checkPolicy := func() (perms *ssh.Permissions, err error) {
 | 
			
		||||
		var (
 | 
			
		||||
			query = "data." + rules.Package()
 | 
			
		||||
			input = struct {
 | 
			
		||||
				Conn       *input.ConnMetadata `json:"conn"`
 | 
			
		||||
				Principals []Principal         `json:"principals"`
 | 
			
		||||
			}{input.NewConnMetadata(meta), principals}
 | 
			
		||||
			result struct {
 | 
			
		||||
				Permit            bool `mapstructure:"permit"`
 | 
			
		||||
				PermitPassword    bool `mapstructure:"permit_password"`
 | 
			
		||||
				PermitToken       bool `mapstructure:"permit_token"`
 | 
			
		||||
				PermitCertificate bool `mapstructure:"permit_certificate"`
 | 
			
		||||
				PermitPublicKey   bool `mapstructure:"permit_publickey"`
 | 
			
		||||
			}
 | 
			
		||||
		)
 | 
			
		||||
		if err = rules.Query(query, input, &result); err != nil {
 | 
			
		||||
			log.Err(err).Warn("Policy query returned error")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !result.PermitPassword && passwordAuth != nil {
 | 
			
		||||
			log.Debug("Policy disabled password authentication")
 | 
			
		||||
			passwordAuth = nil
 | 
			
		||||
		}
 | 
			
		||||
		if passwordAuth == nil && config.PasswordCallback != nil {
 | 
			
		||||
			// Disable password authentication.
 | 
			
		||||
			log.Debug("Policy disabled password callback")
 | 
			
		||||
			config.PasswordCallback = nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !result.PermitToken && tokenAuth != nil {
 | 
			
		||||
			log.Debug("Policy disabled token authentication")
 | 
			
		||||
			tokenAuth = nil
 | 
			
		||||
		}
 | 
			
		||||
		if tokenAuth == nil && config.KeyboardInteractiveCallback != nil {
 | 
			
		||||
			// Disable token authentication.
 | 
			
		||||
			log.Debug("Policy disabled token authentication")
 | 
			
		||||
			config.KeyboardInteractiveCallback = nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !result.PermitCertificate && len(certificateAuth) != 0 {
 | 
			
		||||
			log.Debug("Policy disabled certificate authentication")
 | 
			
		||||
			certificateAuth = nil
 | 
			
		||||
		}
 | 
			
		||||
		if !result.PermitPublicKey && publicKeyAuth != nil {
 | 
			
		||||
			log.Debug("Policy disabled public key authentication")
 | 
			
		||||
			publicKeyAuth = nil
 | 
			
		||||
		}
 | 
			
		||||
		if publicKeyAuth == nil && len(certificateAuth) == 0 && config.PublicKeyCallback != nil {
 | 
			
		||||
			// Disable pubkey authentication.
 | 
			
		||||
			log.Debug("Policy disabled public key callback")
 | 
			
		||||
			config.PublicKeyCallback = nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !result.Permit {
 | 
			
		||||
			log.Debug("Policy rejected principals; not authorized")
 | 
			
		||||
			return nil, ErrUnauthorized
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(principals) == 0 {
 | 
			
		||||
			log.Debug("No valid principals; not authorized")
 | 
			
		||||
			return nil, ErrUnauthorized
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		attr := principals[0].Attributes()
 | 
			
		||||
		return &ssh.Permissions{
 | 
			
		||||
			CriticalOptions: attr.Options,
 | 
			
		||||
			Extensions:      attr.Extensions,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if mfa.PublicKey != nil || len(mfa.UserCA) > 0 {
 | 
			
		||||
		log = log.Values(logger.Values{
 | 
			
		||||
			"permit_pubkey":      mfa.PublicKey != nil,
 | 
			
		||||
			"permit_certificate": len(mfa.UserCA) > 0,
 | 
			
		||||
		})
 | 
			
		||||
		log.Trace("MFA enabling public key authentication")
 | 
			
		||||
 | 
			
		||||
		var checker *ssh.CertChecker
 | 
			
		||||
		if len(mfa.UserCA) > 0 {
 | 
			
		||||
			checker = &ssh.CertChecker{
 | 
			
		||||
				Clock: time.Now,
 | 
			
		||||
			}
 | 
			
		||||
			checker.IsUserAuthority = func(key ssh.PublicKey) bool {
 | 
			
		||||
				blob := key.Marshal()
 | 
			
		||||
				for _, ca := range mfa.UserCA {
 | 
			
		||||
					if bytes.Equal(ca.Marshal(), blob) {
 | 
			
		||||
						return true
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
 | 
			
		||||
			var (
 | 
			
		||||
				principal Principal
 | 
			
		||||
				errs      []error
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			if checker != nil {
 | 
			
		||||
				if _, err := checker.Authenticate(conn, key); err != nil {
 | 
			
		||||
					log.Err(err).Debug("Certificate check failed")
 | 
			
		||||
					errs = append(errs, err)
 | 
			
		||||
				} else {
 | 
			
		||||
					principal = UserCertificatePrincipal{Certificate: key.(*ssh.Certificate)}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if principal == nil && mfa.PublicKey != nil {
 | 
			
		||||
				if _, err := mfa.PublicKey.VerifyPublicKey(conn, key); err != nil {
 | 
			
		||||
					log.Err(err).Debug("Public key check failed")
 | 
			
		||||
					errs = append(errs, err)
 | 
			
		||||
				} else {
 | 
			
		||||
					principal = PublicKeyPrincipal{User: conn.User(), PublicKey: key}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if principal == nil {
 | 
			
		||||
				if len(errs) > 0 {
 | 
			
		||||
					return nil, errors.Join(errs...)
 | 
			
		||||
				}
 | 
			
		||||
				return nil, ErrUnauthorized
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			meta = conn
 | 
			
		||||
			principals = append(principals, principal)
 | 
			
		||||
			return checkPolicy()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if mfa.Password != nil {
 | 
			
		||||
		log.Trace("MFA enabling password authentication")
 | 
			
		||||
 | 
			
		||||
		config.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
 | 
			
		||||
			if _, err := mfa.Password.VerifyPassword(conn, string(password)); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			meta = conn
 | 
			
		||||
			principals = append(principals, PasswordPrincipal{User: conn.User()})
 | 
			
		||||
			return checkPolicy()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if mfa.Token != nil {
 | 
			
		||||
		log.Trace("MFA enabling token authentication")
 | 
			
		||||
 | 
			
		||||
		config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
 | 
			
		||||
			var (
 | 
			
		||||
				multiline = mfa.Token.Multiline()
 | 
			
		||||
				token     string
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			for {
 | 
			
		||||
				answers, err := challenge("", mfa.Token.Instruction(), []string{"Token: "}, []bool{false})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
				} else if len(answers) != 1 {
 | 
			
		||||
					return nil, ErrUnauthorized
 | 
			
		||||
				}
 | 
			
		||||
				if multiline {
 | 
			
		||||
					if answers[0] == "" {
 | 
			
		||||
						break
 | 
			
		||||
					}
 | 
			
		||||
					token += strings.TrimSpace(answers[0])
 | 
			
		||||
				} else {
 | 
			
		||||
					token = answers[0]
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if _, err := mfa.Token.VerifyToken(conn, token); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			meta = conn
 | 
			
		||||
			principals = append(principals, TokenPrincipal{User: conn.User()})
 | 
			
		||||
			return checkPolicy()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ssh.NewServerConn(conn, config)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										148
									
								
								auth/principal.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								auth/principal.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,148 @@
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/policy/input"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TypeCertificate = "certificate"
 | 
			
		||||
	TypeToken       = "token"
 | 
			
		||||
	TypePassword    = "password"
 | 
			
		||||
	TypePublicKey   = "publickey"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Attributes struct {
 | 
			
		||||
	Groups     []string          `json:"groups"`
 | 
			
		||||
	Principals []string          `json:"principals"`
 | 
			
		||||
	Options    map[string]string `json:"options"`
 | 
			
		||||
	Extensions map[string]string `json:"extensions"`
 | 
			
		||||
	Source     any               `json:"source"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MakeAttributes() Attributes {
 | 
			
		||||
	return Attributes{
 | 
			
		||||
		Groups:     make([]string, 0),
 | 
			
		||||
		Principals: make([]string, 0),
 | 
			
		||||
		Options:    make(map[string]string),
 | 
			
		||||
		Extensions: make(map[string]string),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Principal interface {
 | 
			
		||||
	Type() string // "user", "host", "key", etc.
 | 
			
		||||
 | 
			
		||||
	// Identity is the user identity.
 | 
			
		||||
	Identity() string
 | 
			
		||||
 | 
			
		||||
	// Attributes for the principal.
 | 
			
		||||
	Attributes() Attributes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PasswordPrincipal struct {
 | 
			
		||||
	User string
 | 
			
		||||
	Attr Attributes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MakePasswordPrincipal(meta ssh.ConnMetadata) PasswordPrincipal {
 | 
			
		||||
	attr := MakeAttributes()
 | 
			
		||||
	attr.Source = meta
 | 
			
		||||
	return PasswordPrincipal{
 | 
			
		||||
		User: meta.User(),
 | 
			
		||||
		Attr: attr,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p PasswordPrincipal) Type() string           { return TypePassword }
 | 
			
		||||
func (p PasswordPrincipal) Identity() string       { return p.User }
 | 
			
		||||
func (p PasswordPrincipal) Attributes() Attributes { return p.Attr }
 | 
			
		||||
func (p PasswordPrincipal) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(struct {
 | 
			
		||||
		Kind string     `json:"type"`
 | 
			
		||||
		User string     `json:"identity"`
 | 
			
		||||
		Attr Attributes `json:"attr"`
 | 
			
		||||
	}{
 | 
			
		||||
		p.Type(),
 | 
			
		||||
		p.Identity(),
 | 
			
		||||
		p.Attributes(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TokenPrincipal struct {
 | 
			
		||||
	User string     `json:"identity"`
 | 
			
		||||
	Attr Attributes `json:"attr"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p TokenPrincipal) Type() string           { return TypeToken }
 | 
			
		||||
func (p TokenPrincipal) Identity() string       { return p.User }
 | 
			
		||||
func (p TokenPrincipal) Attributes() Attributes { return p.Attr }
 | 
			
		||||
func (p TokenPrincipal) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(struct {
 | 
			
		||||
		Kind string     `json:"type"`
 | 
			
		||||
		User string     `json:"identity"`
 | 
			
		||||
		Attr Attributes `json:"attr"`
 | 
			
		||||
	}{
 | 
			
		||||
		p.Type(),
 | 
			
		||||
		p.Identity(),
 | 
			
		||||
		p.Attributes(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PublicKeyPrincipal struct {
 | 
			
		||||
	User string
 | 
			
		||||
	ssh.PublicKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MakePublicKeyPrincipal(meta ssh.ConnMetadata, key ssh.PublicKey) PublicKeyPrincipal {
 | 
			
		||||
	attr := MakeAttributes()
 | 
			
		||||
	attr.Source = meta
 | 
			
		||||
	return PublicKeyPrincipal{
 | 
			
		||||
		User:      meta.User(),
 | 
			
		||||
		PublicKey: key,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p PublicKeyPrincipal) Type() string           { return TypePublicKey }
 | 
			
		||||
func (p PublicKeyPrincipal) Identity() string       { return p.User }
 | 
			
		||||
func (p PublicKeyPrincipal) Attributes() Attributes { return MakeAttributes() }
 | 
			
		||||
func (p PublicKeyPrincipal) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(struct {
 | 
			
		||||
		Kind string     `json:"type"`
 | 
			
		||||
		User string     `json:"identity"`
 | 
			
		||||
		Attr Attributes `json:"attr"`
 | 
			
		||||
	}{
 | 
			
		||||
		p.Type(),
 | 
			
		||||
		p.Identity(),
 | 
			
		||||
		p.Attributes(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UserCertificatePrincipal struct {
 | 
			
		||||
	*ssh.Certificate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cert UserCertificatePrincipal) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(struct {
 | 
			
		||||
		Kind string     `json:"type"`
 | 
			
		||||
		User string     `json:"identity"`
 | 
			
		||||
		Attr Attributes `json:"attr"`
 | 
			
		||||
	}{
 | 
			
		||||
		cert.Type(),
 | 
			
		||||
		cert.Identity(),
 | 
			
		||||
		cert.Attributes(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cert UserCertificatePrincipal) Type() string     { return TypeCertificate }
 | 
			
		||||
func (cert UserCertificatePrincipal) Identity() string { return cert.KeyId }
 | 
			
		||||
func (cert UserCertificatePrincipal) Attributes() Attributes {
 | 
			
		||||
	return Attributes{
 | 
			
		||||
		Principals: cert.ValidPrincipals,
 | 
			
		||||
		Options:    cert.CriticalOptions,
 | 
			
		||||
		Extensions: cert.Extensions,
 | 
			
		||||
		Source:     input.NewCertificate(cert.Certificate),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										105
									
								
								auth/system_linux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								auth/system_linux.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
			
		||||
//go:build linux
 | 
			
		||||
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/user"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/msteinert/pam"
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type system struct{}
 | 
			
		||||
 | 
			
		||||
type systemPrincipal struct {
 | 
			
		||||
	*user.User
 | 
			
		||||
	attributes Attributes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newSystemPrincipal(name string) (*systemPrincipal, error) {
 | 
			
		||||
	u, err := user.Lookup(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	a := Attributes{
 | 
			
		||||
		Custom: make(map[string]any),
 | 
			
		||||
	}
 | 
			
		||||
	if gids, err := u.GroupIds(); err == nil {
 | 
			
		||||
		a.Custom["groups"] = gids
 | 
			
		||||
		for _, gid := range gids {
 | 
			
		||||
			if g, err := user.LookupGroupId(gid); err == nil {
 | 
			
		||||
				a.Groups = append(a.Groups, g.Name)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &systemPrincipal{
 | 
			
		||||
		User:       u,
 | 
			
		||||
		attributes: a,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u systemPrincipal) Type() string {
 | 
			
		||||
	return "user"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u systemPrincipal) Identity() string {
 | 
			
		||||
	return u.Name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u systemPrincipal) Attributes() Attributes {
 | 
			
		||||
	return u.attributes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SystemPassword() Password {
 | 
			
		||||
	return system{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (system) VerifyPassword(username, password string) (Principal, error) {
 | 
			
		||||
	t, err := pam.StartFunc("sshd", username, func(s pam.Style, msg string) (string, error) {
 | 
			
		||||
		switch s {
 | 
			
		||||
		case pam.PromptEchoOff:
 | 
			
		||||
			return password, nil
 | 
			
		||||
		case pam.PromptEchoOn:
 | 
			
		||||
			return username, nil
 | 
			
		||||
		default:
 | 
			
		||||
			return "", errors.New("unrecognized message style")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err = t.Authenticate(0); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return newSystemPrincipal(username)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (system) VerifyPublicKey(username string, key ssh.PublicKey) (Principal, error) {
 | 
			
		||||
	p, err := newSystemPrincipal(username)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rest, err := os.ReadFile(filepath.Join(p.HomeDir, ".ssh", "authorized_keys"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for len(rest) > 0 {
 | 
			
		||||
		var out ssh.PublicKey
 | 
			
		||||
		if out, _, _, rest, err = ssh.ParseAuthorizedKey(rest); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if bytes.Equal(out.Marshal(), key.Marshal()) {
 | 
			
		||||
			return p, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil, ErrAuthorized
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								auth/testdata/passwd
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								auth/testdata/passwd
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# This line is ignored
 | 
			
		||||
example:$argon2id$v=19$m=2097152,t=1,p=4$BjVeoTI4ntTQc0WkFQdLWg$OAUnkkyx5STI0Ixl+OSpv4JnI6J1TYWKuCuvIbUGHTY
 | 
			
		||||
bcrypt:$2y$10$jeTxJGC9SZ1KZgbBZoZeseq0H8Gi1yqxvGX43YnwodTYeUxvl6TPK
 | 
			
		||||
							
								
								
									
										5
									
								
								auth/testdata/pubkey
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								auth/testdata/pubkey
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
maze:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIjJv5YZX22u40Wr+DRHH6jnCjxqk1u7rvNc6ALsCncK
 | 
			
		||||
maze:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCce9LTIeQ4FvyHUMzOK0NhZsVtuWIdUDOdl3j+5rtvrdsVzlLp2eqaSQEwZwPl7qek0M7+5i8DoSzZjg9MrsUsCiSXqRY9/G3M/KwL+MaL0116R7uwkQX+ndKvUvnqpjKKOJ/PsBFmZlXKYPDy6SFJURpmRATiafenEkg5D64fxBFw1k66ZXcQ81aYAGjpV8nqE18DcfVfj6czadpQ5Ycf/D4InUTPKTuFa2lMdVqrZ1+S6DDGIQdG9HEI7IpzFfYvGEFQc2x4BaeHroNym/k9PtIH+4debEDkrZ7Aaq/ofXTWoWLR4KoZoHcyUSGlPT+M9/aICgqaza/VZfPgobbiXRQTwNfNe4lUcbAhracX4RQDJbyPlHMtGAuDrIi6WLyBKZp4ehZPIT00YbPMP5BRTcPPwUneYVL2D1vbgWLO+0GsYfdr7fsm5TPd6fkajNj08ZSOWYRdJuoZJkplqM1GETlXkv5ictZ9k2Nm6K0qxBKIxaaBMm46jvcTM9K2YRK7QdvLTrXQzUYvR6HqcHUN3rAMT0LuyKY69FzJXuKaO4HYHQB0MaXJW/CVIo1xR92sqHUmcKn5nwyccT4hUscPAx/lNE7AZ8qcTqUqYRACF9dJhKOZD2dyO8dBHgnCqkg5JfifmbdCSEc2nGrGPx3rHRj/JXNqI404T27yB26mQw==
 | 
			
		||||
test_a:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFo1lt6lEk+1VUrMbhlaVpkI0p1TFUGujHaKKn7+VoGb
 | 
			
		||||
test_b:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICA9dQjNeX3eBvkOXJN+nJm1C2W9UtRiLbK9O87Mjkir
 | 
			
		||||
test_b:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFq82Pfsg7KjTU5LN4jikxITDQhCWB3TFxQdXTgYtKt40+gv88hZkemM1MYTzR30bUX/zcRsioUSwr3u7/2La7ti+BoilsHjrEx4w+nxNGCCe8D3M6K5Xi8MPL2AqbXFqkPSEpX+psrs+qILfNhs1lWAsN7GLP0cTIxPynFNECwJnUlleN0hsn8N8bQCoUInZQGmHwIHq62H+3IPbv7Vko3J0Zrqqo4OqfeV5BA0By7ZP+2Jd9ZsLJ2efaiALcs6oTk0v95wVQ36wp605x9ePYg6zHzIZDfpA400RqeuiZF5jpiG7q3eb0+CysfMbU0BpfeHmCq15PFYqre8HKAJZ3
 | 
			
		||||
							
								
								
									
										187
									
								
								cmd/conduit/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								cmd/conduit/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/hashicorp/hcl/v2"
 | 
			
		||||
	"github.com/hashicorp/hcl/v2/gohcl"
 | 
			
		||||
	"github.com/hashicorp/hcl/v2/hclsimple"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/auth"
 | 
			
		||||
	"git.maze.io/maze/conduit/policy"
 | 
			
		||||
	"git.maze.io/maze/conduit/provider"
 | 
			
		||||
	"git.maze.io/maze/conduit/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Listen   []*ListenConfig   `hcl:"listen,block"`
 | 
			
		||||
	Provider []*ProviderConfig `hcl:"provider,block"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ListenConfig struct {
 | 
			
		||||
	Addr           string      `hcl:"addr,label"`
 | 
			
		||||
	Keys           []string    `hcl:"keys"`
 | 
			
		||||
	MaxConnections int         `hcl:"max_connections,optional"`
 | 
			
		||||
	DebugSession   bool        `hcl:"debug_session,optional"`
 | 
			
		||||
	AllowTunnel    bool        `hcl:"allow_tunnel,optional"`
 | 
			
		||||
	Auth           *AuthConfig `hcl:"auth,block"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c ListenConfig) Server() (*ssh.Server, error) {
 | 
			
		||||
	var keys []ssh.Signer
 | 
			
		||||
 | 
			
		||||
	if len(c.Keys) == 0 {
 | 
			
		||||
		return nil, fmt.Errorf("listen %q has no keys configured", c.Addr)
 | 
			
		||||
	}
 | 
			
		||||
	for _, name := range c.Keys {
 | 
			
		||||
		k, err := ssh.LoadPrivateKey(name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		keys = append(keys, k)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		server = ssh.NewServer(keys)
 | 
			
		||||
		err    error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if c.Auth == nil {
 | 
			
		||||
		return nil, fmt.Errorf("listen %q has no auth configured", c.Addr)
 | 
			
		||||
	}
 | 
			
		||||
	if server.AcceptHandler, err = c.Auth.AcceptHandler(); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("listen %q: %w", c.Addr, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.MaxConnections > 0 {
 | 
			
		||||
		server.ConnectHandler = append(server.ConnectHandler, ssh.MaxConnections(c.MaxConnections))
 | 
			
		||||
	}
 | 
			
		||||
	if c.DebugSession {
 | 
			
		||||
		server.ChannelHandler[ssh.ChannelTypeSession] = ssh.DebugSession()
 | 
			
		||||
	}
 | 
			
		||||
	if c.AllowTunnel {
 | 
			
		||||
		//server.ChannelHandler[ssh.ChannelTypeDirectTCPIP] = ssh.ForwardTunnel(nil)
 | 
			
		||||
		server.PortForwardHandler = ssh.PortForwardDialer(nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return server, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AuthConfig struct {
 | 
			
		||||
	Type string   `hcl:"type,label"`
 | 
			
		||||
	Body hcl.Body `hcl:",remain"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c AuthConfig) AcceptHandler() (ssh.AcceptHandler, error) {
 | 
			
		||||
	switch c.Type {
 | 
			
		||||
	/*
 | 
			
		||||
		case "password_file":
 | 
			
		||||
			var config struct {
 | 
			
		||||
				Path string `hcl:"path"`
 | 
			
		||||
			}
 | 
			
		||||
			if diag := gohcl.DecodeBody(c.Body, nil, &config); diag.HasErrors() {
 | 
			
		||||
				return nil, diag
 | 
			
		||||
			}
 | 
			
		||||
			return auth.PasswordFile(config.Path)
 | 
			
		||||
 | 
			
		||||
		case "pubkey_file":
 | 
			
		||||
			var config struct {
 | 
			
		||||
				Path string `hcl:"path"`
 | 
			
		||||
			}
 | 
			
		||||
			if diag := gohcl.DecodeBody(c.Body, nil, &config); diag.HasErrors() {
 | 
			
		||||
				return nil, diag
 | 
			
		||||
			}
 | 
			
		||||
			return auth.PublicKeyFile(config.Path)
 | 
			
		||||
 | 
			
		||||
		case "user_ca":
 | 
			
		||||
			var config struct {
 | 
			
		||||
				Keys []string `hcl:"keys"`
 | 
			
		||||
			}
 | 
			
		||||
			if diag := gohcl.DecodeBody(c.Body, nil, &config); diag.HasErrors() {
 | 
			
		||||
				return nil, diag
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			keys := make([]ssh.PublicKey, 0, len(config.Keys))
 | 
			
		||||
			for _, s := range config.Keys {
 | 
			
		||||
				k, _, err := ssh.ParseAuthorizedKey([]byte(s))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
				}
 | 
			
		||||
				keys = append(keys, k)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return auth.NewUserCertificateAuthority(keys...), nil
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	case "mfa":
 | 
			
		||||
		var config struct {
 | 
			
		||||
			Policy       string   `hcl:"policy"`
 | 
			
		||||
			Token        string   `hcl:"token,optional"`
 | 
			
		||||
			PasswordFile string   `hcl:"password_file,optional"`
 | 
			
		||||
			UserCA       []string `hcl:"user_ca,optional"`
 | 
			
		||||
		}
 | 
			
		||||
		if diag := gohcl.DecodeBody(c.Body, nil, &config); diag.HasErrors() {
 | 
			
		||||
			return nil, diag
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		userCAKeys := make([]ssh.PublicKey, len(config.UserCA))
 | 
			
		||||
		for i, key := range config.UserCA {
 | 
			
		||||
			k, _, err := ssh.ParseAuthorizedKey([]byte(key))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			userCAKeys[i] = k
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var passwordAuth auth.Password
 | 
			
		||||
		if config.PasswordFile != "" {
 | 
			
		||||
			var err error
 | 
			
		||||
			if passwordAuth, err = auth.PasswordFile(config.PasswordFile); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var tokenAuth auth.Password
 | 
			
		||||
		if config.Token != "" {
 | 
			
		||||
			var err error
 | 
			
		||||
			if tokenAuth, err = auth.PasswordFile(config.Token); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		_ = tokenAuth
 | 
			
		||||
 | 
			
		||||
		return &auth.MultiFactor{
 | 
			
		||||
			Loader: policy.FileSystemLoader{Root: config.Policy},
 | 
			
		||||
			UserCA: userCAKeys,
 | 
			
		||||
			//Token:           tokenAuth,
 | 
			
		||||
			Password: passwordAuth,
 | 
			
		||||
		}, nil
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, fmt.Errorf("auth: unsupported type %q", c.Type)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ProviderConfig struct {
 | 
			
		||||
	Name string   `hcl:"name,label"`
 | 
			
		||||
	Body hcl.Body `hcl:",remain"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c ProviderConfig) Init() error {
 | 
			
		||||
	return provider.Init(c.Name, c.Body)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Load(name string) (*Config, error) {
 | 
			
		||||
	var config = new(Config)
 | 
			
		||||
	if err := hclsimple.DecodeFile(name, nil, config); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, providerConfig := range config.Provider {
 | 
			
		||||
		if err := providerConfig.Init(); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return config, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								cmd/conduit/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								cmd/conduit/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"flag"
 | 
			
		||||
	"net"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
	"git.maze.io/maze/conduit/ssh"
 | 
			
		||||
 | 
			
		||||
	_ "git.maze.io/maze/conduit/provider/okta" // Okta support
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	configFlag := flag.String("config", "conduit.hcl", "configuration file path")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	logger.SetLevel(logger.TraceLevel)
 | 
			
		||||
 | 
			
		||||
	config, err := Load(*configFlag)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Err(err).Fatal("Error loading configuration")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	servers := make([]*ssh.Server, len(config.Listen))
 | 
			
		||||
	for i, listenConfig := range config.Listen {
 | 
			
		||||
		if servers[i], err = listenConfig.Server(); err != nil {
 | 
			
		||||
			logger.Err(err).Fatal("Error configuring listener")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var wg sync.WaitGroup
 | 
			
		||||
	for i, server := range servers {
 | 
			
		||||
		wg.Go(func() {
 | 
			
		||||
			l, err := net.Listen("tcp", config.Listen[i].Addr)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.StandardLog.Err(err).Fatal("Can't bind to listening address")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			logger.StandardLog.Value("server", l.Addr().String()).Info("Server starting")
 | 
			
		||||
			if err = server.Serve(l); err != nil {
 | 
			
		||||
				logger.StandardLog.Err(err).Error("Error serving connections")
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								conduit.hcl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								conduit.hcl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
listen ":2222" {
 | 
			
		||||
    keys = [
 | 
			
		||||
        "testdata/conduit.rsa",
 | 
			
		||||
        "testdata/conduit.ed25519",
 | 
			
		||||
    ]
 | 
			
		||||
    debug_session = true
 | 
			
		||||
    allow_tunnel = true
 | 
			
		||||
 | 
			
		||||
    auth "mfa" {
 | 
			
		||||
        policy = "testdata/policy"
 | 
			
		||||
        token = "auth/testdata/passwd"
 | 
			
		||||
        user_ca = [
 | 
			
		||||
            "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCufO7OUqeLZkUX7qGatOk79nZTQGqCbHxTp6z8Nb+52HJiRDXgLfY/3zLX/3kOdjVrQujwEEfbD6IVOjF3gnDkgYvjnJROXEiv3k2UApYzrbJebFohcFPrrk3WqbzOeMQXciEuSNyJV33FMYBnZ8Y+Yrf5a4x5R0pxbmdCmxSOhihOIZYlKNjPq3UVfXwth/NW4KiHUkmuH6d4x4D3OMJ+xKeK9Eu05szBWwRHY3vplf0SYiwDd3xPlFalPG2UzA3j/+kdtQf0qNJGyWpjRjHv9BvJMP/G+Y2CckvygetYBfcvX9JGb/p8G1JyU55ODD5maxDrCSFm8aqDbvCLmeAb",
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
provider "auth0" {
 | 
			
		||||
    client {
 | 
			
		||||
        url = "https://dev-maze.eu.auth0.com"
 | 
			
		||||
        client_id = "DANKWsl8WRGY2pXQoB7CCwSkfoB8ouda"
 | 
			
		||||
        client_secret = "VSPT2o10WQoJZSO3Y18tmWpS6MG0uDNIytn1-cDwPhp7AiMo8gVsLIBUOdF-ilCP"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
provider "okta" {
 | 
			
		||||
    client {
 | 
			
		||||
        org_url     = "https://integrator-8134036.okta.com/"
 | 
			
		||||
        client_id   = "0oaw2uk8mnkKEJJ7s697"
 | 
			
		||||
        token       = "00ebjg06VGdf9Y_AdUf494gf5pcBlURl8of02irbRw"
 | 
			
		||||
        private_key = <<EOT
 | 
			
		||||
-----BEGIN PRIVATE KEY-----
 | 
			
		||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5ACQsZz90gHpJ
 | 
			
		||||
VVCRzyz69wJNlraiOwCmNmJrFRyVaMjVJQ/by1fYWV6hEL2c35BEMr/9wUnAyHe5
 | 
			
		||||
2qHq11ZTsLKwayp7pVQOXt0LbM7XCjtD3j+M+Fj8iWJSEf3CWAHsoPmmBlMEDAgR
 | 
			
		||||
Zm5RbrESVYVJmN/rZwRL6CYparyN0l+1J0nZDPpaF3g5qmr47xZB/aWtqUkQMk6q
 | 
			
		||||
eaGYo9zYgHVQYZFWS++zXlYRx/EMwfX+UyhQiwZQAcH8dr1UjEbGoBvGcckrKYKW
 | 
			
		||||
lYKwM/KSWQq0XYLPK3ypKDfmzZMVsnr36iqNWDq6Q1tHAiiMsQ32RSYGNghRorLF
 | 
			
		||||
voCH8185AgMBAAECggEAG5LR0WxyIMMldtiocZMXeTBnv3i/L97rcdqZQKyc3ggI
 | 
			
		||||
JvynKHNeXHi1ifwcxszri76krwWoIHvAnIrhp0cBLugfOfw/EL7LkHjDKXjGO3bi
 | 
			
		||||
7nKptjEt0jYH2YOk9tp3LvWvP/ehV/ETIsTJnImLCtiETTvj5AouscGgLnwiLmDz
 | 
			
		||||
TCzUlI/pr0kiLf/giOsG2EzLwGWcESBD761AmJavbXvtmZAwI+LPCXlysZinzbyg
 | 
			
		||||
UoEjiKhNbn/3bPUtqqRJ+UOWxj3FyujQDpm/4qlXAI4YRhtnoVhP10008wxvt7dq
 | 
			
		||||
bl4ptExph5v/amizhs2ZvvZ1n8kT04PnikFcv9proQKBgQD3O3sdZHHyDb4zFXwN
 | 
			
		||||
8Z+N8OYY9EG7Wc6QjU3ABeDIJxR23pC/67pyHGspJjZuAqN6ObkMVtuCDC1AZ6KY
 | 
			
		||||
sZR5e62U/r8jhhU4l+3AfgWHMZc/XdYf8JMtlGaG7u+3TwwRM+bymjHzvj3Lsbxa
 | 
			
		||||
8geNa/MVl960xe+aD6KV7SQ4KwKBgQC/j68W0tY0I5mZPUly0BNddUS7IjNwmi5a
 | 
			
		||||
8b1hhmkia+9fYA1dyNcFwlZycux4LdsFv/N6RvN/eVzXn9H6SgTjynyxtpiK/+aB
 | 
			
		||||
Jt0Njz14vy6WmidrV1zxXw4beJBrfy1hb+vxpdUxs/ZH63EMToZDnju4NVizCD9I
 | 
			
		||||
JMnBDzHQKwKBgAuP+5UHUpDodaG7+n5Ic5bW0lwOaFiTvaZjBWTaoYWa1kks5YYk
 | 
			
		||||
Ryb5D0XwZJFGjFC2DGJ4WXG+kgs2DZOoknIQB7E1LMlDhxCLgnIDMsz8078B63a4
 | 
			
		||||
8JksHJNo70saZk0TqVRlQ7rLheZV3KJAOXwytT6oSKEZtLf2zTrHyW7bAoGAFr2T
 | 
			
		||||
5323+BCR12MzKPISmnGlayGwQZnMDvfLp5wxNujhTc01SQDipchgQs3pzIqFCbWz
 | 
			
		||||
zbxGg8eAgghzAOdwlSogi2hFy5p9Xq+iZk2u2nq3qSE7tL52RiEmp5Q0cM50MLD8
 | 
			
		||||
rX8mQ/Q9NGR60x8vSS+rnz6V/QrpmELlwIlxPGkCgYEAt6Ew7alRpRBMEOHzGKFt
 | 
			
		||||
suM0v9vR3XiyqjjQOyZaqSe9oXNW3aWrvCnqhI2+W4C32Knpokt5VVyCxhUX3SL/
 | 
			
		||||
M8hUa8IWVIAvjOyKqX+wVA1/w+1SHbimOE2T4GTT8RctSkTnr3hxOLCtgkRYV82A
 | 
			
		||||
pJbp+yG31SS1DgPsZxGhmB8=
 | 
			
		||||
-----END PRIVATE KEY-----
 | 
			
		||||
EOT
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								core/core.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								core/core.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
package core
 | 
			
		||||
 | 
			
		||||
import "git.maze.io/maze/conduit/ssh"
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	server *ssh.Server
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
module git.maze.io/maze/conduit
 | 
			
		||||
 | 
			
		||||
go 1.25.0
 | 
			
		||||
 | 
			
		||||
replace git.maze.io/maze/conduit/provider/okta => ./provider/okta
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	git.maze.io/maze/conduit/provider/okta v0.0.0-00010101000000-000000000000
 | 
			
		||||
	github.com/go-crypt/crypt v0.4.6
 | 
			
		||||
	github.com/go-viper/mapstructure/v2 v2.4.0
 | 
			
		||||
	github.com/hashicorp/hcl/v2 v2.24.0
 | 
			
		||||
	github.com/msteinert/pam v1.2.0
 | 
			
		||||
	github.com/open-policy-agent/opa v1.9.0
 | 
			
		||||
	github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
 | 
			
		||||
	golang.org/x/crypto v0.42.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
 | 
			
		||||
	github.com/agext/levenshtein v1.2.1 // indirect
 | 
			
		||||
	github.com/agnivade/levenshtein v1.2.1 // indirect
 | 
			
		||||
	github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
 | 
			
		||||
	github.com/beorn7/perks v1.0.1 // indirect
 | 
			
		||||
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 | 
			
		||||
	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
 | 
			
		||||
	github.com/go-crypt/x v0.4.8 // indirect
 | 
			
		||||
	github.com/go-ini/ini v1.67.0 // indirect
 | 
			
		||||
	github.com/go-logr/logr v1.4.3 // indirect
 | 
			
		||||
	github.com/go-logr/stdr v1.2.2 // indirect
 | 
			
		||||
	github.com/go-yaml/yaml v2.1.0+incompatible // indirect
 | 
			
		||||
	github.com/gobwas/glob v0.2.3 // indirect
 | 
			
		||||
	github.com/goccy/go-json v0.10.5 // indirect
 | 
			
		||||
	github.com/goccy/go-yaml v1.18.0 // indirect
 | 
			
		||||
	github.com/google/go-cmp v0.7.0 // indirect
 | 
			
		||||
	github.com/google/uuid v1.6.0 // indirect
 | 
			
		||||
	github.com/kelseyhightower/envconfig v1.3.0 // indirect
 | 
			
		||||
	github.com/lestrrat-go/blackmagic v1.0.4 // indirect
 | 
			
		||||
	github.com/lestrrat-go/dsig v1.0.0 // indirect
 | 
			
		||||
	github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
 | 
			
		||||
	github.com/lestrrat-go/httpcc v1.0.1 // indirect
 | 
			
		||||
	github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
 | 
			
		||||
	github.com/lestrrat-go/jwx/v3 v3.0.11 // indirect
 | 
			
		||||
	github.com/lestrrat-go/option v1.0.1 // indirect
 | 
			
		||||
	github.com/lestrrat-go/option/v2 v2.0.0 // indirect
 | 
			
		||||
	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
 | 
			
		||||
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 | 
			
		||||
	github.com/okta/okta-sdk-golang v1.1.0 // indirect
 | 
			
		||||
	github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 // indirect
 | 
			
		||||
	github.com/prometheus/client_golang v1.23.2 // indirect
 | 
			
		||||
	github.com/prometheus/client_model v0.6.2 // indirect
 | 
			
		||||
	github.com/prometheus/common v0.66.1 // indirect
 | 
			
		||||
	github.com/prometheus/procfs v0.17.0 // indirect
 | 
			
		||||
	github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
 | 
			
		||||
	github.com/segmentio/asm v1.2.0 // indirect
 | 
			
		||||
	github.com/square/go-jose v2.4.1+incompatible // indirect
 | 
			
		||||
	github.com/tchap/go-patricia/v2 v2.3.3 // indirect
 | 
			
		||||
	github.com/valyala/fastjson v1.6.4 // indirect
 | 
			
		||||
	github.com/vektah/gqlparser/v2 v2.5.30 // indirect
 | 
			
		||||
	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 | 
			
		||||
	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 | 
			
		||||
	github.com/yashtewari/glob-intersection v0.2.0 // indirect
 | 
			
		||||
	github.com/zclconf/go-cty v1.16.3 // indirect
 | 
			
		||||
	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/metric v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/sdk v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/trace v1.38.0 // indirect
 | 
			
		||||
	go.yaml.in/yaml/v2 v2.4.2 // indirect
 | 
			
		||||
	golang.org/x/mod v0.27.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.17.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.36.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.29.0 // indirect
 | 
			
		||||
	golang.org/x/tools v0.36.0 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.36.9 // indirect
 | 
			
		||||
	gopkg.in/square/go-jose.v2 v2.4.1 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
			
		||||
	sigs.k8s.io/yaml v1.6.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										166
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
			
		||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
 | 
			
		||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
 | 
			
		||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
 | 
			
		||||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 | 
			
		||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
 | 
			
		||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
 | 
			
		||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
 | 
			
		||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
 | 
			
		||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 | 
			
		||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
 | 
			
		||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
 | 
			
		||||
github.com/go-crypt/crypt v0.4.6 h1:pC2CdIsCAjhvse6Q9oXZH97cV2iGqFOxE8HQCjY1cNg=
 | 
			
		||||
github.com/go-crypt/crypt v0.4.6/go.mod h1:Ts3T2ORhE0nxel6/2mlQNZTZn7dcxRdtnSJjoDqUkbs=
 | 
			
		||||
github.com/go-crypt/x v0.4.8 h1:Cob6IxrSfWTc+MG8CBbNHBM4UqrBgEZDoK5t/SG4oZ4=
 | 
			
		||||
github.com/go-crypt/x v0.4.8/go.mod h1:ozw9N4MYuLKhR5x2REs1e4T/nrEAkbuVkcsh/HbYksg=
 | 
			
		||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
 | 
			
		||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
 | 
			
		||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 | 
			
		||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 | 
			
		||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 | 
			
		||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 | 
			
		||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 | 
			
		||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
 | 
			
		||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 | 
			
		||||
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
 | 
			
		||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
 | 
			
		||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 | 
			
		||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 | 
			
		||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 | 
			
		||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 | 
			
		||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
 | 
			
		||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
 | 
			
		||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 | 
			
		||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
			
		||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 | 
			
		||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 | 
			
		||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 | 
			
		||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
 | 
			
		||||
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
 | 
			
		||||
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
 | 
			
		||||
github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
 | 
			
		||||
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
 | 
			
		||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 | 
			
		||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 | 
			
		||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
			
		||||
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
 | 
			
		||||
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
 | 
			
		||||
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
 | 
			
		||||
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
 | 
			
		||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
 | 
			
		||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
 | 
			
		||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
 | 
			
		||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
 | 
			
		||||
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
 | 
			
		||||
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
 | 
			
		||||
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
 | 
			
		||||
github.com/lestrrat-go/jwx/v3 v3.0.11 h1:yEeUGNUuNjcez/Voxvr7XPTYNraSQTENJgtVTfwvG/w=
 | 
			
		||||
github.com/lestrrat-go/jwx/v3 v3.0.11/go.mod h1:XSOAh2SiXm0QgRe3DulLZLyt+wUuEdFo81zuKTLcvgQ=
 | 
			
		||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
 | 
			
		||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
 | 
			
		||||
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
 | 
			
		||||
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
 | 
			
		||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 | 
			
		||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 | 
			
		||||
github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE=
 | 
			
		||||
github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0=
 | 
			
		||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 | 
			
		||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 | 
			
		||||
github.com/okta/okta-sdk-golang v1.1.0 h1:sr/KYSMRhs4F2NWEbqWXqN4y4cKKcfzrtOiBqR/J6mI=
 | 
			
		||||
github.com/okta/okta-sdk-golang v1.1.0/go.mod h1:KEjmr3Zo+wP3gVa3XhwIvENBfh7L/iRUeIl6ruQYOK0=
 | 
			
		||||
github.com/open-policy-agent/opa v1.9.0 h1:QWFNwbcc29IRy0xwD3hRrMc/RtSersLY1Z6TaID3vgI=
 | 
			
		||||
github.com/open-policy-agent/opa v1.9.0/go.mod h1:72+lKmTda0O48m1VKAxxYl7MjP/EWFZu9fxHQK2xihs=
 | 
			
		||||
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 h1:pSCLCl6joCFRnjpeojzOpEYs4q7Vditq8fySFG5ap3Y=
 | 
			
		||||
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 | 
			
		||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
 | 
			
		||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
 | 
			
		||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 | 
			
		||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
 | 
			
		||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
 | 
			
		||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
 | 
			
		||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
 | 
			
		||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
 | 
			
		||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
 | 
			
		||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 | 
			
		||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
 | 
			
		||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/square/go-jose v2.4.1+incompatible h1:KFYc54wTtgnd3x4B/Y7Zr1s/QaEx2BNzRsB3Hae5LHo=
 | 
			
		||||
github.com/square/go-jose v2.4.1+incompatible/go.mod h1:7MxpAF/1WTVUu8Am+T5kNy+t0902CaLWM4Z745MkOa8=
 | 
			
		||||
github.com/square/go-jose/v3 v3.0.0-20200225220504-708a9fe87ddc/go.mod h1:JbpHhNyeVc538vtj/ECJ3gPYm1VEitNjsLhm4eJQQbg=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 | 
			
		||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
 | 
			
		||||
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
 | 
			
		||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
 | 
			
		||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
 | 
			
		||||
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
 | 
			
		||||
github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
 | 
			
		||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
 | 
			
		||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 | 
			
		||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
 | 
			
		||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 | 
			
		||||
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
 | 
			
		||||
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
 | 
			
		||||
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
 | 
			
		||||
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
 | 
			
		||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 | 
			
		||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 | 
			
		||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
 | 
			
		||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
 | 
			
		||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
 | 
			
		||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
 | 
			
		||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
 | 
			
		||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
 | 
			
		||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
 | 
			
		||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
 | 
			
		||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
 | 
			
		||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
 | 
			
		||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
 | 
			
		||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
 | 
			
		||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
 | 
			
		||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
 | 
			
		||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
 | 
			
		||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
 | 
			
		||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
 | 
			
		||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
 | 
			
		||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
 | 
			
		||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y=
 | 
			
		||||
gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
 | 
			
		||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
 | 
			
		||||
							
								
								
									
										37
									
								
								internal/netutil/conn.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								internal/netutil/conn.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
package netutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ConnCloser struct {
 | 
			
		||||
	net.Conn
 | 
			
		||||
	Closer func() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *ConnCloser) Close() error {
 | 
			
		||||
	if c.Closer == nil {
 | 
			
		||||
		return c.Conn.Close()
 | 
			
		||||
	}
 | 
			
		||||
	return c.Closer()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsClosing checks if the error is because the connection was closed.
 | 
			
		||||
func IsClosing(err error) bool {
 | 
			
		||||
	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, syscall.ECONNRESET) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if err, ok := err.(net.Error); ok && err.Timeout() {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if err, ok := err.(*net.OpError); ok && err.Op == "close" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debugf("not a closing error %T: %#+v", err, err)
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								internal/stringutil/iter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								internal/stringutil/iter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
package stringutil
 | 
			
		||||
 | 
			
		||||
import "sort"
 | 
			
		||||
 | 
			
		||||
func MapKeys(m map[string]string) <-chan string {
 | 
			
		||||
	ch := make(chan string)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		close(ch)
 | 
			
		||||
	} else {
 | 
			
		||||
		go func(ch chan<- string) {
 | 
			
		||||
			defer close(ch)
 | 
			
		||||
			keys := make([]string, 0, len(m))
 | 
			
		||||
			for k := range m {
 | 
			
		||||
				keys = append(keys, k)
 | 
			
		||||
			}
 | 
			
		||||
			sort.Strings(keys)
 | 
			
		||||
			for _, k := range keys {
 | 
			
		||||
				ch <- k
 | 
			
		||||
			}
 | 
			
		||||
		}(ch)
 | 
			
		||||
	}
 | 
			
		||||
	return ch
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										231
									
								
								logger/log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								logger/log.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,231 @@
 | 
			
		||||
package logger
 | 
			
		||||
 | 
			
		||||
import "github.com/sirupsen/logrus"
 | 
			
		||||
 | 
			
		||||
type Level int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TraceLevel Level = -1 + iota
 | 
			
		||||
	DebugLevel
 | 
			
		||||
	InfoLevel
 | 
			
		||||
	WarnLevel
 | 
			
		||||
	ErrorLevel
 | 
			
		||||
	PanicLevel
 | 
			
		||||
	FatalLevel
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Logger is a generic logging interface, similar to logrus's [logrus.ValueLogger].
 | 
			
		||||
// It is used in Styx for logging, so that users can plug in their own logging implementations.
 | 
			
		||||
type Logger interface {
 | 
			
		||||
	SetLevel(Level)
 | 
			
		||||
	GetLevel() Level
 | 
			
		||||
	Trace(...any)
 | 
			
		||||
	Tracef(string, ...any)
 | 
			
		||||
	Debug(...any)
 | 
			
		||||
	Debugf(string, ...any)
 | 
			
		||||
	Info(...any)
 | 
			
		||||
	Infof(string, ...any)
 | 
			
		||||
	Warn(...any)
 | 
			
		||||
	Warnf(string, ...any)
 | 
			
		||||
	Error(...any)
 | 
			
		||||
	Errorf(string, ...any)
 | 
			
		||||
	Panic(...any)
 | 
			
		||||
	Panicf(string, ...any)
 | 
			
		||||
	Fatal(...any)
 | 
			
		||||
	Fatalf(string, ...any)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Structured interface {
 | 
			
		||||
	Logger
 | 
			
		||||
 | 
			
		||||
	// Err adds an error to the log entry and returns the new logger.
 | 
			
		||||
	Err(error) Structured
 | 
			
		||||
 | 
			
		||||
	// Value returns a new logger with the specified Value added to the log entry.
 | 
			
		||||
	Value(string, any) Structured
 | 
			
		||||
 | 
			
		||||
	// Values returns a new logger with the specified Values added to the log entry.
 | 
			
		||||
	Values(Values) Structured
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Values map[string]any
 | 
			
		||||
 | 
			
		||||
// Alias.
 | 
			
		||||
type V = Values
 | 
			
		||||
 | 
			
		||||
// StandardLog is the logger used by the package-level exported functions.
 | 
			
		||||
var StandardLog = NewStandardLogger()
 | 
			
		||||
 | 
			
		||||
// SetLogger sets the logger used by the package-level exported functions.
 | 
			
		||||
func SetLogger(logger Structured) {
 | 
			
		||||
	StandardLog = logger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get returns the logger used by the package-level exported functions.
 | 
			
		||||
func Get() Structured {
 | 
			
		||||
	return StandardLog
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type standardLogger struct {
 | 
			
		||||
	*logrus.Logger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewStandardLogger returns a new Structured logger that wraps the standard logrus logger.
 | 
			
		||||
func NewStandardLogger() Structured {
 | 
			
		||||
	return standardLogger{logrus.StandardLogger()}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type standardLoggerEntry struct {
 | 
			
		||||
	standardLogger
 | 
			
		||||
	*logrus.Entry
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SetLevel(level Level) {
 | 
			
		||||
	StandardLog.SetLevel(level)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l standardLogger) SetLevel(level Level) {
 | 
			
		||||
	switch level {
 | 
			
		||||
	case TraceLevel:
 | 
			
		||||
		l.Logger.SetLevel(logrus.TraceLevel)
 | 
			
		||||
	case DebugLevel:
 | 
			
		||||
		l.Logger.SetLevel(logrus.DebugLevel)
 | 
			
		||||
	case InfoLevel:
 | 
			
		||||
		l.Logger.SetLevel(logrus.InfoLevel)
 | 
			
		||||
	case WarnLevel:
 | 
			
		||||
		l.Logger.SetLevel(logrus.WarnLevel)
 | 
			
		||||
	case ErrorLevel:
 | 
			
		||||
		l.Logger.SetLevel(logrus.ErrorLevel)
 | 
			
		||||
	case PanicLevel:
 | 
			
		||||
		l.Logger.SetLevel(logrus.PanicLevel)
 | 
			
		||||
	case FatalLevel:
 | 
			
		||||
		l.Logger.SetLevel(logrus.FatalLevel)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetLevel() Level {
 | 
			
		||||
	return StandardLog.GetLevel()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l standardLogger) GetLevel() Level {
 | 
			
		||||
	switch l.Logger.GetLevel() {
 | 
			
		||||
	case logrus.TraceLevel:
 | 
			
		||||
		return TraceLevel
 | 
			
		||||
	case logrus.DebugLevel:
 | 
			
		||||
		return DebugLevel
 | 
			
		||||
	case logrus.InfoLevel:
 | 
			
		||||
		return InfoLevel
 | 
			
		||||
	case logrus.WarnLevel:
 | 
			
		||||
		return WarnLevel
 | 
			
		||||
	case logrus.ErrorLevel:
 | 
			
		||||
		return ErrorLevel
 | 
			
		||||
	case logrus.PanicLevel:
 | 
			
		||||
		return PanicLevel
 | 
			
		||||
	case logrus.FatalLevel:
 | 
			
		||||
		return FatalLevel
 | 
			
		||||
	default:
 | 
			
		||||
		return InfoLevel
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Err(err error) Structured {
 | 
			
		||||
	return StandardLog.Err(err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l standardLogger) Err(err error) Structured {
 | 
			
		||||
	return standardLoggerEntry{l, l.Logger.WithError(err)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Value(key string, value any) Structured {
 | 
			
		||||
	return StandardLog.Value(key, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l standardLogger) Value(key string, value any) Structured {
 | 
			
		||||
	return standardLoggerEntry{l, l.Logger.WithField(key, value)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l standardLogger) Values(Values Values) Structured {
 | 
			
		||||
	return standardLoggerEntry{l, l.Logger.WithFields(logrus.Fields(Values))}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l standardLoggerEntry) Err(err error) Structured {
 | 
			
		||||
	return standardLoggerEntry{l.standardLogger, l.Entry.WithError(err)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l standardLoggerEntry) Value(key string, value any) Structured {
 | 
			
		||||
	return standardLoggerEntry{l.standardLogger, l.Entry.WithField(key, value)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l standardLoggerEntry) Values(Values Values) Structured {
 | 
			
		||||
	return standardLoggerEntry{l.standardLogger, l.Entry.WithFields(logrus.Fields(Values))}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Trace logs a message at level Trace on the standard logger.
 | 
			
		||||
func Trace(args ...any) {
 | 
			
		||||
	StandardLog.Trace(args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Tracef logs a message at level Trace on the standard logger.
 | 
			
		||||
func Tracef(format string, args ...any) {
 | 
			
		||||
	StandardLog.Tracef(format, args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Debug logs a message at level Debug on the standard logger.
 | 
			
		||||
func Debug(args ...any) {
 | 
			
		||||
	StandardLog.Debug(args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Debugf logs a message at level Debug on the standard logger.
 | 
			
		||||
func Debugf(format string, args ...any) {
 | 
			
		||||
	StandardLog.Debugf(format, args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Info logs a message at level Info on the standard logger.
 | 
			
		||||
func Info(args ...any) {
 | 
			
		||||
	StandardLog.Info(args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Infof logs a message at level Info on the standard logger.
 | 
			
		||||
func Infof(format string, args ...any) {
 | 
			
		||||
	StandardLog.Infof(format, args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Warn logs a message at level Warn on the standard logger.
 | 
			
		||||
func Warn(args ...any) {
 | 
			
		||||
	StandardLog.Warn(args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Warnf logs a message at level Warn on the standard logger.
 | 
			
		||||
func Warnf(format string, args ...any) {
 | 
			
		||||
	StandardLog.Warnf(format, args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Error logs a message at level Error on the standard logger.
 | 
			
		||||
func Error(args ...any) {
 | 
			
		||||
	StandardLog.Error(args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Errorf logs a message at level Error on the standard logger.
 | 
			
		||||
func Errorf(format string, args ...any) {
 | 
			
		||||
	StandardLog.Errorf(format, args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Panic logs a message at level Panic on the standard logger.
 | 
			
		||||
func Panic(args ...any) {
 | 
			
		||||
	StandardLog.Panic(args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Panicf logs a message at level Panic on the standard logger.
 | 
			
		||||
func Panicf(format string, args ...any) {
 | 
			
		||||
	StandardLog.Panicf(format, args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Fatal logs a message at level Fatal on the standard logger then the process will exit.
 | 
			
		||||
func Fatal(args ...any) {
 | 
			
		||||
	StandardLog.Fatal(args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Fatalf logs a message at level Fatal on the standard logger then the process will exit.
 | 
			
		||||
func Fatalf(format string, args ...any) {
 | 
			
		||||
	StandardLog.Fatalf(format, args...)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								policy/input/net.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								policy/input/net.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
package input
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Addr represents a [net.Addr].
 | 
			
		||||
type Addr struct {
 | 
			
		||||
	Network string `json:"network"`        // Type of address.
 | 
			
		||||
	IP      string `json:"ip"`             // IP address.
 | 
			
		||||
	Port    int    `json:"port,omitempty"` // Port (if any).
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAddr(addr net.Addr) *Addr {
 | 
			
		||||
	if addr == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	switch addr := addr.(type) {
 | 
			
		||||
	case *net.IPAddr:
 | 
			
		||||
		return &Addr{
 | 
			
		||||
			Network: addr.Network(),
 | 
			
		||||
			IP:      addr.IP.String(),
 | 
			
		||||
		}
 | 
			
		||||
	case *net.TCPAddr:
 | 
			
		||||
		return &Addr{
 | 
			
		||||
			Network: addr.Network(),
 | 
			
		||||
			IP:      addr.IP.String(),
 | 
			
		||||
			Port:    addr.Port,
 | 
			
		||||
		}
 | 
			
		||||
	case *net.UDPAddr:
 | 
			
		||||
		return &Addr{
 | 
			
		||||
			Network: addr.Network(),
 | 
			
		||||
			IP:      addr.IP.String(),
 | 
			
		||||
			Port:    addr.Port,
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return &Addr{
 | 
			
		||||
			Network: addr.Network(),
 | 
			
		||||
			IP:      addr.String(),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										98
									
								
								policy/input/ssh.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								policy/input/ssh.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
			
		||||
package input
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/ssh/sshutil"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Certificate represents a [ssh.Certificate].
 | 
			
		||||
type Certificate struct {
 | 
			
		||||
	Nonce           []byte     `json:"nonce"`
 | 
			
		||||
	Key             *PublicKey `json:"key"`
 | 
			
		||||
	Serial          uint64     `json:"serial"`
 | 
			
		||||
	CertType        uint32     `json:"type"`
 | 
			
		||||
	KeyId           string     `json:"key_id"`
 | 
			
		||||
	ValidPrincipals []string   `json:"valid_principals"`
 | 
			
		||||
	ValidAfter      *time.Time `json:"valid_after"`
 | 
			
		||||
	ValidBefore     *time.Time `json:"valid_before"`
 | 
			
		||||
	SignatureKey    *PublicKey `json:"signature_key"`
 | 
			
		||||
	Signature       []byte     `json:"signature"`
 | 
			
		||||
	SignatureFormat string     `json:"signature_format"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewCertificate converts an [ssh.Certificate] to [Certificate] input.
 | 
			
		||||
func NewCertificate(cert *ssh.Certificate) *Certificate {
 | 
			
		||||
	if cert == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	c := &Certificate{
 | 
			
		||||
		Nonce:           cert.Nonce,
 | 
			
		||||
		Key:             NewPublicKey(cert.Key),
 | 
			
		||||
		Serial:          cert.Serial,
 | 
			
		||||
		CertType:        cert.CertType,
 | 
			
		||||
		KeyId:           cert.KeyId,
 | 
			
		||||
		ValidPrincipals: cert.ValidPrincipals,
 | 
			
		||||
		SignatureKey:    NewPublicKey(cert.SignatureKey),
 | 
			
		||||
	}
 | 
			
		||||
	if cert.ValidAfter > 0 {
 | 
			
		||||
		t := time.Unix(int64(cert.ValidAfter), 0)
 | 
			
		||||
		c.ValidAfter = &t
 | 
			
		||||
	}
 | 
			
		||||
	if cert.ValidBefore > 0 {
 | 
			
		||||
		t := time.Unix(int64(cert.ValidBefore), 0)
 | 
			
		||||
		c.ValidBefore = &t
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cert.Signature != nil {
 | 
			
		||||
		c.Signature = cert.Signature.Blob
 | 
			
		||||
		c.SignatureFormat = cert.Signature.Format
 | 
			
		||||
	}
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ConnMetadata is a Rego input that represents a [ssh.ConnMetadata].
 | 
			
		||||
type ConnMetadata struct {
 | 
			
		||||
	User          string `json:"user"`           // User is the user ID for this connection.
 | 
			
		||||
	SessionID     []byte `json:"session_id"`     // SessionID is the session hash, also denoted by H.
 | 
			
		||||
	ClientVersion string `json:"client_version"` // ClientVersion is the client's version.
 | 
			
		||||
	ServerVersion string `json:"server_version"` // ServerVersion is the server's version
 | 
			
		||||
	RemoteAddr    *Addr  `json:"remote_addr"`    // RemoteAddr is the remote address for this connection.
 | 
			
		||||
	LocalAddr     *Addr  `json:"local_addr"`     // LocalAddr is the local address for this connection.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewConnMetadata converts an [ssh.ConnMetadata] to [ConnMetadata] input.
 | 
			
		||||
func NewConnMetadata(meta ssh.ConnMetadata) *ConnMetadata {
 | 
			
		||||
	if meta == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return &ConnMetadata{
 | 
			
		||||
		User:          meta.User(),
 | 
			
		||||
		SessionID:     meta.SessionID(),
 | 
			
		||||
		ClientVersion: string(meta.ClientVersion()),
 | 
			
		||||
		ServerVersion: string(meta.ServerVersion()),
 | 
			
		||||
		RemoteAddr:    NewAddr(meta.RemoteAddr()),
 | 
			
		||||
		LocalAddr:     NewAddr(meta.LocalAddr()),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PublicKey represents a [ssh.PublicKey].
 | 
			
		||||
type PublicKey struct {
 | 
			
		||||
	Type        string `json:"type"`
 | 
			
		||||
	Bits        int    `json:"bits"`
 | 
			
		||||
	Fingerprint string `json:"fingerprint"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewPublicKey converts an [ssh.PublicKey] to [PublicKey] input.
 | 
			
		||||
func NewPublicKey(key ssh.PublicKey) *PublicKey {
 | 
			
		||||
	if key == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return &PublicKey{
 | 
			
		||||
		Type:        sshutil.KeyType(key),
 | 
			
		||||
		Bits:        sshutil.KeyBits(key),
 | 
			
		||||
		Fingerprint: ssh.FingerprintSHA256(key),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										157
									
								
								policy/policy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								policy/policy.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,157 @@
 | 
			
		||||
package policy
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-viper/mapstructure/v2"
 | 
			
		||||
	"github.com/open-policy-agent/opa/v1/rego"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Policy interface {
 | 
			
		||||
	// Package name.
 | 
			
		||||
	Package() string
 | 
			
		||||
 | 
			
		||||
	// Query is the policy query.
 | 
			
		||||
	Query(query string, input, result any) error
 | 
			
		||||
 | 
			
		||||
	// Verify the policy definition.
 | 
			
		||||
	Verify() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Loader interface {
 | 
			
		||||
	LoadPolicy(name string) (Policy, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FileSystemLoader struct {
 | 
			
		||||
	Root string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l FileSystemLoader) LoadPolicy(name string) (Policy, error) {
 | 
			
		||||
	path := name
 | 
			
		||||
	if !strings.ContainsRune(filepath.Base(name), '.') || filepath.Ext(path) == "" {
 | 
			
		||||
		path += ".rego"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log := logger.StandardLog.Value("policy", path)
 | 
			
		||||
	if !filepath.IsAbs(path) {
 | 
			
		||||
		var err error
 | 
			
		||||
		if path, err = filepath.Abs(filepath.Join(l.Root, path)); err != nil {
 | 
			
		||||
			log.Err(err).Warn("Error resolving absolute policy path")
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b, err := os.ReadFile(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Err(err).Value("path", path).Warn("Error reading policy file")
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	pkg, err := decodePackage(b)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Err(err).Warn("Error decoding policy package")
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &policyFile{
 | 
			
		||||
		path: []string{path},
 | 
			
		||||
		pkg:  pkg,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func decodePackage(b []byte) (name string, err error) {
 | 
			
		||||
	s := bufio.NewScanner(bytes.NewReader(b))
 | 
			
		||||
	for s.Scan() {
 | 
			
		||||
		line := s.Text()
 | 
			
		||||
		if strings.HasPrefix(line, "package ") {
 | 
			
		||||
			return strings.TrimSpace(line[len("package "):]), nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if err = s.Err(); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	return "", errors.New("policy: no package name found")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type policyFile struct {
 | 
			
		||||
	path []string
 | 
			
		||||
	pkg  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p policyFile) Package() string {
 | 
			
		||||
	return p.pkg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p policyFile) Query(query string, input, value any) error {
 | 
			
		||||
	log := logger.StandardLog.Values(logger.Values{
 | 
			
		||||
		"policy":  p.path[0],
 | 
			
		||||
		"package": p.pkg,
 | 
			
		||||
		"query":   query,
 | 
			
		||||
	})
 | 
			
		||||
	log.Trace("Policy query evaluating")
 | 
			
		||||
 | 
			
		||||
	options := []func(*rego.Rego){
 | 
			
		||||
		rego.Dump(os.Stderr),
 | 
			
		||||
		rego.Query(query),
 | 
			
		||||
		rego.Load(p.path, nil),
 | 
			
		||||
		rego.Strict(true),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if input != nil {
 | 
			
		||||
		debug := json.NewEncoder(os.Stdout)
 | 
			
		||||
		debug.SetIndent("", "  ")
 | 
			
		||||
		debug.Encode(input)
 | 
			
		||||
		options = append(options, rego.Input(input))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.TODO()
 | 
			
		||||
	q, err := rego.New(options...).PrepareForEval(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Err(err).Warn("Policy query prepare failed")
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	results, err := q.Eval(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Err(err).Warn("Policy evaluation failed")
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log = log.Value("results", len(results))
 | 
			
		||||
	if value == nil {
 | 
			
		||||
		log.Debug("Policy query results processing ended, nil return value")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug("Policy query results processing")
 | 
			
		||||
	mapped := make(map[string]any)
 | 
			
		||||
	for _, result := range results {
 | 
			
		||||
		for _, expr := range result.Expressions {
 | 
			
		||||
			if value, ok := expr.Value.(map[string]any); ok {
 | 
			
		||||
				for k, v := range value {
 | 
			
		||||
					mapped[k] = v
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				log.Debugf("Unhandled expression value %T", expr)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	log.Value("mapped", mapped).Debug("Policy query results done")
 | 
			
		||||
 | 
			
		||||
	return mapstructure.Decode(mapped, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p policyFile) Verify() error {
 | 
			
		||||
	_, err := rego.New(
 | 
			
		||||
		rego.Strict(true),
 | 
			
		||||
		rego.Load(p.path, nil),
 | 
			
		||||
	).PrepareForEval(context.TODO())
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								provider/okta/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								provider/okta/go.mod
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
module git.maze.io/maze/conduit/provider/okta
 | 
			
		||||
 | 
			
		||||
go 1.25.0
 | 
			
		||||
 | 
			
		||||
replace git.maze.io/maze/conduit => ../..
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	git.maze.io/maze/conduit v0.0.0-00010101000000-000000000000
 | 
			
		||||
	github.com/hashicorp/hcl/v2 v2.24.0
 | 
			
		||||
	github.com/okta/okta-sdk-golang v1.1.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/agext/levenshtein v1.2.1 // indirect
 | 
			
		||||
	github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
 | 
			
		||||
	github.com/go-yaml/yaml v2.1.0+incompatible // indirect
 | 
			
		||||
	github.com/google/go-cmp v0.7.0 // indirect
 | 
			
		||||
	github.com/kelseyhightower/envconfig v1.3.0 // indirect
 | 
			
		||||
	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
 | 
			
		||||
	github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 // indirect
 | 
			
		||||
	github.com/square/go-jose v2.4.1+incompatible // indirect
 | 
			
		||||
	github.com/zclconf/go-cty v1.16.3 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.42.0 // indirect
 | 
			
		||||
	golang.org/x/mod v0.27.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.17.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.29.0 // indirect
 | 
			
		||||
	golang.org/x/tools v0.36.0 // indirect
 | 
			
		||||
	gopkg.in/square/go-jose.v2 v2.4.1 // indirect
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										58
									
								
								provider/okta/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								provider/okta/go.sum
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
 | 
			
		||||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 | 
			
		||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
 | 
			
		||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
 | 
			
		||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
 | 
			
		||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 | 
			
		||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 | 
			
		||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
 | 
			
		||||
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
 | 
			
		||||
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
 | 
			
		||||
github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
 | 
			
		||||
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
 | 
			
		||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 | 
			
		||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 | 
			
		||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
			
		||||
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
 | 
			
		||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 | 
			
		||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 | 
			
		||||
github.com/okta/okta-sdk-golang v1.1.0 h1:sr/KYSMRhs4F2NWEbqWXqN4y4cKKcfzrtOiBqR/J6mI=
 | 
			
		||||
github.com/okta/okta-sdk-golang v1.1.0/go.mod h1:KEjmr3Zo+wP3gVa3XhwIvENBfh7L/iRUeIl6ruQYOK0=
 | 
			
		||||
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 h1:pSCLCl6joCFRnjpeojzOpEYs4q7Vditq8fySFG5ap3Y=
 | 
			
		||||
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 | 
			
		||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/square/go-jose v2.4.1+incompatible h1:KFYc54wTtgnd3x4B/Y7Zr1s/QaEx2BNzRsB3Hae5LHo=
 | 
			
		||||
github.com/square/go-jose v2.4.1+incompatible/go.mod h1:7MxpAF/1WTVUu8Am+T5kNy+t0902CaLWM4Z745MkOa8=
 | 
			
		||||
github.com/square/go-jose/v3 v3.0.0-20200225220504-708a9fe87ddc/go.mod h1:JbpHhNyeVc538vtj/ECJ3gPYm1VEitNjsLhm4eJQQbg=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 | 
			
		||||
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
 | 
			
		||||
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
 | 
			
		||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
 | 
			
		||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
 | 
			
		||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
 | 
			
		||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
 | 
			
		||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
 | 
			
		||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
 | 
			
		||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y=
 | 
			
		||||
gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
							
								
								
									
										60
									
								
								provider/okta/provider.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								provider/okta/provider.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
package okta
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/hashicorp/hcl/v2"
 | 
			
		||||
	"github.com/hashicorp/hcl/v2/gohcl"
 | 
			
		||||
	"github.com/okta/okta-sdk-golang/okta"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/provider"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	provider.Register(&provider.Config{
 | 
			
		||||
		Name: "okta",
 | 
			
		||||
		Init: setup,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	configuration []okta.ConfigSetter
 | 
			
		||||
	client        *okta.Client
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func setup(body hcl.Body) (err error) {
 | 
			
		||||
	var config struct {
 | 
			
		||||
		Client struct {
 | 
			
		||||
			OrgURL     string `hcl:"org_url"`
 | 
			
		||||
			Token      string `hcl:"token,optional"`
 | 
			
		||||
			ClientID   string `hcl:"client_id"`
 | 
			
		||||
			PrivateKey string `hcl:"private_key,optional"`
 | 
			
		||||
			JWT        string `hcl:"jwt,optional"`
 | 
			
		||||
		} `hcl:"client,block"`
 | 
			
		||||
	}
 | 
			
		||||
	if diag := gohcl.DecodeBody(body, nil, &config); diag.HasErrors() {
 | 
			
		||||
		return diag
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	configuration = []okta.ConfigSetter{
 | 
			
		||||
		okta.WithOrgUrl(config.Client.OrgURL),
 | 
			
		||||
		okta.WithClientId(config.Client.ClientID),
 | 
			
		||||
	}
 | 
			
		||||
	if config.Client.Token != "" {
 | 
			
		||||
		configuration = append(configuration, okta.WithToken(config.Client.Token))
 | 
			
		||||
	}
 | 
			
		||||
	if config.Client.PrivateKey != "" {
 | 
			
		||||
		configuration = append(configuration, okta.WithPrivateKey(config.Client.PrivateKey))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if client, err = okta.NewClient(context.TODO(), configuration...); err != nil {
 | 
			
		||||
		return fmt.Errorf("okta: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, _, err = client.User.ListUsers(nil); err != nil {
 | 
			
		||||
		return fmt.Errorf("okta: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								provider/provider.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								provider/provider.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
package provider
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/hashicorp/hcl/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	// Name is a unique identifier for the provider.
 | 
			
		||||
	Name string
 | 
			
		||||
 | 
			
		||||
	// Init is called once to initialize the provider from the matching configuration block.
 | 
			
		||||
	Init func(hcl.Body) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var providerConfigs = make(map[string]*Config)
 | 
			
		||||
 | 
			
		||||
func Init(name string, body hcl.Body) error {
 | 
			
		||||
	p, ok := providerConfigs[name]
 | 
			
		||||
	if ok {
 | 
			
		||||
		return p.Init(body)
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Errorf("provider: no %q provider available", name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Register(provider *Config) {
 | 
			
		||||
	providerConfigs[provider.Name] = provider
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										132
									
								
								recorder/asciicast.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								recorder/asciicast.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
			
		||||
package recorder
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type asciiCastRecorder struct {
 | 
			
		||||
	wc     io.WriteCloser
 | 
			
		||||
	mu     sync.Mutex
 | 
			
		||||
	header asciiCastHeader
 | 
			
		||||
	last   time.Time
 | 
			
		||||
	closed bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type asciiCastHeader struct {
 | 
			
		||||
	Version   int               `json:"version"`
 | 
			
		||||
	Term      asciiCastTerm     `json:"term"`
 | 
			
		||||
	Timestamp int64             `json:"timestamp"`
 | 
			
		||||
	Title     string            `json:"title,omitempty"`
 | 
			
		||||
	Env       map[string]string `json:"env,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type asciiCastTerm struct {
 | 
			
		||||
	Columns int    `json:"cols"`
 | 
			
		||||
	Rows    int    `json:"rows"`
 | 
			
		||||
	Type    string `json:"type"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type asciiCastDuration float64
 | 
			
		||||
 | 
			
		||||
func (d asciiCastDuration) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return []byte(strconv.FormatFloat(float64(d), 'f', 6, 64)), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type asciiCastFrame struct {
 | 
			
		||||
	Delay float64
 | 
			
		||||
	Data  []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f asciiCastFrame) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	s, _ := json.Marshal(string(f.Data))
 | 
			
		||||
	return []byte(fmt.Sprintf(`[%.6f, %s]`, f.Delay, s)), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newAsciiCastRecorder(wc io.WriteCloser, info Info) (*asciiCastRecorder, error) {
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	if err := json.NewEncoder(wc).Encode(asciiCastHeader{
 | 
			
		||||
		Version: 3,
 | 
			
		||||
		Term: asciiCastTerm{
 | 
			
		||||
			Columns: info.Columns,
 | 
			
		||||
			Rows:    info.Rows,
 | 
			
		||||
			Type:    info.TerminalType,
 | 
			
		||||
		},
 | 
			
		||||
		Timestamp: now.Unix(),
 | 
			
		||||
		Title:     info.Title,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := io.WriteString(wc, "\n"); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &asciiCastRecorder{
 | 
			
		||||
		wc:   wc,
 | 
			
		||||
		last: now,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *asciiCastRecorder) Close() error {
 | 
			
		||||
	return r.wc.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *asciiCastRecorder) writeFrame(kind rune, p []byte) (int, error) {
 | 
			
		||||
	r.mu.Lock()
 | 
			
		||||
	defer r.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if r.closed {
 | 
			
		||||
		return 0, io.ErrClosedPipe
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		now   = time.Now()
 | 
			
		||||
		delay = now.Sub(r.last)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	v, _ := json.Marshal(string(p))
 | 
			
		||||
	_, err := io.WriteString(r.wc, "["+strconv.FormatFloat(delay.Seconds(), 'f', 6, 64)+", \""+string(kind)+"\", "+string(v)+"]\n")
 | 
			
		||||
 | 
			
		||||
	r.last = now
 | 
			
		||||
	return len(p), err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type asciiCastWriter struct {
 | 
			
		||||
	r    *asciiCastRecorder
 | 
			
		||||
	kind rune
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w *asciiCastWriter) Close() error {
 | 
			
		||||
	return w.r.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w *asciiCastWriter) Write(p []byte) (int, error) {
 | 
			
		||||
	return w.r.writeFrame(w.kind, p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *asciiCastRecorder) Reads() io.WriteCloser {
 | 
			
		||||
	return &asciiCastWriter{r, 'o'}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *asciiCastRecorder) Writes() io.WriteCloser {
 | 
			
		||||
	return &asciiCastWriter{r, 'i'}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *asciiCastRecorder) Resize(columns, rows int) {
 | 
			
		||||
	var (
 | 
			
		||||
		now   = time.Now()
 | 
			
		||||
		delay = now.Sub(r.last)
 | 
			
		||||
	)
 | 
			
		||||
	r.mu.Lock()
 | 
			
		||||
	_, _ = fmt.Fprintf(r.wc, "[%.6f, \"r\", \"%dx%d\"]\n", delay.Seconds(), columns, rows)
 | 
			
		||||
	r.last = now
 | 
			
		||||
	r.mu.Unlock()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	_ Recorder = (*asciiCastRecorder)(nil)
 | 
			
		||||
	_ Resizer  = (*asciiCastRecorder)(nil)
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										68
									
								
								recorder/recorder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								recorder/recorder.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
package recorder
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Format int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	Text Format = iota
 | 
			
		||||
	TTYRec
 | 
			
		||||
	ASCIICastv3
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrFormat = errors.New("recorder: unknown format")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Recorder interface {
 | 
			
		||||
	Reads() io.WriteCloser
 | 
			
		||||
	Writes() io.WriteCloser
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Resizer interface {
 | 
			
		||||
	Resize(columns, rows int)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Info struct {
 | 
			
		||||
	Columns      int
 | 
			
		||||
	Rows         int
 | 
			
		||||
	TerminalType string
 | 
			
		||||
	Title        string
 | 
			
		||||
	Env          map[string]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(name string, format Format, info Info) (Recorder, error) {
 | 
			
		||||
	f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0o640)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return NewWriter(f, format, info)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewWriter(wc io.WriteCloser, format Format, info Info) (Recorder, error) {
 | 
			
		||||
	switch format {
 | 
			
		||||
	case Text:
 | 
			
		||||
		return textRecorder{wc}, nil
 | 
			
		||||
 | 
			
		||||
	case ASCIICastv3:
 | 
			
		||||
		return newAsciiCastRecorder(wc, info)
 | 
			
		||||
 | 
			
		||||
	case TTYRec:
 | 
			
		||||
		return newTTYRecRecorder(wc), nil
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, ErrFormat
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type nopCloser struct {
 | 
			
		||||
	io.Writer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (nopCloser) Close() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								recorder/text.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								recorder/text.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
package recorder
 | 
			
		||||
 | 
			
		||||
import "io"
 | 
			
		||||
 | 
			
		||||
type textRecorder struct {
 | 
			
		||||
	io.WriteCloser
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r textRecorder) Reads() io.WriteCloser {
 | 
			
		||||
	return r.WriteCloser
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r textRecorder) Writes() io.WriteCloser {
 | 
			
		||||
	return r.WriteCloser
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										72
									
								
								recorder/ttyrec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								recorder/ttyrec.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
package recorder
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"io"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ttyRecordHeader struct {
 | 
			
		||||
	Sec  int32
 | 
			
		||||
	USec int32
 | 
			
		||||
	Len  int32
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ttyRecordHeader) Now() {
 | 
			
		||||
	var (
 | 
			
		||||
		now = time.Now()
 | 
			
		||||
		tv  = syscall.NsecToTimeval(now.UnixNano())
 | 
			
		||||
	)
 | 
			
		||||
	h.Sec = int32(tv.Sec)
 | 
			
		||||
	h.USec = tv.Usec
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ttyRecordHeader) WriteTo(w io.Writer) (int64, error) {
 | 
			
		||||
	var b [6]byte
 | 
			
		||||
	binary.LittleEndian.PutUint32(b[0:], uint32(h.Sec))
 | 
			
		||||
	binary.LittleEndian.PutUint32(b[2:], uint32(h.USec))
 | 
			
		||||
	binary.LittleEndian.PutUint32(b[4:], uint32(h.Len))
 | 
			
		||||
	n, err := w.Write(b[:])
 | 
			
		||||
	return int64(n), err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ttyRecRecorder struct {
 | 
			
		||||
	mu     sync.Mutex
 | 
			
		||||
	wc     io.WriteCloser
 | 
			
		||||
	header *ttyRecordHeader
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newTTYRecRecorder(wc io.WriteCloser) *ttyRecRecorder {
 | 
			
		||||
	return &ttyRecRecorder{
 | 
			
		||||
		wc:     wc,
 | 
			
		||||
		header: new(ttyRecordHeader),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *ttyRecRecorder) Close() error {
 | 
			
		||||
	return r.wc.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w *ttyRecRecorder) Write(p []byte) (n int, err error) {
 | 
			
		||||
	w.mu.Lock()
 | 
			
		||||
	defer w.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	w.header.Now()
 | 
			
		||||
	if _, err = w.header.WriteTo(w.wc); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if _, err = w.wc.Write(p); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	return len(p) + 9, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *ttyRecRecorder) Reads() io.WriteCloser {
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *ttyRecRecorder) Writes() io.WriteCloser {
 | 
			
		||||
	return nopCloser{io.Discard}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								ssh/channel.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								ssh/channel.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
package ssh
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DupeChannel struct {
 | 
			
		||||
	ssh.Channel
 | 
			
		||||
 | 
			
		||||
	// Reads writes read actions.
 | 
			
		||||
	Reads io.WriteCloser
 | 
			
		||||
 | 
			
		||||
	// Writer writes write actions.
 | 
			
		||||
	Writes io.WriteCloser
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c DupeChannel) Close() error {
 | 
			
		||||
	var errs []error
 | 
			
		||||
	for _, closer := range []io.Closer{
 | 
			
		||||
		c.Channel,
 | 
			
		||||
		c.Reads,
 | 
			
		||||
		c.Writes,
 | 
			
		||||
	} {
 | 
			
		||||
		if closer == nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if cerr := closer.Close(); cerr != nil {
 | 
			
		||||
			errs = append(errs, cerr)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return errors.Join(errs...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c DupeChannel) Read(p []byte) (n int, err error) {
 | 
			
		||||
	if c.Reads == nil {
 | 
			
		||||
		return c.Channel.Read(p)
 | 
			
		||||
	}
 | 
			
		||||
	return io.TeeReader(c.Channel, c.Reads).Read(p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c DupeChannel) Write(p []byte) (n int, err error) {
 | 
			
		||||
	if c.Writes == nil {
 | 
			
		||||
		return c.Channel.Write(p)
 | 
			
		||||
	}
 | 
			
		||||
	if n, err = c.Channel.Write(p); n > 0 {
 | 
			
		||||
		_, _ = c.Writes.Write(p[:])
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								ssh/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								ssh/client.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
package ssh
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Client struct {
 | 
			
		||||
	netConn net.Conn
 | 
			
		||||
	client  *ssh.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ClientConfig struct {
 | 
			
		||||
	ssh.ClientConfig
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewClient(conn net.Conn, config *ClientConfig) (*Client, error) {
 | 
			
		||||
	sshConn, channels, requests, err := ssh.NewClientConn(conn, conn.RemoteAddr().String(), &config.ClientConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Client{
 | 
			
		||||
		netConn: conn,
 | 
			
		||||
		client:  ssh.NewClient(sshConn, channels, requests),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								ssh/compat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								ssh/compat.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
package ssh
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ChannelTypeDefault     = ""
 | 
			
		||||
	ChannelTypeAgent       = "auth-agent@openssh.com"
 | 
			
		||||
	ChannelTypeDirectTCPIP = "direct-tcpip"
 | 
			
		||||
	ChannelTypeSession     = "session"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	RequestTypeAgent        = "auth-agent-req@openssh.com"
 | 
			
		||||
	RequestTypeEnv          = "env"
 | 
			
		||||
	RequestTypeExec         = "exec"
 | 
			
		||||
	RequestTypePTY          = "pty-req"
 | 
			
		||||
	RequestTypeShell        = "shell"
 | 
			
		||||
	RequestTypeWindowChange = "window-change"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Type aliases for convenience.
 | 
			
		||||
type (
 | 
			
		||||
	CertChecker  = ssh.CertChecker
 | 
			
		||||
	Certificate  = ssh.Certificate
 | 
			
		||||
	Conn         = ssh.Conn
 | 
			
		||||
	ConnMetadata = ssh.ConnMetadata
 | 
			
		||||
	Permissions  = ssh.Permissions
 | 
			
		||||
	PublicKey    = ssh.PublicKey
 | 
			
		||||
	ServerConfig = ssh.ServerConfig
 | 
			
		||||
	Signer       = ssh.Signer
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func MarshalAuthorizedKey(in PublicKey) []byte {
 | 
			
		||||
	return ssh.MarshalAuthorizedKey(in)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseAuthorizedKey(in []byte) (out PublicKey, options []string, err error) {
 | 
			
		||||
	out, _, options, _, err = ssh.ParseAuthorizedKey(in)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParsePublicKey(in []byte) (out PublicKey, err error) {
 | 
			
		||||
	return ssh.ParsePublicKey(in)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										204
									
								
								ssh/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								ssh/context.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,204 @@
 | 
			
		||||
package ssh
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"io"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var seed = rand.NewSource(time.Now().UnixNano())
 | 
			
		||||
 | 
			
		||||
type Context interface {
 | 
			
		||||
	ssh.ConnMetadata
 | 
			
		||||
 | 
			
		||||
	// ID is the unique identifier.
 | 
			
		||||
	ID() string
 | 
			
		||||
 | 
			
		||||
	// Conn is the [ssh.Conn].
 | 
			
		||||
	Conn() ssh.Conn
 | 
			
		||||
 | 
			
		||||
	// NetConn is the underlying [net.Conn].
 | 
			
		||||
	NetConn() net.Conn
 | 
			
		||||
 | 
			
		||||
	// Close the client connection.
 | 
			
		||||
	Close() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type sshContext struct {
 | 
			
		||||
	id      uint64
 | 
			
		||||
	server  *Server
 | 
			
		||||
	netConn net.Conn
 | 
			
		||||
	conn    *ssh.ServerConn
 | 
			
		||||
	log     logger.Structured
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newSSHContext(server *Server, netConn net.Conn, conn *ssh.ServerConn, log logger.Structured) *sshContext {
 | 
			
		||||
	ctx := &sshContext{
 | 
			
		||||
		id:      uint64(seed.Int63()),
 | 
			
		||||
		server:  server,
 | 
			
		||||
		netConn: netConn,
 | 
			
		||||
		conn:    conn,
 | 
			
		||||
	}
 | 
			
		||||
	ctx.log = log.Value("context", ctx.ID())
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// User returns the user ID for this connection.
 | 
			
		||||
func (ctx *sshContext) User() string {
 | 
			
		||||
	return ctx.conn.User()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SessionID returns the session hash, also denoted by H.
 | 
			
		||||
func (ctx *sshContext) SessionID() []byte {
 | 
			
		||||
	return ctx.conn.SessionID()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClientVersion returns the client's version string as hashed
 | 
			
		||||
// into the session ID.
 | 
			
		||||
func (ctx *sshContext) ClientVersion() []byte {
 | 
			
		||||
	return ctx.conn.ClientVersion()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ServerVersion returns the server's version string as hashed
 | 
			
		||||
// into the session ID.
 | 
			
		||||
func (ctx *sshContext) ServerVersion() []byte {
 | 
			
		||||
	return ctx.conn.ServerVersion()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RemoteAddr returns the remote address for this connection.
 | 
			
		||||
func (ctx *sshContext) RemoteAddr() net.Addr {
 | 
			
		||||
	return ctx.netConn.RemoteAddr()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LocalAddr returns the local address for this connection.
 | 
			
		||||
func (ctx *sshContext) LocalAddr() net.Addr {
 | 
			
		||||
	return ctx.netConn.LocalAddr()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *sshContext) handleChannels(channels <-chan ssh.NewChannel) (err error) {
 | 
			
		||||
	for newChan := range channels {
 | 
			
		||||
		var (
 | 
			
		||||
			kind = newChan.ChannelType()
 | 
			
		||||
			log  = ctx.log.Value("channel", kind)
 | 
			
		||||
		)
 | 
			
		||||
		log.Trace("Client requested new channel")
 | 
			
		||||
 | 
			
		||||
		handler, ok := ctx.server.ChannelHandler[kind]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			handler = ctx.server.ChannelHandler[ChannelTypeDefault]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if handler != nil {
 | 
			
		||||
			var (
 | 
			
		||||
				channel  ssh.Channel
 | 
			
		||||
				requests <-chan *ssh.Request
 | 
			
		||||
			)
 | 
			
		||||
			if channel, requests, err = newChan.Accept(); err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if err = handler.HandleChannel(ctx, channel, requests, newChan.ExtraData()); err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else if kind == ChannelTypeDirectTCPIP && ctx.server.PortForwardHandler != nil {
 | 
			
		||||
			if err = ctx.handleDirectTCPIP(newChan); err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.log.Debug("Rejecting unsupported channel type")
 | 
			
		||||
			if err = newChan.Reject(ssh.Prohibited, ""); err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Our client hang up.
 | 
			
		||||
	return io.EOF
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *sshContext) handleDirectTCPIP(newChan ssh.NewChannel) (err error) {
 | 
			
		||||
	var payload struct {
 | 
			
		||||
		Host       string
 | 
			
		||||
		Port       uint32
 | 
			
		||||
		OriginAddr string
 | 
			
		||||
		OriginPort uint32
 | 
			
		||||
	}
 | 
			
		||||
	if err = ssh.Unmarshal(newChan.ExtraData(), &payload); err != nil {
 | 
			
		||||
		_ = newChan.Reject(ssh.Prohibited, "")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var ip net.IP
 | 
			
		||||
	if ip = net.ParseIP(payload.Host); ip == nil {
 | 
			
		||||
		// Not an IP
 | 
			
		||||
		var ips []net.IP
 | 
			
		||||
		if ips, err = net.LookupIP(payload.Host); err != nil {
 | 
			
		||||
			_ = newChan.Reject(ssh.ConnectionFailed, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		} else if len(ips) == 0 {
 | 
			
		||||
			_ = newChan.Reject(ssh.ConnectionFailed, "")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ip = ips[0]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		raddr = &net.TCPAddr{
 | 
			
		||||
			IP:   ip,
 | 
			
		||||
			Port: int(payload.Port),
 | 
			
		||||
		}
 | 
			
		||||
		laddr = &net.TCPAddr{
 | 
			
		||||
			IP:   net.ParseIP(payload.OriginAddr),
 | 
			
		||||
			Port: int(payload.OriginPort),
 | 
			
		||||
		}
 | 
			
		||||
	)
 | 
			
		||||
	if payload.OriginAddr == "" && payload.OriginPort == 0 {
 | 
			
		||||
		laddr = nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var conn net.Conn
 | 
			
		||||
	if conn, err = ctx.server.PortForwardHandler.HandlePortForwardRequest(ctx, raddr, laddr); err != nil {
 | 
			
		||||
		_ = newChan.Reject(ssh.ConnectionFailed, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer func() { _ = conn.Close() }()
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		channel  ssh.Channel
 | 
			
		||||
		requests <-chan *ssh.Request
 | 
			
		||||
	)
 | 
			
		||||
	if channel, requests, err = newChan.Accept(); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer func() { _ = channel.Close() }()
 | 
			
		||||
	go ssh.DiscardRequests(requests)
 | 
			
		||||
 | 
			
		||||
	go io.Copy(channel, conn)
 | 
			
		||||
	_, err = io.Copy(conn, channel)
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *sshContext) Conn() ssh.Conn {
 | 
			
		||||
	return ctx.conn
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *sshContext) NetConn() net.Conn {
 | 
			
		||||
	return ctx.netConn
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *sshContext) Close() error {
 | 
			
		||||
	return ctx.conn.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *sshContext) ID() string {
 | 
			
		||||
	var b [8]byte
 | 
			
		||||
	binary.BigEndian.PutUint64(b[:], ctx.id)
 | 
			
		||||
	return hex.EncodeToString(b[:])
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										352
									
								
								ssh/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										352
									
								
								ssh/handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,352 @@
 | 
			
		||||
package ssh
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"crypto/rsa"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
	"golang.org/x/crypto/ssh/agent"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/internal/netutil"
 | 
			
		||||
	"git.maze.io/maze/conduit/internal/stringutil"
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
	"git.maze.io/maze/conduit/ssh/sshutil"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ConnectHandler interface {
 | 
			
		||||
	HandleConnect(net.Conn) (net.Conn, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ConnectHandlerFunc func(net.Conn) (net.Conn, error)
 | 
			
		||||
 | 
			
		||||
func (f ConnectHandlerFunc) HandleConnect(c net.Conn) (net.Conn, error) {
 | 
			
		||||
	return f(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AcceptHandler interface {
 | 
			
		||||
	HandleAccept(net.Conn, *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MaxConnections limits the maximum number of connections.
 | 
			
		||||
func MaxConnections(max int) ConnectHandler {
 | 
			
		||||
	var connections atomic.Int64
 | 
			
		||||
	return ConnectHandlerFunc(func(c net.Conn) (net.Conn, error) {
 | 
			
		||||
		if max <= 0 {
 | 
			
		||||
			return nil, nil
 | 
			
		||||
		} else if connections.Load() >= int64(max) {
 | 
			
		||||
			return nil, errors.New("server: maximum number of connections reached")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var (
 | 
			
		||||
			once sync.Once
 | 
			
		||||
			cc   = &netutil.ConnCloser{
 | 
			
		||||
				Conn: c,
 | 
			
		||||
				Closer: func() error {
 | 
			
		||||
					once.Do(func() { connections.Add(-1) })
 | 
			
		||||
					return c.Close()
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
		)
 | 
			
		||||
		connections.Add(1)
 | 
			
		||||
		return cc, nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChannelHandler interface {
 | 
			
		||||
	HandleChannel(Context, ssh.Channel, <-chan *ssh.Request, []byte) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChannelHandlerFunc func(Context, ssh.Channel, <-chan *ssh.Request, []byte) error
 | 
			
		||||
 | 
			
		||||
func (f ChannelHandlerFunc) HandleChannel(ctx Context, channel ssh.Channel, requests <-chan *ssh.Request, extra []byte) error {
 | 
			
		||||
	return f(ctx, channel, requests, extra)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type debugSessionInfo struct {
 | 
			
		||||
	start        time.Time
 | 
			
		||||
	duration     time.Duration
 | 
			
		||||
	method       string
 | 
			
		||||
	agent        agent.Agent
 | 
			
		||||
	agentRequest bool
 | 
			
		||||
	agentChannel ssh.Channel
 | 
			
		||||
	agentError   error
 | 
			
		||||
	env          map[string]string
 | 
			
		||||
	pty          *sshutil.PTYRequest
 | 
			
		||||
	windowChange *sshutil.WindowChangeRequest
 | 
			
		||||
	unsupported  []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newDebugSessionInfo() *debugSessionInfo {
 | 
			
		||||
	return &debugSessionInfo{
 | 
			
		||||
		start: time.Now(),
 | 
			
		||||
		env:   make(map[string]string),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DebugSession is a session channel handler that will print debug information to the client, which
 | 
			
		||||
// may aid with troubleshooting SSH connectivity or client issues.
 | 
			
		||||
func DebugSession() ChannelHandler {
 | 
			
		||||
	debugPrivateKey, _ := rsa.GenerateKey(rand.Reader, 1024)
 | 
			
		||||
 | 
			
		||||
	return ChannelHandlerFunc(func(ctx Context, channel ssh.Channel, requests <-chan *ssh.Request, _ []byte) error {
 | 
			
		||||
		defer channel.Close()
 | 
			
		||||
 | 
			
		||||
		var (
 | 
			
		||||
			log  = ctx.(*sshContext).log
 | 
			
		||||
			done = make(chan struct{}, 1)
 | 
			
		||||
			info = newDebugSessionInfo()
 | 
			
		||||
			conn = ctx.Conn()
 | 
			
		||||
			err  error
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		go func() {
 | 
			
		||||
			var (
 | 
			
		||||
				reply     bool
 | 
			
		||||
				reesponse []byte
 | 
			
		||||
			)
 | 
			
		||||
			for request := range requests {
 | 
			
		||||
				log.Values(logger.Values{
 | 
			
		||||
					"request": request.Type,
 | 
			
		||||
				}).Trace("New session channel request")
 | 
			
		||||
				switch request.Type {
 | 
			
		||||
				case RequestTypeAgent:
 | 
			
		||||
					info.agentRequest = true
 | 
			
		||||
					agentChannel, agentRequests, err := conn.OpenChannel(ChannelTypeAgent, nil)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						info.agentError = err
 | 
			
		||||
					} else {
 | 
			
		||||
						go ssh.DiscardRequests(agentRequests)
 | 
			
		||||
						info.agent = agent.NewClient(agentChannel)
 | 
			
		||||
						info.agentChannel = agentChannel
 | 
			
		||||
					}
 | 
			
		||||
					reply = true
 | 
			
		||||
 | 
			
		||||
				case RequestTypeEnv:
 | 
			
		||||
					var payload *sshutil.EnvRequest
 | 
			
		||||
					if payload, err = sshutil.ParseEnvRequest(request.Payload); err != nil {
 | 
			
		||||
						log.Err(err).Debug("Corrupted env request payload, discarding")
 | 
			
		||||
					} else {
 | 
			
		||||
						log.Values(logger.Values{
 | 
			
		||||
							"key":   payload.Key,
 | 
			
		||||
							"value": payload.Value,
 | 
			
		||||
						}).Trace("Client requested env variable")
 | 
			
		||||
						info.env[payload.Key] = payload.Value
 | 
			
		||||
					}
 | 
			
		||||
					reply = true
 | 
			
		||||
 | 
			
		||||
				case RequestTypePTY:
 | 
			
		||||
					if info.pty, err = sshutil.ParsePTYRequest(request.Payload); err != nil {
 | 
			
		||||
						log.Err(err).Debug("Corrupted pty request payload, discarding")
 | 
			
		||||
					} else {
 | 
			
		||||
						log.Values(logger.Values{
 | 
			
		||||
							"term": info.pty.Term,
 | 
			
		||||
							"size": fmt.Sprintf("%dx%d", info.pty.Columns, info.pty.Rows),
 | 
			
		||||
						}).Trace("Client requested PTY")
 | 
			
		||||
					}
 | 
			
		||||
					reply = true
 | 
			
		||||
 | 
			
		||||
				case RequestTypeExec, RequestTypeShell:
 | 
			
		||||
					info.method = request.Type
 | 
			
		||||
					select {
 | 
			
		||||
					case <-done:
 | 
			
		||||
					default:
 | 
			
		||||
						close(done)
 | 
			
		||||
					}
 | 
			
		||||
					//return
 | 
			
		||||
 | 
			
		||||
				case RequestTypeWindowChange:
 | 
			
		||||
					if info.windowChange, err = sshutil.ParseWindowChangeRequest(request.Payload); err != nil {
 | 
			
		||||
						log.Err(err).Debug("Corrupted window change request payload, discarding")
 | 
			
		||||
					} else {
 | 
			
		||||
						log.Values(logger.Values{
 | 
			
		||||
							"size": fmt.Sprintf("%dx%d", info.windowChange.Columns, info.windowChange.Rows),
 | 
			
		||||
						}).Trace("Client requested window change")
 | 
			
		||||
					}
 | 
			
		||||
					reply = true
 | 
			
		||||
 | 
			
		||||
				default:
 | 
			
		||||
					log.Values(logger.Values{
 | 
			
		||||
						"request": request.Type,
 | 
			
		||||
						"payload": hex.EncodeToString(request.Payload),
 | 
			
		||||
					}).Trace("Client requested something we don't understand, ignored")
 | 
			
		||||
					info.unsupported = append(info.unsupported, request.Type)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if request.WantReply {
 | 
			
		||||
					if err := request.Reply(reply, reesponse); err != nil {
 | 
			
		||||
						log.Err(err).Debug("Error sending session channel request reply: terminating")
 | 
			
		||||
						_ = channel.Close()
 | 
			
		||||
						select {
 | 
			
		||||
						case <-done:
 | 
			
		||||
						default:
 | 
			
		||||
							close(done)
 | 
			
		||||
						}
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			select {
 | 
			
		||||
			case <-done:
 | 
			
		||||
			default:
 | 
			
		||||
				close(done)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		select {
 | 
			
		||||
		case <-done:
 | 
			
		||||
		case <-time.After(10 * time.Second):
 | 
			
		||||
			io.WriteString(channel, "Timeout waiting for your client to send either an exec or shell request\r\n")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Any requests that follow are ignored from hereon forward.
 | 
			
		||||
		go ssh.DiscardRequests(requests)
 | 
			
		||||
 | 
			
		||||
		// Attempt to request agent forwarding
 | 
			
		||||
		if info.agent == nil && !info.agentRequest {
 | 
			
		||||
			agentChannel, agentRequests, err := conn.OpenChannel(ChannelTypeAgent, nil)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				info.agentError = err
 | 
			
		||||
			} else {
 | 
			
		||||
				go ssh.DiscardRequests(agentRequests)
 | 
			
		||||
				info.agent = agent.NewClient(agentChannel)
 | 
			
		||||
				info.agentChannel = agentChannel
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		info.duration = time.Since(info.start)
 | 
			
		||||
		printSessionInfo(ctx, channel, info, debugPrivateKey)
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func printSessionInfo(ctx Context, channel ssh.Channel, info *debugSessionInfo, key crypto.PrivateKey) {
 | 
			
		||||
	if info.agentChannel != nil {
 | 
			
		||||
		defer info.agentChannel.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conn := ctx.Conn().(*ssh.ServerConn)
 | 
			
		||||
 | 
			
		||||
	fmt.Fprintf(channel, "It took your client %s to request %s\r\n\r\n", info.duration, info.method)
 | 
			
		||||
	fmt.Fprintf(channel, "SSH connection information:\r\n")
 | 
			
		||||
	fmt.Fprintf(channel, "  Server:\r\n")
 | 
			
		||||
	fmt.Fprintf(channel, "    Version:     \x1b[1m%s\x1b[0m\r\n", conn.ServerVersion())
 | 
			
		||||
	fmt.Fprintf(channel, "  Client:\r\n")
 | 
			
		||||
	fmt.Fprintf(channel, "    Version:     \x1b[1m%s\x1b[0m\r\n", conn.ClientVersion())
 | 
			
		||||
	fmt.Fprintf(channel, "    Username:    \x1b[1m%s\x1b[0m\r\n", conn.User())
 | 
			
		||||
 | 
			
		||||
	if conn.Permissions != nil {
 | 
			
		||||
		if conn.Permissions.CriticalOptions != nil {
 | 
			
		||||
			fmt.Fprint(channel, "    Options:\r\n")
 | 
			
		||||
			for k := range stringutil.MapKeys(conn.Permissions.CriticalOptions) {
 | 
			
		||||
				fmt.Fprintf(channel, "  ✅ %s=%s\r\n", k, conn.Permissions.CriticalOptions[k])
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if conn.Permissions.Extensions != nil {
 | 
			
		||||
			fmt.Fprint(channel, "    Extensions:\r\n")
 | 
			
		||||
			for k := range stringutil.MapKeys(conn.Permissions.Extensions) {
 | 
			
		||||
				fmt.Fprintf(channel, "  ✅ %s=%s\r\n", k, conn.Permissions.Extensions[k])
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Fprintf(channel, "    Environment: (%d variables sent)\r\n", len(info.env))
 | 
			
		||||
	for k := range stringutil.MapKeys(info.env) {
 | 
			
		||||
		fmt.Fprintf(channel, "      ✅ %s=%s\r\n", k, info.env[k])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if info.pty == nil {
 | 
			
		||||
		fmt.Fprint(channel, "    PTY:         not requested\r\n")
 | 
			
		||||
	} else {
 | 
			
		||||
		fmt.Fprintf(channel, "    PTY:         %d cols, %d rows, %dx%d, %q\r\n",
 | 
			
		||||
			info.pty.Columns, info.pty.Rows, info.pty.Width, info.pty.Height, info.pty.Term)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if info.windowChange == nil {
 | 
			
		||||
		fmt.Fprint(channel, "    Window:      not requested\r\n")
 | 
			
		||||
	} else {
 | 
			
		||||
		fmt.Fprintf(channel, "    Window:      %d cols, %d rows, %dx%d",
 | 
			
		||||
			info.windowChange.Columns, info.windowChange.Rows,
 | 
			
		||||
			info.windowChange.Width, info.windowChange.Height)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(info.unsupported) > 0 {
 | 
			
		||||
		fmt.Fprintf(channel, "    Requests:    (%d unsupported):\r\n", len(info.unsupported))
 | 
			
		||||
		sort.Strings(info.unsupported)
 | 
			
		||||
		for _, v := range info.unsupported {
 | 
			
		||||
			fmt.Fprintf(channel, "  ❌ %s\r\n", v)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		fmt.Fprint(channel, "    Requests:    no unsupported requests\r\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if info.agentRequest {
 | 
			
		||||
		if info.agentError != nil {
 | 
			
		||||
			fmt.Fprint(channel, "    Agent:       requested but unavailable:\r\n")
 | 
			
		||||
			fmt.Fprintf(channel, "      ❌ %v\r\n", info.agentError)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if info.agentError == nil {
 | 
			
		||||
			fmt.Fprint(channel, "    Agent:       not requested by client, but accepted, upgrade your client!\r\n")
 | 
			
		||||
		} else {
 | 
			
		||||
			fmt.Fprint(channel, "    Agent:       not requested by client, and client refused our attempt (good!):\r\n")
 | 
			
		||||
			fmt.Fprintf(channel, "      ❌ %v\r\n", info.agentError)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if info.agent != nil {
 | 
			
		||||
		fmt.Fprint(channel, "    Agent:       available, checking keys:\r\n")
 | 
			
		||||
		keys, err := info.agent.List()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Fprintf(channel, "      ❌ %v\r\n", err)
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, key := range keys {
 | 
			
		||||
				blob := make([]byte, 8)
 | 
			
		||||
				rand.Reader.Read(blob)
 | 
			
		||||
				if _, err := info.agent.Sign(key, blob); err != nil {
 | 
			
		||||
					fmt.Fprint(channel, "      ❌ ")
 | 
			
		||||
				} else {
 | 
			
		||||
					fmt.Fprint(channel, "      ✅ ")
 | 
			
		||||
				}
 | 
			
		||||
				fmt.Fprintf(channel, "%-4d %s %s (%s)\r\n",
 | 
			
		||||
					sshutil.KeyBits(key), sshutil.KeyFingerprint(key),
 | 
			
		||||
					key.Comment, sshutil.KeyType(key))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Fprint(channel, "    Agent:       available, checking add/remove:\r\n")
 | 
			
		||||
		if err = info.agent.Add(agent.AddedKey{
 | 
			
		||||
			PrivateKey:   key,
 | 
			
		||||
			LifetimeSecs: 5,
 | 
			
		||||
			Comment:      "Test by conduit",
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			fmt.Fprintf(channel, "      ❌ Add: %v\r\n", err)
 | 
			
		||||
		} else {
 | 
			
		||||
			fmt.Fprint(channel, "      ✅ Add\r\n")
 | 
			
		||||
		}
 | 
			
		||||
		pk, _ := ssh.NewPublicKey(key.(*rsa.PrivateKey).Public())
 | 
			
		||||
		if err = info.agent.Remove(pk); err != nil {
 | 
			
		||||
			fmt.Fprintf(channel, "      ❌ Remove: %v\r\n", err)
 | 
			
		||||
		} else {
 | 
			
		||||
			fmt.Fprint(channel, "      ✅ Remove\r\n")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func StaticSession(message string) ChannelHandler {
 | 
			
		||||
	return ChannelHandlerFunc(func(ctx Context, channel ssh.Channel, requests <-chan *ssh.Request, _ []byte) error {
 | 
			
		||||
		go ssh.DiscardRequests(requests)
 | 
			
		||||
		io.WriteString(channel, message+"\r\n")
 | 
			
		||||
		return channel.Close()
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								ssh/handler_tunnel.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								ssh/handler_tunnel.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
package ssh
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Dialer interface {
 | 
			
		||||
	DialContext(ctx context.Context, network, address string) (net.Conn, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ForwardTunnel(dialer Dialer) ChannelHandler {
 | 
			
		||||
	if dialer == nil {
 | 
			
		||||
		dialer = new(net.Dialer)
 | 
			
		||||
	}
 | 
			
		||||
	return ChannelHandlerFunc(func(ctx Context, channel ssh.Channel, requests <-chan *ssh.Request, _ []byte) error {
 | 
			
		||||
		return errors.New("byez!")
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PortForwardRequestHandler interface {
 | 
			
		||||
	HandlePortForwardRequest(ctx Context, raddr, laddr net.Addr) (net.Conn, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PortForwardRequestHandlerFunc func(Context, net.Addr, net.Addr) (net.Conn, error)
 | 
			
		||||
 | 
			
		||||
func (f PortForwardRequestHandlerFunc) HandlePortForwardRequest(ctx Context, raddr, laddr net.Addr) (net.Conn, error) {
 | 
			
		||||
	return f(ctx, raddr, laddr)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PortForwardDialer(dialer Dialer) PortForwardRequestHandler {
 | 
			
		||||
	if dialer == nil {
 | 
			
		||||
		dialer = new(net.Dialer)
 | 
			
		||||
	}
 | 
			
		||||
	return PortForwardRequestHandlerFunc(func(ctx Context, raddr, laddr net.Addr) (net.Conn, error) {
 | 
			
		||||
		log := ctx.(*sshContext).log.Values(logger.Values{
 | 
			
		||||
			"laddr": laddr.String(),
 | 
			
		||||
			"raddr": raddr.String(),
 | 
			
		||||
		})
 | 
			
		||||
		log.Debug("Dialing port forwarding request")
 | 
			
		||||
		return dialer.DialContext(context.Background(), raddr.Network(), raddr.String())
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								ssh/keys.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								ssh/keys.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
package ssh
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func LoadPrivateKey(name string) (ssh.Signer, error) {
 | 
			
		||||
	if strings.Contains(name, "-----BEGIN") && strings.Contains(name, "PRIVATE KEY-----") {
 | 
			
		||||
		logger.StandardLog.Debug("Loading private key from string")
 | 
			
		||||
		return ssh.ParsePrivateKey([]byte(name))
 | 
			
		||||
	}
 | 
			
		||||
	logger.StandardLog.Value("path", name).Debug("Loading private key")
 | 
			
		||||
	b, err := os.ReadFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return ssh.ParsePrivateKey(b)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LoadPrivateKeyWithPassphrase(name string, passphrase []byte) (ssh.Signer, error) {
 | 
			
		||||
	if strings.Contains(name, "-----BEGIN") && strings.Contains(name, "PRIVATE KEY-----") {
 | 
			
		||||
		logger.StandardLog.Debug("Loading private key from string (with passphrase)")
 | 
			
		||||
		return ssh.ParsePrivateKeyWithPassphrase([]byte(name), passphrase)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.StandardLog.Value("path", name).Debug("Loading private key (with passphrase)")
 | 
			
		||||
	b, err := os.ReadFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return ssh.ParsePrivateKeyWithPassphrase(b, passphrase)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										115
									
								
								ssh/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								ssh/server.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
			
		||||
package ssh
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/auth"
 | 
			
		||||
	"git.maze.io/maze/conduit/internal/netutil"
 | 
			
		||||
	"git.maze.io/maze/conduit/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Server struct {
 | 
			
		||||
	// ConnectHandler gets called before accepting new connections.
 | 
			
		||||
	ConnectHandler []ConnectHandler
 | 
			
		||||
 | 
			
		||||
	// AcceptHandler accepts new SSH connections, it takes care of authenticating, etc.
 | 
			
		||||
	AcceptHandler AcceptHandler
 | 
			
		||||
 | 
			
		||||
	// Handler per channel type.
 | 
			
		||||
	ChannelHandler map[string]ChannelHandler
 | 
			
		||||
 | 
			
		||||
	// PortForwardHandler
 | 
			
		||||
	PortForwardHandler PortForwardRequestHandler
 | 
			
		||||
 | 
			
		||||
	// FIPSMode enables FIPS 140-2 compatible ciphers, key exchanges, etc.
 | 
			
		||||
	FIPSMode bool // TODO(maze): implement
 | 
			
		||||
 | 
			
		||||
	// Logger for our server.
 | 
			
		||||
	Logger logger.Structured
 | 
			
		||||
 | 
			
		||||
	// serverConfig is our SSH server configuration.
 | 
			
		||||
	serverConfig *ssh.ServerConfig
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewServer(keys []ssh.Signer) *Server {
 | 
			
		||||
	config := new(ssh.ServerConfig)
 | 
			
		||||
	config.SetDefaults()
 | 
			
		||||
	config.ServerVersion = "SSH-2.0-conduit"
 | 
			
		||||
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		config.AddHostKey(key)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Server{
 | 
			
		||||
		ChannelHandler: make(map[string]ChannelHandler),
 | 
			
		||||
		Logger:         logger.StandardLog,
 | 
			
		||||
		serverConfig:   config,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) Serve(l net.Listener) error {
 | 
			
		||||
	for {
 | 
			
		||||
		c, err := l.Accept()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		go s.handle(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) handle(c net.Conn) {
 | 
			
		||||
	log := s.Logger.Values(logger.Values{
 | 
			
		||||
		"client": c.RemoteAddr().String(),
 | 
			
		||||
		"server": c.LocalAddr().String(),
 | 
			
		||||
	})
 | 
			
		||||
	log.Debug("New client connection")
 | 
			
		||||
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := c.Close(); err != nil && !netutil.IsClosing(err) {
 | 
			
		||||
			log = log.Err(err)
 | 
			
		||||
		}
 | 
			
		||||
		log.Debug("Closing client connection")
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	for _, h := range s.ConnectHandler {
 | 
			
		||||
		n, err := h.HandleConnect(c)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Err(err).Warn("Error from connect handler, closing client connection")
 | 
			
		||||
			return
 | 
			
		||||
		} else if n != nil {
 | 
			
		||||
			log.Debugf("Replacing client connection with %T", n)
 | 
			
		||||
			c = n
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Configure our SSH server.
 | 
			
		||||
	handler := s.AcceptHandler
 | 
			
		||||
	if handler == nil {
 | 
			
		||||
		log.Warn("No accept handler configured, using NO AUTHENTICATION")
 | 
			
		||||
		handler = auth.None{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We made it, now let's talk some SSH.
 | 
			
		||||
	sshConn, channels, requests, err := handler.HandleAccept(c, s.serverConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Err(err).Warn("Error establishing SSH session with client")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	go ssh.DiscardRequests(requests)
 | 
			
		||||
 | 
			
		||||
	log = log.Value("user", sshConn.User())
 | 
			
		||||
	ctx := newSSHContext(s, c, sshConn, log)
 | 
			
		||||
	log = log.Value("context", ctx.ID())
 | 
			
		||||
	log.Value("version", string(sshConn.ClientVersion())).Info("New SSH client")
 | 
			
		||||
 | 
			
		||||
	if err = ctx.handleChannels(channels); err != nil {
 | 
			
		||||
		if netutil.IsClosing(err) {
 | 
			
		||||
			log.Err(err).Debug("Client handler terminated")
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Err(err).Warn("Error handling channel requests")
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								ssh/sshutil/key.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								ssh/sshutil/key.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
package sshutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"math/big"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func KeyBits(key ssh.PublicKey) int {
 | 
			
		||||
	if key == nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	switch key.Type() {
 | 
			
		||||
	case ssh.KeyAlgoECDSA256:
 | 
			
		||||
		return 256
 | 
			
		||||
	case ssh.KeyAlgoSKECDSA256:
 | 
			
		||||
		return 256
 | 
			
		||||
	case ssh.KeyAlgoECDSA384:
 | 
			
		||||
		return 384
 | 
			
		||||
	case ssh.KeyAlgoECDSA521:
 | 
			
		||||
		return 521
 | 
			
		||||
	case ssh.KeyAlgoED25519:
 | 
			
		||||
		return 256
 | 
			
		||||
	case ssh.KeyAlgoSKED25519:
 | 
			
		||||
		return 256
 | 
			
		||||
	case ssh.KeyAlgoRSA:
 | 
			
		||||
		var w struct {
 | 
			
		||||
			Name string
 | 
			
		||||
			E    *big.Int
 | 
			
		||||
			N    *big.Int
 | 
			
		||||
			Rest []byte `ssh:"rest"`
 | 
			
		||||
		}
 | 
			
		||||
		_ = ssh.Unmarshal(key.Marshal(), &w)
 | 
			
		||||
		return w.N.BitLen()
 | 
			
		||||
	default:
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func KeyType(key ssh.PublicKey) string {
 | 
			
		||||
	if key == nil {
 | 
			
		||||
		return "<nil>"
 | 
			
		||||
	}
 | 
			
		||||
	switch key.Type() {
 | 
			
		||||
	case ssh.KeyAlgoECDSA256, ssh.KeyAlgoSKECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
 | 
			
		||||
		return "ECDSA"
 | 
			
		||||
	case ssh.KeyAlgoED25519, ssh.KeyAlgoSKED25519:
 | 
			
		||||
		return "ED25519"
 | 
			
		||||
	case ssh.KeyAlgoRSA:
 | 
			
		||||
		return "RSA"
 | 
			
		||||
	default:
 | 
			
		||||
		return key.Type()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func KeyFingerprint(key ssh.PublicKey) string {
 | 
			
		||||
	h := sha256.New()
 | 
			
		||||
	h.Write(key.Marshal())
 | 
			
		||||
	return "SHA256:" + base64.RawStdEncoding.EncodeToString(h.Sum(nil))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								ssh/sshutil/request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								ssh/sshutil/request.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
package sshutil
 | 
			
		||||
 | 
			
		||||
import "golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
type EnvRequest struct {
 | 
			
		||||
	Key, Value string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseEnvRequest(data []byte) (*EnvRequest, error) {
 | 
			
		||||
	r := new(EnvRequest)
 | 
			
		||||
	if err := ssh.Unmarshal(data, r); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return r, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PTYRequest struct {
 | 
			
		||||
	Term     string
 | 
			
		||||
	Columns  uint32
 | 
			
		||||
	Rows     uint32
 | 
			
		||||
	Width    uint32
 | 
			
		||||
	Height   uint32
 | 
			
		||||
	ModeList []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParsePTYRequest(data []byte) (*PTYRequest, error) {
 | 
			
		||||
	r := new(PTYRequest)
 | 
			
		||||
	if err := ssh.Unmarshal(data, r); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return r, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type WindowChangeRequest struct {
 | 
			
		||||
	Columns uint32
 | 
			
		||||
	Rows    uint32
 | 
			
		||||
	Width   uint32
 | 
			
		||||
	Height  uint32
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseWindowChangeRequest(data []byte) (*WindowChangeRequest, error) {
 | 
			
		||||
	r := new(WindowChangeRequest)
 | 
			
		||||
	if err := ssh.Unmarshal(data, r); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return r, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										248
									
								
								storage/codec/binary.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								storage/codec/binary.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,248 @@
 | 
			
		||||
package codec
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"math"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	Register("binray", func() Codec { return binaryCodec{order: binary.NativeEndian} })
 | 
			
		||||
	Register("be", func() Codec { return binaryCodec{order: binary.BigEndian} })
 | 
			
		||||
	Register("le", func() Codec { return binaryCodec{order: binary.LittleEndian} })
 | 
			
		||||
	Register("ssh", func() Codec { return sshCodec{} })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type binaryCodec struct {
 | 
			
		||||
	order   binary.ByteOrder
 | 
			
		||||
	scratch [16]byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (binaryCodec) Type() string { return "binary" }
 | 
			
		||||
 | 
			
		||||
func (codec binaryCodec) Encode(value any) ([]byte, error) {
 | 
			
		||||
	switch value := value.(type) {
 | 
			
		||||
	case bool:
 | 
			
		||||
		if value {
 | 
			
		||||
			return []byte{1}, nil
 | 
			
		||||
		}
 | 
			
		||||
		return []byte{0}, nil
 | 
			
		||||
	case *bool:
 | 
			
		||||
		if *value {
 | 
			
		||||
			return []byte{1}, nil
 | 
			
		||||
		}
 | 
			
		||||
		return []byte{0}, nil
 | 
			
		||||
	case int:
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[:], uint64(value))
 | 
			
		||||
		return codec.scratch[:8], nil
 | 
			
		||||
	case *int:
 | 
			
		||||
		return codec.Encode(*value)
 | 
			
		||||
	case int8:
 | 
			
		||||
		return []byte{uint8(value)}, nil
 | 
			
		||||
	case *int8:
 | 
			
		||||
		return []byte{uint8(*value)}, nil
 | 
			
		||||
	case int16:
 | 
			
		||||
		codec.order.PutUint16(codec.scratch[:], uint16(value))
 | 
			
		||||
		return codec.scratch[:2], nil
 | 
			
		||||
	case *int16:
 | 
			
		||||
		return codec.Encode(*value)
 | 
			
		||||
	case int32:
 | 
			
		||||
		codec.order.PutUint32(codec.scratch[:], uint32(value))
 | 
			
		||||
		return codec.scratch[:4], nil
 | 
			
		||||
	case *int32:
 | 
			
		||||
		return codec.Encode(*value)
 | 
			
		||||
	case int64:
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[:], uint64(value))
 | 
			
		||||
		return codec.scratch[:8], nil
 | 
			
		||||
	case *int64:
 | 
			
		||||
		return codec.Encode(*value)
 | 
			
		||||
	case uint:
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[:], uint64(value))
 | 
			
		||||
		return codec.scratch[:8], nil
 | 
			
		||||
	case *uint:
 | 
			
		||||
		return codec.Encode(*value)
 | 
			
		||||
	case uint8:
 | 
			
		||||
		return []byte{value}, nil
 | 
			
		||||
	case *uint8:
 | 
			
		||||
		return []byte{*value}, nil
 | 
			
		||||
	case uint16:
 | 
			
		||||
		codec.order.PutUint16(codec.scratch[:], value)
 | 
			
		||||
		return codec.scratch[:2], nil
 | 
			
		||||
	case *uint16:
 | 
			
		||||
		return codec.Encode(*value)
 | 
			
		||||
	case uint32:
 | 
			
		||||
		codec.order.PutUint32(codec.scratch[:], value)
 | 
			
		||||
		return codec.scratch[:4], nil
 | 
			
		||||
	case *uint32:
 | 
			
		||||
		return codec.Encode(*value)
 | 
			
		||||
	case uint64:
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[:], value)
 | 
			
		||||
		return codec.scratch[:8], nil
 | 
			
		||||
	case *uint64:
 | 
			
		||||
		return codec.Encode(*value)
 | 
			
		||||
	case float32:
 | 
			
		||||
		codec.order.PutUint32(codec.scratch[:], math.Float32bits(value))
 | 
			
		||||
		return codec.scratch[:4], nil
 | 
			
		||||
	case *float32:
 | 
			
		||||
		codec.order.PutUint32(codec.scratch[:], math.Float32bits(*value))
 | 
			
		||||
		return codec.scratch[:4], nil
 | 
			
		||||
	case float64:
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[:], math.Float64bits(value))
 | 
			
		||||
		return codec.scratch[:8], nil
 | 
			
		||||
	case *float64:
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[:], math.Float64bits(*value))
 | 
			
		||||
		return codec.scratch[:8], nil
 | 
			
		||||
	case complex64:
 | 
			
		||||
		codec.order.PutUint32(codec.scratch[0:], math.Float32bits(real(value)))
 | 
			
		||||
		codec.order.PutUint32(codec.scratch[4:], math.Float32bits(imag(value)))
 | 
			
		||||
		return codec.scratch[:8], nil
 | 
			
		||||
	case *complex64:
 | 
			
		||||
		codec.order.PutUint32(codec.scratch[0:], math.Float32bits(real(*value)))
 | 
			
		||||
		codec.order.PutUint32(codec.scratch[4:], math.Float32bits(imag(*value)))
 | 
			
		||||
		return codec.scratch[:8], nil
 | 
			
		||||
	case complex128:
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[0:], math.Float64bits(real(value)))
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[4:], math.Float64bits(imag(value)))
 | 
			
		||||
		return codec.scratch[:16], nil
 | 
			
		||||
	case *complex128:
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[0:], math.Float64bits(real(*value)))
 | 
			
		||||
		codec.order.PutUint64(codec.scratch[4:], math.Float64bits(imag(*value)))
 | 
			
		||||
		return codec.scratch[:16], nil
 | 
			
		||||
	case string:
 | 
			
		||||
		n := binary.PutUvarint(codec.scratch[:], uint64(len(value)))
 | 
			
		||||
		return append(codec.scratch[:n], []byte(value)...), nil
 | 
			
		||||
	case *string:
 | 
			
		||||
		n := binary.PutUvarint(codec.scratch[:], uint64(len(*value)))
 | 
			
		||||
		return append(codec.scratch[:n], []byte(*value)...), nil
 | 
			
		||||
	case []byte:
 | 
			
		||||
		n := binary.PutUvarint(codec.scratch[:], uint64(len(value)))
 | 
			
		||||
		return append(codec.scratch[:n], value...), nil
 | 
			
		||||
	case *[]byte:
 | 
			
		||||
		n := binary.PutUvarint(codec.scratch[:], uint64(len(*value)))
 | 
			
		||||
		return append(codec.scratch[:n], *value...), nil
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, fmt.Errorf("codec: don't know how to binary encode %T", value)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (codec binaryCodec) Decode(data []byte, value any) error {
 | 
			
		||||
	switch value := value.(type) {
 | 
			
		||||
	case *bool:
 | 
			
		||||
		if len(data) < 1 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = data[0] != 0
 | 
			
		||||
 | 
			
		||||
	case *int:
 | 
			
		||||
		if len(data) < 8 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = int(codec.order.Uint64(data))
 | 
			
		||||
	case *int8:
 | 
			
		||||
		if len(data) < 1 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = int8(data[0])
 | 
			
		||||
	case *int16:
 | 
			
		||||
		if len(data) < 2 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = int16(codec.order.Uint16(data))
 | 
			
		||||
	case *int32:
 | 
			
		||||
		if len(data) < 4 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = int32(codec.order.Uint32(data))
 | 
			
		||||
	case *int64:
 | 
			
		||||
		if len(data) < 8 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = int64(codec.order.Uint64(data))
 | 
			
		||||
	case *uint:
 | 
			
		||||
		if len(data) < 8 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = uint(codec.order.Uint64(data))
 | 
			
		||||
	case *uint8:
 | 
			
		||||
		if len(data) < 1 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = data[0]
 | 
			
		||||
	case *uint16:
 | 
			
		||||
		if len(data) < 1 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = codec.order.Uint16(data)
 | 
			
		||||
	case *uint32:
 | 
			
		||||
		if len(data) < 4 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = codec.order.Uint32(data)
 | 
			
		||||
	case *uint64:
 | 
			
		||||
		if len(data) < 8 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = codec.order.Uint64(data)
 | 
			
		||||
	case *float32:
 | 
			
		||||
		if len(data) < 4 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = math.Float32frombits(codec.order.Uint32(data))
 | 
			
		||||
	case *float64:
 | 
			
		||||
		if len(data) < 8 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = math.Float64frombits(codec.order.Uint64(data))
 | 
			
		||||
	case *complex64:
 | 
			
		||||
		if len(data) < 8 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = complex(
 | 
			
		||||
			math.Float32frombits(codec.order.Uint32(data[0:])),
 | 
			
		||||
			math.Float32frombits(codec.order.Uint32(data[4:])),
 | 
			
		||||
		)
 | 
			
		||||
	case *complex128:
 | 
			
		||||
		if len(data) < 16 {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = complex(
 | 
			
		||||
			math.Float64frombits(codec.order.Uint64(data[0:])),
 | 
			
		||||
			math.Float64frombits(codec.order.Uint64(data[8:])),
 | 
			
		||||
		)
 | 
			
		||||
	case *string:
 | 
			
		||||
		r := bytes.NewReader(data)
 | 
			
		||||
		n, err := binary.ReadUvarint(r)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if uint64(r.Len()) < n {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		*value = string(data[len(data)-r.Len():])
 | 
			
		||||
 | 
			
		||||
	case *[]byte:
 | 
			
		||||
		r := bytes.NewReader(data)
 | 
			
		||||
		n, err := binary.ReadUvarint(r)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if uint64(r.Len()) < n {
 | 
			
		||||
			return io.ErrUnexpectedEOF
 | 
			
		||||
		}
 | 
			
		||||
		copy(*value, data[len(data)-r.Len():])
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("codec: don't know how to binary decode %T", value)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type sshCodec struct{}
 | 
			
		||||
 | 
			
		||||
func (sshCodec) Type() string                        { return "ssh" }
 | 
			
		||||
func (sshCodec) Encode(value any) ([]byte, error)    { return ssh.Marshal(value), nil }
 | 
			
		||||
func (sshCodec) Decode(data []byte, value any) error { return ssh.Unmarshal(data, value) }
 | 
			
		||||
							
								
								
									
										66
									
								
								storage/codec/codec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								storage/codec/codec.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
package codec
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Codec interface {
 | 
			
		||||
	Type() string
 | 
			
		||||
	Encode(any) ([]byte, error)
 | 
			
		||||
	Decode([]byte, any) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Stream interface {
 | 
			
		||||
	Encode(any) error
 | 
			
		||||
	Decode(any) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	codecs  = make(map[string]func() Codec)
 | 
			
		||||
	streams = make(map[string]func(io.Reader, io.Writer) Stream)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Register(name string, create func() Codec) {
 | 
			
		||||
	if _, dupe := codecs[name]; dupe {
 | 
			
		||||
		panic(fmt.Sprintf("codec: duplicate codec %q registered", name))
 | 
			
		||||
	}
 | 
			
		||||
	codecs[name] = create
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(name string) (Codec, error) {
 | 
			
		||||
	if f, ok := codecs[name]; ok {
 | 
			
		||||
		return f(), nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, fmt.Errorf("codec: no %q codec registered", name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Must(name string) Codec {
 | 
			
		||||
	c, err := New(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RegisterStream(name string, create func(io.Reader, io.Writer) Stream) {
 | 
			
		||||
	if _, dupe := streams[name]; dupe {
 | 
			
		||||
		panic(fmt.Sprintf("codec: duplicate stream %q registered", name))
 | 
			
		||||
	}
 | 
			
		||||
	streams[name] = create
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewStream(name string, r io.Reader, w io.Writer) (Stream, error) {
 | 
			
		||||
	if f, ok := streams[name]; ok {
 | 
			
		||||
		return f(r, w), nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, fmt.Errorf("codec: no %q stream registered", name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MustStream(name string, r io.Reader, w io.Writer) Stream {
 | 
			
		||||
	s, err := NewStream(name, r, w)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										77
									
								
								storage/codec/default.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								storage/codec/default.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
package codec
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"github.com/goccy/go-yaml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	Register("json", func() Codec { return jsonCodec{} })
 | 
			
		||||
	Register("yaml", func() Codec { return yamlCodec{} })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type jsonCodec struct{}
 | 
			
		||||
 | 
			
		||||
func (jsonCodec) Type() string                        { return "json" }
 | 
			
		||||
func (jsonCodec) Encode(value any) ([]byte, error)    { return json.Marshal(value) }
 | 
			
		||||
func (jsonCodec) Decode(data []byte, value any) error { return json.Unmarshal(data, value) }
 | 
			
		||||
 | 
			
		||||
type jsonStream struct {
 | 
			
		||||
	decoder *json.Decoder
 | 
			
		||||
	encoder *json.Encoder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type JSONOption func(*json.Encoder)
 | 
			
		||||
 | 
			
		||||
func NewJSONStream(r io.Reader, w io.Writer, options ...JSONOption) Stream {
 | 
			
		||||
	s := &jsonStream{
 | 
			
		||||
		decoder: json.NewDecoder(r),
 | 
			
		||||
		encoder: json.NewEncoder(w),
 | 
			
		||||
	}
 | 
			
		||||
	for _, option := range options {
 | 
			
		||||
		option(s.encoder)
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func JSONIndent(prefix, indent string) JSONOption {
 | 
			
		||||
	return func(e *json.Encoder) {
 | 
			
		||||
		e.SetIndent(prefix, indent)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func JSONEscapeHTML(on bool) JSONOption {
 | 
			
		||||
	return func(e *json.Encoder) {
 | 
			
		||||
		e.SetEscapeHTML(on)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s jsonStream) Encode(value any) error { return s.encoder.Encode(value) }
 | 
			
		||||
func (s jsonStream) Decode(value any) error { return s.decoder.Decode(value) }
 | 
			
		||||
 | 
			
		||||
type yamlCodec struct{}
 | 
			
		||||
 | 
			
		||||
func (yamlCodec) Type() string                        { return "yaml" }
 | 
			
		||||
func (yamlCodec) Encode(value any) ([]byte, error)    { return yaml.Marshal(value) }
 | 
			
		||||
func (yamlCodec) Decode(data []byte, value any) error { return yaml.Unmarshal(data, value) }
 | 
			
		||||
 | 
			
		||||
type yamlStream struct {
 | 
			
		||||
	decoder *yaml.Decoder
 | 
			
		||||
	encoder *yaml.Encoder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewYaMLStream(r io.Reader, w io.Writer, options ...yaml.EncodeOption) Stream {
 | 
			
		||||
	s := &yamlStream{
 | 
			
		||||
		decoder: yaml.NewDecoder(r),
 | 
			
		||||
		encoder: yaml.NewEncoder(w),
 | 
			
		||||
	}
 | 
			
		||||
	for _, option := range options {
 | 
			
		||||
		option(s.encoder)
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s yamlStream) Encode(value any) error { return s.encoder.Encode(value) }
 | 
			
		||||
func (s yamlStream) Decode(value any) error { return s.decoder.Decode(value) }
 | 
			
		||||
							
								
								
									
										225
									
								
								storage/io.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								storage/io.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,225 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"syscall"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ValueError struct {
 | 
			
		||||
	Value any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (err ValueError) Error() string {
 | 
			
		||||
	return fmt.Sprintf("kv: can't store value of type %T", err.Value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type readSeekerKV struct {
 | 
			
		||||
	mu  sync.Mutex
 | 
			
		||||
	r   io.ReadSeeker
 | 
			
		||||
	c   io.Closer
 | 
			
		||||
	sep rune
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (kv *readSeekerKV) scan(key string) (data string, ok bool, err error) {
 | 
			
		||||
	kv.mu.Lock()
 | 
			
		||||
 | 
			
		||||
	if _, err = kv.r.Seek(0, io.SeekStart); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s := bufio.NewScanner(kv.r)
 | 
			
		||||
	for s.Scan() {
 | 
			
		||||
		line := s.Text()
 | 
			
		||||
		if i := strings.IndexRune(line, kv.sep); i > -1 && line[:i] == key {
 | 
			
		||||
			kv.mu.Unlock()
 | 
			
		||||
			return line[i+1:], true, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	err = s.Err()
 | 
			
		||||
	kv.mu.Unlock()
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (kv *readSeekerKV) Has(key string) bool {
 | 
			
		||||
	_, ok, err := kv.scan(key)
 | 
			
		||||
	return ok && err == nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (kv *readSeekerKV) Get(key string) (value any, ok bool) {
 | 
			
		||||
	value, ok, _ = kv.scan(key)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (kv *readSeekerKV) Close() error {
 | 
			
		||||
	if kv.c == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return kv.c.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func OpenKV(name string, sep rune) (KV, error) {
 | 
			
		||||
	f, err := os.Open(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &readSeekerKV{
 | 
			
		||||
		r:   f,
 | 
			
		||||
		sep: sep,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type readSeekWriterKV struct {
 | 
			
		||||
	readSeekerKV
 | 
			
		||||
	w io.WriteSeeker
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type truncater interface {
 | 
			
		||||
	Truncate(int64) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (kv *readSeekWriterKV) Set(key string, value any) error {
 | 
			
		||||
	var line string
 | 
			
		||||
	switch value := value.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		line = key + string(kv.sep) + value + "\n"
 | 
			
		||||
	case []string:
 | 
			
		||||
		line = key + string(kv.sep) + strings.Join(value, string(kv.sep)) + "\n"
 | 
			
		||||
	default:
 | 
			
		||||
		return ValueError{Value: value}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kv.mu.Lock()
 | 
			
		||||
	defer kv.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if _, err := kv.r.Seek(0, io.SeekStart); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		lines   []string
 | 
			
		||||
		found   bool
 | 
			
		||||
		scanner = bufio.NewScanner(kv.r)
 | 
			
		||||
	)
 | 
			
		||||
	for scanner.Scan() {
 | 
			
		||||
		text := scanner.Text()
 | 
			
		||||
		if i := strings.IndexRune(text, kv.sep); i > -1 {
 | 
			
		||||
			if found = text[:i] == key; found {
 | 
			
		||||
				lines = append(lines, line)
 | 
			
		||||
			} else {
 | 
			
		||||
				lines = append(lines, text)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			lines = append(lines, line)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !found {
 | 
			
		||||
		lines = append(lines, line)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Writing strategy: if it's a file, write to a new file and move it over.
 | 
			
		||||
	if f, ok := kv.w.(*os.File); ok {
 | 
			
		||||
		return kv.replaceFile(f, lines)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if t, ok := kv.w.(truncater); ok {
 | 
			
		||||
		if err := t.Truncate(0); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := kv.w.Seek(0, io.SeekStart); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, line := range lines {
 | 
			
		||||
		if _, err := io.WriteString(kv.w, line+"\n"); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (kv *readSeekWriterKV) replaceFile(f *os.File, lines []string) (err error) {
 | 
			
		||||
	var (
 | 
			
		||||
		prev = f.Name()
 | 
			
		||||
		info os.FileInfo
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if info, err = f.Stat(); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		name = "." + filepath.Base(f.Name()) + ".*"
 | 
			
		||||
		n    *os.File
 | 
			
		||||
	)
 | 
			
		||||
	if n, err = os.CreateTemp(filepath.Dir(f.Name()), name); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	name = n.Name()
 | 
			
		||||
 | 
			
		||||
	// Replicate original file mode
 | 
			
		||||
	if err = os.Chmod(name, info.Mode()); err != nil {
 | 
			
		||||
		_ = n.Close()
 | 
			
		||||
		_ = os.Remove(name)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Replicate original file ownership
 | 
			
		||||
	if stat, ok := info.Sys().(*syscall.Stat_t); ok {
 | 
			
		||||
		// This may fail if we aren't allowed, ignore it.
 | 
			
		||||
		_ = os.Chown(name, int(stat.Uid), int(stat.Gid))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Write lines to tempfile.
 | 
			
		||||
	for _, line := range lines {
 | 
			
		||||
		if _, err = io.WriteString(n, line+"\n"); err != nil {
 | 
			
		||||
			_ = n.Close()
 | 
			
		||||
			_ = os.Remove(name)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = n.Close(); err != nil {
 | 
			
		||||
		_ = os.Remove(name)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Close original file and replace it.
 | 
			
		||||
	_ = f.Close()
 | 
			
		||||
 | 
			
		||||
	if err = os.Rename(name, prev); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Reopen file and replace our readers/writers/closers
 | 
			
		||||
	if f, err = os.OpenFile(prev, os.O_APPEND|os.O_RDWR, info.Mode()|os.ModePerm); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	kv.r = f
 | 
			
		||||
	kv.w = f
 | 
			
		||||
	kv.c = f
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func OpenWritableKV(name string, sep rune, perm os.FileMode) (WritableKV, error) {
 | 
			
		||||
	f, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, perm)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &readSeekWriterKV{
 | 
			
		||||
		readSeekerKV: readSeekerKV{
 | 
			
		||||
			r:   f,
 | 
			
		||||
			c:   f,
 | 
			
		||||
			sep: sep,
 | 
			
		||||
		},
 | 
			
		||||
		w: f,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								storage/io_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								storage/io_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestKV(t *testing.T) {
 | 
			
		||||
	kv, err := OpenKV(filepath.Join("testdata", "kv"), ':')
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Skip(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		Key   string
 | 
			
		||||
		Value any
 | 
			
		||||
		OK    bool
 | 
			
		||||
	}{
 | 
			
		||||
		{"test", "data", true},
 | 
			
		||||
		{"empty", "", true},
 | 
			
		||||
		{"", "emptykey", true},
 | 
			
		||||
		{"nonexistant", nil, false},
 | 
			
		||||
		{"ignored line because not relevant", nil, false},
 | 
			
		||||
	}
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.Key, func(it *testing.T) {
 | 
			
		||||
			value, ok := kv.Get(test.Key)
 | 
			
		||||
			if ok != test.OK {
 | 
			
		||||
				t.Errorf("expected ok %t, got %t", test.OK, ok)
 | 
			
		||||
			}
 | 
			
		||||
			if ok && !reflect.DeepEqual(value, test.Value) {
 | 
			
		||||
				t.Errorf("expected value %v, got %v", test.Value, value)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestWritableKV(t *testing.T) {
 | 
			
		||||
	orgfile := filepath.Join("testdata", "kv")
 | 
			
		||||
	b, err := os.ReadFile(orgfile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Skip(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w, err := os.CreateTemp(os.TempDir(), "kv.*")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Skip(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	testfile := w.Name()
 | 
			
		||||
	t.Logf("using a copy of %s as %s", orgfile, testfile)
 | 
			
		||||
	defer os.Remove(testfile)
 | 
			
		||||
 | 
			
		||||
	if _, err = w.Write(b); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if err = w.Close(); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kv, err := OpenWritableKV(testfile, ':', 0640)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Skip(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer kv.Close()
 | 
			
		||||
 | 
			
		||||
	t.Run("write", func(t *testing.T) {
 | 
			
		||||
		writeTests := []struct {
 | 
			
		||||
			Key   string
 | 
			
		||||
			Value any
 | 
			
		||||
			Error error
 | 
			
		||||
		}{
 | 
			
		||||
			{"test", "newdata", nil},
 | 
			
		||||
			{"strings", []string{"test", "data"}, nil},
 | 
			
		||||
			{"int", 42, ValueError{Value: 42}},
 | 
			
		||||
		}
 | 
			
		||||
		for _, test := range writeTests {
 | 
			
		||||
			t.Run(test.Key, func(it *testing.T) {
 | 
			
		||||
				err := kv.Set(test.Key, test.Value)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					if test.Error == nil {
 | 
			
		||||
						it.Errorf("unepxected error %q (%T)", err, err)
 | 
			
		||||
					} else if !errors.Is(err, test.Error) {
 | 
			
		||||
						it.Errorf("expected error %q, but got %q (%T)", test.Error, err, err)
 | 
			
		||||
					}
 | 
			
		||||
					return
 | 
			
		||||
				} else if err == nil && test.Error != nil {
 | 
			
		||||
					it.Errorf("expected error %q, but got nil", err)
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("read", func(t *testing.T) {
 | 
			
		||||
		readTests := []struct {
 | 
			
		||||
			Key   string
 | 
			
		||||
			Value any
 | 
			
		||||
			OK    bool
 | 
			
		||||
		}{
 | 
			
		||||
			{"test", "newdata", true},
 | 
			
		||||
			{"empty", "", true},
 | 
			
		||||
			{"", "emptykey", true},
 | 
			
		||||
			{"strings", []string{"test", "data"}, true},
 | 
			
		||||
			{"nonexistant", nil, false},
 | 
			
		||||
			{"ignored line because not relevant", nil, false},
 | 
			
		||||
		}
 | 
			
		||||
		for _, test := range readTests {
 | 
			
		||||
			t.Run(test.Key, func(it *testing.T) {
 | 
			
		||||
				value, ok := kv.Get(test.Key)
 | 
			
		||||
				if ok != test.OK {
 | 
			
		||||
					t.Errorf("expected ok %t, got %t", test.OK, ok)
 | 
			
		||||
				}
 | 
			
		||||
				if ok && !reflect.DeepEqual(value, test.Value) {
 | 
			
		||||
					t.Errorf("expected value %v, got %v", test.Value, value)
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								storage/kv.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								storage/kv.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"git.maze.io/maze/conduit/storage/codec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// KV implements a key-value store.
 | 
			
		||||
type KV interface {
 | 
			
		||||
	// Has checks if the key is present.
 | 
			
		||||
	Has(key string) bool
 | 
			
		||||
 | 
			
		||||
	// Get a value by key.
 | 
			
		||||
	Get(key string) (value []byte, ok bool)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WritableKV can store key-value items.
 | 
			
		||||
type WritableKV interface {
 | 
			
		||||
	KV
 | 
			
		||||
 | 
			
		||||
	// Close the key-value storage.
 | 
			
		||||
	Close() error
 | 
			
		||||
 | 
			
		||||
	// DeleteKey removes a key.
 | 
			
		||||
	Delete(key string) error
 | 
			
		||||
 | 
			
		||||
	// Set a value by key.
 | 
			
		||||
	Set(key string, value []byte) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EncodedKV interface {
 | 
			
		||||
	Has(key string) bool
 | 
			
		||||
	Get(key string, value any) (ok bool, err error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EncodedWritableKV interface {
 | 
			
		||||
	EncodedKV
 | 
			
		||||
 | 
			
		||||
	Close() error
 | 
			
		||||
 | 
			
		||||
	Delete(key string) error
 | 
			
		||||
 | 
			
		||||
	Set(key string, value []byte) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type encodedKV struct {
 | 
			
		||||
	kv     KV
 | 
			
		||||
	encode func(any) ([]byte, error)
 | 
			
		||||
	decode func([]byte, any) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (kv encodedKV) Has(key)
 | 
			
		||||
 | 
			
		||||
func NewEncodedKV(kv KV, codec codec.Codec) EncodedKV {
 | 
			
		||||
	return &encodedKV{
 | 
			
		||||
		kv:     kv,
 | 
			
		||||
		encode: json.Marshal,
 | 
			
		||||
		decode: json.Unmarshal,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								storage/testdata/kv
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								storage/testdata/kv
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
test:data
 | 
			
		||||
ignored line because not relevant
 | 
			
		||||
empty:
 | 
			
		||||
:emptykey
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/conduit.ed25519
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/conduit.ed25519
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDRyBi3zVFplREzSzhuwqZInx5wRAk67DCAeODMiZ4XhQAAAKDwAZrd8AGa
 | 
			
		||||
3QAAAAtzc2gtZWQyNTUxOQAAACDRyBi3zVFplREzSzhuwqZInx5wRAk67DCAeODMiZ4XhQ
 | 
			
		||||
AAAEDhHNQENt5Ldinm1TSlGJTVdjniydYpsTNXYmyLK2BUHNHIGLfNUWmVETNLOG7Cpkif
 | 
			
		||||
HnBECTrsMIB44MyJnheFAAAAGENvbmR1aXQgZGVtbyBwcml2YXRlIGtleQECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/conduit.ed25519.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/conduit.ed25519.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINHIGLfNUWmVETNLOG7CpkifHnBECTrsMIB44MyJnheF Conduit demo private key
 | 
			
		||||
							
								
								
									
										27
									
								
								testdata/conduit.rsa
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								testdata/conduit.rsa
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
 | 
			
		||||
NhAAAAAwEAAQAAAQEArnzuzlKni2ZFF+6hmrTpO/Z2U0Bqgmx8U6es/DW/udhyYkQ14C32
 | 
			
		||||
P98y1/95DnY1a0Lo8BBH2w+iFToxd4Jw5IGL45yUTlxIr95NlAKWM62yXmxaIXBT665N1q
 | 
			
		||||
m8znjEF3IhLkjciVd9xTGAZ2fGPmK3+WuMeUdKcW5nQpsUjoYoTiGWJSjYz6t1FX18LYfz
 | 
			
		||||
VuCoh1JJrh+neMeA9zjCfsSnivRLtObMwVsER2N76ZX9EmIsA3d8T5RWpTxtlMwN4//pHb
 | 
			
		||||
UH9KjSRslqY0Yx7/QbyTD/xvmNgnJL8oHrWAX3L1/SRm/6fBtSclOeTgw+ZmsQ6wkhZvGq
 | 
			
		||||
g27wi5ngGwAAA9DJal7zyWpe8wAAAAdzc2gtcnNhAAABAQCufO7OUqeLZkUX7qGatOk79n
 | 
			
		||||
ZTQGqCbHxTp6z8Nb+52HJiRDXgLfY/3zLX/3kOdjVrQujwEEfbD6IVOjF3gnDkgYvjnJRO
 | 
			
		||||
XEiv3k2UApYzrbJebFohcFPrrk3WqbzOeMQXciEuSNyJV33FMYBnZ8Y+Yrf5a4x5R0pxbm
 | 
			
		||||
dCmxSOhihOIZYlKNjPq3UVfXwth/NW4KiHUkmuH6d4x4D3OMJ+xKeK9Eu05szBWwRHY3vp
 | 
			
		||||
lf0SYiwDd3xPlFalPG2UzA3j/+kdtQf0qNJGyWpjRjHv9BvJMP/G+Y2CckvygetYBfcvX9
 | 
			
		||||
JGb/p8G1JyU55ODD5maxDrCSFm8aqDbvCLmeAbAAAAAwEAAQAAAQEAkqLjffjwXLIhtq8Q
 | 
			
		||||
mJcYuw+w+N3VpK3O/e6X7YyuB1zjI7n3HOMDY0IL1IIaFhE5a17bq4PDH1HQAM7a63hvr1
 | 
			
		||||
k/WpUn/YKIg2PrBkv2No/uqnOceyWPIS1mtNQIm+vZv2pmgCMzUyh3xdSH+F65t4v22GGN
 | 
			
		||||
uA41fYYuuUbiy7KHsJy0JYE04VMO7XipWUOJoaqOBosS/gnRKd4e1axa8kAbOQbpzi4C7a
 | 
			
		||||
EVNHiwiWwSuuBtLzWC1NvTMNVn6dQCmKHcLCdumxZ0iXzz/nvFFkfNkYE5Gn0wdeMZK4Wh
 | 
			
		||||
ffAOqjcA94D+UNzK0pMzO1o4H54wPraJy+0LUP0LPQAm4QAAAIBPHtw0GedG6SKH4hMhsO
 | 
			
		||||
KeKq4tXHu7blSjsMtV+OGYAhl3LT4JOCDUjLczUU8gKbI6wwR3YIk9K/1s7G2DOYbil01F
 | 
			
		||||
5IphOYEYRyzxrDjKm/hnI0GhRaScipgxICqu1HlIR1CtC36ZRpamhufC0P0h4+eY1t1saT
 | 
			
		||||
4Tppu3NAlSLwAAAIEA2EVuuPOhgQxoK21cxG4mdpjYmNbhBbr1xPWO3mmxN3iyilE5wuSY
 | 
			
		||||
A/r3HDJl2qzLpaG9CQ4VT9GywuGX3lTXiOOc98zNAqyUYgDmCcZqlsaeSt+WJVBWgBRCz4
 | 
			
		||||
BwM9MZ4wLSzvBWSZiIls3OZuVWjLGDZuImyfWmN8YV5aPCMsUAAACBAM6KkL5IURjyD3gi
 | 
			
		||||
sQA/1Qtz4j7fMou69GqhMhZPhvt/BPGe761ciH1+4ixweb+lHCqZjW5MB9LKYH1Dpjq7X1
 | 
			
		||||
maiOLcPYPBKvSwYQmCcGKct1DXY8623QKYZHqmBa7NJZUjuvC2reIc/a2uwWRV0CKXexpw
 | 
			
		||||
0mMIZroWkk5xUXVfAAAAGENvbmR1aXQgZGVtbyBwcml2YXRlIGtleQEC
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/conduit.rsa.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/conduit.rsa.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCufO7OUqeLZkUX7qGatOk79nZTQGqCbHxTp6z8Nb+52HJiRDXgLfY/3zLX/3kOdjVrQujwEEfbD6IVOjF3gnDkgYvjnJROXEiv3k2UApYzrbJebFohcFPrrk3WqbzOeMQXciEuSNyJV33FMYBnZ8Y+Yrf5a4x5R0pxbmdCmxSOhihOIZYlKNjPq3UVfXwth/NW4KiHUkmuH6d4x4D3OMJ+xKeK9Eu05szBWwRHY3vplf0SYiwDd3xPlFalPG2UzA3j/+kdtQf0qNJGyWpjRjHv9BvJMP/G+Y2CckvygetYBfcvX9JGb/p8G1JyU55ODD5maxDrCSFm8aqDbvCLmeAb Conduit demo private key
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/example.ed25519
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/example.ed25519
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACCcAzG/ziVMEPefEVafcSXLygbyZ6mgbUqvn2GQhJ+awwAAAJhCdsmgQnbJ
 | 
			
		||||
oAAAAAtzc2gtZWQyNTUxOQAAACCcAzG/ziVMEPefEVafcSXLygbyZ6mgbUqvn2GQhJ+aww
 | 
			
		||||
AAAEAO9YENyIi8hSQ1VYifWhK/+r0q6+zieVq/QIKttoxapZwDMb/OJUwQ958RVp9xJcvK
 | 
			
		||||
BvJnqaBtSq+fYZCEn5rDAAAAEEV4YW1wbGUgdXNlciBrZXkBAgMEBQ==
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/example.ed25519-cert.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/example.ed25519-cert.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIH2JTPouysbfVmVz7meUXWd3Gy0vbaDFBlg27Eg1i1MNAAAAIJwDMb/OJUwQ958RVp9xJcvKBvJnqaBtSq+fYZCEn5rDAAAAAAAAAAAAAAABAAAAB2V4YW1wbGUAAAATAAAAB2V4YW1wbGUAAAAEZGVtbwAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEArnzuzlKni2ZFF+6hmrTpO/Z2U0Bqgmx8U6es/DW/udhyYkQ14C32P98y1/95DnY1a0Lo8BBH2w+iFToxd4Jw5IGL45yUTlxIr95NlAKWM62yXmxaIXBT665N1qm8znjEF3IhLkjciVd9xTGAZ2fGPmK3+WuMeUdKcW5nQpsUjoYoTiGWJSjYz6t1FX18LYfzVuCoh1JJrh+neMeA9zjCfsSnivRLtObMwVsER2N76ZX9EmIsA3d8T5RWpTxtlMwN4//pHbUH9KjSRslqY0Yx7/QbyTD/xvmNgnJL8oHrWAX3L1/SRm/6fBtSclOeTgw+ZmsQ6wkhZvGqg27wi5ngGwAAARQAAAAMcnNhLXNoYTItNTEyAAABAFK5fNyZp5ZwEXsDaOcsxP3+PqycVunqeGig0WdN61amDShHu3r+tO03iDlu1C8nVWdGfwIrfh9CekxorozVpCR9KMkahozcPGcWo7tYN7/CRfJt4PrN4AO1mAhhTC90V1awBaVI3fjK3fOk0xVkkyCWBsFI1Dks8ImnAuX2eFgG0IFboN0fa9yKDPx2cOPVlrV3Coq49q0ivu0aipNd+M80ayntlKFCr4A/t2lTBeKHeiqSP6sP7puN3iWZgztDFkMxNJezMnu0dTq1/5rhSdzcoX1SYkOMS32J+njcXjC8tWsFlhRSPsx7OqFdFVX1NPgc5IArw3Tj+mQGrKdnYsc= Example user key
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/example.ed25519.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/example.ed25519.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJwDMb/OJUwQ958RVp9xJcvKBvJnqaBtSq+fYZCEn5rD Example user key
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_a
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_a
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACBaNZbepRJPtVVKzG4ZWlaZCNKdUxVBrox2iip+/laBmwAAAIi3gDMRt4Az
 | 
			
		||||
EQAAAAtzc2gtZWQyNTUxOQAAACBaNZbepRJPtVVKzG4ZWlaZCNKdUxVBrox2iip+/laBmw
 | 
			
		||||
AAAEBk1ZexnhZdDjMBYGVc1MfVIJPYxLsqxuVT/7QrAPJexlo1lt6lEk+1VUrMbhlaVpkI
 | 
			
		||||
0p1TFUGujHaKKn7+VoGbAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_a.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_a.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFo1lt6lEk+1VUrMbhlaVpkI0p1TFUGujHaKKn7+VoGb 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_b
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_b
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACAgPXUIzXl93gb5DlyTfpyZtQtlvVLUYi2yvTvOzI5IqwAAAIj0LYbK9C2G
 | 
			
		||||
ygAAAAtzc2gtZWQyNTUxOQAAACAgPXUIzXl93gb5DlyTfpyZtQtlvVLUYi2yvTvOzI5Iqw
 | 
			
		||||
AAAECLb8tIZGTc0AocsQx7FxKHoFyQ5fR8aYYitu2NiujlgyA9dQjNeX3eBvkOXJN+nJm1
 | 
			
		||||
C2W9UtRiLbK9O87MjkirAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_b.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_b.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICA9dQjNeX3eBvkOXJN+nJm1C2W9UtRiLbK9O87Mjkir 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_c
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_c
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDm3ZNg2OwpyOgLse4weKA0AoVDc9rY5XEHw/y8wzBhLgAAAIhJAmhKSQJo
 | 
			
		||||
SgAAAAtzc2gtZWQyNTUxOQAAACDm3ZNg2OwpyOgLse4weKA0AoVDc9rY5XEHw/y8wzBhLg
 | 
			
		||||
AAAEDjbxojnzIkz9CuH3QANAkk7Kox3Oz85Nq2uVlhf6kmJObdk2DY7CnI6Aux7jB4oDQC
 | 
			
		||||
hUNz2tjlcQfD/LzDMGEuAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_c.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_c.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIObdk2DY7CnI6Aux7jB4oDQChUNz2tjlcQfD/LzDMGEu 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_d
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_d
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDBQODCk5lDPxFVJnX3xc/yVM8Xq3MVt0RDkjWdd9pDkwAAAIgFh+AQBYfg
 | 
			
		||||
EAAAAAtzc2gtZWQyNTUxOQAAACDBQODCk5lDPxFVJnX3xc/yVM8Xq3MVt0RDkjWdd9pDkw
 | 
			
		||||
AAAEAMdFkVlAmVL0ecGpOYK+o/uJvunuJIiScQbke8xRLfS8FA4MKTmUM/EVUmdffFz/JU
 | 
			
		||||
zxercxW3REOSNZ132kOTAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_d.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_d.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMFA4MKTmUM/EVUmdffFz/JUzxercxW3REOSNZ132kOT 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_e
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_e
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACCSAEB5cNSSrzWoPCCq53E2AvYzV76T3QjWGj7TFZaNYwAAAIhAyiXGQMol
 | 
			
		||||
xgAAAAtzc2gtZWQyNTUxOQAAACCSAEB5cNSSrzWoPCCq53E2AvYzV76T3QjWGj7TFZaNYw
 | 
			
		||||
AAAED2o3NXUBsaKL1aKda3pxLimBGTcPuzkRhYZ5Dys7ERw5IAQHlw1JKvNag8IKrncTYC
 | 
			
		||||
9jNXvpPdCNYaPtMVlo1jAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_e.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_e.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJIAQHlw1JKvNag8IKrncTYC9jNXvpPdCNYaPtMVlo1j 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_f
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_f
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACCanTkfwlCIaVcacPGL3jeNVwoXbWmK+akknwunyq6uWgAAAIg4gKjhOICo
 | 
			
		||||
4QAAAAtzc2gtZWQyNTUxOQAAACCanTkfwlCIaVcacPGL3jeNVwoXbWmK+akknwunyq6uWg
 | 
			
		||||
AAAEBYShYJSTJICVjAX7WyLePUtRfF8ThMl6BUHsgGUcLLS5qdOR/CUIhpVxpw8YveN41X
 | 
			
		||||
ChdtaYr5qSSfC6fKrq5aAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_f.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_f.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJqdOR/CUIhpVxpw8YveN41XChdtaYr5qSSfC6fKrq5a 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_g
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_g
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACCmgF9b/O4j4Mp/gD3akcmOQg3uLiooXqTXGy0IzQCreQAAAIhOHBGGThwR
 | 
			
		||||
hgAAAAtzc2gtZWQyNTUxOQAAACCmgF9b/O4j4Mp/gD3akcmOQg3uLiooXqTXGy0IzQCreQ
 | 
			
		||||
AAAEDEkL3EQCRMpsA2WCw9ItOv+BOaJAj3ukvbr3p/9VsXmaaAX1v87iPgyn+APdqRyY5C
 | 
			
		||||
De4uKihepNcbLQjNAKt5AAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_g.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_g.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKaAX1v87iPgyn+APdqRyY5CDe4uKihepNcbLQjNAKt5 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACCuBduE8JMIOG256dOIHsmL+9746xpONczBzGGwUv9DzgAAAIgVDPWqFQz1
 | 
			
		||||
qgAAAAtzc2gtZWQyNTUxOQAAACCuBduE8JMIOG256dOIHsmL+9746xpONczBzGGwUv9Dzg
 | 
			
		||||
AAAECOgmmc+eoWrBB0hY1yTB7NTS9oIzDSnYYUPSX/p3E4Cq4F24Twkwg4bbnp04geyYv7
 | 
			
		||||
3vjrGk41zMHMYbBS/0POAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_h.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_h.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK4F24Twkwg4bbnp04geyYv73vjrGk41zMHMYbBS/0PO 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_i
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_i
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACAP2hN6igLqAA4zBhse8CSStWYPO31HUPukb7ixYNG85AAAAIie1TyintU8
 | 
			
		||||
ogAAAAtzc2gtZWQyNTUxOQAAACAP2hN6igLqAA4zBhse8CSStWYPO31HUPukb7ixYNG85A
 | 
			
		||||
AAAEDgLZocjGTRre5cBGYPiQhrgW1SS+hyCz1G86Z2Pgg9Tw/aE3qKAuoADjMGGx7wJJK1
 | 
			
		||||
Zg87fUdQ+6RvuLFg0bzkAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_i.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_i.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA/aE3qKAuoADjMGGx7wJJK1Zg87fUdQ+6RvuLFg0bzk 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_j
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_j
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDB4NfG2rYL07aZjhY2QWpHqcosZ0XDIYv1VsU0AwYUAwAAAIgAMqKwADKi
 | 
			
		||||
sAAAAAtzc2gtZWQyNTUxOQAAACDB4NfG2rYL07aZjhY2QWpHqcosZ0XDIYv1VsU0AwYUAw
 | 
			
		||||
AAAEBE92CUagxRtrqwPnOk3NxWJ38lMoI1jCrXF3iZYDjDysHg18batgvTtpmOFjZBakep
 | 
			
		||||
yixnRcMhi/VWxTQDBhQDAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_j.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_j.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMHg18batgvTtpmOFjZBakepyixnRcMhi/VWxTQDBhQD 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_k
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_k
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDlA9NjGTu0MfR5t/6Qbw+X9YhAxY/e4STc8l5l5oiMzwAAAIixZyEHsWch
 | 
			
		||||
BwAAAAtzc2gtZWQyNTUxOQAAACDlA9NjGTu0MfR5t/6Qbw+X9YhAxY/e4STc8l5l5oiMzw
 | 
			
		||||
AAAECJz/+yGGs6Tv3DAb/Bmodgj4nsHShB+qn4dPIIlWE+BuUD02MZO7Qx9Hm3/pBvD5f1
 | 
			
		||||
iEDFj97hJNzyXmXmiIzPAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_k.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_k.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOUD02MZO7Qx9Hm3/pBvD5f1iEDFj97hJNzyXmXmiIzP 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_l
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_l
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACA9c0oRAlg1bQV4aASHj4fsBCtsuvWHG70M7I270P/DHgAAAIh1NgxzdTYM
 | 
			
		||||
cwAAAAtzc2gtZWQyNTUxOQAAACA9c0oRAlg1bQV4aASHj4fsBCtsuvWHG70M7I270P/DHg
 | 
			
		||||
AAAEAKRU+DiCxLCMuuN89DphFJ9pWshOGymhWBFyD37VgkoD1zShECWDVtBXhoBIePh+wE
 | 
			
		||||
K2y69YcbvQzsjbvQ/8MeAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_l.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_l.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID1zShECWDVtBXhoBIePh+wEK2y69YcbvQzsjbvQ/8Me 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_m
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_m
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDwr8Ap993ioXt7nVs16V9Q8K8rem1ebmT6DL0fncKqyQAAAIignn3boJ59
 | 
			
		||||
2wAAAAtzc2gtZWQyNTUxOQAAACDwr8Ap993ioXt7nVs16V9Q8K8rem1ebmT6DL0fncKqyQ
 | 
			
		||||
AAAECUi0UUw2RmFxKAlwcHZxqkT3cCbcNqNRk/rZnkZaFIl/CvwCn33eKhe3udWzXpX1Dw
 | 
			
		||||
ryt6bV5uZPoMvR+dwqrJAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_m.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_m.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPCvwCn33eKhe3udWzXpX1Dwryt6bV5uZPoMvR+dwqrJ 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_n
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_n
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACAL+rCmYdr90w9sQGTc4q2jrXC0T/HiXV7hAC/rGSbAEgAAAIj8310O/N9d
 | 
			
		||||
DgAAAAtzc2gtZWQyNTUxOQAAACAL+rCmYdr90w9sQGTc4q2jrXC0T/HiXV7hAC/rGSbAEg
 | 
			
		||||
AAAECEqFXGeY12fcYUBwBn9ltRldW9IeP6/cI084TuLUBmdwv6sKZh2v3TD2xAZNziraOt
 | 
			
		||||
cLRP8eJdXuEAL+sZJsASAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_n.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_n.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAv6sKZh2v3TD2xAZNziraOtcLRP8eJdXuEAL+sZJsAS 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_o
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_o
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDrlvw5/7W09kgkralSUFN6SU/INhuo1lxMGgm9sF3jiQAAAIgJq4FqCauB
 | 
			
		||||
agAAAAtzc2gtZWQyNTUxOQAAACDrlvw5/7W09kgkralSUFN6SU/INhuo1lxMGgm9sF3jiQ
 | 
			
		||||
AAAECZKFVeNm3E4LFqGUZjE2KRUE6DtqSRAoUspke+mQYGPuuW/Dn/tbT2SCStqVJQU3pJ
 | 
			
		||||
T8g2G6jWXEwaCb2wXeOJAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_o.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_o.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOuW/Dn/tbT2SCStqVJQU3pJT8g2G6jWXEwaCb2wXeOJ 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_p
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_p
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDBVOlRzQJsfAtS8RHRfe8/RUe53TXgeKpJui4+bQ4cXAAAAIiWs4rJlrOK
 | 
			
		||||
yQAAAAtzc2gtZWQyNTUxOQAAACDBVOlRzQJsfAtS8RHRfe8/RUe53TXgeKpJui4+bQ4cXA
 | 
			
		||||
AAAEC5vxPtZLKeN8A3jAyK5O9SLYbp3LWwbDKZLgQWwmOwA8FU6VHNAmx8C1LxEdF97z9F
 | 
			
		||||
R7ndNeB4qkm6Lj5tDhxcAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_p.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_p.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMFU6VHNAmx8C1LxEdF97z9FR7ndNeB4qkm6Lj5tDhxc 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_q
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_q
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACANC9xH5Wixxwpy0IVJNfKHdWEqIz5bpGDX8H21fhmSKAAAAIjzbmzD825s
 | 
			
		||||
wwAAAAtzc2gtZWQyNTUxOQAAACANC9xH5Wixxwpy0IVJNfKHdWEqIz5bpGDX8H21fhmSKA
 | 
			
		||||
AAAEDi1lOpgb7xVeKVcn6NtL+9YuxDKyW8VTqYnB/DTvfrNQ0L3EflaLHHCnLQhUk18od1
 | 
			
		||||
YSojPlukYNfwfbV+GZIoAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_q.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_q.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA0L3EflaLHHCnLQhUk18od1YSojPlukYNfwfbV+GZIo 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_r
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_r
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACADzkeAxKvKL5pKUlTt1SezzmpZS73AgaFA+/oOa6AFfAAAAIgXQ569F0Oe
 | 
			
		||||
vQAAAAtzc2gtZWQyNTUxOQAAACADzkeAxKvKL5pKUlTt1SezzmpZS73AgaFA+/oOa6AFfA
 | 
			
		||||
AAAECg8l+0RHTtTR4KaHGoSDQUwjf1diHrl4X3VksZ2JytwgPOR4DEq8ovmkpSVO3VJ7PO
 | 
			
		||||
allLvcCBoUD7+g5roAV8AAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_r.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_r.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAPOR4DEq8ovmkpSVO3VJ7POallLvcCBoUD7+g5roAV8 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_s
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_s
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACCFd9rETu9NZWCGUIszhtv+HIxpq3SABTvu0ixK2eDHygAAAIjirHXj4qx1
 | 
			
		||||
4wAAAAtzc2gtZWQyNTUxOQAAACCFd9rETu9NZWCGUIszhtv+HIxpq3SABTvu0ixK2eDHyg
 | 
			
		||||
AAAECJzn3k3eM9694KCZiOBZPPFFNXqng2vV5niPVDMT3aQIV32sRO701lYIZQizOG2/4c
 | 
			
		||||
jGmrdIAFO+7SLErZ4MfKAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_s.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_s.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIV32sRO701lYIZQizOG2/4cjGmrdIAFO+7SLErZ4MfK 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_t
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_t
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACCbKLr5W3EoAEEC/y9KFr/nAm7mRcMv246pOw6ZBiSeJwAAAIjEXWV/xF1l
 | 
			
		||||
fwAAAAtzc2gtZWQyNTUxOQAAACCbKLr5W3EoAEEC/y9KFr/nAm7mRcMv246pOw6ZBiSeJw
 | 
			
		||||
AAAED3mCjxtEhxY5/Jm0ByKs2+/8PsNdWJPjdQ6QZFIBbczZsouvlbcSgAQQL/L0oWv+cC
 | 
			
		||||
buZFwy/bjqk7DpkGJJ4nAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_t.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_t.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJsouvlbcSgAQQL/L0oWv+cCbuZFwy/bjqk7DpkGJJ4n 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_u
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_u
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDAQJBCBUJ55Aqy7c0eqvlM8FdrTynepQr72uaWofnVrAAAAIiWeKP9lnij
 | 
			
		||||
/QAAAAtzc2gtZWQyNTUxOQAAACDAQJBCBUJ55Aqy7c0eqvlM8FdrTynepQr72uaWofnVrA
 | 
			
		||||
AAAEAoLltBG4iUsZDBTaWDv3mie5zJn6tMIQz9l5jCB1phlcBAkEIFQnnkCrLtzR6q+Uzw
 | 
			
		||||
V2tPKd6lCvva5pah+dWsAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_u.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_u.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMBAkEIFQnnkCrLtzR6q+UzwV2tPKd6lCvva5pah+dWs 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_v
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_v
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACDK3NCwJUuiCYMoAh3WcP0uDgSMJxdOTjNVkAZFMjbjOwAAAIj2Cvar9gr2
 | 
			
		||||
qwAAAAtzc2gtZWQyNTUxOQAAACDK3NCwJUuiCYMoAh3WcP0uDgSMJxdOTjNVkAZFMjbjOw
 | 
			
		||||
AAAEDw5HzNtISf8jObCiX1TIfMG4riUafWdmaMnbHylD9Pdsrc0LAlS6IJgygCHdZw/S4O
 | 
			
		||||
BIwnF05OM1WQBkUyNuM7AAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_v.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_v.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMrc0LAlS6IJgygCHdZw/S4OBIwnF05OM1WQBkUyNuM7 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_w
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_w
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACA2M9JkSMBC875ww1f5tYpFGc0sHbvTVW0ZbiOO/6mGBgAAAIi+yvZUvsr2
 | 
			
		||||
VAAAAAtzc2gtZWQyNTUxOQAAACA2M9JkSMBC875ww1f5tYpFGc0sHbvTVW0ZbiOO/6mGBg
 | 
			
		||||
AAAECTIiMBCKvnKDMSTN/cSJh0vTRlQQ5274pROcyxYXYOIzYz0mRIwELzvnDDV/m1ikUZ
 | 
			
		||||
zSwdu9NVbRluI47/qYYGAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										1
									
								
								testdata/keys/test_id_ed25519_w.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/keys/test_id_ed25519_w.pub
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDYz0mRIwELzvnDDV/m1ikUZzSwdu9NVbRluI47/qYYG 
 | 
			
		||||
							
								
								
									
										7
									
								
								testdata/keys/test_id_ed25519_x
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								testdata/keys/test_id_ed25519_x
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-----BEGIN OPENSSH PRIVATE KEY-----
 | 
			
		||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
			
		||||
QyNTUxOQAAACBsnmWDuYTUNvauU7ZBdrvOyHo5eNqkk80OcuOSMj9tcgAAAIirtwdgq7cH
 | 
			
		||||
YAAAAAtzc2gtZWQyNTUxOQAAACBsnmWDuYTUNvauU7ZBdrvOyHo5eNqkk80OcuOSMj9tcg
 | 
			
		||||
AAAED42nmxcvm2cvfubKitLujExxqkPWrOftVpFaHf/AzghGyeZYO5hNQ29q5TtkF2u87I
 | 
			
		||||
ejl42qSTzQ5y45IyP21yAAAAAAECAwQF
 | 
			
		||||
-----END OPENSSH PRIVATE KEY-----
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user