diff --git a/src/config.js b/src/config.js index b0a0947a4..9ddcd51cc 100644 --- a/src/config.js +++ b/src/config.js @@ -1,7 +1,8 @@ import clone from 'lodash/clone'; let defaultConfig = { - allowHttp: false + allowHttp: false, + timeout: 0 }; let config = clone(defaultConfig); @@ -13,11 +14,13 @@ let config = clone(defaultConfig); * ``` * import {Config} from 'stellar-sdk'; * Config.setAllowHttp(true); + * Config.setTimout(5000); * ``` * * Usage browser: * ``` * StellarSdk.Config.setAllowHttp(true); + * StellarSdk.Config.setTimout(5000); * ``` * @static */ @@ -32,6 +35,16 @@ class Config { config.allowHttp = value; } + /** + * Sets `timeout` flag globally. When set to anything besides 0, the request will timeout after specified time (ms). + * Default: 0. + * @param {number} value + * @static + */ + static setTimeout(value) { + config.timeout = value; + } + /** * Returns the value of `allowHttp` flag. * @static @@ -40,6 +53,14 @@ class Config { return clone(config.allowHttp); } + /** + * Returns the value of `timeout` flag. + * @static + */ + static getTimeout() { + return clone(config.timeout); + } + /** * Sets all global config flags to default values. * @static diff --git a/src/federation_server.js b/src/federation_server.js index 386439899..8625f8fdd 100644 --- a/src/federation_server.js +++ b/src/federation_server.js @@ -20,6 +20,7 @@ export const FEDERATION_RESPONSE_MAX_SIZE = 100 * 1024; * @param {string} domain Domain this server represents * @param {object} [opts] * @param {boolean} [opts.allowHttp] - Allow connecting to http servers, default: `false`. This must be set to false in production deployments! You can also use {@link Config} class to set this globally. + * @param {number} [opts.timeout] - Allow a timeout, default: 0. Allows user to avoid nasty lag due to TOML resolve issue. You can also use {@link Config} class to set this globally. */ export class FederationServer { constructor(serverURL, domain, opts = {}) { @@ -32,6 +33,11 @@ export class FederationServer { allowHttp = opts.allowHttp; } + this.timeout = Config.getTimeout(); + if (typeof opts.timeout === 'number') { + this.timeout = opts.timeout; + } + if (this.serverURL.protocol() != 'https' && !allowHttp) { throw new Error('Cannot connect to insecure federation server'); } @@ -71,6 +77,7 @@ export class FederationServer { * @param {string} value Stellar Address (ex. `bob*stellar.org`) * @param {object} [opts] * @param {boolean} [opts.allowHttp] - Allow connecting to http servers, default: `false`. This must be set to false in production deployments! + * @param {number} [opts.timeout] - Allow a timeout, default: 0. Allows user to avoid nasty lag due to TOML resolve issue. * @returns {Promise} */ static resolve(value, opts = {}) { @@ -109,10 +116,11 @@ export class FederationServer { * @param {string} domain Domain to get federation server for * @param {object} [opts] * @param {boolean} [opts.allowHttp] - Allow connecting to http servers, default: `false`. This must be set to false in production deployments! + * @param {number} [opts.timeout] - Allow a timeout, default: 0. Allows user to avoid nasty lag due to TOML resolve issue. * @returns {Promise} */ static createForDomain(domain, opts = {}) { - return StellarTomlResolver.resolve(domain) + return StellarTomlResolver.resolve(domain, opts) .then(tomlObject => { if (!tomlObject.FEDERATION_SERVER) { return Promise.reject(new Error('stellar.toml does not contain FEDERATION_SERVER field')); @@ -161,7 +169,9 @@ export class FederationServer { } _sendRequest(url) { - return axios.get(url.toString(), {maxContentLength: FEDERATION_RESPONSE_MAX_SIZE}) + let timeout = this.timeout; + + return axios.get(url.toString(), {maxContentLength: FEDERATION_RESPONSE_MAX_SIZE, timeout}) .then(response => { if (typeof response.data.memo != "undefined" && typeof response.data.memo != 'string') { throw new Error("memo value should be of type string"); diff --git a/src/stellar_toml_resolver.js b/src/stellar_toml_resolver.js index 917ea6d27..a49b7adff 100644 --- a/src/stellar_toml_resolver.js +++ b/src/stellar_toml_resolver.js @@ -26,19 +26,27 @@ export class StellarTomlResolver { * @param {string} domain Domain to get stellar.toml file for * @param {object} [opts] * @param {boolean} [opts.allowHttp] - Allow connecting to http servers, default: `false`. This must be set to false in production deployments! + * @param {number} [opts.timeout] - Allow a timeout, default: 0. Allows user to avoid nasty lag due to TOML resolve issue. * @returns {Promise} */ static resolve(domain, opts = {}) { let allowHttp = Config.isAllowHttp(); + let timeout = Config.getTimeout(); + if (typeof opts.allowHttp !== 'undefined') { allowHttp = opts.allowHttp; } + if (typeof opts.timeout === 'number') { + timeout = opts.timeout; + } + let protocol = 'https'; if (allowHttp) { protocol = 'http'; } - return axios.get(`${protocol}://${domain}/.well-known/stellar.toml`, {maxContentLength: STELLAR_TOML_MAX_SIZE}) + + return axios.get(`${protocol}://${domain}/.well-known/stellar.toml`, {maxContentLength: STELLAR_TOML_MAX_SIZE, timeout}) .then(response => { try { let tomlObject = toml.parse(response.data); diff --git a/test/unit/federation_server_test.js b/test/unit/federation_server_test.js index f3d8f4583..1b2ea4bb2 100644 --- a/test/unit/federation_server_test.js +++ b/test/unit/federation_server_test.js @@ -1,4 +1,5 @@ import http from "http"; +import { Config } from "../../src/config"; describe("federation-server.js tests", function () { beforeEach(function () { @@ -227,5 +228,106 @@ FEDERATION_SERVER="https://api.stellar.org/federation" .then(() => tempServer.close()); }); }); + + }); + + describe('FederationServer times out when response lags and timeout set', function () { + afterEach(function() { + Config.setDefault(); + }); + + let opts = {allowHttp: true}; + let message; + for (let i = 0; i < 2; i++) { + if (i === 0) { + Config.setTimeout(1000); + message = "with global config set"; + } else { + opts = {allowHttp: true, timeout: 1000}; + message = "with instance opts set"; + } + + it(`resolveAddress times out ${message}`, function (done) { + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + new StellarSdk.FederationServer('http://localhost:4444/federation', 'stellar.org', opts) + .resolveAddress('bob*stellar.org') + .should.be.rejectedWith(/timeout of 1000ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + + it(`resolveAccountId times out ${message}`, function (done) { + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + new StellarSdk.FederationServer('http://localhost:4444/federation', 'stellar.org', opts) + .resolveAccountId('GB5XVAABEQMY63WTHDQ5RXADGYF345VWMNPTN2GFUDZT57D57ZQTJ7PS') + .should.be.rejectedWith(/timeout of 1000ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + + it(`resolveTransactionId times out ${message}`, function (done) { + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + new StellarSdk.FederationServer('http://localhost:4444/federation', 'stellar.org', opts) + .resolveTransactionId('3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889') + .should.be.rejectedWith(/timeout of 1000ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + + it(`createForDomain times out ${message}`, function (done) { + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + StellarSdk.FederationServer + .createForDomain("localhost:4444", opts) + .should.be.rejectedWith(/timeout of 1000ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + + it(`resolve times out ${message}`, function (done) { + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + StellarSdk.FederationServer + .resolve('bob*localhost:4444', opts) + .should.eventually.be.rejectedWith(/timeout of 1000ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + } }); }); diff --git a/test/unit/stellar_toml_resolver_test.js b/test/unit/stellar_toml_resolver_test.js index 88e97048a..987c672e2 100644 --- a/test/unit/stellar_toml_resolver_test.js +++ b/test/unit/stellar_toml_resolver_test.js @@ -12,6 +12,10 @@ describe("stellar_toml_resolver.js tests", function () { }); describe('StellarTomlResolver.resolve', function () { + afterEach(function() { + StellarSdk.Config.setDefault(); + }); + it("returns stellar.toml object for valid request and stellar.toml file", function (done) { this.axiosMock.expects('get') .withArgs(sinon.match('https://acme.com/.well-known/stellar.toml')) @@ -106,5 +110,42 @@ FEDERATION_SERVER="https://api.stellar.org/federation" .then(() => tempServer.close()); }); }); + + it("rejects after given timeout when global Config.timeout flag is set", function (done) { + StellarSdk.Config.setTimeout(1000); + + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + StellarSdk.StellarTomlResolver.resolve("localhost:4444", {allowHttp: true}) + .should.be.rejectedWith(/timeout of 1000ms exceeded/) + .notify(done) + .then(() => { + StellarSdk.Config.setDefault(); + tempServer.close(); + }); + }); + }); + + it("rejects after given timeout when timeout specified in StellarTomlResolver opts param", function (done) { + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + StellarSdk.StellarTomlResolver.resolve("localhost:4444", {allowHttp: true, timeout: 1000}) + .should.be.rejectedWith(/timeout of 1000ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); }); });