2121import java .util .Calendar ;
2222import java .util .Collections ;
2323import java .util .HashMap ;
24+ import java .util .HashSet ;
2425import java .util .List ;
2526import java .util .Locale ;
2627import java .util .Map ;
2728import java .util .Objects ;
2829import java .util .Set ;
2930import java .util .TimeZone ;
3031import java .util .concurrent .atomic .AtomicLong ;
32+ import java .util .regex .Matcher ;
33+ import java .util .regex .Pattern ;
3134import java .util .stream .Collectors ;
3235
3336import javax .annotation .PostConstruct ;
4144import org .apache .sling .api .resource .ResourceResolver ;
4245import org .apache .sling .models .annotations .Model ;
4346import org .apache .sling .models .annotations .injectorspecific .InjectionStrategy ;
47+ import org .apache .sling .models .annotations .injectorspecific .OSGiService ;
4448import org .apache .sling .models .annotations .injectorspecific .ScriptVariable ;
4549import org .slf4j .Logger ;
4650import org .slf4j .LoggerFactory ;
@@ -92,6 +96,11 @@ public class MagentoGraphqlClientImpl implements MagentoGraphqlClient {
9296 private Resource resource ;
9397 @ ScriptVariable (injectionStrategy = InjectionStrategy .OPTIONAL )
9498 private Page currentPage ;
99+ // Admin-configured forwarding of incoming request headers (generic headers, plus a dedicated client IP
100+ // section) to the outbound Commerce request. Which header/source and how to parse the client IP differs per
101+ // CDN/dispatcher (AEMaaCS vs. on-premise, and across CDN vendors), hence configuration, not hardcoded here.
102+ @ OSGiService (injectionStrategy = InjectionStrategy .OPTIONAL )
103+ private ForwardedHeadersConfigService forwardedHeadersConfigService ;
95104
96105 private GraphqlClient graphqlClient ;
97106 private RequestOptions requestOptions ;
@@ -208,6 +217,20 @@ private void initModel(Resource resource, Page page, SlingHttpServletRequest req
208217 httpMethod = HttpMethod .POST ;
209218 }
210219
220+ // Headers carrying per-request metadata (client IP, forwarded tracing/correlation ids, ...) must not
221+ // influence the GraphQL response cache key, since they do not affect the response itself.
222+ Set <String > nonCacheKeyHeaderNames = new HashSet <>();
223+
224+ if (request != null && forwardedHeadersConfigService != null && forwardedHeadersConfigService .isEnabled ()) {
225+ // Checked separately from the generic forwarded headers below, since the client IP needs its own
226+ // source pattern and outbound name rather than a plain same-name pass-through.
227+ if (forwardedHeadersConfigService .isClientIpEnabled ()) {
228+ applyClientIpForwarding (request , forwardedHeadersConfigService , headers , nonCacheKeyHeaderNames );
229+ }
230+
231+ applyGenericHeaderForwarding (request , forwardedHeadersConfigService , headers , nonCacheKeyHeaderNames );
232+ }
233+
211234 this .httpHeaders = headers ;
212235 // In certain situations resource.getResourceType() returns an enforced resource type.
213236 // We prefer the resource type of the component proxy for the cache name.
@@ -218,6 +241,7 @@ private void initModel(Resource resource, Page page, SlingHttpServletRequest req
218241 .withCacheName (cacheName )
219242 .withDataFetchingPolicy (DataFetchingPolicy .CACHE_FIRST ))
220243 .withHeaders (headers .size () > 0 ? headers : null )
244+ .withNonCacheKeyHeaderNames (nonCacheKeyHeaderNames )
221245 .withHttpMethod (httpMethod );
222246
223247 if (request != null ) {
@@ -361,6 +385,85 @@ private static List<Header> getCustomHttpHeaders(ComponentsConfiguration configu
361385 return headers ;
362386 }
363387
388+ /**
389+ * Forwards the client IP using the dedicated source header/pattern/outbound name from
390+ * {@link ForwardedHeadersConfig}, since the client IP needs more than a plain same-name pass-through: a source
391+ * that may be {@code REMOTE_ADDR}, a value extraction pattern, and typically a different outbound name.
392+ */
393+ private static void applyClientIpForwarding (SlingHttpServletRequest request , ForwardedHeadersConfigService config ,
394+ List <Header > headers , Set <String > nonCacheKeyHeaderNames ) {
395+ String outboundHeaderName = config .getClientIpOutboundHeaderName ();
396+ String value = readRequestValue (request , config .getClientIpHeaderName (), config .getClientIpHeaderValuePattern ());
397+ addForwardedHeader (outboundHeaderName , value , headers , nonCacheKeyHeaderNames );
398+ }
399+
400+ /**
401+ * Forwards each configured generic header (e.g. a tracing/correlation id) as-is, under the same name, with no
402+ * value extraction pattern. Kept separate from client IP forwarding above so each stays simple to read.
403+ */
404+ private static void applyGenericHeaderForwarding (SlingHttpServletRequest request , ForwardedHeadersConfigService config ,
405+ List <Header > headers , Set <String > nonCacheKeyHeaderNames ) {
406+ for (String headerName : config .getForwardedHeaderNames ()) {
407+ String value = readRequestValue (request , headerName , null );
408+ addForwardedHeader (headerName , value , headers , nonCacheKeyHeaderNames );
409+ }
410+ }
411+
412+ /**
413+ * Adds {@code value} to {@code headers} under {@code outboundHeaderName} and marks it as excluded from the
414+ * response cache key, unless the value is missing, the name is denylisted, or a header with that name is
415+ * already present (a statically configured header always takes precedence).
416+ */
417+ private static void addForwardedHeader (String outboundHeaderName , String value , List <Header > headers ,
418+ Set <String > nonCacheKeyHeaderNames ) {
419+ if (value == null ) {
420+ return ;
421+ }
422+ if (DENIED_HEADERS .contains (outboundHeaderName .toLowerCase (Locale .ROOT ))) {
423+ LOGGER .warn ("Ignoring denylisted outbound header '{}' configured for forwarding" , outboundHeaderName );
424+ return ;
425+ }
426+ if (headers .stream ().noneMatch (header -> header .getName ().equalsIgnoreCase (outboundHeaderName ))) {
427+ headers .add (new BasicHeader (outboundHeaderName , value ));
428+ nonCacheKeyHeaderNames .add (outboundHeaderName );
429+ }
430+ }
431+
432+ /**
433+ * Reads the value to forward from the configured source: either the direct TCP connection IP
434+ * ({@code REMOTE_ADDR}), or the named incoming header, optionally parsed with a pattern. A {@code null}
435+ * pattern means the header's raw value is forwarded as-is. Which source to read, and how to parse it, is
436+ * configuration ({@link ForwardedHeadersConfig}) rather than hardcoded here, since different CDNs/dispatchers
437+ * in front of AEM (AEMaaCS vs. on-premise, and across CDN vendors) expose values like the client IP
438+ * differently.
439+ */
440+ private static String readRequestValue (SlingHttpServletRequest request , String headerName , Pattern headerValuePattern ) {
441+ if (StringUtils .isBlank (headerName )) {
442+ return null ;
443+ }
444+
445+ if (ForwardedHeadersConfigService .REMOTE_ADDR .equalsIgnoreCase (headerName )) {
446+ return StringUtils .trimToNull (request .getRemoteAddr ());
447+ }
448+
449+ String headerValue = StringUtils .trimToNull (request .getHeader (headerName ));
450+ if (headerValue == null ) {
451+ return null ;
452+ }
453+
454+ if (headerValuePattern == null ) {
455+ return headerValue ;
456+ }
457+
458+ Matcher matcher = headerValuePattern .matcher (headerValue );
459+ if (matcher .find ()) {
460+ return matcher .group (1 );
461+ }
462+
463+ LOGGER .warn ("Could not extract a value from header '{}' using the configured pattern" , headerName );
464+ return null ;
465+ }
466+
364467 private static Long getTimeWarpEpoch (SlingHttpServletRequest request ) {
365468 String timeWarp = request .getParameter ("timewarp" );
366469 if (timeWarp == null ) {
0 commit comments