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