diff --git a/README.md b/README.md index 60272889..54287027 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,21 @@ if (MAJOR_NODEJS_VERSION >= 10) { ``` +### Setup proxy using `createGlobalProxyAgent` + +If you do not want to use `global.GLOBAL_AGENT` variable, then you can use `createGlobalProxyAgent` to instantiate a controlled instance of `global-agent`, e.g. + +```js +import { + createGlobalProxyAgent +} from 'global-agent'; + +const globalProxyAgent = createGlobalProxyAgent(); + +``` + +Unlike `bootstrap` routine, `createGlobalProxyAgent` factory does not create `global.GLOBAL_AGENT` variable and does not guard against multiple initializations of `global-agent`. The result object of `createGlobalProxyAgent` is equivalent to `global.GLOBAL_AGENT`. + ### Runtime configuration `global-agent/bootstrap` script copies `process.env.GLOBAL_AGENT_HTTP_PROXY` value to `global.GLOBAL_AGENT.HTTP_PROXY` and continues to use the latter variable. diff --git a/src/factories/createGlobalProxyAgent.js b/src/factories/createGlobalProxyAgent.js new file mode 100644 index 00000000..77f29793 --- /dev/null +++ b/src/factories/createGlobalProxyAgent.js @@ -0,0 +1,145 @@ +// @flow + +import EventEmitter from 'events'; +import http from 'http'; +import https from 'https'; +import semver from 'semver'; +import Logger from '../Logger'; +import { + HttpProxyAgent, + HttpsProxyAgent +} from '../classes'; +import { + UnexpectedStateError +} from '../errors'; +import { + bindHttpMethod, + isUrlMatchingNoProxy, + parseProxyUrl +} from '../utilities'; +import type { + ProxyAgentConfigurationInputType, + ProxyAgentConfigurationType +} from '../types'; +import createProxyController from './createProxyController'; + +const defaultConfigurationInput = { + environmentVariableNamespace: undefined +}; + +const log = Logger.child({ + namespace: 'createGlobalProxyAgent' +}); + +const createConfiguration = (configurationInput: ProxyAgentConfigurationInputType): ProxyAgentConfigurationType => { + // eslint-disable-next-line no-process-env + const DEFAULT_ENVIRONMENT_VARIABLE_NAMESPACE = typeof process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE === 'string' ? process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE : 'GLOBAL_AGENT_'; + + return { + ...configurationInput, + environmentVariableNamespace: typeof configurationInput.environmentVariableNamespace === 'string' ? configurationInput.environmentVariableNamespace : DEFAULT_ENVIRONMENT_VARIABLE_NAMESPACE + }; +}; + +export default (configurationInput: ProxyAgentConfigurationInputType = defaultConfigurationInput) => { + const configuration = createConfiguration(configurationInput); + + const proxyController = createProxyController(); + + // eslint-disable-next-line no-process-env + proxyController.HTTP_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTP_PROXY'] || null; + + // eslint-disable-next-line no-process-env + proxyController.HTTPS_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTPS_PROXY'] || null; + + // eslint-disable-next-line no-process-env + proxyController.NO_PROXY = process.env[configuration.environmentVariableNamespace + 'NO_PROXY'] || null; + + log.info({ + configuration: proxyController + }, 'global agent has been initialized'); + + const isProxyConfigured = (getProxy) => { + return () => { + return getProxy(); + }; + }; + + const mustUrlUseProxy = (getProxy) => { + return (url) => { + if (!getProxy()) { + return false; + } + + if (!proxyController.NO_PROXY) { + return true; + } + + return !isUrlMatchingNoProxy(url, proxyController.NO_PROXY); + }; + }; + + const getUrlProxy = (getProxy) => { + return () => { + const proxy = getProxy(); + if (!proxy) { + throw new UnexpectedStateError('HTTP(S) proxy must be configured.'); + } + + return parseProxyUrl(proxy); + }; + }; + + const eventEmitter = new EventEmitter(); + + const getHttpProxy = () => { + return proxyController.HTTP_PROXY; + }; + + const httpAgent = new HttpProxyAgent( + isProxyConfigured(getHttpProxy), + mustUrlUseProxy(getHttpProxy), + getUrlProxy(getHttpProxy), + http.globalAgent, + eventEmitter + ); + + const getHttpsProxy = () => { + return proxyController.HTTPS_PROXY || proxyController.HTTP_PROXY; + }; + + const httpsAgent = new HttpsProxyAgent( + isProxyConfigured(getHttpsProxy), + mustUrlUseProxy(getHttpsProxy), + getUrlProxy(getHttpsProxy), + https.globalAgent, + eventEmitter + ); + + // Overriding globalAgent was added in v11.7. + // @see https://nodejs.org/uk/blog/release/v11.7.0/ + if (semver.gte(process.version, 'v11.7.0')) { + // @see https://github.com/facebook/flow/issues/7670 + // $FlowFixMe + http.globalAgent = httpAgent; + + // $FlowFixMe + https.globalAgent = httpsAgent; + } else if (semver.gte(process.version, 'v10')) { + // $FlowFixMe + http.get = bindHttpMethod(http.get, httpAgent); + + // $FlowFixMe + http.request = bindHttpMethod(http.request, httpAgent); + + // $FlowFixMe + https.get = bindHttpMethod(https.get, httpsAgent); + + // $FlowFixMe + https.request = bindHttpMethod(https.request, httpsAgent); + } else { + log.warn('attempt to initialize global-agent in unsupported Node.js version was ignored'); + } + + return proxyController; +}; diff --git a/src/factories/createGlobalAgentGlobal.js b/src/factories/createProxyController.js similarity index 62% rename from src/factories/createGlobalAgentGlobal.js rename to src/factories/createProxyController.js index af9e4e29..b209b20d 100644 --- a/src/factories/createGlobalAgentGlobal.js +++ b/src/factories/createProxyController.js @@ -1,25 +1,33 @@ // @flow +import Events from 'events'; import Logger from '../Logger'; +type ProxyControllerType = {| + eventEmitter: Events | null, + HTTP_PROXY: string | null, + HTTPS_PROXY: string | null, + NO_PROXY: string | null +|}; + const log = Logger.child({ - namespace: 'createGlobalAgentGlobal' + namespace: 'createProxyController' }); const KNOWN_PROPERTY_NAMES = [ - 'bootstrapped', + 'eventEmitter', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY' ]; -export default () => { +export default (): ProxyControllerType => { // eslint-disable-next-line fp/no-proxy return new Proxy({ - bootstrapped: false, - HTTP_PROXY: '', - HTTPS_PROXY: '', - NO_PROXY: '' + eventEmitter: null, + HTTP_PROXY: null, + HTTPS_PROXY: null, + NO_PROXY: null }, { set: (subject, name, value) => { if (!KNOWN_PROPERTY_NAMES.includes(name)) { diff --git a/src/factories/index.js b/src/factories/index.js index a021598c..c16eca67 100644 --- a/src/factories/index.js +++ b/src/factories/index.js @@ -1,3 +1,4 @@ // @flow -export {default as createGlobalAgentGlobal} from './createGlobalAgentGlobal'; +export {default as createGlobalProxyAgent} from './createGlobalProxyAgent'; +export {default as createProxyController} from './createProxyController'; diff --git a/src/index.js b/src/index.js index 3f4c90a0..14da1ba0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ // @flow export {bootstrap} from './routines'; +export {createGlobalProxyAgent} from './factories'; diff --git a/src/routines/bootstrap.js b/src/routines/bootstrap.js index 88a0b5ba..316f875f 100644 --- a/src/routines/bootstrap.js +++ b/src/routines/bootstrap.js @@ -1,153 +1,25 @@ // @flow -import EventEmitter from 'events'; -import http from 'http'; -import https from 'https'; -import semver from 'semver'; import Logger from '../Logger'; import { - HttpProxyAgent, - HttpsProxyAgent -} from '../classes'; -import { - UnexpectedStateError -} from '../errors'; -import { - createGlobalAgentGlobal + createGlobalProxyAgent } from '../factories'; -import { - bindHttpMethod, - isUrlMatchingNoProxy, - parseProxyUrl -} from '../utilities'; - -type ConfigurationInputType = {| - +environmentVariableNamespace?: string -|}; - -type ConfigurationType = {| - +environmentVariableNamespace: string -|}; - -const defaultConfigurationInput = { - environmentVariableNamespace: undefined -}; +import type { + ProxyAgentConfigurationInputType +} from '../types'; const log = Logger.child({ namespace: 'bootstrap' }); -const createConfiguration = (configurationInput: ConfigurationInputType): ConfigurationType => { - // eslint-disable-next-line no-process-env - const DEFAULT_ENVIRONMENT_VARIABLE_NAMESPACE = typeof process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE === 'string' ? process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE : 'GLOBAL_AGENT_'; +export default (configurationInput?: ProxyAgentConfigurationInputType): boolean => { + if (global.GLOBAL_AGENT) { + log.warn('found global.GLOBAL_AGENT; second attempt to bootstrap global-agent was ignored'); - return { - ...configurationInput, - environmentVariableNamespace: typeof configurationInput.environmentVariableNamespace === 'string' ? configurationInput.environmentVariableNamespace : DEFAULT_ENVIRONMENT_VARIABLE_NAMESPACE - }; -}; - -export default (configurationInput: ConfigurationInputType = defaultConfigurationInput) => { - const configuration = createConfiguration(configurationInput); - - global.GLOBAL_AGENT = global.GLOBAL_AGENT || createGlobalAgentGlobal(); - - if (global.GLOBAL_AGENT.bootstrapped) { - log.warn('found global.globalAgent; second attempt to bootstrap global-agent was ignored'); - - return; + return false; } - global.GLOBAL_AGENT.bootstrapped = true; - - // eslint-disable-next-line no-process-env - global.GLOBAL_AGENT.HTTP_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTP_PROXY'] || null; - - // eslint-disable-next-line no-process-env - global.GLOBAL_AGENT.HTTPS_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTPS_PROXY'] || null; - - // eslint-disable-next-line no-process-env - global.GLOBAL_AGENT.NO_PROXY = process.env[configuration.environmentVariableNamespace + 'NO_PROXY'] || null; + global.GLOBAL_AGENT = createGlobalProxyAgent(configurationInput); - log.info({ - configuration: global.GLOBAL_AGENT - }, 'global agent has been initialized'); - - const isProxyConfigured = (getProxy) => { - return () => { - return getProxy(); - }; - }; - - const mustUrlUseProxy = (getProxy) => { - return (url) => { - if (!getProxy()) { - return false; - } - - if (!global.GLOBAL_AGENT.NO_PROXY) { - return true; - } - - return !isUrlMatchingNoProxy(url, global.GLOBAL_AGENT.NO_PROXY); - }; - }; - - const getUrlProxy = (getProxy) => { - return () => { - const proxy = getProxy(); - if (!proxy) { - throw new UnexpectedStateError('HTTP(S) proxy must be configured.'); - } - - return parseProxyUrl(proxy); - }; - }; - - const eventEmitter = new EventEmitter(); - - const getHttpProxy = () => { - return global.GLOBAL_AGENT.HTTP_PROXY; - }; - const httpAgent = new HttpProxyAgent( - isProxyConfigured(getHttpProxy), - mustUrlUseProxy(getHttpProxy), - getUrlProxy(getHttpProxy), - http.globalAgent, - eventEmitter - ); - - const getHttpsProxy = () => { - return global.GLOBAL_AGENT.HTTPS_PROXY || global.GLOBAL_AGENT.HTTP_PROXY; - }; - const httpsAgent = new HttpsProxyAgent( - isProxyConfigured(getHttpsProxy), - mustUrlUseProxy(getHttpsProxy), - getUrlProxy(getHttpsProxy), - https.globalAgent, - eventEmitter - ); - - // Overriding globalAgent was added in v11.7. - // @see https://nodejs.org/uk/blog/release/v11.7.0/ - if (semver.gte(process.version, 'v11.7.0')) { - // @see https://github.com/facebook/flow/issues/7670 - // $FlowFixMe - http.globalAgent = httpAgent; - - // $FlowFixMe - https.globalAgent = httpsAgent; - } else { - // $FlowFixMe - http.get = bindHttpMethod(http.get, httpAgent); - - // $FlowFixMe - http.request = bindHttpMethod(http.request, httpAgent); - - // $FlowFixMe - https.get = bindHttpMethod(https.get, httpsAgent); - - // $FlowFixMe - https.request = bindHttpMethod(https.request, httpsAgent); - } + return true; }; diff --git a/src/types.js b/src/types.js index a54942ed..f9de8aca 100644 --- a/src/types.js +++ b/src/types.js @@ -32,3 +32,11 @@ export type IsProxyConfiguredMethodType = () => boolean; export type MustUrlUseProxyMethodType = (url: string) => boolean; export type GetUrlProxyMethodType = (url: string) => ProxyConfigurationType; export type ProtocolType = 'http:' | 'https:'; + +export type ProxyAgentConfigurationInputType = {| + +environmentVariableNamespace?: string +|}; + +export type ProxyAgentConfigurationType = {| + +environmentVariableNamespace: string +|}; diff --git a/test/global-agent/factories/createGlobalAgentGlobal.js b/test/global-agent/factories/createGlobalAgentGlobal.js deleted file mode 100644 index 5f658964..00000000 --- a/test/global-agent/factories/createGlobalAgentGlobal.js +++ /dev/null @@ -1,51 +0,0 @@ -// @flow - -import test from 'ava'; -import createGlobalAgentGlobal from '../../../src/factories/createGlobalAgentGlobal'; - -test('defaults bootstrapped to false', (t) => { - const globalAgentGlobal = createGlobalAgentGlobal(); - - t.is(globalAgentGlobal.bootstrapped, false); -}); - -test('sets bootstrapped', (t) => { - const globalAgentGlobal = createGlobalAgentGlobal(); - - globalAgentGlobal.bootstrapped = true; - - t.is(globalAgentGlobal.bootstrapped, true); -}); - -test('sets HTTP_PROXY', (t) => { - const globalAgentGlobal = createGlobalAgentGlobal(); - - globalAgentGlobal.HTTP_PROXY = 'http://127.0.0.1'; - - t.is(globalAgentGlobal.HTTP_PROXY, 'http://127.0.0.1'); -}); - -test('sets HTTPS_PROXY', (t) => { - const globalAgentGlobal = createGlobalAgentGlobal(); - - globalAgentGlobal.HTTPS_PROXY = 'http://127.0.0.1'; - - t.is(globalAgentGlobal.HTTPS_PROXY, 'http://127.0.0.1'); -}); - -test('sets NO_PROXY', (t) => { - const globalAgentGlobal = createGlobalAgentGlobal(); - - globalAgentGlobal.NO_PROXY = '*'; - - t.is(globalAgentGlobal.NO_PROXY, '*'); -}); - -test('throws an error if unknown property is set', (t) => { - const globalAgentGlobal = createGlobalAgentGlobal(); - - t.throws(() => { - // $FlowFixMe - globalAgentGlobal.FOO = 'BAR'; - }, 'Cannot set an unmapped property "FOO".'); -}); diff --git a/test/global-agent/routines/bootstrap.js b/test/global-agent/factories/createGlobalProxyAgent.js similarity index 80% rename from test/global-agent/routines/bootstrap.js rename to test/global-agent/factories/createGlobalProxyAgent.js index e2594515..c11fd2cd 100644 --- a/test/global-agent/routines/bootstrap.js +++ b/test/global-agent/factories/createGlobalProxyAgent.js @@ -16,7 +16,7 @@ import test, { afterEach, beforeEach } from 'ava'; -import bootstrap from '../../../src/routines/bootstrap'; +import createGlobalProxyAgent from '../../../src/factories/createGlobalProxyAgent'; const defaultHttpAgent = http.globalAgent; const defaultHttpsAgent = https.globalAgent; @@ -47,8 +47,6 @@ before(() => { }); beforeEach(() => { - global.GLOBAL_AGENT = {}; - // $FlowFixMe http.globalAgent = defaultHttpAgent; @@ -151,11 +149,11 @@ const createHttpServer = async () => { }; test('proxies HTTP request', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTP_PROXY = proxyServer.url; const response = await new Promise((resolve) => { http.get('http://127.0.0.1', createHttpResponseResolver(resolve)); @@ -165,11 +163,11 @@ test('proxies HTTP request', async (t) => { }); test('proxies HTTPS request', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTP_PROXY = proxyServer.url; const response = await new Promise((resolve) => { https.get('https://127.0.0.1', createHttpResponseResolver(resolve)); @@ -179,21 +177,21 @@ test('proxies HTTPS request', async (t) => { }); test('does not produce unhandled rejection when cannot connect to proxy', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const port = await getNextPort(); - global.GLOBAL_AGENT.HTTP_PROXY = 'http://127.0.0.1:' + port; + globalProxyAgent.HTTP_PROXY = 'http://127.0.0.1:' + port; await t.throwsAsync(got('http://127.0.0.1')); }); test('proxies HTTPS request with dedicated proxy', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTPS_PROXY = proxyServer.url; + globalProxyAgent.HTTPS_PROXY = proxyServer.url; const response = await new Promise((resolve) => { https.get('https://127.0.0.1', createHttpResponseResolver(resolve)); @@ -203,12 +201,12 @@ test('proxies HTTPS request with dedicated proxy', async (t) => { }); test('ignores dedicated HTTPS proxy for HTTP urls', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; - global.GLOBAL_AGENT.HTTPS_PROXY = 'http://example.org'; + globalProxyAgent.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTPS_PROXY = 'http://example.org'; const response = await new Promise((resolve) => { http.get('http://127.0.0.1', createHttpResponseResolver(resolve)); @@ -218,13 +216,13 @@ test('ignores dedicated HTTPS proxy for HTTP urls', async (t) => { }); test('forwards requests matching NO_PROXY', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); const httpServer = await createHttpServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; - global.GLOBAL_AGENT.NO_PROXY = '127.0.0.1'; + globalProxyAgent.HTTP_PROXY = proxyServer.url; + globalProxyAgent.NO_PROXY = '127.0.0.1'; const response = await new Promise((resolve) => { http.get(httpServer.url, createHttpResponseResolver(resolve)); @@ -234,11 +232,11 @@ test('forwards requests matching NO_PROXY', async (t) => { }); test('proxies HTTP request (using http.get(host))', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTP_PROXY = proxyServer.url; const response = await new Promise((resolve) => { http.get({ @@ -250,11 +248,11 @@ test('proxies HTTP request (using http.get(host))', async (t) => { }); test('proxies HTTP request (using got)', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTP_PROXY = proxyServer.url; const response = await got('http://127.0.0.1'); @@ -262,11 +260,11 @@ test('proxies HTTP request (using got)', async (t) => { }); test('proxies HTTPS request (using got)', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTP_PROXY = proxyServer.url; const response = await got('https://127.0.0.1'); @@ -274,11 +272,11 @@ test('proxies HTTPS request (using got)', async (t) => { }); test('proxies HTTP request (using axios)', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTP_PROXY = proxyServer.url; const response = await axios.get('http://127.0.0.1'); @@ -286,11 +284,11 @@ test('proxies HTTP request (using axios)', async (t) => { }); test('proxies HTTPS request (using axios)', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTP_PROXY = proxyServer.url; const response = await axios.get('https://127.0.0.1'); @@ -298,11 +296,11 @@ test('proxies HTTPS request (using axios)', async (t) => { }); test('proxies HTTP request (using request)', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTP_PROXY = proxyServer.url; const response = await new Promise((resolve) => { makeRequest('http://127.0.0.1', (error, requestResponse, body) => { @@ -316,11 +314,11 @@ test('proxies HTTP request (using request)', async (t) => { }); test('proxies HTTPS request (using request)', async (t) => { - bootstrap(); + const globalProxyAgent = createGlobalProxyAgent(); const proxyServer = await createProxyServer(); - global.GLOBAL_AGENT.HTTP_PROXY = proxyServer.url; + globalProxyAgent.HTTP_PROXY = proxyServer.url; const response = await new Promise((resolve) => { makeRequest('https://127.0.0.1', (error, requestResponse, body) => { diff --git a/test/global-agent/factories/createProxyController.js b/test/global-agent/factories/createProxyController.js new file mode 100644 index 00000000..bdaf26dc --- /dev/null +++ b/test/global-agent/factories/createProxyController.js @@ -0,0 +1,37 @@ +// @flow + +import test from 'ava'; +import createProxyController from '../../../src/factories/createProxyController'; + +test('sets HTTP_PROXY', (t) => { + const globalAgentGlobal = createProxyController(); + + globalAgentGlobal.HTTP_PROXY = 'http://127.0.0.1'; + + t.is(globalAgentGlobal.HTTP_PROXY, 'http://127.0.0.1'); +}); + +test('sets HTTPS_PROXY', (t) => { + const globalAgentGlobal = createProxyController(); + + globalAgentGlobal.HTTPS_PROXY = 'http://127.0.0.1'; + + t.is(globalAgentGlobal.HTTPS_PROXY, 'http://127.0.0.1'); +}); + +test('sets NO_PROXY', (t) => { + const globalAgentGlobal = createProxyController(); + + globalAgentGlobal.NO_PROXY = '*'; + + t.is(globalAgentGlobal.NO_PROXY, '*'); +}); + +test('throws an error if unknown property is set', (t) => { + const globalAgentGlobal = createProxyController(); + + t.throws(() => { + // $FlowFixMe + globalAgentGlobal.FOO = 'BAR'; + }, 'Cannot set an unmapped property "FOO".'); +});