Files
Recordly/electron/permissionPolicy.ts

158 lines
3.9 KiB
TypeScript

const CAPTURE_WINDOW_TYPE = "hud-overlay";
const TRUSTED_RENDERER_PROTOCOLS = new Set(["http:", "https:", "file:"]);
const FILE_SECURITY_ORIGINS = new Set(["null", "file://", "file:///"]);
export interface MediaPermissionPolicyRequest {
permission: string;
isTrustedCaptureWindow: boolean;
isMainFrame: boolean;
currentDocumentUrl: string;
requestingUrl: string;
securityOrigins: readonly string[];
}
export interface DisplayCapturePolicyRequest {
isTrustedCaptureWindow: boolean;
isMainFrame: boolean;
currentDocumentUrl: string;
securityOrigin: string;
videoRequested: boolean;
}
function parseUrl(value: string): URL | null {
try {
return new URL(value);
} catch {
return null;
}
}
function hasExactCaptureWindowQuery(url: URL): boolean {
const entries = [...url.searchParams.entries()];
return (
entries.length === 1 &&
entries[0]?.[0] === "windowType" &&
entries[0][1] === CAPTURE_WINDOW_TYPE
);
}
function isValidTrustedBaseUrl(url: URL): boolean {
return (
TRUSTED_RENDERER_PROTOCOLS.has(url.protocol) &&
url.username === "" &&
url.password === "" &&
url.search === "" &&
url.hash === ""
);
}
function hasSameBaseLocation(candidate: URL, trustedBase: URL): boolean {
return (
candidate.protocol === trustedBase.protocol &&
candidate.hostname === trustedBase.hostname &&
candidate.port === trustedBase.port &&
candidate.pathname === trustedBase.pathname
);
}
export function isTrustedCaptureDocumentUrl(
candidateUrl: string,
trustedDocumentBaseUrls: readonly string[],
): boolean {
const candidate = parseUrl(candidateUrl);
if (
!candidate ||
!TRUSTED_RENDERER_PROTOCOLS.has(candidate.protocol) ||
candidate.username !== "" ||
candidate.password !== "" ||
!hasExactCaptureWindowQuery(candidate)
) {
return false;
}
return trustedDocumentBaseUrls.some((trustedBaseUrl) => {
const trustedBase = parseUrl(trustedBaseUrl);
return Boolean(
trustedBase &&
isValidTrustedBaseUrl(trustedBase) &&
hasSameBaseLocation(candidate, trustedBase),
);
});
}
function isSameDocument(firstUrl: string, secondUrl: string): boolean {
const first = parseUrl(firstUrl);
const second = parseUrl(secondUrl);
if (!first || !second) {
return false;
}
first.hash = "";
second.hash = "";
return first.href === second.href;
}
function isSecurityOriginForDocument(securityOrigin: string, documentUrl: string): boolean {
const document = parseUrl(documentUrl);
if (!document) {
return false;
}
if (document.protocol === "file:") {
return FILE_SECURITY_ORIGINS.has(securityOrigin);
}
const origin = parseUrl(securityOrigin);
return Boolean(
origin &&
(origin.protocol === "http:" || origin.protocol === "https:") &&
origin.username === "" &&
origin.password === "" &&
origin.origin === document.origin &&
origin.pathname === "/" &&
origin.search === "" &&
origin.hash === "",
);
}
export function shouldGrantMediaPermission(
request: MediaPermissionPolicyRequest,
trustedDocumentBaseUrls: readonly string[],
): boolean {
if (
request.permission !== "media" ||
!request.isTrustedCaptureWindow ||
!request.isMainFrame ||
!isTrustedCaptureDocumentUrl(request.currentDocumentUrl, trustedDocumentBaseUrls)
) {
return false;
}
if (
!isTrustedCaptureDocumentUrl(request.requestingUrl, trustedDocumentBaseUrls) ||
!isSameDocument(request.currentDocumentUrl, request.requestingUrl)
) {
return false;
}
return (
request.securityOrigins.length > 0 &&
request.securityOrigins.every((securityOrigin) =>
isSecurityOriginForDocument(securityOrigin, request.currentDocumentUrl),
)
);
}
export function shouldGrantDisplayCapture(
request: DisplayCapturePolicyRequest,
trustedDocumentBaseUrls: readonly string[],
): boolean {
return (
request.isTrustedCaptureWindow &&
request.isMainFrame &&
request.videoRequested &&
isTrustedCaptureDocumentUrl(request.currentDocumentUrl, trustedDocumentBaseUrls) &&
isSecurityOriginForDocument(request.securityOrigin, request.currentDocumentUrl)
);
}