mirror of
https://github.com/minio/minio.git
synced 2024-12-24 22:25:54 -05:00
Make unit testable cert parsing functions. (#3863)
This commit is contained in:
parent
47ac410ab0
commit
8a9852220d
@ -803,7 +803,7 @@ func (adminAPI adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http
|
|||||||
// Take a lock on minio/config.json. NB minio is a reserved
|
// Take a lock on minio/config.json. NB minio is a reserved
|
||||||
// bucket name and wouldn't conflict with normal object
|
// bucket name and wouldn't conflict with normal object
|
||||||
// operations.
|
// operations.
|
||||||
configLock := globalNSMutex.NewNSLock(minioReservedBucket, globalMinioConfigFile)
|
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
||||||
configLock.Lock()
|
configLock.Lock()
|
||||||
defer configLock.Unlock()
|
defer configLock.Unlock()
|
||||||
|
|
||||||
|
152
cmd/certs.go
152
cmd/certs.go
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -19,120 +19,90 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getCertsPath get certs path.
|
|
||||||
func getCertsPath() string {
|
|
||||||
return filepath.Join(getConfigDir(), globalMinioCertsDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getCertFile must get cert file.
|
|
||||||
func getCertFile() string {
|
|
||||||
return filepath.Join(getCertsPath(), globalMinioCertFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getKeyFile must get key file.
|
|
||||||
func getKeyFile() string {
|
|
||||||
return filepath.Join(getCertsPath(), globalMinioKeyFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// createCertsPath create certs path.
|
|
||||||
func createCertsPath() error {
|
|
||||||
rootCAsPath := filepath.Join(getCertsPath(), globalMinioCertsCADir)
|
|
||||||
return mkdirAll(rootCAsPath, 0700)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getCAFiles must get the list of the CA certificates stored in minio config dir
|
|
||||||
func getCAFiles() (caCerts []string) {
|
|
||||||
CAsDir := filepath.Join(getCertsPath(), globalMinioCertsCADir)
|
|
||||||
if caFiles, err := ioutil.ReadDir(CAsDir); err == nil {
|
|
||||||
// Ignore any error.
|
|
||||||
for _, cert := range caFiles {
|
|
||||||
caCerts = append(caCerts, filepath.Join(CAsDir, cert.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return caCerts
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSystemCertPool returns empty cert pool in case of error (windows)
|
|
||||||
func getSystemCertPool() *x509.CertPool {
|
|
||||||
pool, err := x509.SystemCertPool()
|
|
||||||
if err != nil {
|
|
||||||
pool = x509.NewCertPool()
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool
|
|
||||||
}
|
|
||||||
|
|
||||||
// isCertFileExists verifies if cert file exists, returns true if
|
|
||||||
// found, false otherwise.
|
|
||||||
func isCertFileExists() bool {
|
|
||||||
return isFile(getCertFile())
|
|
||||||
}
|
|
||||||
|
|
||||||
// isKeyFileExists verifies if key file exists, returns true if found,
|
|
||||||
// false otherwise.
|
|
||||||
func isKeyFileExists() bool {
|
|
||||||
return isFile(getKeyFile())
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSSL - returns true with both cert and key exists.
|
// isSSL - returns true with both cert and key exists.
|
||||||
func isSSL() bool {
|
func isSSL() bool {
|
||||||
return isCertFileExists() && isKeyFileExists()
|
return isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads certificated file and returns a list of parsed certificates.
|
func parsePublicCertFile(certFile string) (certs []*x509.Certificate, err error) {
|
||||||
func readCertificateChain() ([]*x509.Certificate, error) {
|
var bytes []byte
|
||||||
bytes, err := ioutil.ReadFile(getCertFile())
|
|
||||||
if err != nil {
|
if bytes, err = ioutil.ReadFile(certFile); err != nil {
|
||||||
return nil, err
|
return certs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed to parse the certificates.
|
|
||||||
return parseCertificateChain(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parses certificate chain, returns a list of parsed certificates.
|
|
||||||
func parseCertificateChain(bytes []byte) ([]*x509.Certificate, error) {
|
|
||||||
var certs []*x509.Certificate
|
|
||||||
var block *pem.Block
|
|
||||||
current := bytes
|
|
||||||
|
|
||||||
// Parse all certs in the chain.
|
// Parse all certs in the chain.
|
||||||
|
var block *pem.Block
|
||||||
|
var cert *x509.Certificate
|
||||||
|
current := bytes
|
||||||
for len(current) > 0 {
|
for len(current) > 0 {
|
||||||
block, current = pem.Decode(current)
|
if block, current = pem.Decode(current); block == nil {
|
||||||
if block == nil {
|
err = fmt.Errorf("Could not read PEM block from file %s", certFile)
|
||||||
return nil, errors.New("Could not PEM block")
|
return certs, err
|
||||||
}
|
}
|
||||||
// Parse the decoded certificate.
|
|
||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
if cert, err = x509.ParseCertificate(block.Bytes); err != nil {
|
||||||
|
return certs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certs = append(certs, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(certs) == 0 {
|
||||||
|
err = fmt.Errorf("Empty public certificate file %s", certFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return certs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads certificate file and returns a list of parsed certificates.
|
||||||
|
func readCertificateChain() ([]*x509.Certificate, error) {
|
||||||
|
return parsePublicCertFile(getPublicCertFile())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
||||||
|
// Get all CA file names.
|
||||||
|
var caFiles []string
|
||||||
|
fis, err := ioutil.ReadDir(certsCAsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
certs = append(certs, cert)
|
for _, fi := range fis {
|
||||||
|
caFiles = append(caFiles, filepath.Join(certsCAsDir, fi.Name()))
|
||||||
}
|
}
|
||||||
return certs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadRootCAs fetches CA files provided in minio config and adds them to globalRootCAs
|
|
||||||
// Currently under Windows, there is no way to load system + user CAs at the same time
|
|
||||||
func loadRootCAs() {
|
|
||||||
caFiles := getCAFiles()
|
|
||||||
if len(caFiles) == 0 {
|
if len(caFiles) == 0 {
|
||||||
return
|
return nil, nil
|
||||||
}
|
}
|
||||||
// Get system cert pool, and empty cert pool under Windows because it is not supported
|
|
||||||
globalRootCAs = getSystemCertPool()
|
rootCAs, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
// In some systems like Windows, system cert pool is not supported.
|
||||||
|
// Hence we create a new cert pool.
|
||||||
|
rootCAs = x509.NewCertPool()
|
||||||
|
}
|
||||||
|
|
||||||
// Load custom root CAs for client requests
|
// Load custom root CAs for client requests
|
||||||
for _, caFile := range caFiles {
|
for _, caFile := range caFiles {
|
||||||
caCert, err := ioutil.ReadFile(caFile)
|
caCert, err := ioutil.ReadFile(caFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalIf(err, "Unable to load a CA file")
|
return rootCAs, err
|
||||||
}
|
}
|
||||||
globalRootCAs.AppendCertsFromPEM(caCert)
|
|
||||||
|
rootCAs.AppendCertsFromPEM(caCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return rootCAs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadRootCAs fetches CA files provided in minio config and adds them to globalRootCAs
|
||||||
|
// Currently under Windows, there is no way to load system + user CAs at the same time
|
||||||
|
func loadRootCAs() (err error) {
|
||||||
|
globalRootCAs, err = getRootCAs(getCADir())
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -17,49 +17,86 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Make sure we have a valid certs path.
|
func createTempFile(prefix, content string) (tempFile string, err error) {
|
||||||
func TestGetCertsPath(t *testing.T) {
|
var tmpfile *os.File
|
||||||
path := getCertsPath()
|
|
||||||
if path == "" {
|
if tmpfile, err = ioutil.TempFile("", prefix); err != nil {
|
||||||
t.Errorf("expected path to not be an empty string, got: '%s'", path)
|
return tempFile, err
|
||||||
}
|
|
||||||
// Ensure it contains some sort of path separator.
|
|
||||||
if !strings.ContainsRune(path, os.PathSeparator) {
|
|
||||||
t.Errorf("expected path to contain file separator")
|
|
||||||
}
|
|
||||||
// It should also be an absolute path.
|
|
||||||
if !filepath.IsAbs(path) {
|
|
||||||
t.Errorf("expected path to be an absolute path")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will error if something goes wrong, so just call it.
|
if _, err = tmpfile.Write([]byte(content)); err != nil {
|
||||||
getCertsPath()
|
return tempFile, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tmpfile.Close(); err != nil {
|
||||||
|
return tempFile, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile = tmpfile.Name()
|
||||||
|
return tempFile, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that the certificate and key file getters contain their respective
|
func TestParsePublicCertFile(t *testing.T) {
|
||||||
// file name and endings.
|
tempFile1, err := createTempFile("public-cert-file", "")
|
||||||
func TestGetFiles(t *testing.T) {
|
if err != nil {
|
||||||
file := getCertFile()
|
t.Fatalf("Unable to create temporary file. %v", err)
|
||||||
if !strings.Contains(file, globalMinioCertFile) {
|
|
||||||
t.Errorf("CertFile does not contain %s", globalMinioCertFile)
|
|
||||||
}
|
}
|
||||||
|
defer os.Remove(tempFile1)
|
||||||
|
|
||||||
file = getKeyFile()
|
tempFile2, err := createTempFile("public-cert-file",
|
||||||
if !strings.Contains(file, globalMinioKeyFile) {
|
`-----BEGIN CERTIFICATE-----
|
||||||
t.Errorf("KeyFile does not contain %s", globalMinioKeyFile)
|
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||||
|
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||||
|
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||||
|
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
|
||||||
|
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
|
||||||
|
ZXJuZXQxDjAMBgNVBA-some-junk-Q4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
|
||||||
|
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
|
||||||
|
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
|
||||||
|
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
|
||||||
|
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
|
||||||
|
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
||||||
|
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||||
|
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||||
|
M9ofSEt/bdRD
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create temporary file. %v", err)
|
||||||
}
|
}
|
||||||
}
|
defer os.Remove(tempFile2)
|
||||||
|
|
||||||
// Parses .crt file contents
|
tempFile3, err := createTempFile("public-cert-file",
|
||||||
func TestParseCertificateChain(t *testing.T) {
|
`-----BEGIN CERTIFICATE-----
|
||||||
// given
|
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||||
cert := `-----BEGIN CERTIFICATE-----
|
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||||
|
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||||
|
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
|
||||||
|
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
|
||||||
|
ZXJuZXQxDjAMBgNVBAabababababaQ4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
|
||||||
|
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
|
||||||
|
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
|
||||||
|
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
|
||||||
|
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
|
||||||
|
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
||||||
|
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||||
|
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||||
|
M9ofSEt/bdRD
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create temporary file. %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFile3)
|
||||||
|
|
||||||
|
tempFile4, err := createTempFile("public-cert-file",
|
||||||
|
`-----BEGIN CERTIFICATE-----
|
||||||
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||||
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||||
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||||
@ -74,35 +111,149 @@ JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
|||||||
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||||
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||||
M9ofSEt/bdRD
|
M9ofSEt/bdRD
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`)
|
||||||
|
|
||||||
// when
|
|
||||||
certs, err := parseCertificateChain([]byte(cert))
|
|
||||||
|
|
||||||
// then
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Could not parse certificate: %s", err)
|
t.Fatalf("Unable to create temporary file. %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFile4)
|
||||||
|
|
||||||
|
tempFile5, err := createTempFile("public-cert-file",
|
||||||
|
`-----BEGIN CERTIFICATE-----
|
||||||
|
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||||
|
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||||
|
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||||
|
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
|
||||||
|
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
|
||||||
|
ZXJuZXQxDjAMBgNVBAoTBU1pbmlvMQ4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
|
||||||
|
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
|
||||||
|
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
|
||||||
|
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
|
||||||
|
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
|
||||||
|
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
||||||
|
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||||
|
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||||
|
M9ofSEt/bdRD
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||||
|
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||||
|
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||||
|
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
|
||||||
|
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
|
||||||
|
ZXJuZXQxDjAMBgNVBAoTBU1pbmlvMQ4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
|
||||||
|
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
|
||||||
|
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
|
||||||
|
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
|
||||||
|
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
|
||||||
|
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
||||||
|
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||||
|
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||||
|
M9ofSEt/bdRD
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create temporary file. %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFile5)
|
||||||
|
|
||||||
|
nonexistentErr := fmt.Errorf("open nonexistent-file: no such file or directory")
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Below concatenation is done to get rid of goline error
|
||||||
|
// "error strings should not be capitalized or end with punctuation or a newline"
|
||||||
|
nonexistentErr = fmt.Errorf("open nonexistent-file:" + " The system cannot find the file specified.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(certs) != 1 {
|
testCases := []struct {
|
||||||
t.Fatalf("Expected number of certificates in chain was 1, actual: %d", len(certs))
|
certFile string
|
||||||
|
expectedResultLen int
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{"nonexistent-file", 0, nonexistentErr},
|
||||||
|
{tempFile1, 0, fmt.Errorf("Empty public certificate file %s", tempFile1)},
|
||||||
|
{tempFile2, 0, fmt.Errorf("Could not read PEM block from file %s", tempFile2)},
|
||||||
|
{tempFile3, 0, fmt.Errorf("asn1: structure error: sequence tag mismatch")},
|
||||||
|
{tempFile4, 1, nil},
|
||||||
|
{tempFile5, 2, nil},
|
||||||
}
|
}
|
||||||
|
|
||||||
if certs[0].Subject.CommonName != "Minio" {
|
for _, testCase := range testCases {
|
||||||
t.Fatalf("Expected Subject.CommonName was Minio, actual: %s", certs[0].Subject.CommonName)
|
certs, err := parsePublicCertFile(testCase.certFile)
|
||||||
|
|
||||||
|
if testCase.expectedErr == nil {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||||
|
}
|
||||||
|
} else if err == nil {
|
||||||
|
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||||
|
} else if testCase.expectedErr.Error() != err.Error() {
|
||||||
|
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(certs) != testCase.expectedResultLen {
|
||||||
|
t.Fatalf("certs: expected = %v, got = %v", testCase.expectedResultLen, len(certs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses invalid .crt file contents and returns error
|
func TestGetRootCAs(t *testing.T) {
|
||||||
func TestParseInvalidCertificateChain(t *testing.T) {
|
emptydir, err := ioutil.TempDir("", "test-get-root-cas")
|
||||||
// given
|
if err != nil {
|
||||||
cert := `This is now valid certificate`
|
t.Fatalf("Unable create temp directory. %v", emptydir)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(emptydir)
|
||||||
|
|
||||||
// when
|
dir1, err := ioutil.TempDir("", "test-get-root-cas")
|
||||||
_, err := parseCertificateChain([]byte(cert))
|
if err != nil {
|
||||||
|
t.Fatalf("Unable create temp directory. %v", dir1)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
if err = os.Mkdir(filepath.Join(dir1, "empty-dir"), 0755); err != nil {
|
||||||
|
t.Fatalf("Unable create empty dir. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// then
|
dir2, err := ioutil.TempDir("", "test-get-root-cas")
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Fatalf("Expected error but none occurred")
|
t.Fatalf("Unable create temp directory. %v", dir2)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir2)
|
||||||
|
if err = ioutil.WriteFile(filepath.Join(dir2, "empty-file"), []byte{}, 0644); err != nil {
|
||||||
|
t.Fatalf("Unable create test file. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonexistentErr := fmt.Errorf("open nonexistent-dir: no such file or directory")
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Below concatenation is done to get rid of goline error
|
||||||
|
// "error strings should not be capitalized or end with punctuation or a newline"
|
||||||
|
nonexistentErr = fmt.Errorf("open nonexistent-dir:" + " The system cannot find the file specified.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err1 := fmt.Errorf("read %s: is a directory", filepath.Join(dir1, "empty-dir"))
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Below concatenation is done to get rid of goline error
|
||||||
|
// "error strings should not be capitalized or end with punctuation or a newline"
|
||||||
|
err1 = fmt.Errorf("read %s:"+" The handle is invalid.", filepath.Join(dir1, "empty-dir"))
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
certCAsDir string
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{"nonexistent-dir", nonexistentErr},
|
||||||
|
{dir1, err1},
|
||||||
|
{emptydir, nil},
|
||||||
|
{dir2, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
_, err := getRootCAs(testCase.certCAsDir)
|
||||||
|
|
||||||
|
if testCase.expectedErr == nil {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||||
|
}
|
||||||
|
} else if err == nil {
|
||||||
|
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||||
|
} else if testCase.expectedErr.Error() != err.Error() {
|
||||||
|
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
139
cmd/config-dir.go
Normal file
139
cmd/config-dir.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
||||||
|
*
|
||||||
|
* 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
homedir "github.com/minio/go-homedir"
|
||||||
|
"github.com/minio/mc/pkg/console"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Default minio configuration directory where below configuration files/directories are stored.
|
||||||
|
defaultMinioConfigDir = ".minio"
|
||||||
|
|
||||||
|
// Minio configuration file.
|
||||||
|
minioConfigFile = "config.json"
|
||||||
|
|
||||||
|
// Directory contains below files/directories for HTTPS configuration.
|
||||||
|
certsDir = "certs"
|
||||||
|
|
||||||
|
// Directory contains all CA certificates other than system defaults for HTTPS.
|
||||||
|
certsCADir = "CAs"
|
||||||
|
|
||||||
|
// Public certificate file for HTTPS.
|
||||||
|
publicCertFile = "public.crt"
|
||||||
|
|
||||||
|
// Private key file for HTTPS.
|
||||||
|
privateKeyFile = "private.key"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigDir - configuration directory with locking.
|
||||||
|
type ConfigDir struct {
|
||||||
|
sync.Mutex
|
||||||
|
dir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set - saves given directory as configuration directory.
|
||||||
|
func (config *ConfigDir) Set(dir string) {
|
||||||
|
config.Lock()
|
||||||
|
defer config.Unlock()
|
||||||
|
|
||||||
|
config.dir = dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get - returns current configuration directory.
|
||||||
|
func (config *ConfigDir) Get() string {
|
||||||
|
config.Lock()
|
||||||
|
defer config.Unlock()
|
||||||
|
|
||||||
|
return config.dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *ConfigDir) getCertsDir() string {
|
||||||
|
return filepath.Join(config.Get(), certsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCADir - returns certificate CA directory.
|
||||||
|
func (config *ConfigDir) GetCADir() string {
|
||||||
|
return filepath.Join(config.getCertsDir(), certsCADir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create - creates configuration directory tree.
|
||||||
|
func (config *ConfigDir) Create() error {
|
||||||
|
return mkdirAll(config.GetCADir(), 0700)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMinioConfigFile - returns absolute path of config.json file.
|
||||||
|
func (config *ConfigDir) GetMinioConfigFile() string {
|
||||||
|
return filepath.Join(config.Get(), minioConfigFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPublicCertFile - returns absolute path of public.crt file.
|
||||||
|
func (config *ConfigDir) GetPublicCertFile() string {
|
||||||
|
return filepath.Join(config.getCertsDir(), publicCertFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrivateKeyFile - returns absolute path of private.key file.
|
||||||
|
func (config *ConfigDir) GetPrivateKeyFile() string {
|
||||||
|
return filepath.Join(config.getCertsDir(), privateKeyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetDefaultConfigDir() string {
|
||||||
|
homeDir, err := homedir.Dir()
|
||||||
|
if err != nil {
|
||||||
|
console.Fatalln("Unable to get home directory.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(homeDir, defaultMinioConfigDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
var configDir = &ConfigDir{dir: mustGetDefaultConfigDir()}
|
||||||
|
|
||||||
|
func setConfigDir(dir string) {
|
||||||
|
configDir.Set(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigDir() string {
|
||||||
|
return configDir.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCADir() string {
|
||||||
|
return configDir.GetCADir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createConfigDir() error {
|
||||||
|
return configDir.Create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigFile() string {
|
||||||
|
return configDir.GetMinioConfigFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPublicCertFile() string {
|
||||||
|
return configDir.GetPublicCertFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPrivateKeyFile() string {
|
||||||
|
return configDir.GetPrivateKeyFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isConfigFileExists() bool {
|
||||||
|
return isFile(getConfigFile())
|
||||||
|
}
|
@ -66,7 +66,7 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
|||||||
defer removeAll(rootPath)
|
defer removeAll(rootPath)
|
||||||
|
|
||||||
setConfigDir(rootPath)
|
setConfigDir(rootPath)
|
||||||
configPath := rootPath + "/" + globalMinioConfigFile
|
configPath := rootPath + "/" + minioConfigFile
|
||||||
|
|
||||||
// Remove config file
|
// Remove config file
|
||||||
if err := os.Remove(configPath); err != nil {
|
if err := os.Remove(configPath); err != nil {
|
||||||
@ -121,7 +121,7 @@ func TestServerConfigMigrateV2toV14(t *testing.T) {
|
|||||||
defer removeAll(rootPath)
|
defer removeAll(rootPath)
|
||||||
|
|
||||||
setConfigDir(rootPath)
|
setConfigDir(rootPath)
|
||||||
configPath := rootPath + "/" + globalMinioConfigFile
|
configPath := rootPath + "/" + minioConfigFile
|
||||||
|
|
||||||
// Create a corrupted config file
|
// Create a corrupted config file
|
||||||
if err := ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\","), 0644); err != nil {
|
if err := ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\","), 0644); err != nil {
|
||||||
@ -175,7 +175,7 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
|||||||
defer removeAll(rootPath)
|
defer removeAll(rootPath)
|
||||||
|
|
||||||
setConfigDir(rootPath)
|
setConfigDir(rootPath)
|
||||||
configPath := rootPath + "/" + globalMinioConfigFile
|
configPath := rootPath + "/" + minioConfigFile
|
||||||
|
|
||||||
// Create a corrupted config file
|
// Create a corrupted config file
|
||||||
if err := ioutil.WriteFile(configPath, []byte("{ \"version\":\""), 0644); err != nil {
|
if err := ioutil.WriteFile(configPath, []byte("{ \"version\":\""), 0644); err != nil {
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
|
||||||
*
|
|
||||||
* 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 cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
homedir "github.com/minio/go-homedir"
|
|
||||||
"github.com/minio/mc/pkg/console"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConfigDir - configuration directory with locking.
|
|
||||||
type ConfigDir struct {
|
|
||||||
sync.Mutex
|
|
||||||
dir string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set - saves given directory as configuration directory.
|
|
||||||
func (config *ConfigDir) Set(dir string) {
|
|
||||||
config.Lock()
|
|
||||||
defer config.Unlock()
|
|
||||||
|
|
||||||
config.dir = dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get - returns current configuration directory.
|
|
||||||
func (config *ConfigDir) Get() string {
|
|
||||||
config.Lock()
|
|
||||||
defer config.Unlock()
|
|
||||||
|
|
||||||
return config.dir
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustGetDefaultConfigDir() string {
|
|
||||||
homeDir, err := homedir.Dir()
|
|
||||||
if err != nil {
|
|
||||||
console.Fatalln("Unable to get home directory.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(homeDir, globalMinioConfigDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
var configDir = &ConfigDir{dir: mustGetDefaultConfigDir()}
|
|
||||||
|
|
||||||
func setConfigDir(dir string) {
|
|
||||||
configDir.Set(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfigDir() string {
|
|
||||||
return configDir.Get()
|
|
||||||
}
|
|
||||||
|
|
||||||
func createConfigDir() error {
|
|
||||||
return mkdirAll(getConfigDir(), 0700)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfigFile() string {
|
|
||||||
return filepath.Join(getConfigDir(), globalMinioConfigFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isConfigFileExists() bool {
|
|
||||||
return isFile(getConfigFile())
|
|
||||||
}
|
|
@ -28,12 +28,6 @@ import (
|
|||||||
|
|
||||||
// minio configuration related constants.
|
// minio configuration related constants.
|
||||||
const (
|
const (
|
||||||
globalMinioConfigDir = ".minio"
|
|
||||||
globalMinioCertsDir = "certs"
|
|
||||||
globalMinioCertsCADir = "CAs"
|
|
||||||
globalMinioCertFile = "public.crt"
|
|
||||||
globalMinioKeyFile = "private.key"
|
|
||||||
globalMinioConfigFile = "config.json"
|
|
||||||
globalMinioCertExpireWarnDays = time.Hour * 24 * 30 // 30 days.
|
globalMinioCertExpireWarnDays = time.Hour * 24 * 30 // 30 days.
|
||||||
|
|
||||||
globalMinioDefaultRegion = "us-east-1"
|
globalMinioDefaultRegion = "us-east-1"
|
||||||
|
@ -144,6 +144,9 @@ func initConfig() {
|
|||||||
|
|
||||||
// Generic Minio initialization to create/load config, prepare loggers, etc..
|
// Generic Minio initialization to create/load config, prepare loggers, etc..
|
||||||
func minioInit(ctx *cli.Context) {
|
func minioInit(ctx *cli.Context) {
|
||||||
|
// Create certs path.
|
||||||
|
fatalIf(createConfigDir(), "Unable to create \"certs\" directory.")
|
||||||
|
|
||||||
// Is TLS configured?.
|
// Is TLS configured?.
|
||||||
globalIsSSL = isSSL()
|
globalIsSSL = isSSL()
|
||||||
|
|
||||||
@ -155,7 +158,6 @@ func minioInit(ctx *cli.Context) {
|
|||||||
|
|
||||||
// Init the error tracing module.
|
// Init the error tracing module.
|
||||||
initError()
|
initError()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverCmdConfig struct {
|
type serverCmdConfig struct {
|
||||||
@ -208,11 +210,8 @@ func initServerConfig(c *cli.Context) {
|
|||||||
// Initialization such as config generating/loading config, enable logging, ..
|
// Initialization such as config generating/loading config, enable logging, ..
|
||||||
minioInit(c)
|
minioInit(c)
|
||||||
|
|
||||||
// Create certs path.
|
|
||||||
fatalIf(createCertsPath(), "Unable to create \"certs\" directory.")
|
|
||||||
|
|
||||||
// Load user supplied root CAs
|
// Load user supplied root CAs
|
||||||
loadRootCAs()
|
fatalIf(loadRootCAs(), "Unable to load a CA files")
|
||||||
|
|
||||||
// Set system resources to maximum.
|
// Set system resources to maximum.
|
||||||
errorIf(setMaxResources(), "Unable to change resource limit")
|
errorIf(setMaxResources(), "Unable to change resource limit")
|
||||||
@ -540,7 +539,7 @@ func serverMain(c *cli.Context) {
|
|||||||
go func() {
|
go func() {
|
||||||
cert, key := "", ""
|
cert, key := "", ""
|
||||||
if globalIsSSL {
|
if globalIsSSL {
|
||||||
cert, key = getCertFile(), getKeyFile()
|
cert, key = getPublicCertFile(), getPrivateKeyFile()
|
||||||
}
|
}
|
||||||
fatalIf(apiServer.ListenAndServe(cert, key), "Failed to start minio server.")
|
fatalIf(apiServer.ListenAndServe(cert, key), "Failed to start minio server.")
|
||||||
}()
|
}()
|
||||||
|
@ -354,12 +354,12 @@ func TestServerListenAndServeTLS(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Create a cert
|
// Create a cert
|
||||||
err := createCertsPath()
|
err := createConfigDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
certFile := getCertFile()
|
certFile := getPublicCertFile()
|
||||||
keyFile := getKeyFile()
|
keyFile := getPrivateKeyFile()
|
||||||
defer os.RemoveAll(certFile)
|
defer os.RemoveAll(certFile)
|
||||||
defer os.RemoveAll(keyFile)
|
defer os.RemoveAll(keyFile)
|
||||||
|
|
||||||
@ -420,8 +420,8 @@ func TestServerListenAndServeTLS(t *testing.T) {
|
|||||||
|
|
||||||
// generateTestCert creates a cert and a key used for testing only
|
// generateTestCert creates a cert and a key used for testing only
|
||||||
func generateTestCert(host string) error {
|
func generateTestCert(host string) error {
|
||||||
certPath := getCertFile()
|
certPath := getPublicCertFile()
|
||||||
keyPath := getKeyFile()
|
keyPath := getPrivateKeyFile()
|
||||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
Loading…
Reference in New Issue
Block a user