github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/core/ledger/kvledger/txmgmt/statedb/statecouchdb/couchdbutil.go (about)

     1  /*
     2  Copyright hechain. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package statecouchdb
     8  
     9  import (
    10  	"bytes"
    11  	"encoding/hex"
    12  	"net"
    13  	"net/http"
    14  	"net/url"
    15  	"regexp"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/hechain20/hechain/common/metrics"
    21  	"github.com/hechain20/hechain/common/metrics/disabled"
    22  	"github.com/hechain20/hechain/common/util"
    23  	"github.com/hechain20/hechain/core/ledger"
    24  	"github.com/pkg/errors"
    25  )
    26  
    27  var (
    28  	expectedDatabaseNamePattern = `[a-z][a-z0-9.$_()+-]*`
    29  	maxLength                   = 238
    30  )
    31  
    32  // To restrict the length of couchDB database name to the
    33  // allowed length of 249 chars, the string length limit
    34  // for chain/channel name, namespace/chaincode name, and
    35  // collection name, which constitutes the database name,
    36  // is defined.
    37  var (
    38  	chainNameAllowedLength      = 50
    39  	namespaceNameAllowedLength  = 50
    40  	collectionNameAllowedLength = 50
    41  	disableKeepAlive            bool
    42  )
    43  
    44  func createCouchInstance(config *ledger.CouchDBConfig, metricsProvider metrics.Provider) (*couchInstance, error) {
    45  	// make sure the address is valid
    46  	connectURL := &url.URL{
    47  		Host:   config.Address,
    48  		Scheme: "http",
    49  	}
    50  	_, err := url.Parse(connectURL.String())
    51  	if err != nil {
    52  		return nil, errors.WithMessagef(
    53  			err,
    54  			"failed to parse CouchDB address '%s'",
    55  			config.Address,
    56  		)
    57  	}
    58  
    59  	// Create the http client once
    60  	// Clients and Transports are safe for concurrent use by multiple goroutines
    61  	// and for efficiency should only be created once and re-used.
    62  	client := &http.Client{Timeout: config.RequestTimeout}
    63  
    64  	transport := &http.Transport{
    65  		Proxy: http.ProxyFromEnvironment,
    66  		DialContext: (&net.Dialer{
    67  			Timeout:   5 * time.Second,
    68  			KeepAlive: 30 * time.Second,
    69  			DualStack: true,
    70  		}).DialContext,
    71  		ForceAttemptHTTP2:     true,
    72  		MaxIdleConns:          2000,
    73  		MaxIdleConnsPerHost:   2000,
    74  		IdleConnTimeout:       90 * time.Second,
    75  		TLSHandshakeTimeout:   10 * time.Second,
    76  		ExpectContinueTimeout: 1 * time.Second,
    77  		DisableKeepAlives:     disableKeepAlive,
    78  	}
    79  
    80  	client.Transport = transport
    81  
    82  	// Create the CouchDB instance
    83  	couchInstance := &couchInstance{
    84  		conf:   config,
    85  		client: client,
    86  		stats:  newStats(metricsProvider),
    87  	}
    88  	connectInfo, retVal, verifyErr := couchInstance.verifyCouchConfig()
    89  	if verifyErr != nil {
    90  		return nil, verifyErr
    91  	}
    92  
    93  	// return an error if the http return value is not 200
    94  	if retVal.StatusCode != 200 {
    95  		return nil, errors.Errorf("CouchDB connection error, expecting return code of 200, received %v", retVal.StatusCode)
    96  	}
    97  
    98  	// check the CouchDB version number, return an error if the version is not at least 2.0.0
    99  	errVersion := checkCouchDBVersion(connectInfo.Version)
   100  	if errVersion != nil {
   101  		return nil, errVersion
   102  	}
   103  
   104  	return couchInstance, nil
   105  }
   106  
   107  func checkCouchDBVersion(version string) error {
   108  	couchVersion := strings.Split(version, ".")
   109  	majorVersion, _ := strconv.Atoi(couchVersion[0])
   110  	minorVersion, _ := strconv.Atoi(couchVersion[1])
   111  	if majorVersion < 2 {
   112  		return errors.Errorf("CouchDB v%s detected. CouchDB must be at least version 2.0.0", version)
   113  	}
   114  	if majorVersion != 3 || minorVersion < 1 {
   115  		couchdbLogger.Warnf("CouchDB v%s detected. CouchDB versions before 3.1.0 are unsupported.", version)
   116  	}
   117  	return nil
   118  }
   119  
   120  // createCouchDatabase creates a CouchDB database object, as well as the underlying database if it does not exist
   121  func createCouchDatabase(couchInstance *couchInstance, dbName string) (*couchDatabase, error) {
   122  	databaseName, err := mapAndValidateDatabaseName(dbName)
   123  	if err != nil {
   124  		couchdbLogger.Errorf("Error calling CouchDB CreateDatabaseIfNotExist() for dbName: %s, error: %s", dbName, err)
   125  		return nil, err
   126  	}
   127  
   128  	couchDBDatabase := couchDatabase{couchInstance: couchInstance, dbName: databaseName}
   129  
   130  	// Create CouchDB database upon ledger startup, if it doesn't already exist
   131  	err = couchDBDatabase.createDatabaseIfNotExist()
   132  	if err != nil {
   133  		couchdbLogger.Errorf("Error calling CouchDB CreateDatabaseIfNotExist() for dbName: %s, error: %s", dbName, err)
   134  		return nil, err
   135  	}
   136  
   137  	return &couchDBDatabase, nil
   138  }
   139  
   140  // createSystemDatabasesIfNotExist - creates the system databases if they do not exist
   141  func createSystemDatabasesIfNotExist(couchInstance *couchInstance) error {
   142  	dbName := "_users"
   143  	systemCouchDBDatabase := couchDatabase{couchInstance: couchInstance, dbName: dbName}
   144  	err := systemCouchDBDatabase.createDatabaseIfNotExist()
   145  	if err != nil {
   146  		couchdbLogger.Errorf("Error calling CouchDB createDatabaseIfNotExist() for system dbName: %s, error: %s", dbName, err)
   147  		return err
   148  	}
   149  
   150  	dbName = "_replicator"
   151  	systemCouchDBDatabase = couchDatabase{couchInstance: couchInstance, dbName: dbName}
   152  	err = systemCouchDBDatabase.createDatabaseIfNotExist()
   153  	if err != nil {
   154  		couchdbLogger.Errorf("Error calling CouchDB createDatabaseIfNotExist() for system dbName: %s, error: %s", dbName, err)
   155  		return err
   156  	}
   157  	if couchInstance.conf.CreateGlobalChangesDB {
   158  		dbName = "_global_changes"
   159  		systemCouchDBDatabase = couchDatabase{couchInstance: couchInstance, dbName: dbName}
   160  		err = systemCouchDBDatabase.createDatabaseIfNotExist()
   161  		if err != nil {
   162  			couchdbLogger.Errorf("Error calling CouchDB createDatabaseIfNotExist() for system dbName: %s, error: %s", dbName, err)
   163  			return err
   164  		}
   165  	}
   166  	return nil
   167  }
   168  
   169  // constructCouchDBUrl constructs a couchDB url with encoding for the database name
   170  // and all path elements
   171  func constructCouchDBUrl(connectURL *url.URL, dbName string, pathElements ...string) *url.URL {
   172  	var buffer bytes.Buffer
   173  	buffer.WriteString(connectURL.String())
   174  	if dbName != "" {
   175  		buffer.WriteString("/")
   176  		buffer.WriteString(encodePathElement(dbName))
   177  	}
   178  	for _, pathElement := range pathElements {
   179  		buffer.WriteString("/")
   180  		buffer.WriteString(encodePathElement(pathElement))
   181  	}
   182  	return &url.URL{Opaque: buffer.String()}
   183  }
   184  
   185  // constructMetadataDBName truncates the db name to couchdb allowed length to
   186  // construct the metadataDBName
   187  // Note:
   188  // Currently there is a non-deterministic collision between metadataDB and namespaceDB with namespace="".
   189  // When channel name is not truncated, metadataDB and namespaceDB with namespace="" have the same db name.
   190  // When channel name is truncated, these two DBs have different db names.
   191  // We have to deal with this behavior for now. In the future, we may rename metadataDB and
   192  // migrate the content to avoid the collision.
   193  func constructMetadataDBName(dbName string) string {
   194  	if len(dbName) > maxLength {
   195  		untruncatedDBName := dbName
   196  		// Truncate the name if the length violates the allowed limit
   197  		// As the passed dbName is same as chain/channel name, truncate using chainNameAllowedLength
   198  		dbName = dbName[:chainNameAllowedLength]
   199  		// For metadataDB (i.e., chain/channel DB), the dbName contains <first 50 chars
   200  		// (i.e., chainNameAllowedLength) of chainName> + (SHA256 hash of actual chainName)
   201  		dbName = dbName + "(" + hex.EncodeToString(util.ComputeSHA256([]byte(untruncatedDBName))) + ")"
   202  		// 50 chars for dbName + 1 char for ( + 64 chars for sha256 + 1 char for ) = 116 chars
   203  	}
   204  	return dbName + "_"
   205  }
   206  
   207  // constructNamespaceDBName truncates db name to couchdb allowed length to construct the final namespaceDBName
   208  // The passed namespace will be in one of the following formats:
   209  // <chaincode>                 - for namespaces containing regular public data
   210  // <chaincode>$$p<collection>  - for namespaces containing private data collections
   211  // <chaincode>$$h<collection>  - for namespaces containing hashes of private data collections
   212  func constructNamespaceDBName(chainName, namespace string) string {
   213  	// replace upper-case in namespace with a escape sequence '$' and the respective lower-case letter
   214  	escapedNamespace := escapeUpperCase(namespace)
   215  	namespaceDBName := chainName + "_" + escapedNamespace
   216  
   217  	// For namespaceDBName of form 'chainName_namespace', on length limit violation, the truncated
   218  	// namespaceDBName would contain <first 50 chars (i.e., chainNameAllowedLength) of chainName> + "_" +
   219  	// <first 50 chars (i.e., namespaceNameAllowedLength) chars of namespace> +
   220  	// (<SHA256 hash of [chainName_namespace]>)
   221  	//
   222  	// For namespaceDBName of form 'chainName_namespace$$[hp]collection', on length limit violation, the truncated
   223  	// namespaceDBName would contain <first 50 chars (i.e., chainNameAllowedLength) of chainName> + "_" +
   224  	// <first 50 chars (i.e., namespaceNameAllowedLength) of namespace> + "$$" + <first 50 chars
   225  	// (i.e., collectionNameAllowedLength) of [hp]collection> + (<SHA256 hash of [chainName_namespace$$[hp]collection]>)
   226  
   227  	if len(namespaceDBName) > maxLength {
   228  		// Compute the hash of untruncated namespaceDBName that needs to be appended to
   229  		// truncated namespaceDBName for maintaining uniqueness
   230  		hashOfNamespaceDBName := hex.EncodeToString(util.ComputeSHA256([]byte(chainName + "_" + namespace)))
   231  
   232  		// As truncated namespaceDBName is of form 'chainName_escapedNamespace', both chainName
   233  		// and escapedNamespace need to be truncated to defined allowed length.
   234  		if len(chainName) > chainNameAllowedLength {
   235  			// Truncate chainName to chainNameAllowedLength
   236  			chainName = chainName[0:chainNameAllowedLength]
   237  		}
   238  		// As escapedNamespace can be of either 'namespace' or 'namespace$$collectionName',
   239  		// both 'namespace' and 'collectionName' need to be truncated to defined allowed length.
   240  		// '$$' is used as joiner between namespace and collection name.
   241  		// Split the escapedNamespace into escaped namespace and escaped collection name if exist.
   242  		names := strings.Split(escapedNamespace, "$$")
   243  		namespace := names[0]
   244  		if len(namespace) > namespaceNameAllowedLength {
   245  			// Truncate the namespace
   246  			namespace = namespace[0:namespaceNameAllowedLength]
   247  		}
   248  
   249  		escapedNamespace = namespace
   250  
   251  		// Check and truncate the length of collection name if exist
   252  		if len(names) == 2 {
   253  			collection := names[1]
   254  			if len(collection) > collectionNameAllowedLength {
   255  				// Truncate the escaped collection name
   256  				collection = collection[0:collectionNameAllowedLength]
   257  			}
   258  			// Append truncated collection name to escapedNamespace
   259  			escapedNamespace = escapedNamespace + "$$" + collection
   260  		}
   261  		// Construct and return the namespaceDBName
   262  		// 50 chars for chainName + 1 char for '_' + 102 chars for escaped namespace + 1 char for '(' + 64 chars
   263  		// for sha256 hash + 1 char for ')' = 219 chars
   264  		return chainName + "_" + escapedNamespace + "(" + hashOfNamespaceDBName + ")"
   265  	}
   266  	return namespaceDBName
   267  }
   268  
   269  //mapAndValidateDatabaseName checks to see if the database name contains illegal characters
   270  //CouchDB Rules: Only lowercase characters (a-z), digits (0-9), and any of the characters
   271  //_, $, (, ), +, -, and / are allowed. Must begin with a letter.
   272  //
   273  //Restrictions have already been applied to the database name from Orderer based on
   274  //restrictions required by Kafka and couchDB (except a '.' char). The databaseName
   275  // passed in here is expected to follow `[a-z][a-z0-9.$_()+-]*` pattern.
   276  //
   277  //This validation will simply check whether the database name matches the above pattern and will replace
   278  // all occurrence of '.' by '$'. This will not cause collisions in the transformed named
   279  func mapAndValidateDatabaseName(databaseName string) (string, error) {
   280  	// test Length
   281  	if len(databaseName) <= 0 {
   282  		return "", errors.Errorf("database name is illegal, cannot be empty")
   283  	}
   284  	if len(databaseName) > maxLength {
   285  		return "", errors.Errorf("database name is illegal, cannot be longer than %d", maxLength)
   286  	}
   287  	re, err := regexp.Compile(expectedDatabaseNamePattern)
   288  	if err != nil {
   289  		return "", errors.Wrapf(err, "error compiling regexp: %s", expectedDatabaseNamePattern)
   290  	}
   291  	matched := re.FindString(databaseName)
   292  	if len(matched) != len(databaseName) {
   293  		return "", errors.Errorf("databaseName '%s' does not match pattern '%s'", databaseName, expectedDatabaseNamePattern)
   294  	}
   295  	// replace all '.' to '$'. The databaseName passed in will never contain an '$'.
   296  	// So, this translation will not cause collisions
   297  	databaseName = strings.Replace(databaseName, ".", "$", -1)
   298  	return databaseName, nil
   299  }
   300  
   301  // escapeUpperCase replaces every upper case letter with a '$' and the respective
   302  // lower-case letter
   303  func escapeUpperCase(dbName string) string {
   304  	re := regexp.MustCompile(`([A-Z])`)
   305  	dbName = re.ReplaceAllString(dbName, "$$"+"$1")
   306  	return strings.ToLower(dbName)
   307  }
   308  
   309  // DropApplicationDBs drops all application databases.
   310  func DropApplicationDBs(config *ledger.CouchDBConfig) error {
   311  	couchdbLogger.Info("Dropping CouchDB application databases ...")
   312  	couchInstance, err := createCouchInstance(config, &disabled.Provider{})
   313  	if err != nil {
   314  		return err
   315  	}
   316  	dbNames, err := couchInstance.retrieveApplicationDBNames()
   317  	if err != nil {
   318  		return err
   319  	}
   320  	for _, dbName := range dbNames {
   321  		if err = dropDB(couchInstance, dbName); err != nil {
   322  			couchdbLogger.Errorf("Error dropping CouchDB database %s", dbName)
   323  			return err
   324  		}
   325  	}
   326  	return nil
   327  }
   328  
   329  func dropDB(couchInstance *couchInstance, dbName string) error {
   330  	db := &couchDatabase{
   331  		couchInstance: couchInstance,
   332  		dbName:        dbName,
   333  	}
   334  	return db.dropDatabase()
   335  }