2021-11-23 09:45:38 +00:00
|
|
|
package storage
|
|
|
|
|
|
|
|
import (
|
2022-10-22 05:41:27 +00:00
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
|
|
|
"errors"
|
2021-11-23 09:45:38 +00:00
|
|
|
"fmt"
|
2022-10-22 05:41:27 +00:00
|
|
|
"os"
|
|
|
|
"path"
|
2021-11-23 09:45:38 +00:00
|
|
|
|
2022-10-22 05:41:27 +00:00
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/jackc/pgx/v5/stdlib"
|
2021-11-23 09:45:38 +00:00
|
|
|
|
|
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
2022-10-22 08:27:59 +00:00
|
|
|
"github.com/authelia/authelia/v4/internal/utils"
|
2021-11-23 09:45:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// PostgreSQLProvider is a PostgreSQL provider.
|
|
|
|
type PostgreSQLProvider struct {
|
|
|
|
SQLProvider
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewPostgreSQLProvider a PostgreSQL provider.
|
2022-10-22 05:41:27 +00:00
|
|
|
func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPool) (provider *PostgreSQLProvider) {
|
2021-11-23 09:45:38 +00:00
|
|
|
provider = &PostgreSQLProvider{
|
2022-10-22 05:41:27 +00:00
|
|
|
SQLProvider: NewSQLProvider(config, providerPostgres, "pgx", dsnPostgreSQL(config.Storage.PostgreSQL, caCertPool)),
|
2021-11-23 09:45:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// All providers have differing SELECT existing table statements.
|
|
|
|
provider.sqlSelectExistingTables = queryPostgreSelectExistingTables
|
|
|
|
|
|
|
|
// Specific alterations to this provider.
|
|
|
|
// PostgreSQL doesn't have a UPSERT statement but has an ON CONFLICT operation instead.
|
2023-04-14 16:04:42 +00:00
|
|
|
provider.sqlUpsertWebAuthnDevice = fmt.Sprintf(queryFmtUpsertWebAuthnDevicePostgreSQL, tableWebAuthnDevices)
|
2022-04-07 05:33:53 +00:00
|
|
|
provider.sqlUpsertDuoDevice = fmt.Sprintf(queryFmtUpsertDuoDevicePostgreSQL, tableDuoDevices)
|
|
|
|
provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtUpsertTOTPConfigurationPostgreSQL, tableTOTPConfigurations)
|
|
|
|
provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtUpsertPreferred2FAMethodPostgreSQL, tableUserPreferences)
|
|
|
|
provider.sqlUpsertEncryptionValue = fmt.Sprintf(queryFmtUpsertEncryptionValuePostgreSQL, tableEncryption)
|
|
|
|
provider.sqlUpsertOAuth2BlacklistedJTI = fmt.Sprintf(queryFmtUpsertOAuth2BlacklistedJTIPostgreSQL, tableOAuth2BlacklistedJTI)
|
2022-10-20 02:16:36 +00:00
|
|
|
provider.sqlInsertOAuth2ConsentPreConfiguration = fmt.Sprintf(queryFmtInsertOAuth2ConsentPreConfigurationPostgreSQL, tableOAuth2ConsentPreConfiguration)
|
2021-11-23 09:45:38 +00:00
|
|
|
|
|
|
|
// PostgreSQL requires rebinding of any query that contains a '?' placeholder to use the '$#' notation placeholders.
|
|
|
|
provider.sqlFmtRenameTable = provider.db.Rebind(provider.sqlFmtRenameTable)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
2021-11-23 09:45:38 +00:00
|
|
|
provider.sqlSelectPreferred2FAMethod = provider.db.Rebind(provider.sqlSelectPreferred2FAMethod)
|
|
|
|
provider.sqlSelectUserInfo = provider.db.Rebind(provider.sqlSelectUserInfo)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
|
|
|
provider.sqlInsertUserOpaqueIdentifier = provider.db.Rebind(provider.sqlInsertUserOpaqueIdentifier)
|
|
|
|
provider.sqlSelectUserOpaqueIdentifier = provider.db.Rebind(provider.sqlSelectUserOpaqueIdentifier)
|
|
|
|
provider.sqlSelectUserOpaqueIdentifierBySignature = provider.db.Rebind(provider.sqlSelectUserOpaqueIdentifierBySignature)
|
|
|
|
|
2021-12-04 04:34:20 +00:00
|
|
|
provider.sqlSelectIdentityVerification = provider.db.Rebind(provider.sqlSelectIdentityVerification)
|
2021-11-23 09:45:38 +00:00
|
|
|
provider.sqlInsertIdentityVerification = provider.db.Rebind(provider.sqlInsertIdentityVerification)
|
2021-12-03 00:04:11 +00:00
|
|
|
provider.sqlConsumeIdentityVerification = provider.db.Rebind(provider.sqlConsumeIdentityVerification)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
2021-11-23 09:45:38 +00:00
|
|
|
provider.sqlSelectTOTPConfig = provider.db.Rebind(provider.sqlSelectTOTPConfig)
|
2022-03-03 11:20:43 +00:00
|
|
|
provider.sqlUpdateTOTPConfigRecordSignIn = provider.db.Rebind(provider.sqlUpdateTOTPConfigRecordSignIn)
|
|
|
|
provider.sqlUpdateTOTPConfigRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigRecordSignInByUsername)
|
2021-11-23 09:45:38 +00:00
|
|
|
provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig)
|
2021-11-25 01:56:58 +00:00
|
|
|
provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
2023-04-14 16:04:42 +00:00
|
|
|
provider.sqlSelectWebAuthnDevices = provider.db.Rebind(provider.sqlSelectWebAuthnDevices)
|
|
|
|
provider.sqlSelectWebAuthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebAuthnDevicesByUsername)
|
|
|
|
provider.sqlUpdateWebAuthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebAuthnDeviceRecordSignIn)
|
|
|
|
provider.sqlUpdateWebAuthnDeviceRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateWebAuthnDeviceRecordSignInByUsername)
|
|
|
|
provider.sqlDeleteWebAuthnDevice = provider.db.Rebind(provider.sqlDeleteWebAuthnDevice)
|
|
|
|
provider.sqlDeleteWebAuthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebAuthnDeviceByUsername)
|
|
|
|
provider.sqlDeleteWebAuthnDeviceByUsernameAndDescription = provider.db.Rebind(provider.sqlDeleteWebAuthnDeviceByUsernameAndDescription)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
2021-12-02 06:06:04 +00:00
|
|
|
provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice)
|
|
|
|
provider.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
2021-11-23 09:45:38 +00:00
|
|
|
provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt)
|
|
|
|
provider.sqlSelectAuthenticationAttemptsByUsername = provider.db.Rebind(provider.sqlSelectAuthenticationAttemptsByUsername)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
2021-11-23 09:45:38 +00:00
|
|
|
provider.sqlInsertMigration = provider.db.Rebind(provider.sqlInsertMigration)
|
2021-12-02 06:06:04 +00:00
|
|
|
provider.sqlSelectMigrations = provider.db.Rebind(provider.sqlSelectMigrations)
|
|
|
|
provider.sqlSelectLatestMigration = provider.db.Rebind(provider.sqlSelectLatestMigration)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
2021-11-25 01:56:58 +00:00
|
|
|
provider.sqlSelectEncryptionValue = provider.db.Rebind(provider.sqlSelectEncryptionValue)
|
2021-11-23 09:45:38 +00:00
|
|
|
|
2022-10-20 02:16:36 +00:00
|
|
|
provider.sqlSelectOAuth2ConsentPreConfigurations = provider.db.Rebind(provider.sqlSelectOAuth2ConsentPreConfigurations)
|
|
|
|
|
2022-04-07 05:33:53 +00:00
|
|
|
provider.sqlInsertOAuth2ConsentSession = provider.db.Rebind(provider.sqlInsertOAuth2ConsentSession)
|
2022-04-25 00:31:05 +00:00
|
|
|
provider.sqlUpdateOAuth2ConsentSessionSubject = provider.db.Rebind(provider.sqlUpdateOAuth2ConsentSessionSubject)
|
2022-04-07 05:33:53 +00:00
|
|
|
provider.sqlUpdateOAuth2ConsentSessionResponse = provider.db.Rebind(provider.sqlUpdateOAuth2ConsentSessionResponse)
|
|
|
|
provider.sqlUpdateOAuth2ConsentSessionGranted = provider.db.Rebind(provider.sqlUpdateOAuth2ConsentSessionGranted)
|
|
|
|
provider.sqlSelectOAuth2ConsentSessionByChallengeID = provider.db.Rebind(provider.sqlSelectOAuth2ConsentSessionByChallengeID)
|
|
|
|
|
2023-03-06 03:58:50 +00:00
|
|
|
provider.sqlInsertOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlInsertOAuth2AccessTokenSession)
|
|
|
|
provider.sqlRevokeOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlRevokeOAuth2AccessTokenSession)
|
|
|
|
provider.sqlRevokeOAuth2AccessTokenSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2AccessTokenSessionByRequestID)
|
|
|
|
provider.sqlDeactivateOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlDeactivateOAuth2AccessTokenSession)
|
|
|
|
provider.sqlDeactivateOAuth2AccessTokenSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2AccessTokenSessionByRequestID)
|
|
|
|
provider.sqlSelectOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlSelectOAuth2AccessTokenSession)
|
|
|
|
|
2022-04-07 05:33:53 +00:00
|
|
|
provider.sqlInsertOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlInsertOAuth2AuthorizeCodeSession)
|
|
|
|
provider.sqlRevokeOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlRevokeOAuth2AuthorizeCodeSession)
|
|
|
|
provider.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID)
|
|
|
|
provider.sqlDeactivateOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlDeactivateOAuth2AuthorizeCodeSession)
|
|
|
|
provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID)
|
|
|
|
provider.sqlSelectOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlSelectOAuth2AuthorizeCodeSession)
|
|
|
|
|
2023-03-06 03:58:50 +00:00
|
|
|
provider.sqlInsertOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlInsertOAuth2OpenIDConnectSession)
|
|
|
|
provider.sqlRevokeOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSession)
|
|
|
|
provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID)
|
|
|
|
provider.sqlDeactivateOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlDeactivateOAuth2OpenIDConnectSession)
|
|
|
|
provider.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID)
|
|
|
|
provider.sqlSelectOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlSelectOAuth2OpenIDConnectSession)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
2023-03-06 03:58:50 +00:00
|
|
|
provider.sqlInsertOAuth2PARContext = provider.db.Rebind(provider.sqlInsertOAuth2PARContext)
|
|
|
|
provider.sqlRevokeOAuth2PARContext = provider.db.Rebind(provider.sqlRevokeOAuth2PARContext)
|
|
|
|
provider.sqlSelectOAuth2PARContext = provider.db.Rebind(provider.sqlSelectOAuth2PARContext)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
|
|
|
provider.sqlInsertOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlInsertOAuth2PKCERequestSession)
|
|
|
|
provider.sqlRevokeOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlRevokeOAuth2PKCERequestSession)
|
|
|
|
provider.sqlRevokeOAuth2PKCERequestSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2PKCERequestSessionByRequestID)
|
|
|
|
provider.sqlDeactivateOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlDeactivateOAuth2PKCERequestSession)
|
|
|
|
provider.sqlDeactivateOAuth2PKCERequestSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2PKCERequestSessionByRequestID)
|
|
|
|
provider.sqlSelectOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlSelectOAuth2PKCERequestSession)
|
|
|
|
|
2023-03-06 03:58:50 +00:00
|
|
|
provider.sqlInsertOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlInsertOAuth2RefreshTokenSession)
|
|
|
|
provider.sqlRevokeOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlRevokeOAuth2RefreshTokenSession)
|
|
|
|
provider.sqlRevokeOAuth2RefreshTokenSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2RefreshTokenSessionByRequestID)
|
|
|
|
provider.sqlDeactivateOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlDeactivateOAuth2RefreshTokenSession)
|
|
|
|
provider.sqlDeactivateOAuth2RefreshTokenSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2RefreshTokenSessionByRequestID)
|
|
|
|
provider.sqlSelectOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlSelectOAuth2RefreshTokenSession)
|
2022-04-07 05:33:53 +00:00
|
|
|
|
|
|
|
provider.sqlSelectOAuth2BlacklistedJTI = provider.db.Rebind(provider.sqlSelectOAuth2BlacklistedJTI)
|
|
|
|
|
2021-12-03 06:29:55 +00:00
|
|
|
provider.schema = config.Storage.PostgreSQL.Schema
|
|
|
|
|
2021-11-23 09:45:38 +00:00
|
|
|
return provider
|
|
|
|
}
|
|
|
|
|
2022-10-22 05:41:27 +00:00
|
|
|
func dsnPostgreSQL(config *schema.PostgreSQLStorageConfiguration, globalCACertPool *x509.CertPool) (dsn string) {
|
|
|
|
dsnConfig, _ := pgx.ParseConfig("")
|
|
|
|
|
2022-10-22 08:27:59 +00:00
|
|
|
dsnConfig.Host = config.Host
|
|
|
|
dsnConfig.Port = uint16(config.Port)
|
|
|
|
dsnConfig.Database = config.Database
|
|
|
|
dsnConfig.User = config.Username
|
|
|
|
dsnConfig.Password = config.Password
|
|
|
|
dsnConfig.TLSConfig = loadPostgreSQLTLSConfig(config, globalCACertPool)
|
|
|
|
dsnConfig.ConnectTimeout = config.Timeout
|
|
|
|
dsnConfig.RuntimeParams = map[string]string{
|
|
|
|
"search_path": config.Schema,
|
|
|
|
}
|
|
|
|
|
|
|
|
if dsnConfig.Port == 0 && !path.IsAbs(dsnConfig.Host) {
|
2022-10-23 19:09:38 +00:00
|
|
|
dsnConfig.Port = 5432
|
2022-10-22 08:27:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return stdlib.RegisterConnConfig(dsnConfig)
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadPostgreSQLTLSConfig(config *schema.PostgreSQLStorageConfiguration, globalCACertPool *x509.CertPool) (tlsConfig *tls.Config) {
|
|
|
|
if config.TLS == nil && config.SSL == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if config.TLS != nil {
|
|
|
|
return utils.NewTLSConfig(config.TLS, globalCACertPool)
|
|
|
|
}
|
|
|
|
|
|
|
|
ca, certs := loadPostgreSQLLegacyTLSConfig(config)
|
2022-10-22 05:41:27 +00:00
|
|
|
|
|
|
|
switch config.SSL.Mode {
|
|
|
|
case "disable":
|
2022-10-22 08:27:59 +00:00
|
|
|
return nil
|
2022-10-22 05:41:27 +00:00
|
|
|
default:
|
|
|
|
var caCertPool *x509.CertPool
|
|
|
|
|
|
|
|
switch ca {
|
|
|
|
case nil:
|
|
|
|
caCertPool = globalCACertPool
|
|
|
|
default:
|
|
|
|
caCertPool = globalCACertPool.Clone()
|
|
|
|
caCertPool.AddCert(ca)
|
|
|
|
}
|
|
|
|
|
2022-10-22 08:27:59 +00:00
|
|
|
tlsConfig = &tls.Config{
|
2022-10-22 05:41:27 +00:00
|
|
|
Certificates: certs,
|
|
|
|
RootCAs: caCertPool,
|
|
|
|
InsecureSkipVerify: true, //nolint:gosec
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case config.SSL.Mode == "require" && config.SSL.RootCertificate != "" || config.SSL.Mode == "verify-ca":
|
2022-10-22 08:27:59 +00:00
|
|
|
tlsConfig.VerifyPeerCertificate = newPostgreSQLVerifyCAFunc(tlsConfig)
|
2022-10-22 05:41:27 +00:00
|
|
|
case config.SSL.Mode == "verify-full":
|
2022-10-22 08:27:59 +00:00
|
|
|
tlsConfig.InsecureSkipVerify = false
|
|
|
|
tlsConfig.ServerName = config.Host
|
2022-10-22 05:41:27 +00:00
|
|
|
}
|
2021-11-23 09:45:38 +00:00
|
|
|
}
|
|
|
|
|
2022-10-22 08:27:59 +00:00
|
|
|
return tlsConfig
|
2022-10-22 05:41:27 +00:00
|
|
|
}
|
|
|
|
|
2022-10-22 08:27:59 +00:00
|
|
|
func loadPostgreSQLLegacyTLSConfig(config *schema.PostgreSQLStorageConfiguration) (ca *x509.Certificate, certs []tls.Certificate) {
|
2022-10-22 05:41:27 +00:00
|
|
|
var (
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
if config.SSL.RootCertificate != "" {
|
|
|
|
var caPEMBlock []byte
|
|
|
|
|
|
|
|
if caPEMBlock, err = os.ReadFile(config.SSL.RootCertificate); err != nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if ca, err = x509.ParseCertificate(caPEMBlock); err != nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2021-12-02 05:36:03 +00:00
|
|
|
}
|
|
|
|
|
2022-10-22 05:41:27 +00:00
|
|
|
if config.SSL.Certificate != "" && config.SSL.Key != "" {
|
|
|
|
var (
|
|
|
|
keyPEMBlock []byte
|
|
|
|
certPEMBlock []byte
|
|
|
|
)
|
|
|
|
|
|
|
|
if keyPEMBlock, err = os.ReadFile(config.SSL.Key); err != nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if certPEMBlock, err = os.ReadFile(config.SSL.Certificate); err != nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var cert tls.Certificate
|
|
|
|
|
|
|
|
if cert, err = tls.X509KeyPair(certPEMBlock, keyPEMBlock); err != nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
certs = []tls.Certificate{cert}
|
2021-11-23 09:45:38 +00:00
|
|
|
}
|
|
|
|
|
2022-10-22 05:41:27 +00:00
|
|
|
return ca, certs
|
|
|
|
}
|
|
|
|
|
|
|
|
func newPostgreSQLVerifyCAFunc(config *tls.Config) func(certificates [][]byte, _ [][]*x509.Certificate) (err error) {
|
|
|
|
return func(certificates [][]byte, _ [][]*x509.Certificate) (err error) {
|
|
|
|
certs := make([]*x509.Certificate, len(certificates))
|
|
|
|
|
|
|
|
var cert *x509.Certificate
|
|
|
|
|
|
|
|
for i, asn1Data := range certificates {
|
|
|
|
if cert, err = x509.ParseCertificate(asn1Data); err != nil {
|
|
|
|
return errors.New("failed to parse certificate from server: " + err.Error())
|
|
|
|
}
|
2021-11-23 09:45:38 +00:00
|
|
|
|
2022-10-22 05:41:27 +00:00
|
|
|
certs[i] = cert
|
|
|
|
}
|
|
|
|
|
|
|
|
// Leave DNSName empty to skip hostname verification.
|
|
|
|
opts := x509.VerifyOptions{
|
|
|
|
Roots: config.RootCAs,
|
|
|
|
Intermediates: x509.NewCertPool(),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Skip the first cert because it's the leaf. All others
|
|
|
|
// are intermediates.
|
|
|
|
for _, cert = range certs[1:] {
|
|
|
|
opts.Intermediates.AddCert(cert)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = certs[0].Verify(opts)
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
2021-11-23 09:45:38 +00:00
|
|
|
}
|