diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index bd1268a43..f59c75c42 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -60,6 +60,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.UIManager; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeArray; @@ -488,7 +489,15 @@ private WebViewAssetLoader buildAssetLoader(RNCWebViewContainer view, ReadableMa FLog.w(TAG, "WebViewAssetLoader error. Skipping Path Handler. Directory is missing for internal handler path"); continue; } - builder.addPathHandler(handlerPath, new WebViewAssetLoader.InternalStoragePathHandler(view.getContext(), new File(directory))); + WebViewAssetLoader.PathHandler internalHandler = + new WebViewAssetLoader.InternalStoragePathHandler(view.getContext(), new File(directory)); + // Optional fixed response headers (e.g. Permissions-Policy on a locally served document). The default + // InternalStoragePathHandler sets none, so wrap it when any are configured. + Map responseHeaders = readStringMap(handler.getMap("responseHeaders")); + if (!responseHeaders.isEmpty()) { + internalHandler = new HeaderInjectingPathHandler(internalHandler, responseHeaders); + } + builder.addPathHandler(handlerPath, internalHandler); } } } else { @@ -498,6 +507,51 @@ private WebViewAssetLoader buildAssetLoader(RNCWebViewContainer view, ReadableMa return builder.build(); } + private static Map readStringMap(@Nullable ReadableMap map) { + Map result = new HashMap<>(); + if (map == null) { + return result; + } + ReadableMapKeySetIterator iterator = map.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + if (map.getType(key) == ReadableType.String) { + result.put(key, map.getString(key)); + } + } + return result; + } + + /** + * Wraps a PathHandler and merges a fixed set of response headers onto whatever it serves. InternalStoragePathHandler + * sets no headers, so this lets a locally served document (the embedded-app shell) carry a header-based policy such + * as Permissions-Policy. A header policy on the top-level document applies to its whole frame tree and cannot be + * re-enabled by scripts or a child frame's allow attribute. + */ + private static class HeaderInjectingPathHandler implements WebViewAssetLoader.PathHandler { + private final WebViewAssetLoader.PathHandler delegate; + private final Map extraHeaders; + + HeaderInjectingPathHandler(WebViewAssetLoader.PathHandler delegate, Map extraHeaders) { + this.delegate = delegate; + this.extraHeaders = extraHeaders; + } + + @Nullable + @Override + public WebResourceResponse handle(@NonNull String path) { + WebResourceResponse response = delegate.handle(path); + if (response == null) { + return null; + } + Map headers = response.getResponseHeaders(); + Map merged = headers != null ? new HashMap<>(headers) : new HashMap<>(); + merged.putAll(extraHeaders); + response.setResponseHeaders(merged); + return response; + } + } + @ReactProp(name = "overScrollMode") public void setOverScrollMode(RNCWebViewContainer view, String overScrollModeString) { diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index 28208a75c..f27a3e77c 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -214,9 +214,20 @@ export interface AndroidAssetLoaderPathHandler { * Certain existing subdirectories of getDataDir are also not permitted as they are often sensitive. * These files are ("app_webview/", "databases/", "lib/", "shared_prefs/" and "code_cache/"). * - * This field is unused for type 'resources' or 'assets' + * This field is unused for type 'resources' or 'assets' */ directory?: string; + + /** + * Fixed response headers to merge onto every response this handler serves. + * + * InternalStoragePathHandler sets no headers of its own, so this is the way to attach a header-based policy + * (e.g. `Permissions-Policy`) to a locally served document. A header policy on the top-level document applies to + * its whole frame tree and cannot be re-enabled by scripts or a child frame's `allow` attribute. + * + * Only honored for type 'internal'. + */ + responseHeaders?: {[header: string]: string}; }