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