mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-16 12:22:17 +01:00
feat(api): added tls/ssl support
This commit is contained in:
committed by
Julien Neuhart
parent
e50de4201d
commit
20b8991fa4
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
|||||||
/coverage.html
|
/coverage.html
|
||||||
/coverage.txt
|
/coverage.txt
|
||||||
/TODO.txt
|
/TODO.txt
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ func init() {
|
|||||||
// middlewares or health checks.
|
// middlewares or health checks.
|
||||||
type Api struct {
|
type Api struct {
|
||||||
port int
|
port int
|
||||||
|
tlsCertFile string
|
||||||
|
tlsKeyFile string
|
||||||
startTimeout time.Duration
|
startTimeout time.Duration
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
rootPath string
|
rootPath string
|
||||||
@@ -159,6 +161,8 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
||||||
fs.Int("api-port", 3000, "Set the port on which the API should listen")
|
fs.Int("api-port", 3000, "Set the port on which the API should listen")
|
||||||
fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port")
|
fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port")
|
||||||
|
fs.String("api-tls-cert-file", "", "Path to the TLS/SSL certificate file - for HTTPS support")
|
||||||
|
fs.String("api-tls-key-file", "", "Path to the TLS/SSL key file - for HTTPS support")
|
||||||
fs.Duration("api-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start")
|
fs.Duration("api-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start")
|
||||||
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
|
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
|
||||||
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
||||||
@@ -175,6 +179,8 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
func (a *Api) Provision(ctx *gotenberg.Context) error {
|
func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
a.port = flags.MustInt("api-port")
|
a.port = flags.MustInt("api-port")
|
||||||
|
a.tlsCertFile = flags.MustString("api-tls-cert-file")
|
||||||
|
a.tlsKeyFile = flags.MustString("api-tls-key-file")
|
||||||
a.startTimeout = flags.MustDuration("api-start-timeout")
|
a.startTimeout = flags.MustDuration("api-start-timeout")
|
||||||
a.timeout = flags.MustDuration("api-timeout")
|
a.timeout = flags.MustDuration("api-timeout")
|
||||||
a.rootPath = flags.MustString("api-root-path")
|
a.rootPath = flags.MustString("api-root-path")
|
||||||
@@ -301,6 +307,12 @@ func (a *Api) Validate() error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (a.tlsCertFile != "" && a.tlsKeyFile == "") || (a.tlsCertFile == "" && a.tlsKeyFile != "") {
|
||||||
|
err = multierr.Append(err,
|
||||||
|
errors.New("tls certificate and key file must both be set"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(a.rootPath, "/") {
|
if !strings.HasPrefix(a.rootPath, "/") {
|
||||||
err = multierr.Append(err,
|
err = multierr.Append(err,
|
||||||
errors.New("root path must start with /"),
|
errors.New("root path must start with /"),
|
||||||
@@ -478,10 +490,19 @@ func (a *Api) Start() error {
|
|||||||
|
|
||||||
// As the following code is blocking, run it in a goroutine.
|
// As the following code is blocking, run it in a goroutine.
|
||||||
go func() {
|
go func() {
|
||||||
server := &http2.Server{}
|
if a.tlsCertFile != "" && a.tlsKeyFile != "" {
|
||||||
err := a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server)
|
// Start an HTTPS server (supports HTTP/2).
|
||||||
if !errors.Is(err, http.ErrServerClosed) {
|
err := a.srv.StartTLS(fmt.Sprintf(":%d", a.port), a.tlsCertFile, a.tlsKeyFile)
|
||||||
a.logger.Fatal(err.Error())
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
a.logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start an HTTP/2 Cleartext (non-HTTPS) server.
|
||||||
|
server := &http2.Server{}
|
||||||
|
err := a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server)
|
||||||
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
a.logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -462,6 +462,8 @@ func TestApi_Validate(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
port int
|
port int
|
||||||
|
tlsCertFile string
|
||||||
|
tlsKeyFile string
|
||||||
rootPath string
|
rootPath string
|
||||||
traceHeader string
|
traceHeader string
|
||||||
routes []Route
|
routes []Route
|
||||||
@@ -486,6 +488,26 @@ func TestApi_Validate(t *testing.T) {
|
|||||||
middlewares: nil,
|
middlewares: nil,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid tls files: only cert file provided",
|
||||||
|
port: 10,
|
||||||
|
tlsCertFile: "cert.pem",
|
||||||
|
rootPath: "/foo/",
|
||||||
|
traceHeader: "foo",
|
||||||
|
routes: nil,
|
||||||
|
middlewares: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid tls files: only key file provided",
|
||||||
|
port: 10,
|
||||||
|
tlsKeyFile: "key.pem",
|
||||||
|
rootPath: "/foo/",
|
||||||
|
traceHeader: "foo",
|
||||||
|
routes: nil,
|
||||||
|
middlewares: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "invalid root path: missing / prefix",
|
scenario: "invalid root path: missing / prefix",
|
||||||
port: 10,
|
port: 10,
|
||||||
@@ -647,10 +669,45 @@ func TestApi_Validate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "success with tls",
|
||||||
|
port: 10,
|
||||||
|
tlsCertFile: "cert.pem",
|
||||||
|
tlsKeyFile: "key.pem",
|
||||||
|
rootPath: "/foo/",
|
||||||
|
traceHeader: "foo",
|
||||||
|
routes: []Route{
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/foo",
|
||||||
|
Handler: func(_ echo.Context) error { return nil },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/forms/foo",
|
||||||
|
Handler: func(_ echo.Context) error { return nil },
|
||||||
|
IsMultipart: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
middlewares: []Middleware{
|
||||||
|
{
|
||||||
|
Priority: HighPriority,
|
||||||
|
Handler: func() echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod := Api{
|
mod := Api{
|
||||||
port: tc.port,
|
port: tc.port,
|
||||||
|
tlsCertFile: tc.tlsCertFile,
|
||||||
|
tlsKeyFile: tc.tlsKeyFile,
|
||||||
rootPath: tc.rootPath,
|
rootPath: tc.rootPath,
|
||||||
traceHeader: tc.traceHeader,
|
traceHeader: tc.traceHeader,
|
||||||
routes: tc.routes,
|
routes: tc.routes,
|
||||||
@@ -673,6 +730,8 @@ func TestApi_Start(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
readyFn []func() error
|
readyFn []func() error
|
||||||
|
tlsCertFile string
|
||||||
|
tlsKeyFile string
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -691,10 +750,22 @@ func TestApi_Start(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "success with tls",
|
||||||
|
readyFn: []func() error{
|
||||||
|
func() error { return nil },
|
||||||
|
func() error { return nil },
|
||||||
|
},
|
||||||
|
tlsCertFile: "/tests/test/testdata/api/cert.pem",
|
||||||
|
tlsKeyFile: "/tests/test/testdata/api/key.pem",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod := new(Api)
|
mod := new(Api)
|
||||||
mod.port = 3000
|
mod.port = 3000
|
||||||
|
mod.tlsCertFile = tc.tlsCertFile
|
||||||
|
mod.tlsKeyFile = tc.tlsKeyFile
|
||||||
mod.startTimeout = time.Duration(30) * time.Second
|
mod.startTimeout = time.Duration(30) * time.Second
|
||||||
mod.rootPath = "/"
|
mod.rootPath = "/"
|
||||||
mod.basicAuthUsername = "foo"
|
mod.basicAuthUsername = "foo"
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
# Credits: https://github.com/thecodingmachine/docker-images-php.
|
# Credits: https://github.com/thecodingmachine/docker-images-php.
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
mkdir testing_file_system_rights.foo
|
mkdir -p testing_file_system_rights.foo
|
||||||
chmod 700 testing_file_system_rights.foo
|
chmod 700 testing_file_system_rights.foo
|
||||||
su gotenberg -c "touch testing_file_system_rights.foo/foo > /dev/null 2>&1"
|
su gotenberg -c "touch testing_file_system_rights.foo/foo > /dev/null 2>&1"
|
||||||
HAS_CONSISTENT_RIGHTS=$?
|
HAS_CONSISTENT_RIGHTS=$?
|
||||||
|
|
||||||
if [[ "$HAS_CONSISTENT_RIGHTS" != "0" ]]; then
|
if [[ "$HAS_CONSISTENT_RIGHTS" != "0" ]]; then
|
||||||
# If not specified, the DOCKER_USER is the owner of the current working directory (heuristic!).
|
# If not specified, the DOCKER_USER is the owner of the current working directory (heuristic!).
|
||||||
DOCKER_USER=$(stat -c '%U' "$(pwd)")
|
DOCKER_USER=$(stat -c '%u' "$(pwd)")
|
||||||
else
|
else
|
||||||
# macOs or Windows.
|
# macOs or Windows.
|
||||||
# Note: in most cases, we don't care about the rights (they are not respected).
|
# Note: in most cases, we don't care about the rights (they are not respected).
|
||||||
FILE_OWNER=$(stat -c '%U' "testing_file_system_rights.foo/foo")
|
FILE_OWNER=$(stat -c '%u' "testing_file_system_rights.foo/foo")
|
||||||
if [[ "$FILE_OWNER" == "root" ]]; then
|
if [[ "$FILE_OWNER" == "0" ]]; then
|
||||||
# If root, we are likely on a Windows host.
|
# If root, we are likely on a Windows host.
|
||||||
# All files will belong to root, but it does not matter as everybody can write/delete
|
# All files will belong to root, but it does not matter as everybody can write/delete
|
||||||
# those (0777 access rights).
|
# those (0777 access rights).
|
||||||
|
|||||||
14
test/testdata/api/README.md
vendored
Normal file
14
test/testdata/api/README.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
To generate a valid certificate and private key use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In OpenSSL ≥ 1.1.1
|
||||||
|
openssl req -x509 -newkey rsa:4096 -sha256 -days 9999 -nodes \
|
||||||
|
-keyout key.pem -out cert.pem -subj "/CN=localhost" \
|
||||||
|
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1"
|
||||||
|
```
|
||||||
|
|
||||||
|
To check a certificate use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl x509 -in cert.pem -text
|
||||||
|
```
|
||||||
30
test/testdata/api/cert.pem
vendored
Normal file
30
test/testdata/api/cert.pem
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFOjCCAyKgAwIBAgIUZaht2QoZwWDK8wEuwm+wmip9sqwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI0MDUyNjA1MDExNFoYDzIwNTEx
|
||||||
|
MDExMDUwMTE0WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4ICDwAwggIKAoICAQCm3TNckD55ae6pxXS03iV2P3X/edZT4vRa8D15elsi
|
||||||
|
CSRf6mdU2TVAQyg22GEcMXgsbba7sotJVg7559LkWAgb3Dj9rbPputdS29ebncat
|
||||||
|
x1nzo1QHMCS/v7nWoPe9fQVlDEf3ebM198f+5vhccSYFxFa+YvsUmSw9yMF8z1nA
|
||||||
|
6POZTpm6ErByAY9EGvC8EcNOQd9xLOfBZc0e+mY/rMRxmL+0N+sSRE/ACU+2zxHN
|
||||||
|
KcCiFX5WQdLDYW1ZjX5xAVatCPNlxSQpj97/ktGsnqwGnJWSTeMUOPv7L0LrQasf
|
||||||
|
k1VXBvfTW2/ZqhI9WA8qfdC5XOuzVwfU14gv+6vv9AGZnUFmOjeo9XG4xWBxlIKj
|
||||||
|
AQAzeNwAGFltPkp5a3yPczxMjKY4xLsMGcDfpaNtv1B24+7fV77Nyty/gLLT9pUa
|
||||||
|
FtXLuRfIPSaUK3kFhAuYzGUL/sO9O9jJkNlxxygxQ+VyIgmdw0zWrUi3q+Lfmdya
|
||||||
|
FFjHdM7LnLrLCjSv3dEoMINngR3+IejsY+98Ehd25O3oW+ZnGIQX0VwykV8sOQWq
|
||||||
|
rpnnZ+2iHMtF6Vsj40z/Y9y7yVE18AIJFC+Yr/dD/nQyg6SE70wdNcOx0Ay1xHbP
|
||||||
|
WuV3fqKOfhONR7NEBSO2UbHi6GZAiYMMwwul+4yKAQ4+lHOEXG4JQcKvtZasxq9v
|
||||||
|
GwIDAQABo4GBMH8wHQYDVR0OBBYEFPPMaH3C0rAkD/E0gtBZwM0x8N9MMB8GA1Ud
|
||||||
|
IwQYMBaAFPPMaH3C0rAkD/E0gtBZwM0x8N9MMA8GA1UdEwEB/wQFMAMBAf8wLAYD
|
||||||
|
VR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqG
|
||||||
|
SIb3DQEBCwUAA4ICAQB0DXz59JfQgfJvRJfraa+Cv/3nG4NNmHYtNGhrid5sagH3
|
||||||
|
LM8jkbA+JKRAn34SmsqUZb1f2XFI64TwrScgMfAvfi+5ibRqBjM/YL06bTlG61Ua
|
||||||
|
t2IQchV2kcX4zQnaMCIYG0dUj6JoXIspiFULMHyGDs7G9BKVcZxlaLrEvPXBqQjd
|
||||||
|
nwF80KHS/rCConmICU9uWHPgHxS1VmhQiwYGbrakETilau1ppRS0bpH2k/stVyjc
|
||||||
|
plttIr1DUD6xaRV4mZ0NZVlIXl2nDf78iQvreJeMrGl6AcNHzV+/rGHpY/gOrziP
|
||||||
|
PQkB1VtSiHtBe/VMKXcVB69SEzsoGEknqs4GPXuVYm47POo3/U5HmGU+FadSATNP
|
||||||
|
qtorRECUMbVn0IfDzJX2jBLUjZlhPI9gqO9DhiAT8FWHRV2yPsZKuiBBg5/hUvyb
|
||||||
|
Qsswtmi/YV5e6AnXEuvylwE3wYzlqDzxwlZM08bWFyZYnSf38d3hAjVwUHAugiaX
|
||||||
|
4sgE6GPAiONW5FkTZ4Z2oXYiN213QDIIcQeFAl04qSEFkGBV8x2hM0uXgFH1S++j
|
||||||
|
JePdKBbV/CjxegyyYsmnln+eO1kC8pCalB6k6pEtYMqNYOnmimC8UKrJATrH9EVq
|
||||||
|
eAehGBiIe514Oz06Ws2Vm4PTiY2x0KVbqphwkuz85ohSF9UXOHUcMryusr1zlg==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
52
test/testdata/api/key.pem
vendored
Normal file
52
test/testdata/api/key.pem
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCm3TNckD55ae6p
|
||||||
|
xXS03iV2P3X/edZT4vRa8D15elsiCSRf6mdU2TVAQyg22GEcMXgsbba7sotJVg75
|
||||||
|
59LkWAgb3Dj9rbPputdS29ebncatx1nzo1QHMCS/v7nWoPe9fQVlDEf3ebM198f+
|
||||||
|
5vhccSYFxFa+YvsUmSw9yMF8z1nA6POZTpm6ErByAY9EGvC8EcNOQd9xLOfBZc0e
|
||||||
|
+mY/rMRxmL+0N+sSRE/ACU+2zxHNKcCiFX5WQdLDYW1ZjX5xAVatCPNlxSQpj97/
|
||||||
|
ktGsnqwGnJWSTeMUOPv7L0LrQasfk1VXBvfTW2/ZqhI9WA8qfdC5XOuzVwfU14gv
|
||||||
|
+6vv9AGZnUFmOjeo9XG4xWBxlIKjAQAzeNwAGFltPkp5a3yPczxMjKY4xLsMGcDf
|
||||||
|
paNtv1B24+7fV77Nyty/gLLT9pUaFtXLuRfIPSaUK3kFhAuYzGUL/sO9O9jJkNlx
|
||||||
|
xygxQ+VyIgmdw0zWrUi3q+LfmdyaFFjHdM7LnLrLCjSv3dEoMINngR3+IejsY+98
|
||||||
|
Ehd25O3oW+ZnGIQX0VwykV8sOQWqrpnnZ+2iHMtF6Vsj40z/Y9y7yVE18AIJFC+Y
|
||||||
|
r/dD/nQyg6SE70wdNcOx0Ay1xHbPWuV3fqKOfhONR7NEBSO2UbHi6GZAiYMMwwul
|
||||||
|
+4yKAQ4+lHOEXG4JQcKvtZasxq9vGwIDAQABAoICAA3s7URkT/0XTMZOUZcRPanI
|
||||||
|
p12oGq2Vhd8GUvBjDbKhdgUAgu8fJsUzgD+o1Ic4eaxQ26x+Z1wPb+CF6vZeLQ8Q
|
||||||
|
1JDB1lSYiy/BuRSmQ4EX30yTQjKiCe8Ws7j9u1nM/Tju9XKUbX2rgr1QeURTDQvz
|
||||||
|
fr8UyO9xzly5oawmLHoKTDTBzS0s3Md8Pys9NFSoXCxDwxSm3WXTy8fcuHvH51Je
|
||||||
|
SmFik/OfpSgu9BVwN9lvX+wm7wwzzMBmzmwS1Y0zbLsJbDFUv9fYHJF8/JKeC1fh
|
||||||
|
6Xme67yG+kB+3o7TmXxhHKt5TBAPfsjOMLIojzDJ9IlY/91PqLpSjRhetdKorRlf
|
||||||
|
gvTUGLPY8lHXwz6UBpdHmMhNHTBBvgHcOVAMD46Y81SdqMhH6TAYSh7XQVuA06Ql
|
||||||
|
MTWzGqzKriaQDWmJ6Nx8eZCRDZGXDq4rKiJFvRFUrGYYr818qU8yC+qv5uLrOF1w
|
||||||
|
YZV0WnZ7DtcaFliBrJVUfeTmrSJBa3yoIKbeaTDuh3TMHqgAbTx0lBdGoI6noRxx
|
||||||
|
v7fCGho/aSOJ2rtFOjRoLQ5K8onZ226arJEyfkTh16wztQgh9DC430OkL4cQkfA0
|
||||||
|
BxN8tGh4P0lqwcBnCOyzi4NV1NkwaaVv+pt757Cz+Cy6e2DQOtDkrFEh4p56NqDr
|
||||||
|
KcJz88LYbbChoFKlb/kBAoIBAQDhcuI0KWTErojWu/dB2Okth8OFaTdvAPEvo+qO
|
||||||
|
ItJgxyYL8JIdWUzS9vKro6vMctwIXvNa3K2AAwdZ1kHvaxDZaikQbnjnSjOqRlZa
|
||||||
|
4rVnI6SSKrYj6xmKcsxxTi9YP8b2msjdLuGhPfO8Dw4EQgOt6xg0rq4msnv6bYki
|
||||||
|
G7vsk/InnKLm8SEhR1XsmtVrpC7KbXniLQEehgs1Kp0nMLptSsOTnA1kGoZDVlXc
|
||||||
|
u+kBBai6MM0MMDpsJgM1IFkUousQl3ItBcTXD2ZCMcg5psQJbdEhu7oXiGkHdKlL
|
||||||
|
1UEIdSGNlzHiVM62uNxhNfwh7I6SPesDksJbE0Yd//jz/7FRAoIBAQC9ee+pDsSs
|
||||||
|
yqunQjMWImTpD5h9fSRTzWT8h0dXsJyYi7s9Dj4mQIuoarscMI6L7njeTqHE8ptc
|
||||||
|
b9DzIEWL+233nTOS59grCTD84Kyrn+Hw6EebgKYmtpZnvMw5cccoZ3qUsxVGurhA
|
||||||
|
lzVaKpHtPKWT1fduNmnqt6RkZ7aRBLZre/i9UntZ7VSZYRKMOBvJLpLdqmiSPkvM
|
||||||
|
w8ra+cedHwDegqLkoX0sxdI1yNYO1GSkJ+w1I1m1rzuCd9X5QXvyQhPezVB9wUwr
|
||||||
|
anNGb6/YgvthzvMQAXYqv4pmb769przieMJ6geQL3O8QEtvpDAV4SvrwnA+H/wfs
|
||||||
|
BvwI4MTHNJ6rAoIBADEfnzpOaq6QeTCQ9GdcpDJPisLfEj0Vr3f593nRDJYZzqh7
|
||||||
|
WtsaQisVs/rCRwTdYiRQzCXYP9XM0yU4ElXgtmMpRplV+PIssOVBPj+/dldq5MkU
|
||||||
|
lWtuJrqMGQ/3QUmxW+EQCQRo2rEPlrQ7c4pp9/NFbpjMFxZHfcwrYd4UvPXnprQK
|
||||||
|
5VP85oMh0A8mDarOs4NYJ16o/71u57JF/sgzTevShpr7Pc+6F9dEUKEwMK0QVpt7
|
||||||
|
VrJ5L8Gw5rIEwmREu4N5/F7jCujxag6yWjZ1p/GGBDOdfAb0qi39tYXYibsWCqGX
|
||||||
|
eD8gy4n07dAguzeJG+expiu5JrBBmt2ekNhZtoECggEAQ90VoraAaOcaPgju5UGi
|
||||||
|
ZPtKAcA9r6pEPlJxkg3WuKhrgr3EMs0xeDvqEnmtxJ7AOdduB21hieUesMXhQvbl
|
||||||
|
9ycd3sMdYKSlIB2Ums/kC/YWF7MnT3xcOIGdoAGJdIZDmFjgCPo9nhzKZiYTKCZt
|
||||||
|
o5qiSZ5Bp2jo/3FKKXzHnY3ElIKjXZ0sjNcA3qoGAbOltLEhTSf03D1QPcfy3+rH
|
||||||
|
0+p0T3ErtUqRNORfDuaxuT33Uzz7F6DQrrUfNWxn6WgTV9yvtKr0DiTNbu/3ypN3
|
||||||
|
WBvYYOY1HbCG8JObfK/ovTDFbz1woXMbSOWIUFsW5nPslxs+UNZeTwJdaQygTj0h
|
||||||
|
iwKCAQBuXOPtjWGZINgfjo/n67Q8FuzUXxDK/IuXzuDXhi+B3/O3/PcVEDHFByhI
|
||||||
|
KHOh2mTD25vgFWh+8dNg1R+m5uIeQ5zbZ5U0205XzmrZgBXXCAHKNruTwqZfQ6er
|
||||||
|
np+8wHhcCbBuy0Z0ponzBGlrDYmbxO9anXGxoe/rE3+pWbzMneZ+cHgNjXCtfZCY
|
||||||
|
XGfLeJWW3+B8y5Lff39ADawmjm8VWVEdd8DP5o2zulbXhgMMnAsSVsR/XgwBqpbf
|
||||||
|
82k1bqvN2v08ID5nmTRdzD+D51Hjw80Uoqhm5DJL4IwaoK/YstCu2TCbmLGiyqSk
|
||||||
|
lrSOZMPTLX1tN/cWa+Ii+/WALnkI
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
Reference in New Issue
Block a user