Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/polite-dots-dig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@salesforce/mrt-utilities': minor
---

Update proxy to keep user agent and ACH for SCAPI proxy
13 changes: 11 additions & 2 deletions packages/mrt-utilities/src/middleware/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* @version 0.0.1
*/

import {Headers} from '../utils/ssr-proxying.js';
import {Headers, DEFAULT_ACCESS_CONTROL_FORWARDING_HOSTNAMES} from '../utils/ssr-proxying.js';
import {
configureProxying,
type ProxyResult,
Expand Down Expand Up @@ -327,12 +327,21 @@ export const createMRTProxyMiddlewares = (
appProtocol: string = 'http',
includeCaching: boolean = false,
createProxyFn?: CreateProxyMiddlewareFn,
accessControlHeaderForwardingHostnames: string[] = DEFAULT_ACCESS_CONTROL_FORWARDING_HOSTNAMES,
preserveUserAgent: boolean = true,
): ProxyResult[] => {
if (!proxyConfigs) {
return [];
}
const {appHostname} = getRequestProcessorParameters();
const proxies: ProxyResult[] = configureProxying(proxyConfigs, appHostname, appProtocol, createProxyFn);
const proxies: ProxyResult[] = configureProxying(
proxyConfigs,
appHostname,
appProtocol,
createProxyFn,
accessControlHeaderForwardingHostnames,
preserveUserAgent,
);
const middlewares: ProxyResult[] = [];
proxies.forEach((proxy) => {
const proxyPath = `${PROXY_PATH_BASE}/${proxy.path}`;
Expand Down
20 changes: 20 additions & 0 deletions packages/mrt-utilities/src/utils/configure-proxying.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ interface ApplyProxyRequestHeadersParams {
targetHost: string;
/** The protocol to use for the target */
targetProtocol: string;
/** Hostname suffixes for which x-sfdc-access-control should be forwarded */
accessControlHeaderForwardingHostnames?: string[];
/** When true, preserve the original User-Agent header in non-caching proxy requests */
preserveUserAgent?: boolean;
/** @internal Test hook: override rewrite function */
rewriteRequestHeaders?: (
opts: Parameters<typeof rewriteProxyRequestHeaders>[0],
Expand All @@ -57,6 +61,10 @@ interface ConfigureProxyParams {
appProtocol?: string;
/** Whether this is a caching proxy */
caching?: boolean;
/** Hostname suffixes for which x-sfdc-access-control should be forwarded */
accessControlHeaderForwardingHostnames?: string[];
/** When true, preserve the original User-Agent header in non-caching proxy requests */
preserveUserAgent?: boolean;
}

/**
Expand Down Expand Up @@ -122,6 +130,8 @@ export const applyProxyRequestHeaders = ({
proxyPath,
targetHost,
targetProtocol,
accessControlHeaderForwardingHostnames,
preserveUserAgent,
rewriteRequestHeaders: rewriteFn,
}: ApplyProxyRequestHeadersParams): void => {
const headers = incomingRequest.headers;
Expand All @@ -133,6 +143,8 @@ export const applyProxyRequestHeaders = ({
headerFormat: 'http',
targetHost,
targetProtocol,
accessControlHeaderForwardingHostnames,
preserveUserAgent,
});

// Copy any new and updated headers to the proxyRequest
Expand Down Expand Up @@ -195,6 +207,8 @@ export const configureProxy = (
targetHost,
appProtocol = /* istanbul ignore next */ 'https',
caching,
accessControlHeaderForwardingHostnames,
preserveUserAgent,
}: ConfigureProxyParams,
createProxyFn?: CreateProxyMiddlewareFn,
): ProxyResult => {
Expand Down Expand Up @@ -242,6 +256,8 @@ export const configureProxy = (
proxyPath,
targetHost,
targetProtocol,
accessControlHeaderForwardingHostnames,
preserveUserAgent,
});
},

Expand Down Expand Up @@ -302,6 +318,8 @@ export const configureProxying = (
appHostname: string,
appProtocol: string = 'https',
createProxyFn?: CreateProxyMiddlewareFn,
accessControlHeaderForwardingHostnames?: string[],
preserveUserAgent?: boolean,
): ProxyResult[] => {
const proxies: ProxyResult[] = [];
proxyConfigs.forEach((config) => {
Expand All @@ -315,6 +333,8 @@ export const configureProxying = (
appProtocol,
appHostname,
caching: false,
accessControlHeaderForwardingHostnames,
preserveUserAgent,
},
createProxyFn,
);
Expand Down
32 changes: 28 additions & 4 deletions packages/mrt-utilities/src/utils/ssr-proxying.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,19 @@ export const rewriteProxyResponseHeaders = ({
* List of x- headers that are removed from proxied requests.
* @private
*/
export const X_HEADERS_TO_REMOVE_PROXY: string[] = ['x-mobify-access-key', 'x-sfdc-access-control'];
export const X_HEADERS_TO_REMOVE_PROXY: string[] = ['x-mobify-access-key'];

const X_SFDC_ACCESS_CONTROL = 'x-sfdc-access-control';

export const DEFAULT_ACCESS_CONTROL_FORWARDING_HOSTNAMES: string[] = ['.commercecloud.salesforce.com'];

export const hostnameMatchesTransformationList = (hostname: string, hostnameSuffixes?: string[] | null): boolean => {
if (!hostnameSuffixes || hostnameSuffixes.length === 0) {
return false;
}
const hostnameOnly = hostname.split(':')[0];
return hostnameSuffixes.some((suffix) => hostnameOnly.endsWith(suffix));
};

/**
* List of x- headers that are removed from origin requests.
Expand Down Expand Up @@ -846,6 +858,10 @@ interface RewriteProxyRequestHeadersParams {
targetHost: string;
/** true to log operations */
logging?: boolean;
/** hostname suffixes for which x-sfdc-access-control should be forwarded; empty/undefined = always strip */
accessControlHeaderForwardingHostnames?: string[];
/** when true, preserve the original User-Agent header in non-caching proxy requests */
preserveUserAgent?: boolean;
}

/**
Expand All @@ -869,6 +885,8 @@ export const rewriteProxyRequestHeaders = ({
targetProtocol,
targetHost,
logging = false,
accessControlHeaderForwardingHostnames,
preserveUserAgent = true,
}: RewriteProxyRequestHeadersParams): AWSHeaders | HTTPHeaders | IncomingHttpHeaders => {
if (!headers) {
return {};
Expand All @@ -878,6 +896,12 @@ export const rewriteProxyRequestHeaders = ({
// Strip out some specific X-headers
X_HEADERS_TO_REMOVE_PROXY.forEach((key) => workingHeaders.deleteHeader(key));

// Conditionally strip x-sfdc-access-control.
// Forward it only for non-caching requests to hosts matching the suffix list.
if (caching || !hostnameMatchesTransformationList(targetHost, accessControlHeaderForwardingHostnames)) {
workingHeaders.deleteHeader(X_SFDC_ACCESS_CONTROL);
}

// For a caching proxy, apply special header processing
if (caching) {
// Remove any headers that are not on the allowlist
Expand Down Expand Up @@ -912,9 +936,9 @@ export const rewriteProxyRequestHeaders = ({
workingHeaders.setHeader(ORIGIN, targetOrigin);
}

// Replace some headers with hardwired values
if (workingHeaders.getHeader(USER_AGENT)) {
// Mimic the behaviour of CloudFront
// Replace User-Agent unless preserveUserAgent is set for non-caching proxies.
// Caching proxies always override User-Agent (handled above).
if (!preserveUserAgent && workingHeaders.getHeader(USER_AGENT)) {
workingHeaders.setHeader(USER_AGENT, 'Amazon CloudFront');
}

Expand Down
39 changes: 39 additions & 0 deletions packages/mrt-utilities/test/utils/configure-proxying.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,45 @@ describe('proxying', () => {
expect(mockProxyRequest.setHeader.called).to.be.false;
expect(mockProxyRequest.removeHeader.called).to.be.false;
});

it('forwards x-sfdc-access-control when hostname matches forwarding list', () => {
mockIncomingRequest.headers = {
'x-sfdc-access-control': 'test-value',
'content-type': 'application/json',
};

applyProxyRequestHeaders({
proxyRequest: mockProxyRequest as unknown as ClientRequest,
incomingRequest: mockIncomingRequest as unknown as IncomingMessage,
caching: false,
proxyPath: '/api',
targetHost: 'api.commercecloud.salesforce.com',
targetProtocol: 'https',
accessControlHeaderForwardingHostnames: ['.commercecloud.salesforce.com'],
});

expect(mockProxyRequest.setHeader.calledWith('x-sfdc-access-control', 'test-value')).to.be.true;
expect(mockProxyRequest.removeHeader.calledWith('x-sfdc-access-control')).to.be.false;
});

it('strips x-sfdc-access-control when hostname does not match forwarding list', () => {
mockIncomingRequest.headers = {
'x-sfdc-access-control': 'test-value',
'content-type': 'application/json',
};

applyProxyRequestHeaders({
proxyRequest: mockProxyRequest as unknown as ClientRequest,
incomingRequest: mockIncomingRequest as unknown as IncomingMessage,
caching: false,
proxyPath: '/api',
targetHost: 'other.example.com',
targetProtocol: 'https',
accessControlHeaderForwardingHostnames: ['.commercecloud.salesforce.com'],
});

expect(mockProxyRequest.removeHeader.calledWith('x-sfdc-access-control')).to.be.true;
});
});

describe('configureProxy', () => {
Expand Down
Loading
Loading