Skip to content

Commit

Permalink
[FAB-9335] add timeout for fabric-ca-client request
Browse files Browse the repository at this point in the history
add two options 'connection-timeout' and 'socket-operation-timeout' for
fabric-ca-client

Change-Id: I99b9a0e87e9ea122f3429085ca0d7c1e43147f54
Signed-off-by: zhaochy <zhaochy_2015@hotmail.com>
  • Loading branch information
zhaochy1990 committed Apr 20, 2018
1 parent 2d06357 commit 2d81c17
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 12 deletions.
5 changes: 3 additions & 2 deletions fabric-ca-client/config/default.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
{
"request-timeout" : 3000,
"tcert-batch-size" : 10,
"crypto-hash-algo": "SHA2",
Expand All @@ -10,5 +10,6 @@
"crypto-suite-hsm": {
"EC": "fabric-ca-client/lib/impl/bccsp_pkcs11.js"
},
"key-value-store": "fabric-ca-client/lib/impl/FileKeyValueStore.js"
"key-value-store": "fabric-ca-client/lib/impl/FileKeyValueStore.js",
"connection-timeout": 3000
}
29 changes: 26 additions & 3 deletions fabric-ca-client/lib/FabricCAClientImpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,15 @@ var FabricCAClient = class {
if (requestObj) {
requestObj.caName = this._caName;
}
// establish socket timeout
// default: 3000ms
const CONNECTION_TIMEOUT = config.get('connection-timeout', 3000);
// SO_TIMEOUT is the timeout that a read() call will block,
// it means that if no data arrives within SO_TIMEOUT,
// socket will throw an error
// default: infinite
const SO_TIMEOUT = config.get('socket-operation-timeout');
logger.debug('CONNECTION_TIMEOUT = %s, SO_TIMEOUT = %s', CONNECTION_TIMEOUT, SO_TIMEOUT? SO_TIMEOUT: 'infinite');

var self = this;
return new Promise(function (resolve, reject) {
Expand All @@ -703,7 +712,8 @@ var FabricCAClient = class {
Authorization: self.generateAuthToken(requestObj, signingIdentity)
},
ca: self._tlsOptions.trustedRoots,
rejectUnauthorized: self._tlsOptions.verify
rejectUnauthorized: self._tlsOptions.verify,
timeout: CONNECTION_TIMEOUT
};

var request = self._httpClient.request(requestOptions, function (response) {
Expand All @@ -714,7 +724,6 @@ var FabricCAClient = class {
});

response.on('end', function () {

var payload = responseBody.join('');

if (!payload) {
Expand All @@ -737,10 +746,24 @@ var FabricCAClient = class {
util.format('Could not parse %s response [%s] as JSON due to error [%s]', api_method, payload, err)));
}
});
});

request.on('socket', (socket) => {
socket.setTimeout(CONNECTION_TIMEOUT);
socket.on('timeout', () => {
request.abort();
reject(new Error(util.format('Calling %s endpoint failed, CONNECTION Timeout', api_method)));
});
});

request.on('error', function (err) {
// If socket-operation-timeout is not set, read operations will not time out (infinite timeout).
if(SO_TIMEOUT && Number.isInteger(SO_TIMEOUT) && SO_TIMEOUT > 0) {
request.setTimeout(SO_TIMEOUT, () => {
reject(new Error(util.format('Calling %s endpoint failed, READ Timeout', api_method)));
});
}

request.on('error', function (err) {
reject(new Error(util.format('Calling %s endpoint failed with error [%s]', api_method, err)));
});

Expand Down
64 changes: 57 additions & 7 deletions test/integration/fabric-ca-services-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ var X509 = require('x509');
var util = require('util');
var fs = require('fs-extra');
var path = require('path');
const http = require('http');

var testUtil = require('../unit/util.js');

var LocalMSP = require('fabric-ca-client/lib/msp/msp.js');
Expand Down Expand Up @@ -75,14 +77,14 @@ test('\n\n ** FabricCAServices: Test enroll() With Dynamic CSR **\n\n', function
FabricCAServices.getConfigSetting('crypto-keysize', '256');//force for gulp test
FabricCAServices.setConfigSetting('crypto-hash-algo', 'SHA2');//force for gulp test

var caService = new FabricCAServices(fabricCAEndpoint, tlsOptions, ORGS[userOrg].ca.name);
var caService = new FabricCAServices(fabricCAEndpoint, tlsOptions, ORGS[userOrg].ca.name);

var req = {
enrollmentID: 'admin',
enrollmentSecret: 'adminpw'
};

var eResult, client, member, webAdmin;
let eResult, client, member, webAdmin, signingIdentity;
return caService.enroll(req)
.then((enrollment) => {
t.pass('Successfully enrolled \'' + req.enrollmentID + '\'.');
Expand Down Expand Up @@ -111,14 +113,16 @@ test('\n\n ** FabricCAServices: Test enroll() With Dynamic CSR **\n\n', function
cryptoSuite: caService.getCryptoSuite()
});

var signingIdentity = new SigningIdentity(eResult.certificate, pubKey, msp.getId(), msp.cryptoSuite,
signingIdentity = new SigningIdentity(eResult.certificate, pubKey, msp.getId(), msp.cryptoSuite,
new Signer(msp.cryptoSuite, eResult.key));
return caService._fabricCAClient.register(enrollmentID, null, 'client', userOrg, 1, [], signingIdentity);
return timeOutTest(signingIdentity, t);
},(err) => {
t.fail('Failed to import the public key from the enrollment certificate. ' + err.stack ? err.stack : err);
t.end();
}).then(() => {
return caService._fabricCAClient.register(enrollmentID, null, 'client', userOrg, 1, [], signingIdentity);
}).then((secret) => {
console.log('secret: ' + JSON.stringify(secret));
console.log('secret: ' + JSON.stringify(secret));
enrollmentSecret = secret; // to be used in the next test case

t.pass('testUser \'' + enrollmentID + '\'');
Expand Down Expand Up @@ -374,10 +378,8 @@ function checkoutCertForAttributes(t, pem, should_find, attr_name) {
t.pass('Successfully enrolled with certificate without the added attribute ::'+attr_name);
}
}

}


test('\n\n ** FabricCAClient: Test enroll With Static CSR **\n\n', function (t) {
var endpoint = FabricCAServices._parseURL(fabricCAEndpoint);
var client = new FabricCAClient({
Expand Down Expand Up @@ -419,3 +421,51 @@ function readPem() {
var pem = fs.readFileSync(file_path);
return pem;
}

async function timeOutTest(signingIdentity, t) {
const CONNECTION_TIMEOUT = FabricCAServices.getConfigSetting('connection-timeout');
t.equal(CONNECTION_TIMEOUT, 3000, 'connection-timeout should have default value 3000');
const SO_TIMEOUT = FabricCAServices.getConfigSetting('socket-operation-timeout');
t.equal(SO_TIMEOUT, undefined, 'socket-operation-timeout should have default value undefined');

let start, end;
// test CONNECTION_TIMEOUT
// Connect to a non-routable IP address should throw error connection_timeout
try {
const caClient = new FabricCAServices('http://10.255.255.1:3000')._fabricCAClient;
start = Date.now();
const response = await caClient.request('GET', '/aMethod', signingIdentity);
t.fail('Should throw error by CONNECTION_TIMEOUT');
} catch(e) {
end = Date.now();
t.equal(Math.floor((end-start)/1000), 3, 'should have duration roughly equals 3000');
t.equal(e.message, 'Calling /aMethod endpoint failed, CONNECTION Timeout');
}

// create a mock server, the mock server wait for 10 seconds until send response
const mockServer = http.createServer((req, res) => {
setTimeout(() => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('Response');
res.end();
}, 10000);
});
mockServer.listen(3000);

// set SO_TIMEOUT to 5000
FabricCAServices.setConfigSetting('socket-operation-timeout', 5000);

// test SO_TIMEOUT
try {
const caClient = new FabricCAServices('http://localhost:3000')._fabricCAClient;
start = Date.now();
const response = await caClient.request('GET', '/aMethod', signingIdentity);
t.fail('Should throw error by SO_TIMEOUT');
} catch(e) {
end = Date.now();
t.equal(Math.floor((end-start)/1000), 5, 'should have duration roughly equals 5000');
t.equal(e.message, 'Calling /aMethod endpoint failed, READ Timeout', 'should throw error after SO_TIMEOUT');
mockServer.close();
t.pass('Successfully tested SO_TIMEOUT');
}
}

0 comments on commit 2d81c17

Please sign in to comment.