mirror of
https://github.com/versity/versitygw.git
synced 2026-09-24 08:54:47 +00:00
feat: add multi-address listener for s3/admin/webui
This allows specifying the following options more than once: port, admin-port, webui or using a comma-separated list for the env vars: e.g., VGW_PORT=:7070,:8080,localhost:9090 This will also expand multiple interfaces from hostnames, for example "localhost" in this case would resolve to both IPv4 and IPv6 interfaces: localhost has address 127.0.0.1 localhost has IPv6 address ::1 This updates the banner to reflect all of the listening interfaces/ports, and starts the service listener on all requested interfaces/ports. Fixes #1761
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MultiListener implements net.Listener and accepts connections from multiple
|
||||
// underlying listeners. This is useful for listening on multiple IP addresses
|
||||
// that a hostname resolves to (e.g., both IPv4 and IPv6 for "localhost").
|
||||
type MultiListener struct {
|
||||
listeners []net.Listener
|
||||
acceptCh chan acceptResult
|
||||
closeCh chan struct{}
|
||||
closeOnce sync.Once
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type acceptResult struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
|
||||
// NewMultiListener creates a new MultiListener that accepts connections from
|
||||
// all provided listeners.
|
||||
func NewMultiListener(listeners ...net.Listener) *MultiListener {
|
||||
if len(listeners) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ml := &MultiListener{
|
||||
listeners: listeners,
|
||||
acceptCh: make(chan acceptResult, 2*len(listeners)),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Start accepting from each listener in its own goroutine
|
||||
for _, ln := range listeners {
|
||||
ml.wg.Add(1)
|
||||
go ml.acceptLoop(ln)
|
||||
}
|
||||
|
||||
return ml
|
||||
}
|
||||
|
||||
// acceptLoop continuously accepts connections from a single listener
|
||||
// and forwards them to the accept channel
|
||||
func (ml *MultiListener) acceptLoop(ln net.Listener) {
|
||||
defer ml.wg.Done()
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
|
||||
select {
|
||||
case <-ml.closeCh:
|
||||
// MultiListener is closing
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
return
|
||||
case ml.acceptCh <- acceptResult{conn: conn, err: err}:
|
||||
// Connection or error sent successfully
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accept waits for and returns the next connection from any of the listeners
|
||||
func (ml *MultiListener) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case <-ml.closeCh:
|
||||
return nil, errors.New("listener closed")
|
||||
case result, ok := <-ml.acceptCh:
|
||||
if !ok {
|
||||
// Channel closed
|
||||
return nil, errors.New("listener closed")
|
||||
}
|
||||
return result.conn, result.err
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes all underlying listeners
|
||||
func (ml *MultiListener) Close() error {
|
||||
var errs []error
|
||||
|
||||
ml.closeOnce.Do(func() {
|
||||
close(ml.closeCh)
|
||||
|
||||
// Close all listeners
|
||||
for _, ln := range ml.listeners {
|
||||
if err := ln.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all accept loops to finish
|
||||
ml.wg.Wait()
|
||||
|
||||
// Drain any remaining accepts
|
||||
close(ml.acceptCh)
|
||||
for range ml.acceptCh {
|
||||
}
|
||||
})
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("errors closing listeners: %v", errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Addr returns the address of the first listener
|
||||
func (ml *MultiListener) Addr() net.Addr {
|
||||
if len(ml.listeners) > 0 {
|
||||
return ml.listeners[0].Addr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveHostnameIPs resolves a hostname to all its IP addresses (IPv4 and IPv6).
|
||||
// If the input is already an IP address or empty, it returns it as-is.
|
||||
// This is useful for determining all addresses a server will listen on.
|
||||
func ResolveHostnameIPs(address string) ([]string, error) {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid address %q: %w", address, err)
|
||||
}
|
||||
|
||||
// Handle empty host (e.g., ":8080" means all interfaces)
|
||||
if host == "" {
|
||||
return []string{""}, nil
|
||||
}
|
||||
|
||||
// If already an IP address, return as is
|
||||
if net.ParseIP(host) != nil {
|
||||
return []string{host}, nil
|
||||
}
|
||||
|
||||
// Resolve hostname to all IP addresses
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve hostname %q: %w", host, err)
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("no addresses found for hostname %q", host)
|
||||
}
|
||||
|
||||
// Convert IPs to strings
|
||||
result := make([]string, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
result = append(result, ip.String())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// resolveHostnameAddrs resolves a hostname to all its IP addresses (IPv4 and IPv6)
|
||||
// and returns them as a list of addresses with the port attached.
|
||||
func resolveHostnameAddrs(address string) ([]string, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid address %q: %w", address, err)
|
||||
}
|
||||
|
||||
// If host is empty or already an IP address, return as is
|
||||
if host == "" || net.ParseIP(host) != nil {
|
||||
return []string{address}, nil
|
||||
}
|
||||
|
||||
// Resolve hostname to all IP addresses
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve hostname %q: %w", host, err)
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("no addresses found for hostname %q", host)
|
||||
}
|
||||
|
||||
// Build list of addresses with port
|
||||
addrs := make([]string, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
addr := net.JoinHostPort(ip.String(), port)
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// NewMultiAddrListener creates listeners for all IP addresses that the hostname
|
||||
// in the address resolves to. If the address is already an IP, it creates a
|
||||
// single listener. Returns a MultiListener if multiple addresses are resolved,
|
||||
// or a single listener if only one address is found.
|
||||
func NewMultiAddrListener(network, address string) (net.Listener, error) {
|
||||
addrs, err := resolveHostnameAddrs(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create listeners for all resolved addresses
|
||||
listeners := make([]net.Listener, 0, len(addrs))
|
||||
|
||||
for _, addr := range addrs {
|
||||
ln, err := net.Listen(network, addr)
|
||||
if err != nil {
|
||||
// Close any listeners we've already created
|
||||
for _, l := range listeners {
|
||||
l.Close()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to bind listener %s: %w", addr, err)
|
||||
}
|
||||
listeners = append(listeners, ln)
|
||||
}
|
||||
|
||||
// Return MultiListener for multiple addresses
|
||||
return NewMultiListener(listeners...), nil
|
||||
}
|
||||
|
||||
// NewMultiAddrTLSListener creates TLS listeners for all IP addresses that the
|
||||
// hostname in the address resolves to. Similar to NewMultiAddrListener but with TLS.
|
||||
func NewMultiAddrTLSListener(network, address string, getCertificateFunc func(*tls.ClientHelloInfo) (*tls.Certificate, error)) (net.Listener, error) {
|
||||
config := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
GetCertificate: getCertificateFunc,
|
||||
}
|
||||
|
||||
addrs, err := resolveHostnameAddrs(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create TLS listeners for all resolved addresses
|
||||
listeners := make([]net.Listener, 0, len(addrs))
|
||||
|
||||
for _, addr := range addrs {
|
||||
ln, err := net.Listen(network, addr)
|
||||
if err != nil {
|
||||
// Close any listeners we've already created
|
||||
for _, l := range listeners {
|
||||
l.Close()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to bind TLS listener %s: %w", addr, err)
|
||||
}
|
||||
listeners = append(listeners, tls.NewListener(ln, config))
|
||||
}
|
||||
|
||||
// Return MultiListener for multiple addresses
|
||||
return NewMultiListener(listeners...), nil
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMultiListener(t *testing.T) {
|
||||
// Create multiple underlying listeners
|
||||
ln1, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create listener 1: %v", err)
|
||||
}
|
||||
defer ln1.Close()
|
||||
|
||||
ln2, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create listener 2: %v", err)
|
||||
}
|
||||
defer ln2.Close()
|
||||
|
||||
// Create MultiListener
|
||||
ml := NewMultiListener(ln1, ln2)
|
||||
if ml == nil {
|
||||
t.Fatal("NewMultiListener returned nil")
|
||||
}
|
||||
defer ml.Close()
|
||||
|
||||
// Test connections to both listeners
|
||||
addr1 := ln1.Addr().String()
|
||||
addr2 := ln2.Addr().String()
|
||||
|
||||
// Connect to first listener
|
||||
go func() {
|
||||
conn, err := net.Dial("tcp", addr1)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to dial first address: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
conn.Write([]byte("hello from ln1"))
|
||||
}()
|
||||
|
||||
// Accept from MultiListener
|
||||
conn1, err := ml.Accept()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to accept from MultiListener: %v", err)
|
||||
}
|
||||
defer conn1.Close()
|
||||
|
||||
buf := make([]byte, 100)
|
||||
n, _ := conn1.Read(buf)
|
||||
if string(buf[:n]) != "hello from ln1" {
|
||||
t.Errorf("Unexpected data from first connection: %s", string(buf[:n]))
|
||||
}
|
||||
|
||||
// Connect to second listener
|
||||
go func() {
|
||||
conn, err := net.Dial("tcp", addr2)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to dial second address: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
conn.Write([]byte("hello from ln2"))
|
||||
}()
|
||||
|
||||
// Accept from MultiListener
|
||||
conn2, err := ml.Accept()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to accept second connection: %v", err)
|
||||
}
|
||||
defer conn2.Close()
|
||||
|
||||
n, _ = conn2.Read(buf)
|
||||
if string(buf[:n]) != "hello from ln2" {
|
||||
t.Errorf("Unexpected data from second connection: %s", string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiListenerClose(t *testing.T) {
|
||||
ln1, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create listener: %v", err)
|
||||
}
|
||||
|
||||
ml := NewMultiListener(ln1)
|
||||
if ml == nil {
|
||||
t.Fatal("NewMultiListener returned nil")
|
||||
}
|
||||
|
||||
// Start accepting in a goroutine
|
||||
acceptErrors := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := ml.Accept()
|
||||
acceptErrors <- err
|
||||
}()
|
||||
|
||||
// Give the accept goroutine time to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Close the MultiListener
|
||||
if err := ml.Close(); err != nil {
|
||||
t.Errorf("Close() returned error: %v", err)
|
||||
}
|
||||
|
||||
// The accept should now return an error
|
||||
select {
|
||||
case err := <-acceptErrors:
|
||||
if err == nil {
|
||||
t.Error("Accept() should fail after Close()")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("Accept() did not return after Close()")
|
||||
}
|
||||
|
||||
// Try to accept after close - should fail immediately
|
||||
_, err = ml.Accept()
|
||||
if err == nil {
|
||||
t.Error("Accept() should fail after Close() on subsequent calls")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveHostnameAddrs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address string
|
||||
wantErr bool
|
||||
checkResult func([]string) bool
|
||||
}{
|
||||
{
|
||||
name: "IPv4 address",
|
||||
address: "127.0.0.1:8080",
|
||||
wantErr: false,
|
||||
checkResult: func(addrs []string) bool {
|
||||
return len(addrs) == 1 && addrs[0] == "127.0.0.1:8080"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6 address",
|
||||
address: "[::1]:8080",
|
||||
wantErr: false,
|
||||
checkResult: func(addrs []string) bool {
|
||||
return len(addrs) == 1 && addrs[0] == "[::1]:8080"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "localhost hostname",
|
||||
address: "localhost:8080",
|
||||
wantErr: false,
|
||||
checkResult: func(addrs []string) bool {
|
||||
// localhost should resolve to at least one address
|
||||
return len(addrs) >= 1
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid address",
|
||||
address: "invalid-no-port",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
addrs, err := resolveHostnameAddrs(tt.address)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("resolveHostnameAddrs() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && tt.checkResult != nil {
|
||||
if !tt.checkResult(addrs) {
|
||||
t.Errorf("resolveHostnameAddrs() returned unexpected result: %v", addrs)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveHostnameIPs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address string
|
||||
wantErr bool
|
||||
checkResult func([]string) bool
|
||||
}{
|
||||
{
|
||||
name: "IPv4 address",
|
||||
address: "127.0.0.1:8080",
|
||||
wantErr: false,
|
||||
checkResult: func(ips []string) bool {
|
||||
return len(ips) == 1 && ips[0] == "127.0.0.1"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6 address",
|
||||
address: "[::1]:8080",
|
||||
wantErr: false,
|
||||
checkResult: func(ips []string) bool {
|
||||
return len(ips) == 1 && ips[0] == "::1"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "localhost hostname",
|
||||
address: "localhost:8080",
|
||||
wantErr: false,
|
||||
checkResult: func(ips []string) bool {
|
||||
// localhost should resolve to at least one address
|
||||
// On most systems, it resolves to both 127.0.0.1 and ::1
|
||||
return len(ips) >= 1
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty host",
|
||||
address: ":8080",
|
||||
wantErr: false,
|
||||
checkResult: func(ips []string) bool {
|
||||
return len(ips) == 1 && ips[0] == ""
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid address",
|
||||
address: "invalid-no-port",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ips, err := ResolveHostnameIPs(tt.address)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ResolveHostnameIPs() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && tt.checkResult != nil {
|
||||
if !tt.checkResult(ips) {
|
||||
t.Errorf("ResolveHostnameIPs() returned unexpected result: %v", ips)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMultiAddrListener(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "IPv4 loopback",
|
||||
address: "127.0.0.1:0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "IPv6 loopback",
|
||||
address: "[::1]:0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "localhost with port",
|
||||
address: "localhost:0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid hostname",
|
||||
address: "this-hostname-should-not-exist-12345.invalid:8080",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ln, err := NewMultiAddrListener("tcp", tt.address)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewMultiAddrListener() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if ln != nil {
|
||||
defer ln.Close()
|
||||
|
||||
// Try to connect to verify listener is working
|
||||
addr := ln.Addr().String()
|
||||
go func() {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
// Accept connection with timeout
|
||||
type result struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
ch := make(chan result, 1)
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
ch <- result{conn, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case res := <-ch:
|
||||
if res.err != nil {
|
||||
t.Errorf("Failed to accept connection: %v", res.err)
|
||||
}
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("Timeout waiting for connection")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMultiAddrTLSListener(t *testing.T) {
|
||||
// Create a simple test certificate
|
||||
getCertFunc := func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := tls.X509KeyPair([]byte(testCert), []byte(testKey))
|
||||
return &cert, err
|
||||
}
|
||||
|
||||
ln, err := NewMultiAddrTLSListener("tcp", "127.0.0.1:0", getCertFunc)
|
||||
if err != nil {
|
||||
t.Fatalf("NewMultiAddrTLSListener() error = %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
addr := ln.Addr().String()
|
||||
|
||||
// Try to connect with TLS
|
||||
go func() {
|
||||
conn, err := tls.Dial("tcp", addr, &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Write([]byte("test"))
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
// Accept connection
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to accept TLS connection: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
buf := make([]byte, 100)
|
||||
_, err = io.ReadAtLeast(conn, buf, 4)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to read from TLS connection: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test certificate and key for TLS tests
|
||||
const testCert = `-----BEGIN CERTIFICATE-----
|
||||
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
|
||||
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
|
||||
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
|
||||
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
|
||||
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
|
||||
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
|
||||
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
|
||||
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
|
||||
6MF9+Yw1Yy0t
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const testKey = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49
|
||||
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q
|
||||
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
Reference in New Issue
Block a user