github.com/sykesm/fabric@v1.1.0-preview.0.20200129034918-2aa12b1a0181/core/ledger/util/couchdb/couchdbutil.go (about)

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