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 }