github.com/osdi23p228/fabric@v0.0.0-20221218062954-77808885f5db/core/ledger/kvledger/txmgmt/statedb/statecouchdb/couchdbutil.go (about) 1 /* 2 Copyright IBM Corp. 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/osdi23p228/fabric/common/metrics" 21 "github.com/osdi23p228/fabric/common/metrics/disabled" 22 "github.com/osdi23p228/fabric/common/util" 23 "github.com/osdi23p228/fabric/core/ledger" 24 "github.com/pkg/errors" 25 ) 26 27 var expectedDatabaseNamePattern = `[a-z][a-z0-9.$_()+-]*` 28 var maxLength = 238 29 30 // To restrict the length of couchDB database name to the 31 // allowed length of 249 chars, the string length limit 32 // for chain/channel name, namespace/chaincode name, and 33 // collection name, which constitutes the database name, 34 // is defined. 35 var chainNameAllowedLength = 50 36 var namespaceNameAllowedLength = 50 37 var collectionNameAllowedLength = 50 38 39 func createCouchInstance(config *ledger.CouchDBConfig, metricsProvider metrics.Provider) (*couchInstance, error) { 40 // make sure the address is valid 41 connectURL := &url.URL{ 42 Host: config.Address, 43 Scheme: "http", 44 } 45 _, err := url.Parse(connectURL.String()) 46 if err != nil { 47 return nil, errors.WithMessagef( 48 err, 49 "failed to parse CouchDB address '%s'", 50 config.Address, 51 ) 52 } 53 54 // Create the http client once 55 // Clients and Transports are safe for concurrent use by multiple goroutines 56 // and for efficiency should only be created once and re-used. 57 client := &http.Client{Timeout: config.RequestTimeout} 58 59 transport := &http.Transport{ 60 Proxy: http.ProxyFromEnvironment, 61 DialContext: (&net.Dialer{ 62 Timeout: 5 * time.Second, 63 KeepAlive: 30 * time.Second, 64 DualStack: true, 65 }).DialContext, 66 ForceAttemptHTTP2: true, 67 MaxIdleConns: 2000, 68 MaxIdleConnsPerHost: 2000, 69 IdleConnTimeout: 90 * time.Second, 70 TLSHandshakeTimeout: 10 * time.Second, 71 ExpectContinueTimeout: 1 * time.Second, 72 } 73 74 client.Transport = transport 75 76 //Create the CouchDB instance 77 couchInstance := &couchInstance{ 78 conf: config, 79 client: client, 80 stats: newStats(metricsProvider), 81 } 82 connectInfo, retVal, verifyErr := couchInstance.verifyCouchConfig() 83 if verifyErr != nil { 84 return nil, verifyErr 85 } 86 87 //return an error if the http return value is not 200 88 if retVal.StatusCode != 200 { 89 return nil, errors.Errorf("CouchDB connection error, expecting return code of 200, received %v", retVal.StatusCode) 90 } 91 92 //check the CouchDB version number, return an error if the version is not at least 2.0.0 93 errVersion := checkCouchDBVersion(connectInfo.Version) 94 if errVersion != nil { 95 return nil, errVersion 96 } 97 98 return couchInstance, nil 99 } 100 101 func checkCouchDBVersion(version string) error { 102 couchVersion := strings.Split(version, ".") 103 majorVersion, _ := strconv.Atoi(couchVersion[0]) 104 minorVersion, _ := strconv.Atoi(couchVersion[1]) 105 if majorVersion < 2 { 106 return errors.Errorf("CouchDB v%s detected. CouchDB must be at least version 2.0.0", version) 107 } 108 if majorVersion != 3 || minorVersion < 1 { 109 couchdbLogger.Warnf("CouchDB v%s detected. CouchDB versions before 3.1.0 are unsupported.", version) 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 couchdbLogger.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 couchdbLogger.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 couchdbLogger.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 couchdbLogger.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 couchdbLogger.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 // Note: 185 // Currently there is a non-deterministic collision between metadataDB and namespaceDB with namespace="". 186 // When channel name is not truncated, metadataDB and namespaceDB with namespace="" have the same db name. 187 // When channel name is truncated, these two DBs have different db names. 188 // We have to deal with this behavior for now. In the future, we may rename metadataDB and 189 // migrate the content to avoid the collision. 190 func constructMetadataDBName(dbName string) string { 191 if len(dbName) > maxLength { 192 untruncatedDBName := dbName 193 // Truncate the name if the length violates the allowed limit 194 // As the passed dbName is same as chain/channel name, truncate using chainNameAllowedLength 195 dbName = dbName[:chainNameAllowedLength] 196 // For metadataDB (i.e., chain/channel DB), the dbName contains <first 50 chars 197 // (i.e., chainNameAllowedLength) of chainName> + (SHA256 hash of actual chainName) 198 dbName = dbName + "(" + hex.EncodeToString(util.ComputeSHA256([]byte(untruncatedDBName))) + ")" 199 // 50 chars for dbName + 1 char for ( + 64 chars for sha256 + 1 char for ) = 116 chars 200 } 201 return dbName + "_" 202 } 203 204 // constructNamespaceDBName truncates db name to couchdb allowed length to construct the final namespaceDBName 205 // The passed namespace will be in one of the following formats: 206 // <chaincode> - for namespaces containing regular public data 207 // <chaincode>$$p<collection> - for namespaces containing private data collections 208 // <chaincode>$$h<collection> - for namespaces containing hashes of private data collections 209 func constructNamespaceDBName(chainName, namespace string) string { 210 // replace upper-case in namespace with a escape sequence '$' and the respective lower-case letter 211 escapedNamespace := escapeUpperCase(namespace) 212 namespaceDBName := chainName + "_" + escapedNamespace 213 214 // For namespaceDBName of form 'chainName_namespace', 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) chars of namespace> + 217 // (<SHA256 hash of [chainName_namespace]>) 218 // 219 // For namespaceDBName of form 'chainName_namespace$$[hp]collection', on length limit violation, the truncated 220 // namespaceDBName would contain <first 50 chars (i.e., chainNameAllowedLength) of chainName> + "_" + 221 // <first 50 chars (i.e., namespaceNameAllowedLength) of namespace> + "$$" + <first 50 chars 222 // (i.e., collectionNameAllowedLength) of [hp]collection> + (<SHA256 hash of [chainName_namespace$$[hp]collection]>) 223 224 if len(namespaceDBName) > maxLength { 225 // Compute the hash of untruncated namespaceDBName that needs to be appended to 226 // truncated namespaceDBName for maintaining uniqueness 227 hashOfNamespaceDBName := hex.EncodeToString(util.ComputeSHA256([]byte(chainName + "_" + namespace))) 228 229 // As truncated namespaceDBName is of form 'chainName_escapedNamespace', both chainName 230 // and escapedNamespace need to be truncated to defined allowed length. 231 if len(chainName) > chainNameAllowedLength { 232 // Truncate chainName to chainNameAllowedLength 233 chainName = chainName[0:chainNameAllowedLength] 234 } 235 // As escapedNamespace can be of either 'namespace' or 'namespace$$collectionName', 236 // both 'namespace' and 'collectionName' need to be truncated to defined allowed length. 237 // '$$' is used as joiner between namespace and collection name. 238 // Split the escapedNamespace into escaped namespace and escaped collection name if exist. 239 names := strings.Split(escapedNamespace, "$$") 240 namespace := names[0] 241 if len(namespace) > namespaceNameAllowedLength { 242 // Truncate the namespace 243 namespace = namespace[0:namespaceNameAllowedLength] 244 } 245 246 escapedNamespace = namespace 247 248 // Check and truncate the length of collection name if exist 249 if len(names) == 2 { 250 collection := names[1] 251 if len(collection) > collectionNameAllowedLength { 252 // Truncate the escaped collection name 253 collection = collection[0:collectionNameAllowedLength] 254 } 255 // Append truncated collection name to escapedNamespace 256 escapedNamespace = escapedNamespace + "$$" + collection 257 } 258 // Construct and return the namespaceDBName 259 // 50 chars for chainName + 1 char for '_' + 102 chars for escaped namespace + 1 char for '(' + 64 chars 260 // for sha256 hash + 1 char for ')' = 219 chars 261 return chainName + "_" + escapedNamespace + "(" + hashOfNamespaceDBName + ")" 262 } 263 return namespaceDBName 264 } 265 266 //mapAndValidateDatabaseName checks to see if the database name contains illegal characters 267 //CouchDB Rules: Only lowercase characters (a-z), digits (0-9), and any of the characters 268 //_, $, (, ), +, -, and / are allowed. Must begin with a letter. 269 // 270 //Restrictions have already been applied to the database name from Orderer based on 271 //restrictions required by Kafka and couchDB (except a '.' char). The databaseName 272 // passed in here is expected to follow `[a-z][a-z0-9.$_()+-]*` pattern. 273 // 274 //This validation will simply check whether the database name matches the above pattern and will replace 275 // all occurrence of '.' by '$'. This will not cause collisions in the transformed named 276 func mapAndValidateDatabaseName(databaseName string) (string, error) { 277 // test Length 278 if len(databaseName) <= 0 { 279 return "", errors.Errorf("database name is illegal, cannot be empty") 280 } 281 if len(databaseName) > maxLength { 282 return "", errors.Errorf("database name is illegal, cannot be longer than %d", maxLength) 283 } 284 re, err := regexp.Compile(expectedDatabaseNamePattern) 285 if err != nil { 286 return "", errors.Wrapf(err, "error compiling regexp: %s", expectedDatabaseNamePattern) 287 } 288 matched := re.FindString(databaseName) 289 if len(matched) != len(databaseName) { 290 return "", errors.Errorf("databaseName '%s' does not match pattern '%s'", databaseName, expectedDatabaseNamePattern) 291 } 292 // replace all '.' to '$'. The databaseName passed in will never contain an '$'. 293 // So, this translation will not cause collisions 294 databaseName = strings.Replace(databaseName, ".", "$", -1) 295 return databaseName, nil 296 } 297 298 // escapeUpperCase replaces every upper case letter with a '$' and the respective 299 // lower-case letter 300 func escapeUpperCase(dbName string) string { 301 re := regexp.MustCompile(`([A-Z])`) 302 dbName = re.ReplaceAllString(dbName, "$$"+"$1") 303 return strings.ToLower(dbName) 304 } 305 306 // DropApplicationDBs drops all application databases. 307 func DropApplicationDBs(config *ledger.CouchDBConfig) error { 308 couchdbLogger.Info("Dropping CouchDB application databases ...") 309 couchInstance, err := createCouchInstance(config, &disabled.Provider{}) 310 if err != nil { 311 return err 312 } 313 dbNames, err := couchInstance.retrieveApplicationDBNames() 314 if err != nil { 315 return err 316 } 317 for _, dbName := range dbNames { 318 if _, err = dropDB(couchInstance, dbName); err != nil { 319 couchdbLogger.Errorf("Error dropping CouchDB database %s", dbName) 320 return err 321 } 322 } 323 return nil 324 } 325 326 func dropDB(couchInstance *couchInstance, dbName string) (*dbOperationResponse, error) { 327 db := &couchDatabase{ 328 couchInstance: couchInstance, 329 dbName: dbName, 330 } 331 return db.dropDatabase() 332 }