diff --git a/.gitignore b/.gitignore index 321a442..2ba0115 100644 --- a/.gitignore +++ b/.gitignore @@ -29,10 +29,13 @@ package-json.lock /api dist/ +# testdata +/cmd/asynqmon/testdata/ + # Editor configs .idea/ .vscode/ .editorconfig # examples -examples/ \ No newline at end of file +examples/ diff --git a/cmd/asynqmon/main.go b/cmd/asynqmon/main.go index a5e8d01..7c1f9f9 100644 --- a/cmd/asynqmon/main.go +++ b/cmd/asynqmon/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "crypto/tls" + "crypto/x509" "flag" "fmt" "log" @@ -30,6 +31,9 @@ type Config struct { RedisDB int RedisPassword string RedisTLS string + RedisCaCert string + RedisClientCert string + RedisClientKey string RedisURL string RedisInsecureTLS bool RedisClusterNodes string @@ -64,6 +68,9 @@ func parseFlags(progname string, args []string) (cfg *Config, output string, err flags.IntVar(&conf.RedisDB, "redis-db", getEnvOrDefaultInt("REDIS_DB", 0), "redis database number") flags.StringVar(&conf.RedisPassword, "redis-password", getEnvDefaultString("REDIS_PASSWORD", ""), "password to use when connecting to redis server") flags.StringVar(&conf.RedisTLS, "redis-tls", getEnvDefaultString("REDIS_TLS", ""), "server name for TLS validation used when connecting to redis server") + flags.StringVar(&conf.RedisCaCert, "redis-ca-cert", getEnvDefaultString("REDIS_CA_CERT", ""), "path to CA certificate file used when connecting to redis server") + flags.StringVar(&conf.RedisClientCert, "redis-client-cert", getEnvDefaultString("REDIS_CLIENT_CERT", ""), "path to client certificate file used when connecting to redis server") + flags.StringVar(&conf.RedisClientKey, "redis-client-key", getEnvDefaultString("REDIS_CLIENT_KEY", ""), "path to client key file used when connecting to redis server") flags.StringVar(&conf.RedisURL, "redis-url", getEnvDefaultString("REDIS_URL", ""), "URL to redis server") flags.BoolVar(&conf.RedisInsecureTLS, "redis-insecure-tls", getEnvOrDefaultBool("REDIS_INSECURE_TLS", false), "disable TLS certificate host checks") flags.StringVar(&conf.RedisClusterNodes, "redis-cluster-nodes", getEnvDefaultString("REDIS_CLUSTER_NODES", ""), "comma separated list of host:port addresses of cluster nodes") @@ -128,6 +135,32 @@ func makeRedisConnOpt(cfg *Config) (asynq.RedisConnOpt, error) { if connOpt.TLSConfig == nil { connOpt.TLSConfig = makeTLSConfig(cfg) } + + if cfg.RedisClientCert == "" && cfg.RedisCaCert == "" { + return connOpt, nil + } + + if connOpt.TLSConfig == nil { + connOpt.TLSConfig = &tls.Config{} + } + + if cfg.RedisClientCert != "" { + cert, err := tls.LoadX509KeyPair(cfg.RedisClientCert, cfg.RedisClientKey) + if err != nil { + return nil, fmt.Errorf("get certificate error RedisClientCert:%s RedisClientKey:%s error:%s", cfg.RedisClientCert, cfg.RedisClientKey, err) + } + connOpt.TLSConfig.Certificates = []tls.Certificate{cert} + } + if cfg.RedisCaCert != "" { + caCert, err := os.ReadFile(cfg.RedisCaCert) + if err != nil { + return nil, fmt.Errorf("read ca cert error RedisCaCert:%s error:%s", cfg.RedisCaCert, err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + connOpt.TLSConfig.RootCAs = caCertPool + } + return connOpt, nil } diff --git a/cmd/asynqmon/main_test.go b/cmd/asynqmon/main_test.go index 6d70082..a57a533 100644 --- a/cmd/asynqmon/main_test.go +++ b/cmd/asynqmon/main_test.go @@ -1,13 +1,30 @@ package main import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "io" + "math/big" + "net" + "os" + "os/exec" + "strconv" "strings" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hibiken/asynq" + "github.com/redis/go-redis/v9" ) func TestParseFlags(t *testing.T) { @@ -135,3 +152,252 @@ func TestMakeRedisConnOpt(t *testing.T) { }) } } + +type redisServer struct { + addr string + port int + pid int + args []string + output io.ReadWriter + cmd *exec.Cmd +} + +func findFreePort(t *testing.T) int { + t.Helper() + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("net.Listen failed: %v", err) + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port +} + +func newRedisServer(t *testing.T, args ...string) *redisServer { + t.Helper() + + port := findFreePort(t) + addr := fmt.Sprintf("127.0.0.1:%d", port) + + cmdArgs := []string{} + defaultArgs := map[string]string{ + "--port": strconv.Itoa(port), + "--save": "", + "--appendonly": "no", + } + for _, arg := range args { + cmdArgs = append(cmdArgs, arg) + if _, ok := defaultArgs[arg]; ok { + // Remove the default argument from the map. + delete(defaultArgs, arg) + } + } + + for k, v := range defaultArgs { + cmdArgs = append(cmdArgs, k, v) + } + + buf := new(bytes.Buffer) + + cmd := exec.Command("redis-server", cmdArgs...) + cmd.Stderr = buf + cmd.Stdout = buf + + if err := cmd.Start(); err != nil { + t.Fatalf("redis-server failed to start: %v", err) + } + + return &redisServer{ + addr: addr, + port: port, + pid: cmd.Process.Pid, + args: cmdArgs, + output: buf, + cmd: cmd, + } + +} + +var regenerateTLSFiles = flag.Bool("regenerate-tls-files", false, "regenerate TLS files") + +// newRedisServerWithTLS creates a new redis-server instance with TLS enabled. +// If the regenerate-tls-files flag is set, it will generate new TLS files. +func newRedisServerWithTLS(t *testing.T, args ...string) *redisServer { + t.Helper() + + caFile := "testdata/ca.crt" + caPrivKeyFile := "testdata/ca.key" + serverCertFile := "testdata/server.crt" + serverPrivKeyFile := "testdata/server.key" + + if *regenerateTLSFiles { + caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("rsa.GenerateKey failed: %v", err) + } + + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + CommonName: "asynqmon test CA", + Organization: []string{"asynqmon"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + IsCA: true, + } + + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + t.Fatalf("x509.CreateCertificate failed: %v", err) + } + + writePemToFile(caPrivKeyFile, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(caPrivKey)) + + writePemToFile(caFile, "CERTIFICATE", caBytes) + + // Generate server private key + serverPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + + // Define server certificate template + cert := &x509.Certificate{ + SerialNumber: big.NewInt(2024), + Subject: pkix.Name{ + CommonName: "localhost", + }, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), // 1 year + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + // Create server Certificate + serverCert, err := x509.CreateCertificate(rand.Reader, cert, ca, &serverPrivKey.PublicKey, caPrivKey) + if err != nil { + panic(err) + } + + writePemToFile(serverPrivKeyFile, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(serverPrivKey)) + + writePemToFile(serverCertFile, "CERTIFICATE", serverCert) + } + + tlsPort := findFreePort(t) + args = append(args, "--tls-port", strconv.Itoa(tlsPort)) + args = append(args, "--tls-cert-file", serverCertFile) + args = append(args, "--tls-key-file", serverPrivKeyFile) + args = append(args, "--tls-ca-cert-file", caFile) + args = append(args, "--tls-auth-clients", "no") + + red := newRedisServer(t, args...) + red.port = tlsPort + red.addr = fmt.Sprintf("127.0.0.1:%d", tlsPort) + + return red + +} + +// writePemToFile writes PEM-encoded data to a file with the specified filename and PEM block type. +func writePemToFile(filename, pemType string, bytes []byte) { + file, err := os.Create(filename) + if err != nil { + panic(err) + } + defer file.Close() + + pemBlock := &pem.Block{ + Type: pemType, + Bytes: bytes, + } + if err := pem.Encode(file, pemBlock); err != nil { + panic(err) + } +} + +func TestConfigureAndPingRedis(t *testing.T) { + _, err := exec.LookPath("redis-server") + if err != nil { + t.Skip("redis-server not found in PATH") + } + + tests := []struct { + desc string + cfg *Config + getServer func() *redisServer + }{ + { + desc: "Basic", + cfg: &Config{ + RedisAddr: "localhost:6380", + }, + getServer: func() *redisServer { + return newRedisServer(t) + }, + }, + { + desc: "With TLS certs", + cfg: &Config{ + RedisCaCert: "testdata/ca.crt", + RedisClientCert: "testdata/server.crt", + RedisClientKey: "testdata/server.key", + RedisTLS: "", + }, + + getServer: func() *redisServer { + return newRedisServerWithTLS(t) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + red := tc.getServer() + defer red.cmd.Process.Kill() + time.Sleep(5 * time.Second) + cfg := tc.cfg + cfg.RedisAddr = red.addr + cfg.RedisDB = 1 + + printServerOutput := func() { + t.Helper() + t.Logf("redis-server output:\n%s", red.output) + } + + got, err := makeRedisConnOpt(cfg) + if err != nil { + printServerOutput() + t.Fatalf("makeRedisConnOpt returned error: %v", err) + } + + client, ok := got.MakeRedisClient().(redis.UniversalClient) + if !ok { + printServerOutput() + t.Fatalf("got.MakeRedisClient() returned a non-redis.UniversalClient type") + } + + defer client.Close() + if _, err := client.Ping(context.Background()).Result(); err != nil { + printServerOutput() + t.Errorf("client.Ping() returned error: %v", err) + } + + client.Set(context.Background(), "foo", "bar", 0) + key, err := client.Get(context.Background(), "foo").Result() + if err != nil { + printServerOutput() + t.Errorf("client.Get() returned error: %v", err) + } + if key != "bar" { + printServerOutput() + t.Errorf("client.Get() returned %q, want %q", key, "bar") + + } + + }) + } +} diff --git a/cmd/asynqmon/testdata/ca.crt b/cmd/asynqmon/testdata/ca.crt new file mode 100644 index 0000000..7d7fb46 --- /dev/null +++ b/cmd/asynqmon/testdata/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGjCCAgKgAwIBAgICB+MwDQYJKoZIhvcNAQELBQAwLjERMA8GA1UEChMIYXN5 +bnFtb24xGTAXBgNVBAMTEGFzeW5xbW9uIHRlc3QgQ0EwHhcNMjQwMjI4MjA0ODA2 +WhcNMjUwMjI4MjA0ODA2WjAuMREwDwYDVQQKEwhhc3lucW1vbjEZMBcGA1UEAxMQ +YXN5bnFtb24gdGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN327FkhXwh8KKitVIEsZydLFBJWzpZ1rRGOF554IEsptXQZhCxTrwJfcLWDilVU +PCPATqJ5X4v4HxAUNhrV5g9TBjeR8YKn753YPwDfOYllC1NN6tOyCe++3rg5YPt2 +8GF9oNkNWbAEnkV9E6mejLOJns/XTfY/VtAox9qqTK0q5ER6b9pMWEVT0sFcgQAy +cQbnx9rgB8hkNddiQb28e7gaPI2H+NBYOkdSZYy1lL2uQxEq/pP1RmVa8wS+Cfwv +WwIYytw06yQRim0q1Wm/76mU5D3cSSkoe10Y0zLUL7spw3DPD1Wj9H1McAjeuyRe +EEE8/JiZ1As7KDrgSItvf9cCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKEMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFEakM/CIItCYPgz48kDktCAoTNxrMA0GCSqG +SIb3DQEBCwUAA4IBAQA682/3dsF/bqhrYgOHrzPvjtITx6CmwCDfVRJdSLBiMwEz +YCo4muNScfftXyFl3GYmRROydDjgWqsYkwWBGD/wqNXBoxn61EsWhuNAp6Yzr9hm +YnshBAdjVlqu4Nwfhi5fwbzUnJViD/jwBjV+IU8YzHjF+ZApalboiUCDDLt8m8z/ +m5rc7T/5Z0wNacc6+hNMhNbT8FFlARgV74yqXhDHECbIgqlsG+udOeHoGUIQrmoW +k/bhRMerORtxPvHrrLPIf1YTDmohCz5++kaKXLen874NsaZ0SpseoUK28GucQpUN +I/1PqmsO/vi/r7cUvPtbIHL0dJRrN5zBwIS1vPgy +-----END CERTIFICATE----- diff --git a/cmd/asynqmon/testdata/ca.key b/cmd/asynqmon/testdata/ca.key new file mode 100644 index 0000000..c79f28e --- /dev/null +++ b/cmd/asynqmon/testdata/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA3fbsWSFfCHwoqK1UgSxnJ0sUElbOlnWtEY4XnnggSym1dBmE +LFOvAl9wtYOKVVQ8I8BOonlfi/gfEBQ2GtXmD1MGN5Hxgqfvndg/AN85iWULU03q +07IJ777euDlg+3bwYX2g2Q1ZsASeRX0TqZ6Ms4mez9dN9j9W0CjH2qpMrSrkRHpv +2kxYRVPSwVyBADJxBufH2uAHyGQ112JBvbx7uBo8jYf40Fg6R1JljLWUva5DESr+ +k/VGZVrzBL4J/C9bAhjK3DTrJBGKbSrVab/vqZTkPdxJKSh7XRjTMtQvuynDcM8P +VaP0fUxwCN67JF4QQTz8mJnUCzsoOuBIi29/1wIDAQABAoIBAQDb8wl1sRno4I+x +xkCM2CFH0KANJDQG6Ikdcj55a/QkRypl57sP6cTshwK6+6Qithv6GWBSpA9INhEh +78VFhlw5Jz5r5pT5scxCD70u8gSj35r/a6CdMjmidvNgfotZ5ByDnue67f3H7Guh +1DWdyV0HtAHJV0MMFuvBzgds6YCdvqBpKxvFJUq0N++QFGa3BHaHllaGtIDHsEjb +L+ntHe2Gvc6EVPDaDmoyXHZne3shy1A0rtl5Our7sjuLHO4ZNLHr8SmzkKYSHgSw +F4CVKWBVEoCfCwLpx3nZcb+CbF8X0PIcWTzXihpkfzpOhSP1Ke4QRPhOcqmMKKWR ++hEHLOfRAoGBAPSowes/niv0nlXRJDdl+aa2Qxrwi2xZVgzNdTWbsazhwQrLhIjK +DkqM1PeYWT1X9XgAV4ElYwamPz26pg1ZfTyBbszhDbRrBcbb2zIVIy5c1JTvdd8A +2CzTCFFpmJ09zYDcGNixHJE7FcgzUs/RV9JDh47hAtfCyuxHx5tmLrONAoGBAOhA +3CxlwlE0zBWWZeip3+6WwoABFFzhuSlE/VLI8jkmW4HdwFcnle3NPkir6Feg+9qr +CD2sjazr0DjmH4z5eKwUiAF6inFUQ9TbUSoy4om5WXT2OjGYR7b5d2WRlpQi/osz +7EjqJftQbgV/H/JQ8YayNTWiCgMvonzTAJI1gpXzAoGAE8VPZmNNtN+fq++qrY9g +DUjNQ3AM1ESj34T648ohIYdcwjKQEz3AyeV3kEqPa5WgEIJ2j8klp3PnyGU85fdF +V45eFdBZ+ypq3RcHL5TlsulthFuVet/mmDi1g161Jn/IC5G9sEUfudy8deEv3/ta +zXMHkVQ9lpH3NADY8IXhYEECgYEA5VDFE4EVr6B1sQribCruU2C/giuOs3abn8fi +Z47IuuzIhR0x/9uyCS4RRSeXLI5inbEpXdu1tvrOiJ+On17iauWKtAsODn+oyc4S +AZxkWJ+NWBKVusokZOFDpiFtj65NrZwCvKuT/OOY/gxauqJ5Fwl1yBLJ2AN8Z8re +UX5MBUkCgYEA7bW2Y2LLZYnqAhkwVCpSFNelX+lD9jG5JGmgnU4Q2hukRME9UrmJ +AoNBbKDOZchiSRDQcogTUujS5ZlEqpsC69gXg/E+c6KyccmSVTDfMkzBBBnFzKlT +M7tSkU9xUbVyVN0iNqRfRZsEQEp5m+6roCnFymxmkhcqyHB3ZcNZnk0= +-----END RSA PRIVATE KEY----- diff --git a/cmd/asynqmon/testdata/server.crt b/cmd/asynqmon/testdata/server.crt new file mode 100644 index 0000000..1ee7501 --- /dev/null +++ b/cmd/asynqmon/testdata/server.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9jCCAd6gAwIBAgICB+gwDQYJKoZIhvcNAQELBQAwLjERMA8GA1UEChMIYXN5 +bnFtb24xGTAXBgNVBAMTEGFzeW5xbW9uIHRlc3QgQ0EwHhcNMjQwMjI4MjA0ODA2 +WhcNMjUwMjI4MjA0ODA2WjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDfTiic0uPF6OOVR6f+RPf2W3JI5bEzMAl8 +Jgn6eXGdkRwCDfWJb70idQjAifqnpdaRKPcaLI9G9zSbIXXRTowMwhmJCN9qH9Qi +iSJ9Sg8iLQdU2pcffTl0b3NpFhipCOReDpYLiPwwX8h5Q+OAWDW75XDfsBcukV8m +d3ejgZQQkBqRad5zGpmkZsRhzYkb6Wojzcww58OZtcVsmvUJZ8RE++uuu6rIh82t +KHxYRmTN+0bOFYYTMRrkMzbv6akNY/qPoEwaqA/DdMQqUXlvzKg/XIQECHGLUqxu +xRH426df7bkkJOoEx+zvKj/ZEY5xWcIGBHj/spWyK7zmNX+TIDeTAgMBAAGjODA2 +MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHREECDAG +hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQBjbzlAnCmCk4O7e9i/ysnJeBcAcQeC +x18HSNr1hD8iXsA/r5giBeBJQx1KGezyi3kc77d8j9yk60eclQLtJs5Lsr305xx6 +a0djy23SvXnH6rU6Hr09nyi4C14VFoc7pd7vc7MSzDiga24nuTBF2jXDbCOYIdnV +1j1f35/2RL4O4rLwwe20P3gnPAQ6Ju61JWAdxhrn3sOWtiqjIypLDUcJpx0UMZas +QamN54tMByWSkS139eqXPaXgoYLVz/2vUSlsZ8P4R9uLhRKtBc2Ku9ZBFdyTlUV5 +b0SUEcNKVU9MNHcl41bHC9tXIoZVyMUfS+qB0wP6F5rI+DzK3EmUyvSg +-----END CERTIFICATE----- diff --git a/cmd/asynqmon/testdata/server.key b/cmd/asynqmon/testdata/server.key new file mode 100644 index 0000000..9251805 --- /dev/null +++ b/cmd/asynqmon/testdata/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA304onNLjxejjlUen/kT39ltySOWxMzAJfCYJ+nlxnZEcAg31 +iW+9InUIwIn6p6XWkSj3GiyPRvc0myF10U6MDMIZiQjfah/UIokifUoPIi0HVNqX +H305dG9zaRYYqQjkXg6WC4j8MF/IeUPjgFg1u+Vw37AXLpFfJnd3o4GUEJAakWne +cxqZpGbEYc2JG+lqI83MMOfDmbXFbJr1CWfERPvrrruqyIfNrSh8WEZkzftGzhWG +EzEa5DM27+mpDWP6j6BMGqgPw3TEKlF5b8yoP1yEBAhxi1KsbsUR+NunX+25JCTq +BMfs7yo/2RGOcVnCBgR4/7KVsiu85jV/kyA3kwIDAQABAoIBAEyfrR/i3XWTrEQV +CngdglhumJCbAGroGNkY1GO2OF4w5MNvtskqJmQkdJRcxD2ykiXNQL0ifSeEu/Bf +UuY3ZacbE1gKS19G/Ku9ErCbMQYxHUroluKfPY/OjnOIuX2HJ5V+u83Je3+93jR+ +LxpjKk0HNewLqGi6SUQRymO4mu3zYHFLOj57zCcPLFre6xFoSYn+7bMWwAQWKRGg ++fj+5naymssUwHZV+pXtTgjeB+ow2LZ7zpVwUopLNo/3yH33LPfLTSzvNB0gV5R/ +awZHDLQSpVwLURoppgnG793OoCT0PBK5e+A9TEOH/4GU8ZFDy7oNv1geCW9N6cDe +dn+DosECgYEA5X88lwvrb/L+yyAMqCTRSBH73IbFu7sD5eF/CYDZ5jPXyxHDhi3r +Dx/3peW5jZk2mRflSspSAumW1QS5EFc3fwrvngDOgrnclwoeM2nPtg4BC/Jtrdg4 +9YCJkj8luCQfOOLkwawAsO9yt53YiXppYTVMO4ZuiaQ0zLizCGNPXC8CgYEA+Rff +X4RwtC3csAM92fL1OCx/nJXGA46qEtGTFzmV+pl2QZttIO0MkZvyC3iyWlh1mJdD +kmzbXClDwsglewxUnH9nrQFkmqYYKxEvXOiMcJBCrnP0C9s7OfFFrh4WfiyMR+TX +cS8N8fLrIny+OFlTULhjErHZv/MbLB3AffL0zd0CgYEAlEFjAezoVnSy1sPIiWLn +c9hyTR8fY8xHk1zd9WSw3z7Ee+Ho3qiRPj8Xe6tw+CFvHO1L6cnTux/tmYUojH7b +Ug3dh8PbpKWu9D/MDMihL2nSkUY2RmT1Ptufg8OZeWCUbupcfyS/eY3mHOoydXWH +2A1XRujsRay3kz0KIzQMk28CgYEAtB+2MF0WHsTXRBRkApn1B1TuRq3rjaD5jTgt +dGr48ElOwWyCQoAISbcKFY+G8VvsVZZ0j4rWKVPRoyWWLN+iw7RBpVJPjKE08tev +dzDWdYNsJLjGrlgvANxeteUeAMl3+3kY7cjH/cDalYq9BwRZAhMD2X3wZySF7qXp +D2rD6aUCgYAgpH/a/MVs7PEEUjIpzk/JwGos0EjEHVGbxOJGfk80RBYNzJdfaVrP +cXUlv5IuU9nCzF5/1dPfbyKekFp7o3NIPF+oe6oIn55QIaAFRh40CT75C+OSwnx5 +JRGog1u0Iu3mxBHBCI8fwThp68NMD2OVIX/kGO+gl7jAtfVOtjQPQA== +-----END RSA PRIVATE KEY-----