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