From 9803e37707acb3d495567dcbe026debe58482e32 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 17 Feb 2025 13:22:26 +0000 Subject: [PATCH 1/5] feat: add support for including target path in request filtering --- lib/core/http.js | 1 - lib/index.js | 53 +++----- lib/utils/domainTargets.d.ts | 14 +++ lib/utils/domainTargets.js | 114 +++++++++++++++++ lib/utils/httpsHelper.js | 39 ++++-- lib/utils/index.js | 1 + tests/utils/domainTargets.test.js | 202 ++++++++++++++++++++++++++++++ 7 files changed, 378 insertions(+), 46 deletions(-) create mode 100644 lib/utils/domainTargets.d.ts create mode 100644 lib/utils/domainTargets.js create mode 100644 tests/utils/domainTargets.test.js diff --git a/lib/core/http.js b/lib/core/http.js index c3577d9..a0bc27c 100644 --- a/lib/core/http.js +++ b/lib/core/http.js @@ -6,7 +6,6 @@ const axios = require('axios'); * @param {string} appUuid * @param {string} apiKey * @param {import('../types').HttpConfig} config - * @returns */ module.exports = (appUuid, apiKey, config) => { const request = ( diff --git a/lib/index.js b/lib/index.js index 6aa659d..54d57bb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -20,6 +20,7 @@ const { } = require('./core'); const { TokenCreationError } = require('./utils/errors'); const HttpsProxyAgent = require('./utils/proxyAgent'); +const { importTarget, matchTarget } = require('./utils/domainTargets'); const originalRequest = https.request; @@ -222,13 +223,17 @@ class EvervaultClient { /** * @private * @param {string[]} decryptionDomains - * @returns {(domain: string) => boolean} + * @returns {(domain: string, path: string) => boolean} */ _decryptionDomainsFilter(decryptionDomains) { - return (domain) => + const parsedDomains = decryptionDomains + .map((decryptionDomain) => importTarget(decryptionDomain)) + .filter((importedTarget) => importedTarget != null); + return (domain, path) => this._isDecryptionDomain( domain, - decryptionDomains, + path, + parsedDomains, this._alwaysIgnoreDomains() ); } @@ -236,44 +241,22 @@ class EvervaultClient { /** * @private * @param {string} domain - * @param {string[]} decryptionDomains + * @param {string} path + * @param {import('./utils/domainTargets').Target[]} decryptionDomains * @param {string[]} alwaysIgnore */ - _isDecryptionDomain(domain, decryptionDomains, alwaysIgnore) { + _isDecryptionDomain(domain, path, decryptionDomains, alwaysIgnore) { if (alwaysIgnore.includes(domain)) return false; - return decryptionDomains.some((decryptionDomain) => { - if (decryptionDomain.charAt(0) === '*') { - return domain.endsWith(decryptionDomain.substring(1)); - } else { - return decryptionDomain === domain; - } - }); + return decryptionDomains.some((decryptionDomain) => + matchTarget(domain, path, decryptionDomain) + ); } - /** @private @returns {(domain: string) => boolean} */ + /** @private @returns {(domain: string, path: string) => boolean} */ _relayOutboundConfigDomainFilter() { - return (domain) => { - return this._isDecryptionDomain( - domain, - RelayOutboundConfig.getDecryptionDomains(), - this._alwaysIgnoreDomains() - ); - }; - } - - /** - * @private - * @param {string} domain - * @param {string[]} exactDomains - * @param {string[]} endsWithDomains - * @returns {boolean} - */ - _exactOrEndsWith(domain, exactDomains, endsWithDomains) { - if (exactDomains.includes(domain)) return true; - for (let end of endsWithDomains) { - if (domain && domain.endsWith(end)) return true; - } - return false; + return this._decryptionDomainsFilter( + RelayOutboundConfig.getDecryptionDomains() + ).bind(this); } /** diff --git a/lib/utils/domainTargets.d.ts b/lib/utils/domainTargets.d.ts new file mode 100644 index 0000000..7c90faf --- /dev/null +++ b/lib/utils/domainTargets.d.ts @@ -0,0 +1,14 @@ +export type MatcherSpecificity = "wildcard" | "absolute"; +export type MatcherType = "host" | "path"; + +export interface Target { + rawValue: string; + host: Matcher; + path?: Matcher; +} + +export interface Matcher { + specificity: MatcherSpecificity; + type: MatcherType; + value: string; +} \ No newline at end of file diff --git a/lib/utils/domainTargets.js b/lib/utils/domainTargets.js new file mode 100644 index 0000000..6f3d00f --- /dev/null +++ b/lib/utils/domainTargets.js @@ -0,0 +1,114 @@ +/** + * Grouping of helpers and types to aid reasoning about destinations when intercepting outbound traffic. + */ + +/** + * Apply the matching logic based on the matcher type and specificity. + * @param {string} givenValue + * @param {import("./domainTargets").Matcher} matcher + */ +function applyMatch(givenValue, matcher) { + if (matcher.specificity === 'absolute') { + return givenValue === matcher.value; + } + if (matcher.type === 'host') { + return givenValue.endsWith(matcher.value); + } + if (matcher.type === 'path') { + return givenValue.startsWith(matcher.value); + } + return false; +} + +/** + * Apply a target matcher against the given host and path combination. If a path matcher is defined on the target, it will only match if *both* the host and path match. + * @param {string} requestedHost + * @param {string} requestedPath + * @param {import('./domainTargets').Target} target + */ +function matchTarget(requestedHost, requestedPath, target) { + if (target.path != null) { + return ( + applyMatch(requestedHost, target.host) && + applyMatch(requestedPath, target.path) + ); + } + return applyMatch(requestedHost, target.host); +} + +/** + * Check if the given hostname includes a protocol prefix. + * @param {string} val + * @returns {boolean} + */ +function startsWithProto(val) { + return val.startsWith('https://') || val.startsWith('http://'); +} + +/** + * Create a matcher object from a hostname string. If the hostname begins with an asterisk, it will be treated as a wildcard matcher. + * @param {string} host + * @return {import('./domainTargets').Matcher} + */ +function buildHostMatcher(host) { + const specificity = host.startsWith('*') ? 'wildcard' : 'absolute'; + const value = specificity === 'wildcard' ? host.slice(1) : host; + return { + type: 'host', + specificity, + value, + }; +} + +/** + * Create a matcher object from a path string. If the string ends with an asterisk, it will be treated as a wildcard matcher. + * @param {string} path + * @return {import('./domainTargets').Matcher} + */ +function buildPathMatcher(path) { + const specificity = path.endsWith('*') ? 'wildcard' : 'absolute'; + const value = specificity === 'wildcard' ? path.slice(0, -1) : path; + return { + type: 'path', + specificity, + value, + }; +} + +/** + * Convert a user provided input into a target matcher, lightly validating the input in the process. + * @param {unknown} rawInputValue + * @returns {import("./domainTargets").Target | null} + */ +function importTarget(rawInputValue) { + if (typeof rawInputValue !== 'string' || rawInputValue.length === 0) { + return null; + } + + // Targets are expected to be domains with optional paths. + if (startsWithProto(rawInputValue)) { + return null; + } + + const startPathIndex = rawInputValue.indexOf('/'); + if (startPathIndex === -1) { + return { + rawValue: rawInputValue, + host: buildHostMatcher(rawInputValue), + }; + } + + const hostname = rawInputValue.slice(0, startPathIndex); + const path = rawInputValue.slice(startPathIndex); + + return { + rawValue: rawInputValue, + host: buildHostMatcher(hostname), + path: buildPathMatcher(path), + }; +} + +module.exports = { + importTarget, + matchTarget, +}; diff --git a/lib/utils/httpsHelper.js b/lib/utils/httpsHelper.js index d5204e7..0935704 100644 --- a/lib/utils/httpsHelper.js +++ b/lib/utils/httpsHelper.js @@ -38,6 +38,11 @@ const certificateUtil = (evClient) => { /** * @param {string} apiKey + * @param {string} tunnelHostname + * @param {(domain: string, path: string) => boolean} domainFilter + * @param {boolean} debugRequests + * @param {ReturnType} evClient + * @param {typeof import('node:https').request} originalRequest * @returns {void} */ const overloadHttpsModule = ( @@ -48,27 +53,41 @@ const overloadHttpsModule = ( evClient, originalRequest ) => { - function getDomainFromArgs(args) { + /** + * + * @param {Parameters} args + * @returns {{ host: string, path?: string }} + */ + function getDomainAndPathFromArgs(args) { if (typeof args[0] === 'string') { - return new URL(args[0]).host; + const parsedUrl = new URL(args[0]); + return { host: parsedUrl.host, path: parsedUrl.pathname }; } - if (args.url) { - return args.url.match(domainRegex)[0]; + if (args[0] instanceof URL) { + return { host: args[0].host, path: args[0].pathname }; } - let domain; + let domain, path; for (const arg of args) { if (arg instanceof Object) { - domain = domain || arg.hostname || arg.host; + domain = domain ?? arg.hostname ?? arg.host; + path = domain ?? arg.pathname; } } - return domain; + return { + domain, + path, + }; } + /** + * @param {Parameters} args + * @returns {ReturnType} + */ function wrapMethodRequest(...args) { - const domain = getDomainFromArgs(args); - const shouldProxy = domainFilter(domain); + const { domain, path } = getDomainAndPathFromArgs(args); + const shouldProxy = domainFilter(domain, path); if ( debugRequests && !EVERVAULT_DOMAINS.some((evervault_domain) => @@ -76,7 +95,7 @@ const overloadHttpsModule = ( ) ) { console.log( - `EVERVAULT DEBUG :: Request to domain: ${domain}, Outbound Proxy enabled: ${shouldProxy}` + `EVERVAULT DEBUG :: Request to domain: ${domain}${path}, Outbound Proxy enabled: ${shouldProxy}` ); } args = args.map((arg) => { diff --git a/lib/utils/index.js b/lib/utils/index.js index 3f21356..88bc544 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1,5 +1,6 @@ module.exports = { Datatypes: require('./datatypes'), + domainTargets: require('./domainTargets'), errors: require('./errors'), certHelper: require('./certHelper'), environment: require('./environment'), diff --git a/tests/utils/domainTargets.test.js b/tests/utils/domainTargets.test.js new file mode 100644 index 0000000..d1ffa36 --- /dev/null +++ b/tests/utils/domainTargets.test.js @@ -0,0 +1,202 @@ +const { expect } = require('chai'); + +const { domainTargets } = require('../../lib/utils'); + +describe('domainTargets', () => { + context('importTarget', () => { + context('Absolute domain correctly formatted, no path', () => { + it('imports the domain correctly', () => { + const result = domainTargets.importTarget('google.com'); + expect(result).to.not.be.null; + expect(result.rawValue).to.equal('google.com'); + expect(result.host.value).to.equal('google.com'); + expect(result.host.specificity).to.equal('absolute'); + expect(result.path).to.be.undefined; + }); + }); + + context('Absolute domain and path', () => { + it('imports the domain and path correctly', () => { + const result = domainTargets.importTarget('google.com/users'); + expect(result).to.not.be.null; + expect(result.rawValue).to.equal('google.com/users'); + expect(result.host.value).to.equal('google.com'); + expect(result.host.specificity).to.equal('absolute'); + expect(result.path).to.not.be.undefined; + expect(result.path.value).to.equal('/users'); + expect(result.path.specificity).to.equal('absolute'); + }); + }); + + context('Wildcard domain, absolute path', () => { + it('imports the domain and path correctly', () => { + const result = domainTargets.importTarget('*.google.com/users'); + expect(result).to.not.be.null; + expect(result.rawValue).to.equal('*.google.com/users'); + expect(result.host.value).to.equal('.google.com'); + expect(result.host.specificity).to.equal('wildcard'); + expect(result.path).to.not.be.undefined; + expect(result.path.value).to.equal('/users'); + expect(result.path.specificity).to.equal('absolute'); + }); + }); + + context('Wildcard domain, wildcard path', () => { + it('imports the domain and path correctly', () => { + const result = domainTargets.importTarget('*.google.com/users/*'); + expect(result).to.not.be.null; + expect(result.rawValue).to.equal('*.google.com/users/*'); + expect(result.host.value).to.equal('.google.com'); + expect(result.host.specificity).to.equal('wildcard'); + expect(result.path).to.not.be.undefined; + expect(result.path.value).to.equal('/users/'); + expect(result.path.specificity).to.equal('wildcard'); + }); + }); + + context('Absolute domain, wildcard path', () => { + it('imports the domain and path correctly', () => { + const result = domainTargets.importTarget('google.com/users/*'); + expect(result).to.not.be.null; + expect(result.rawValue).to.equal('google.com/users/*'); + expect(result.host.value).to.equal('google.com'); + expect(result.host.specificity).to.equal('absolute'); + expect(result.path).to.not.be.undefined; + expect(result.path.value).to.equal('/users/'); + expect(result.path.specificity).to.equal('wildcard'); + }); + }); + + context('Invalid domain with leading protocol', () => { + it('Ignores the domain', () => { + const result = domainTargets.importTarget('https://google.com/users/*'); + expect(result).to.be.null; + }); + }); + + context('Invalid domain type', () => { + it('Ignores the domain', () => { + const result = domainTargets.importTarget(false); + expect(result).to.be.null; + }); + }); + }); + + context('matchTarget', () => { + context('absolute domain matching input', () => { + it('returns true', () => { + const target = domainTargets.importTarget('google.com'); + const result = domainTargets.matchTarget( + 'google.com', + '/users', + target + ); + expect(result).to.be.true; + }); + }); + + context('absolute domain not matching input', () => { + it('returns false', () => { + const target = domainTargets.importTarget('google.com'); + const result = domainTargets.matchTarget('api.com', '/users', target); + expect(result).to.be.false; + }); + }); + + context('wildcard domain matching input', () => { + it('returns true', () => { + const target = domainTargets.importTarget('*.google.com'); + const result = domainTargets.matchTarget( + 'api.google.com', + '/users', + target + ); + expect(result).to.be.true; + }); + }); + + context('wildcard domain not matching input', () => { + it('returns false', () => { + const target = domainTargets.importTarget('*.google.com'); + const result = domainTargets.matchTarget( + 'api.somewhere.com', + '/users', + target + ); + expect(result).to.be.false; + }); + }); + + context('absolute domain and path matching input', () => { + it('returns true', () => { + const target = domainTargets.importTarget('google.com/users/foo'); + const result = domainTargets.matchTarget( + 'google.com', + '/users/foo', + target + ); + expect(result).to.be.true; + }); + }); + + context('absolute domain and path not matching input', () => { + it('returns true', () => { + const target = domainTargets.importTarget('google.com/users/foo'); + const result = domainTargets.matchTarget( + 'google.com', + '/users', + target + ); + expect(result).to.be.false; + }); + }); + + context('absolute domain and wildcard path matching input', () => { + it('returns true', () => { + const target = domainTargets.importTarget('google.com/users/*'); + const result = domainTargets.matchTarget( + 'google.com', + '/users/foo', + target + ); + expect(result).to.be.true; + }); + }); + + context('absolute domain and wildcard path not matching input', () => { + it('returns true', () => { + const target = domainTargets.importTarget('google.com/users/*'); + const result = domainTargets.matchTarget( + 'google.com', + '/settings', + target + ); + expect(result).to.be.false; + }); + }); + + context('wildcard domain and wildcard path matching input', () => { + it('returns true', () => { + const target = domainTargets.importTarget('*.google.com/users/*'); + const result = domainTargets.matchTarget( + 'api.google.com', + '/users/foo', + target + ); + expect(result).to.be.true; + }); + }); + + context('wildcard domain and wildcard path not matching input', () => { + it('returns true', () => { + const target = domainTargets.importTarget('*.google.com/users/*'); + const result = domainTargets.matchTarget( + 'api.somewhere.com', + '/settings', + target + ); + expect(result).to.be.false; + }); + }); + }); +}); From bc6ec8473945416e4100c187b40146e1816729ce Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 17 Feb 2025 16:04:52 +0000 Subject: [PATCH 2/5] chore: add symbol to track proxied reqs --- e2e/outboundRelay.test.js | 4 ++++ lib/config.js | 1 + lib/types.d.ts | 1 + lib/utils/httpsHelper.js | 7 ++++++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/e2e/outboundRelay.test.js b/e2e/outboundRelay.test.js index 9afe54f..a9c7b06 100644 --- a/e2e/outboundRelay.test.js +++ b/e2e/outboundRelay.test.js @@ -1,5 +1,8 @@ const { expect } = require('chai'); const Evervault = require('../lib'); +const { + http: { proxiedMarker }, +} = require('../lib/config'); const axios = require('axios'); const { v4 } = require('uuid'); @@ -32,6 +35,7 @@ describe('Outbound Relay Test', () => { expect(body.request.string).to.equal(false); expect(body.request.number).to.equal(false); expect(body.request.boolean).to.equal(false); + expect(response.request[proxiedMarker]).to.equal(true); }); }); }); diff --git a/lib/config.js b/lib/config.js index a7de164..1ec7237 100644 --- a/lib/config.js +++ b/lib/config.js @@ -24,6 +24,7 @@ module.exports = { pcrProviderPollInterval: process.env.EV_PCR_PROVIDER_POLL_INTERVAL || DEFAULT_PCR_PROVIDER_POLL_INTERVAL, + proxiedMarker: Symbol.for('request-proxied'), }, encryption: { secp256k1: { diff --git a/lib/types.d.ts b/lib/types.d.ts index f6b0a1e..7f86138 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -7,6 +7,7 @@ export interface HttpConfig { pollInterval: string | number; attestationDocPollInterval: string | number; pcrProviderPollInterval: string | number; + proxiedMarker: Symbol; } export interface CurveConfig { diff --git a/lib/utils/httpsHelper.js b/lib/utils/httpsHelper.js index 0935704..d5a6966 100644 --- a/lib/utils/httpsHelper.js +++ b/lib/utils/httpsHelper.js @@ -3,6 +3,9 @@ const tls = require('tls'); const Datatypes = require('./datatypes'); const certHelper = require('./certHelper'); const HttpsProxyAgent = require('./proxyAgent'); +const { + http: { proxiedMarker }, +} = require('../config'); const origCreateSecureContext = tls.createSecureContext; const EVERVAULT_DOMAINS = ['evervault.com', 'evervault.io', 'evervault.dev']; @@ -111,7 +114,9 @@ const overloadHttpsModule = ( } return arg; }); - return originalRequest.apply(this, args); + const request = originalRequest.apply(this, args); + request[proxiedMarker] = shouldProxy; + return request; } https.request = wrapMethodRequest; From cdfdf1c33dedfcae92b6a169eb134d24c0b816af Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 17 Feb 2025 16:14:12 +0000 Subject: [PATCH 3/5] chore: expand e2e tests to stress test decryption domain filtering --- e2e/outboundRelay.test.js | 75 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/e2e/outboundRelay.test.js b/e2e/outboundRelay.test.js index a9c7b06..b6feca6 100644 --- a/e2e/outboundRelay.test.js +++ b/e2e/outboundRelay.test.js @@ -38,4 +38,79 @@ describe('Outbound Relay Test', () => { expect(response.request[proxiedMarker]).to.equal(true); }); }); + + context('Enable Outbound Relay with decryption domains', () => { + it('Proxies the listed domains correctly, ignoring others', async () => { + const payload = { + string: 'some_string', + number: 1234567890, + boolean: true, + }; + const encrypted = await evervaultClient.encrypt(payload); + + await evervaultClient.enableOutboundRelay({ + decryptionDomains: ['httpbin.org'], + }); + + const [httpbinRes, syntheticRes] = await Promise.allSettled([ + axios.post('https://httpbin.org/post', encrypted), + axios.get(syntheticEndpointUrl), + ]); + + expect(httpbinRes.value.request[proxiedMarker]).to.equal(true); + expect(syntheticRes.value.request[proxiedMarker]).to.equal(false); + }); + }); + + context( + 'Enable Outbound Relay with decryption domains including paths', + () => { + it('Filters requests by path', async () => { + const payload = { + string: 'some_string', + number: 1234567890, + boolean: true, + }; + const encrypted = await evervaultClient.encrypt(payload); + + await evervaultClient.enableOutboundRelay({ + decryptionDomains: ['httpbin.org/post'], + }); + + const [postRes, getRes] = await Promise.allSettled([ + axios.post('https://httpbin.org/post', encrypted), + axios.get('https://httpbin.org/get'), + ]); + + expect(postRes.value.request[proxiedMarker]).to.equal(true); + expect(getRes.value.request[proxiedMarker]).to.equal(false); + }); + } + ); + + context( + 'Enable Outbound Relay with decryption domains including paths with wildcards', + () => { + it('Filters requests by path, respecting wildcards', async () => { + const payload = { + string: 'some_string', + number: 1234567890, + boolean: true, + }; + const encrypted = await evervaultClient.encrypt(payload); + + await evervaultClient.enableOutboundRelay({ + decryptionDomains: ['httpbin.org/post/*'], + }); + + const [matchingPostRes, failingPostRes] = await Promise.allSettled([ + axios.post('https://httpbin.org/post/somewhere', encrypted), + axios.get('https://httpbin.org/post'), + ]); + + expect(matchingPostRes.value.request[proxiedMarker]).to.equal(true); + expect(failingPostRes.value.request[proxiedMarker]).to.equal(false); + }); + } + ); }); From e7f6fd8c3c5006ad641c87129ac574083f302be5 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 17 Feb 2025 16:42:03 +0000 Subject: [PATCH 4/5] chore: update tests to ignore network failures if possible, fix bug in path matching --- e2e/outboundRelay.test.js | 50 +++++++++++++++++++++++++++++---------- lib/utils/httpsHelper.js | 2 +- package.json | 1 + 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/e2e/outboundRelay.test.js b/e2e/outboundRelay.test.js index b6feca6..0824122 100644 --- a/e2e/outboundRelay.test.js +++ b/e2e/outboundRelay.test.js @@ -1,7 +1,7 @@ const { expect } = require('chai'); const Evervault = require('../lib'); const { - http: { proxiedMarker }, + http: { proxiedMarker, certHostname }, } = require('../lib/config'); const axios = require('axios'); const { v4 } = require('uuid'); @@ -46,19 +46,35 @@ describe('Outbound Relay Test', () => { number: 1234567890, boolean: true, }; - const encrypted = await evervaultClient.encrypt(payload); await evervaultClient.enableOutboundRelay({ decryptionDomains: ['httpbin.org'], }); const [httpbinRes, syntheticRes] = await Promise.allSettled([ - axios.post('https://httpbin.org/post', encrypted), + axios.post('https://httpbin.org/post', payload), axios.get(syntheticEndpointUrl), ]); - expect(httpbinRes.value.request[proxiedMarker]).to.equal(true); - expect(syntheticRes.value.request[proxiedMarker]).to.equal(false); + // We don't need these requests to succeed, we just need them to be attempted so we can check if the symbol was set + // so we can check the proxy marker on either value or reason. + expect( + (httpbinRes.value ?? httpbinRes.reason).request[proxiedMarker] + ).to.equal(true); + expect( + (syntheticRes.value ?? syntheticRes.reason).request[proxiedMarker] + ).to.equal(false); + }); + + it('Ignores domains to always ignore even when set in decryption domains', async () => { + const caHost = new URL(certHostname).hostname; + await evervaultClient.enableOutboundRelay({ + decryptionDomains: [caHost], + }); + + const caResponse = await axios.get(certHostname); + + expect(caResponse.request[proxiedMarker]).to.equal(false); }); }); @@ -71,19 +87,22 @@ describe('Outbound Relay Test', () => { number: 1234567890, boolean: true, }; - const encrypted = await evervaultClient.encrypt(payload); await evervaultClient.enableOutboundRelay({ decryptionDomains: ['httpbin.org/post'], }); const [postRes, getRes] = await Promise.allSettled([ - axios.post('https://httpbin.org/post', encrypted), + axios.post('https://httpbin.org/post', payload), axios.get('https://httpbin.org/get'), ]); - expect(postRes.value.request[proxiedMarker]).to.equal(true); - expect(getRes.value.request[proxiedMarker]).to.equal(false); + expect( + (postRes.value ?? postRes.reason).request[proxiedMarker] + ).to.equal(true); + expect((getRes.value ?? getRes.reason).request[proxiedMarker]).to.equal( + false + ); }); } ); @@ -97,19 +116,24 @@ describe('Outbound Relay Test', () => { number: 1234567890, boolean: true, }; - const encrypted = await evervaultClient.encrypt(payload); await evervaultClient.enableOutboundRelay({ decryptionDomains: ['httpbin.org/post/*'], }); const [matchingPostRes, failingPostRes] = await Promise.allSettled([ - axios.post('https://httpbin.org/post/somewhere', encrypted), + axios.post('https://httpbin.org/post/somewhere', payload), axios.get('https://httpbin.org/post'), ]); - expect(matchingPostRes.value.request[proxiedMarker]).to.equal(true); - expect(failingPostRes.value.request[proxiedMarker]).to.equal(false); + expect( + (matchingPostRes.value ?? matchingPostRes.reason).request[ + proxiedMarker + ] + ).to.equal(true); + expect( + (failingPostRes.value ?? failingPostRes.reason).request[proxiedMarker] + ).to.equal(false); }); } ); diff --git a/lib/utils/httpsHelper.js b/lib/utils/httpsHelper.js index d5a6966..ed5ee79 100644 --- a/lib/utils/httpsHelper.js +++ b/lib/utils/httpsHelper.js @@ -75,7 +75,7 @@ const overloadHttpsModule = ( for (const arg of args) { if (arg instanceof Object) { domain = domain ?? arg.hostname ?? arg.host; - path = domain ?? arg.pathname; + path = path ?? arg.pathname; } } return { diff --git a/package.json b/package.json index 148fa7d..0a007a5 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test": "mocha 'tests/**/*.test.js' --timeout 30000", "test:e2e": "mocha 'e2e/**/*.test.js' --timeout 5000 --exit", "test:filter": "mocha 'tests/**/*.test.js' --grep", + "test:e2e:filter": "mocha 'e2e/**/*.test.js' --timeout 5000 --grep", "test:coverage": "nyc --reporter=text npm run test", "prepublishOnly": "npm run generate-types", "generate-types": "tsc lib/*.js lib/**/*.js --declaration --allowJs --emitDeclarationOnly --allowSyntheticDefaultImports --outDir types" From 880d3658e7fa3a836102b7d11c869ade0fc21f9f Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 17 Feb 2025 16:48:41 +0000 Subject: [PATCH 5/5] chore: add changeset --- .changeset/tiny-years-bake.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .changeset/tiny-years-bake.md diff --git a/.changeset/tiny-years-bake.md b/.changeset/tiny-years-bake.md new file mode 100644 index 0000000..be01f90 --- /dev/null +++ b/.changeset/tiny-years-bake.md @@ -0,0 +1,27 @@ +--- +'@evervault/sdk': minor +--- + +Extend Relay outbound/forward proxy support in Node to include the ability to filter requests by path using `decryptionDomains`. + +Requests can be filtered at the path level by appending an absolute or wildcard path to the decryption domains option, following similar wildcard logic to the domains +themselves. For example: + +```js +// Existing behaviour will be observed, proxying requests to the host 'api.com'. +const ev = new Evervault('app_uuid', 'api_key', { + decryptionDomains: ['api.com'] +}); + +// Will only proxy requests to host 'api.com' which have a path starting with '/users/'. +const ev = new Evervault('app_uuid', 'api_key', { + decryptionDomains: ['api.com/users/*'] +}); + +// Will only proxy requests to host 'api.com' which have an exact path of '/settings'. +const ev = new Evervault('app_uuid', 'api_key', { + decryptionDomains: ['api.com/settings'] +}); +``` + +This change is compatible with the existing hostname wildcard behaviour of `decryptionDomains`. \ No newline at end of file