Skip to content

Commit b32e374

Browse files
committed
@W-22283814: Forward x-sfdc-access-control header and preserve User-Agent in local dev proxy
Update the local dev proxy to conditionally forward the x-sfdc-access-control header to SCAPI proxy targets whose hostname matches a configured suffix list, mirroring the Lambda@Edge behavior from portal_app PRs 7680 and 7712. Also preserve the original User-Agent header in non-caching proxy requests instead of overwriting it with 'Amazon CloudFront'.
1 parent ac0da1b commit b32e374

5 files changed

Lines changed: 254 additions & 6 deletions

File tree

packages/mrt-utilities/src/middleware/middleware.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* @version 0.0.1
1616
*/
1717

18-
import {Headers} from '../utils/ssr-proxying.js';
18+
import {Headers, DEFAULT_ACCESS_CONTROL_FORWARDING_HOSTNAMES} from '../utils/ssr-proxying.js';
1919
import {
2020
configureProxying,
2121
type ProxyResult,
@@ -327,12 +327,21 @@ export const createMRTProxyMiddlewares = (
327327
appProtocol: string = 'http',
328328
includeCaching: boolean = false,
329329
createProxyFn?: CreateProxyMiddlewareFn,
330+
accessControlHeaderForwardingHostnames: string[] = DEFAULT_ACCESS_CONTROL_FORWARDING_HOSTNAMES,
331+
preserveUserAgent: boolean = true,
330332
): ProxyResult[] => {
331333
if (!proxyConfigs) {
332334
return [];
333335
}
334336
const {appHostname} = getRequestProcessorParameters();
335-
const proxies: ProxyResult[] = configureProxying(proxyConfigs, appHostname, appProtocol, createProxyFn);
337+
const proxies: ProxyResult[] = configureProxying(
338+
proxyConfigs,
339+
appHostname,
340+
appProtocol,
341+
createProxyFn,
342+
accessControlHeaderForwardingHostnames,
343+
preserveUserAgent,
344+
);
336345
const middlewares: ProxyResult[] = [];
337346
proxies.forEach((proxy) => {
338347
const proxyPath = `${PROXY_PATH_BASE}/${proxy.path}`;

packages/mrt-utilities/src/utils/configure-proxying.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ interface ApplyProxyRequestHeadersParams {
3535
targetHost: string;
3636
/** The protocol to use for the target */
3737
targetProtocol: string;
38+
/** Hostname suffixes for which x-sfdc-access-control should be forwarded */
39+
accessControlHeaderForwardingHostnames?: string[];
40+
/** When true, preserve the original User-Agent header in non-caching proxy requests */
41+
preserveUserAgent?: boolean;
3842
/** @internal Test hook: override rewrite function */
3943
rewriteRequestHeaders?: (
4044
opts: Parameters<typeof rewriteProxyRequestHeaders>[0],
@@ -57,6 +61,10 @@ interface ConfigureProxyParams {
5761
appProtocol?: string;
5862
/** Whether this is a caching proxy */
5963
caching?: boolean;
64+
/** Hostname suffixes for which x-sfdc-access-control should be forwarded */
65+
accessControlHeaderForwardingHostnames?: string[];
66+
/** When true, preserve the original User-Agent header in non-caching proxy requests */
67+
preserveUserAgent?: boolean;
6068
}
6169

6270
/**
@@ -122,6 +130,8 @@ export const applyProxyRequestHeaders = ({
122130
proxyPath,
123131
targetHost,
124132
targetProtocol,
133+
accessControlHeaderForwardingHostnames,
134+
preserveUserAgent,
125135
rewriteRequestHeaders: rewriteFn,
126136
}: ApplyProxyRequestHeadersParams): void => {
127137
const headers = incomingRequest.headers;
@@ -133,6 +143,8 @@ export const applyProxyRequestHeaders = ({
133143
headerFormat: 'http',
134144
targetHost,
135145
targetProtocol,
146+
accessControlHeaderForwardingHostnames,
147+
preserveUserAgent,
136148
});
137149

138150
// Copy any new and updated headers to the proxyRequest
@@ -195,6 +207,8 @@ export const configureProxy = (
195207
targetHost,
196208
appProtocol = /* istanbul ignore next */ 'https',
197209
caching,
210+
accessControlHeaderForwardingHostnames,
211+
preserveUserAgent,
198212
}: ConfigureProxyParams,
199213
createProxyFn?: CreateProxyMiddlewareFn,
200214
): ProxyResult => {
@@ -242,6 +256,8 @@ export const configureProxy = (
242256
proxyPath,
243257
targetHost,
244258
targetProtocol,
259+
accessControlHeaderForwardingHostnames,
260+
preserveUserAgent,
245261
});
246262
},
247263

@@ -302,6 +318,8 @@ export const configureProxying = (
302318
appHostname: string,
303319
appProtocol: string = 'https',
304320
createProxyFn?: CreateProxyMiddlewareFn,
321+
accessControlHeaderForwardingHostnames?: string[],
322+
preserveUserAgent?: boolean,
305323
): ProxyResult[] => {
306324
const proxies: ProxyResult[] = [];
307325
proxyConfigs.forEach((config) => {
@@ -315,6 +333,8 @@ export const configureProxying = (
315333
appProtocol,
316334
appHostname,
317335
caching: false,
336+
accessControlHeaderForwardingHostnames,
337+
preserveUserAgent,
318338
},
319339
createProxyFn,
320340
);

packages/mrt-utilities/src/utils/ssr-proxying.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,24 @@ export const rewriteProxyResponseHeaders = ({
766766
* List of x- headers that are removed from proxied requests.
767767
* @private
768768
*/
769-
export const X_HEADERS_TO_REMOVE_PROXY: string[] = ['x-mobify-access-key', 'x-sfdc-access-control'];
769+
export const X_HEADERS_TO_REMOVE_PROXY: string[] = ['x-mobify-access-key'];
770+
771+
const X_SFDC_ACCESS_CONTROL = 'x-sfdc-access-control';
772+
773+
export const DEFAULT_ACCESS_CONTROL_FORWARDING_HOSTNAMES: string[] = [
774+
'.commercecloud.salesforce.com',
775+
];
776+
777+
export const hostnameMatchesTransformationList = (
778+
hostname: string,
779+
hostnameSuffixes?: string[] | null,
780+
): boolean => {
781+
if (!hostnameSuffixes || hostnameSuffixes.length === 0) {
782+
return false;
783+
}
784+
const hostnameOnly = hostname.split(':')[0];
785+
return hostnameSuffixes.some((suffix) => hostnameOnly.endsWith(suffix));
786+
};
770787

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

851872
/**
@@ -869,6 +890,8 @@ export const rewriteProxyRequestHeaders = ({
869890
targetProtocol,
870891
targetHost,
871892
logging = false,
893+
accessControlHeaderForwardingHostnames,
894+
preserveUserAgent = false,
872895
}: RewriteProxyRequestHeadersParams): AWSHeaders | HTTPHeaders | IncomingHttpHeaders => {
873896
if (!headers) {
874897
return {};
@@ -878,6 +901,12 @@ export const rewriteProxyRequestHeaders = ({
878901
// Strip out some specific X-headers
879902
X_HEADERS_TO_REMOVE_PROXY.forEach((key) => workingHeaders.deleteHeader(key));
880903

904+
// Conditionally strip x-sfdc-access-control.
905+
// Forward it only for non-caching requests to hosts matching the suffix list.
906+
if (caching || !hostnameMatchesTransformationList(targetHost, accessControlHeaderForwardingHostnames)) {
907+
workingHeaders.deleteHeader(X_SFDC_ACCESS_CONTROL);
908+
}
909+
881910
// For a caching proxy, apply special header processing
882911
if (caching) {
883912
// Remove any headers that are not on the allowlist
@@ -912,9 +941,9 @@ export const rewriteProxyRequestHeaders = ({
912941
workingHeaders.setHeader(ORIGIN, targetOrigin);
913942
}
914943

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

packages/mrt-utilities/test/utils/configure-proxying.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,45 @@ describe('proxying', () => {
9696
expect(mockProxyRequest.setHeader.called).to.be.false;
9797
expect(mockProxyRequest.removeHeader.called).to.be.false;
9898
});
99+
100+
it('forwards x-sfdc-access-control when hostname matches forwarding list', () => {
101+
mockIncomingRequest.headers = {
102+
'x-sfdc-access-control': 'test-value',
103+
'content-type': 'application/json',
104+
};
105+
106+
applyProxyRequestHeaders({
107+
proxyRequest: mockProxyRequest as unknown as ClientRequest,
108+
incomingRequest: mockIncomingRequest as unknown as IncomingMessage,
109+
caching: false,
110+
proxyPath: '/api',
111+
targetHost: 'api.commercecloud.salesforce.com',
112+
targetProtocol: 'https',
113+
accessControlHeaderForwardingHostnames: ['.commercecloud.salesforce.com'],
114+
});
115+
116+
expect(mockProxyRequest.setHeader.calledWith('x-sfdc-access-control', 'test-value')).to.be.true;
117+
expect(mockProxyRequest.removeHeader.calledWith('x-sfdc-access-control')).to.be.false;
118+
});
119+
120+
it('strips x-sfdc-access-control when hostname does not match forwarding list', () => {
121+
mockIncomingRequest.headers = {
122+
'x-sfdc-access-control': 'test-value',
123+
'content-type': 'application/json',
124+
};
125+
126+
applyProxyRequestHeaders({
127+
proxyRequest: mockProxyRequest as unknown as ClientRequest,
128+
incomingRequest: mockIncomingRequest as unknown as IncomingMessage,
129+
caching: false,
130+
proxyPath: '/api',
131+
targetHost: 'other.example.com',
132+
targetProtocol: 'https',
133+
accessControlHeaderForwardingHostnames: ['.commercecloud.salesforce.com'],
134+
});
135+
136+
expect(mockProxyRequest.removeHeader.calledWith('x-sfdc-access-control')).to.be.true;
137+
});
99138
});
100139

101140
describe('configureProxy', () => {

0 commit comments

Comments
 (0)