Files
arch-panel-extension-browser/page-bridge.js

794 lines
20 KiB
JavaScript

(() => {
if (window.__archPanelPageBridgeInstalled) {
return;
}
window.__archPanelPageBridgeInstalled = true;
const MESSAGE_SOURCE = "arch-panel-extension";
const API_PATH_FRAGMENT = "/oalcrm/web/api/";
const EXTRA_CAPTURE_ORIGINS = new Set([
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com",
]);
const ALLOWED_TOP_MESSAGE_ORIGINS = new Set([
"https://eeho.fa.us2.oraclecloud.com",
"https://spa.oracle.com",
]);
const AUTH_STORAGE_HINT_PATTERN =
/(authorization|token|wwb|wlwb|provider|spa-ts|spats)/i;
const SAFE_HEADER_BLOCKLIST = new Set([
"accept-encoding",
"accept-language",
"connection",
"content-length",
"cookie",
"host",
"origin",
"referer",
"sec-ch-ua",
"sec-ch-ua-mobile",
"sec-ch-ua-platform",
"sec-fetch-dest",
"sec-fetch-mode",
"sec-fetch-site",
"user-agent",
]);
const bridgeState = {
globalHeaders: new Map(),
scopedHeaders: new Map(),
};
function getSafeTargetOrigin() {
const origin = String(window.location?.origin || "");
return origin && origin !== "null" ? origin : "*";
}
function safePostMessage(payload, targetWindow = window, targetOrigin = getSafeTargetOrigin()) {
try {
targetWindow.postMessage(payload, targetOrigin);
return true;
} catch {
return false;
}
}
function getAllowedTopMessageOrigin() {
const candidates = [];
if (window.top === window) {
return getSafeTargetOrigin();
}
try {
const referrerOrigin = document.referrer
? new URL(document.referrer).origin
: "";
if (referrerOrigin) {
candidates.push(referrerOrigin);
}
} catch {
// Referrer parsing is best-effort; fall back to ancestorOrigins/current origin.
}
try {
const ancestorOrigins = Array.from(window.location?.ancestorOrigins || []);
candidates.push(...ancestorOrigins);
} catch {
// ancestorOrigins is browser-specific and may be unavailable.
}
for (const origin of candidates) {
if (ALLOWED_TOP_MESSAGE_ORIGINS.has(origin)) {
return origin;
}
}
return "";
}
function safePostToTop(payload) {
const targetOrigin = getAllowedTopMessageOrigin();
if (!targetOrigin) {
return false;
}
return safePostMessage(payload, window.top, targetOrigin);
}
const nativeFetch = window.fetch.bind(window);
const originalOpen = window.XMLHttpRequest.prototype.open;
const originalSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
const originalSend = window.XMLHttpRequest.prototype.send;
function isIntegrationOrigin(url) {
try {
return (
new URL(url, window.location.origin).origin ===
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com"
);
} catch {
return false;
}
}
function normalizeUrl(input) {
try {
return new URL(input, window.location.origin).toString();
} catch {
return "";
}
}
function shouldCapture(url) {
if (!url) {
return false;
}
if (url.includes(API_PATH_FRAGMENT)) {
return true;
}
try {
return EXTRA_CAPTURE_ORIGINS.has(new URL(url).origin);
} catch {
return false;
}
}
function normalizeHeaders(input) {
const normalized = new Map();
if (!input) {
return normalized;
}
const headers = new Headers(input);
headers.forEach((value, key) => {
const lowerKey = key.toLowerCase();
if (SAFE_HEADER_BLOCKLIST.has(lowerKey)) {
return;
}
if (lowerKey.startsWith("sec-")) {
return;
}
normalized.set(lowerKey, value);
});
return normalized;
}
function getScopeKeys(url) {
const normalizedUrl = normalizeUrl(url);
if (!normalizedUrl) {
return [];
}
try {
const parsedUrl = new URL(normalizedUrl);
const path = parsedUrl.pathname;
const keys = [`path:${path}`];
const providerProxyMatch = path.match(
/\/provider-proxy\/([^/]+)\//
);
const serviceMatch = path.match(
/\/provider-proxy\/[^/]+\/service\/([^/]+)\//
);
if (path.includes("/provider-proxy/")) {
keys.push("scope:provider-proxy");
}
if (path.includes("/identity-management/")) {
keys.push("scope:identity-management");
}
if (path.includes("/workbench-proxy/")) {
keys.push("scope:workbench-proxy");
}
if (path.includes("/elastic/")) {
keys.push("scope:elastic");
}
if (providerProxyMatch?.[1]) {
keys.push(`provider:${providerProxyMatch[1]}`);
}
if (serviceMatch?.[1]) {
keys.push(`service:${serviceMatch[1]}`);
}
keys.push("scope:api");
return keys;
} catch {
return [];
}
}
function mergeIntoBucket(bucket, headers) {
headers.forEach((value, key) => {
bucket.set(key, value);
});
}
function isAuthorizationLikeHeader(key) {
const normalizedKey = String(key || "").toLowerCase();
return (
normalizedKey === "authorization" ||
normalizedKey.endsWith("-authorization") ||
normalizedKey.includes("token")
);
}
function rememberHeaders(url, input) {
const headers = normalizeHeaders(input);
if (headers.size === 0) {
return;
}
headers.forEach((value, key) => {
if (!isAuthorizationLikeHeader(key)) {
bridgeState.globalHeaders.set(key, value);
}
});
for (const scopeKey of getScopeKeys(url)) {
const bucket = bridgeState.scopedHeaders.get(scopeKey) || new Map();
mergeIntoBucket(bucket, headers);
bridgeState.scopedHeaders.set(scopeKey, bucket);
}
publishCapturedAuthHeaders(url, headers);
}
function publishCapturedAuthHeaders(url, headers) {
const authHeaders = {};
headers.forEach((value, key) => {
if (isAuthorizationLikeHeader(key)) {
authHeaders[key] = value;
}
});
if (!Object.keys(authHeaders).length) {
return;
}
safePostToTop(
{
source: MESSAGE_SOURCE,
type: "ARCH_PANEL_WORKBENCH_AUTH_HEADERS",
url: normalizeUrl(url),
headers: authHeaders,
}
);
}
function buildHeaders(url, input, body) {
const merged = new Headers();
const activeScopeKeys = getScopeKeys(url);
bridgeState.globalHeaders.forEach((value, key) => {
merged.set(key, value);
});
for (const scopeKey of activeScopeKeys) {
const bucket = bridgeState.scopedHeaders.get(scopeKey);
if (!bucket) {
continue;
}
bucket.forEach((value, key) => {
merged.set(key, value);
});
}
normalizeHeaders(input).forEach((value, key) => {
merged.set(key, value);
});
if (activeScopeKeys.includes("scope:provider-proxy")) {
collectStoredAuthHeaders().forEach((value, key) => {
if (!merged.has(key)) {
merged.set(key, value);
}
});
}
if (!merged.has("accept")) {
merged.set("accept", "application/json, text/plain, */*");
}
if (!merged.has("x-requested-with")) {
merged.set("x-requested-with", "XMLHttpRequest");
}
if (activeScopeKeys.includes("scope:provider-proxy")) {
const allowedProviderHeaders = new Set([
"accept",
"authorization",
"content-type",
"spa-ts-authorization",
"wwb-provider-authorization",
"x-id-token",
"x-requested-with",
]);
for (const key of Array.from(merged.keys())) {
if (key.endsWith("-authorization") && !allowedProviderHeaders.has(key)) {
merged.delete(key);
}
}
if (merged.has("wwb-provider-authorization")) {
merged.delete("authorization");
}
}
if (isIntegrationOrigin(url)) {
const allowedIntegrationHeaders = new Set([
"accept",
"authorization",
"content-type",
"spa-ts-authorization",
"wwb-provider-authorization",
"x-id-token",
"x-requested-with",
]);
for (const key of Array.from(merged.keys())) {
if (!allowedIntegrationHeaders.has(key)) {
merged.delete(key);
}
}
if (merged.has("wwb-provider-authorization")) {
merged.delete("authorization");
}
}
if (!body) {
merged.delete("content-type");
}
return merged;
}
function buildHeaderSnapshot(options = {}) {
const snapshot = new Headers();
const scopeUrl = options.url || window.location.href;
const merged = buildHeaders(scopeUrl, options.headers, options.body);
merged.forEach((value, key) => {
snapshot.set(key, value);
});
if (options.includeAllAuth) {
bridgeState.scopedHeaders.forEach((bucket) => {
bucket.forEach((value, key) => {
if (isAuthorizationLikeHeader(key)) {
snapshot.set(key, value);
}
});
});
collectStoredAuthHeaders().forEach((value, key) => {
if (!snapshot.has(key)) {
snapshot.set(key, value);
}
});
}
return Object.fromEntries(snapshot.entries());
}
function collectStoredAuthHeaders() {
const headers = new Map();
const candidates = [];
for (const storage of [window.localStorage, window.sessionStorage]) {
try {
for (let index = 0; index < storage.length; index += 1) {
const key = storage.key(index) || "";
const value = storage.getItem(key) || "";
if (isAuthStorageCandidate(key, value)) {
candidates.push([key, value]);
}
}
} catch {
// Ignore storage access restrictions.
}
}
for (const [key, value] of candidates) {
collectAuthFromStorageEntry(headers, key, value);
}
return headers;
}
function isAuthStorageCandidate(key, value) {
const normalizedKey = String(key || "");
const normalizedValue = String(value || "").trim();
if (!normalizedKey && !normalizedValue) {
return false;
}
if (AUTH_STORAGE_HINT_PATTERN.test(normalizedKey)) {
return true;
}
if (looksLikeJwt(normalizedValue)) {
return true;
}
return AUTH_STORAGE_HINT_PATTERN.test(normalizedValue);
}
function collectAuthFromStorageEntry(headers, key, value) {
const normalizedKey = String(key || "").toLowerCase();
const normalizedValue = String(value || "").trim();
if (!normalizedValue) {
return;
}
if (
normalizedKey === "wwb-provider-authorization" ||
normalizedKey.includes("wwb-provider-authorization")
) {
headers.set("wwb-provider-authorization", normalizedValue);
}
if (
normalizedKey === "spa-ts-authorization" ||
normalizedKey.includes("spa-ts-authorization")
) {
headers.set("spa-ts-authorization", normalizedValue);
}
if (normalizedKey === "authorization") {
headers.set("authorization", normalizedValue);
}
collectAuthFromTokenString(headers, normalizedValue);
if (!(normalizedValue.startsWith("{") || normalizedValue.startsWith("["))) {
return;
}
try {
collectAuthFromObject(headers, JSON.parse(normalizedValue));
} catch {
// Ignore non-JSON values.
}
}
function collectAuthFromObject(headers, value, depth = 0) {
if (!value || typeof value !== "object" || depth > 5) {
return;
}
if (Array.isArray(value)) {
value.forEach((item) => collectAuthFromObject(headers, item, depth + 1));
return;
}
for (const [key, entryValue] of Object.entries(value)) {
const normalizedKey = String(key || "").toLowerCase();
if (typeof entryValue === "string" && entryValue.trim()) {
collectAuthFromTokenString(headers, entryValue.trim());
if (
normalizedKey === "wwb-provider-authorization" ||
normalizedKey.includes("wwbprovider") ||
normalizedKey.includes("wwb-provider")
) {
headers.set("wwb-provider-authorization", entryValue.trim());
}
if (
normalizedKey === "spa-ts-authorization" ||
normalizedKey.includes("spats") ||
normalizedKey.includes("spa-ts")
) {
headers.set("spa-ts-authorization", entryValue.trim());
}
if (normalizedKey === "authorization") {
headers.set("authorization", entryValue.trim());
}
}
collectAuthFromObject(headers, entryValue, depth + 1);
}
}
function collectAuthFromTokenString(headers, value) {
const token = String(value || "").trim();
if (!looksLikeJwt(token)) {
return;
}
const payload = decodeJwtPayload(token);
if (!payload) {
return;
}
const issuedFor = String(payload.issuedFor || payload.aud || "").toUpperCase();
const authorities = Array.isArray(payload.authorities)
? payload.authorities.map((authority) => String(authority || "").toUpperCase())
: [];
const scopes = Array.isArray(payload.scope)
? payload.scope.map((scope) => String(scope || "").toUpperCase())
: String(payload.scope || "")
.split(/\s+/)
.map((scope) => scope.toUpperCase())
.filter(Boolean);
const isWwbToken =
issuedFor.includes("WWB") ||
authorities.some((authority) => authority.startsWith("WLWB.")) ||
scopes.some((scope) => scope.includes("WWB") || scope.includes("WLWB"));
if (isWwbToken && !headers.has("wwb-provider-authorization")) {
headers.set("wwb-provider-authorization", token);
}
}
function looksLikeJwt(value) {
return /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(
String(value || "").trim()
);
}
function decodeJwtPayload(token) {
try {
const payloadPart = String(token).split(".")[1];
const base64 = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
const paddedBase64 = base64.padEnd(
base64.length + (4 - (base64.length % 4 || 4)),
"="
);
const json = decodeURIComponent(
Array.from(atob(paddedBase64), (char) => {
return `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`;
}).join("")
);
return JSON.parse(json);
} catch {
return null;
}
}
async function fetchWithCapturedHeaders(url, options = {}) {
const mergedHeaders = buildHeaders(url, options.headers, options.body);
const response = await nativeFetch(url, {
method: options.method || "GET",
credentials: "include",
cache: "no-store",
headers: mergedHeaders,
body: options.body,
});
const rawText = await response.text();
let payload;
try {
payload = rawText ? JSON.parse(rawText) : null;
} catch {
payload = rawText;
}
return {
ok: response.ok,
status: response.status,
statusText: response.statusText,
payload,
debug: {
url,
headerKeys: Array.from(mergedHeaders.keys()),
scopeKeys: getScopeKeys(url),
spaTabFetch: true,
},
};
}
window.__archPanelExtensionBridge = {
buildHeaderSnapshot,
fetchWithCapturedHeaders,
};
const trackedFetch = async function archPanelTrackedFetch(input, init) {
const url = normalizeUrl(typeof input === "string" ? input : input?.url);
if (shouldCapture(url)) {
rememberHeaders(url, init?.headers || input?.headers);
}
return nativeFetch(input, init);
};
window.fetch = trackedFetch;
window.XMLHttpRequest.prototype.open = function archPanelTrackedOpen(method, url, ...rest) {
this.__archPanelTrackedUrl = normalizeUrl(url);
this.__archPanelTrackedHeaders = new Headers();
return originalOpen.call(this, method, url, ...rest);
};
window.XMLHttpRequest.prototype.setRequestHeader = function archPanelTrackedSetRequestHeader(name, value) {
if (this.__archPanelTrackedHeaders) {
this.__archPanelTrackedHeaders.set(name, value);
}
return originalSetRequestHeader.call(this, name, value);
};
window.XMLHttpRequest.prototype.send = function archPanelTrackedSend(body) {
if (shouldCapture(this.__archPanelTrackedUrl)) {
rememberHeaders(this.__archPanelTrackedUrl, this.__archPanelTrackedHeaders);
}
return originalSend.call(this, body);
};
function requestViaOriginalXhr(url, options, headers) {
return new Promise((resolve, reject) => {
const xhr = new window.XMLHttpRequest();
originalOpen.call(xhr, options?.method || "GET", url, true);
xhr.withCredentials = true;
xhr.responseType = "text";
xhr.timeout = 60000;
headers.forEach((value, key) => {
originalSetRequestHeader.call(xhr, key, value);
});
xhr.onload = () => {
resolve({
ok: xhr.status >= 200 && xhr.status < 300,
status: xhr.status,
text: xhr.responseText || "",
});
};
xhr.onerror = () => {
reject(new Error("Failed to fetch"));
};
xhr.onabort = () => {
reject(new Error("Request aborted"));
};
xhr.ontimeout = () => {
reject(new Error(`Timed out while requesting ${url}`));
};
originalSend.call(xhr, options?.body ?? null);
});
}
window.addEventListener("message", async (event) => {
if (event.source !== window) {
return;
}
const message = event.data;
if (!message || message.source !== MESSAGE_SOURCE) {
return;
}
if (message.type === "ARCH_PANEL_HEADERS") {
safePostMessage(
{
source: MESSAGE_SOURCE,
type: "ARCH_PANEL_HEADERS_RESULT",
requestId: message.requestId,
headers: buildHeaderSnapshot(message.options),
}
);
return;
}
if (message.type !== "ARCH_PANEL_FETCH") {
return;
}
try {
const mergedHeaders = buildHeaders(
message.url,
message.options?.headers,
message.options?.body
);
const useXhr = message.options?.transport === "xhr";
const response = useXhr
? await requestViaOriginalXhr(message.url, message.options, mergedHeaders)
: await (async () => {
const activeFetch =
typeof window.fetch === "function"
? window.fetch.bind(window)
: nativeFetch;
const fetchResponse = await activeFetch(message.url, {
method: message.options?.method || "GET",
credentials: "include",
headers: mergedHeaders,
body: message.options?.body,
});
return {
ok: fetchResponse.ok,
status: fetchResponse.status,
text: await fetchResponse.text(),
};
})();
const rawText = response.text;
let payload;
try {
payload = rawText ? JSON.parse(rawText) : null;
} catch {
payload = rawText;
}
safePostMessage(
{
source: MESSAGE_SOURCE,
type: "ARCH_PANEL_FETCH_RESULT",
requestId: message.requestId,
ok: response.ok,
status: response.status,
debug: {
headerKeys: Array.from(mergedHeaders.keys()),
scopeKeys: getScopeKeys(message.url),
transport: useXhr ? "xhr" : "fetch",
},
payload,
}
);
} catch (error) {
safePostMessage(
{
source: MESSAGE_SOURCE,
type: "ARCH_PANEL_FETCH_RESULT",
requestId: message.requestId,
ok: false,
status: 0,
debug: {
scopeKeys: getScopeKeys(message.url),
transport: message.options?.transport || "fetch",
},
error: error instanceof Error ? error.message : String(error),
}
);
}
});
safePostMessage(
{
source: MESSAGE_SOURCE,
type: "ARCH_PANEL_BRIDGE_READY",
}
);
})();