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 }