w3c / webextensions

Charter and administrivia for the WebExtensions Community Group (WECG)
Other
576 stars 50 forks source link

Proposal: Add RuleCondition to declarativeNetRequest that can check the headers #627

Closed danielhjacobs closed 3 weeks ago

danielhjacobs commented 3 weeks ago

This is as a substitute for webRequest.onHeadersReceived with webRequestBlocking. As a real-life example of where this would be useful, https://github.com/ruffle-rs/ruffle used to have code which caused SWF files loaded directly in the browser to be able to be opened with an internal extension page automagically with a redirect rule, but which does not work with Manifest V3. We tried replacing it with a regex filter for regexFilter: "^.*\\.s(?:wf|pl)(\\?.*|#.*|)$", but without also being able to check the Content-Type header that did not work properly since some SWF files are at URLs not ending in .swf (or .spl) and some URLs ending in .swf (or .spl) are not actually SWF files.

My suggestion would be a RuleCondition, headers, which is an object with keys matching the request header's keys and values of regular expressions for the corresponding values

danielhjacobs commented 3 weeks ago

This is the aforementioned code:


/**
 * Returns whether the given filename ends in a known Flash extension.
 *
 * @param filename The filename to test.
 * @returns True if the filename is a Flash movie (swf or spl).
 */
function isSwfFilename(filename: string): boolean {
    let pathname = "";
    try {
        // A base URL is required if `filename` is a relative URL, but we don't need to detect the real URL origin.
        pathname = new URL(filename, "https://example.com").pathname;
    } catch (err) {
        // Some invalid filenames, like `///`, could raise a TypeError. Let's fail silently in this situation.
    }
    if (pathname && pathname.length >= 4) {
        const extension = pathname.slice(-4).toLowerCase();
        if (extension === ".swf" || extension === ".spl") {
            return true;
        }
    }
    return false;
}

/**
 * Returns whether the given MIME type is a known Flash type.
 *
 * @param mimeType The MIME type to test.
 * @param allowExtraMimes Whether extra MIME types, non-Flash related, are allowed.
 * @returns True if the MIME type is a Flash MIME type.
 */
function isSwfMimeType(mimeType: string, allowExtraMimes: boolean): boolean {
    mimeType = mimeType.toLowerCase();
    switch (mimeType) {
        case "application/x-shockwave-flash":
        case "application/futuresplash":
        case "application/x-shockwave-flash2-preview":
        case "application/vnd.adobe.flash.movie":
            return true;
        default:
            if (allowExtraMimes) {
                // Allow extra MIME types to improve detection of Flash content.
                // Extension: Some sites (e.g. swfchan.net) might (wrongly?) serve files with octet-stream.
                // Polyfill: Other sites (e.g. #11050) might use octet-stream when defining an <embed> tag.
                switch (mimeType) {
                    case "application/octet-stream":
                    case "binary/octet-stream":
                        return true;
                }
            }
    }
    return false;
}

/**
 * Returns whether the given filename and MIME type resolve as a Flash content.
 *
 * @param filename The filename to test.
 * @param mimeType The MIME type to test.
 * @returns True if the given arguments resolve as a Flash content.
 */
export function isSwfCore(filename: string, mimeType: string | null): boolean {
    const isSwfExtension = isSwfFilename(filename);
    if (!mimeType) {
        // If no MIME type is specified (null or empty string), returns whether the movie ends in a known Flash extension.
        return isSwfExtension;
    } else {
        return isSwfMimeType(mimeType, isSwfExtension);
    }
}
function isSwf(
    details:
        | chrome.webRequest.WebResponseHeadersDetails
        | browser.webRequest._OnHeadersReceivedDetails,
) {
    // TypeScript doesn't compile without this explicit type declaration.
    const headers: (
        | chrome.webRequest.HttpHeader
        | browser.webRequest._HttpHeaders
    )[] = details.responseHeaders!;
    const typeHeader = headers.find(
        ({ name }) => name.toLowerCase() === "content-type",
    );
    if (!typeHeader) {
        return false;
    }
    const mimeType = typeHeader
        .value!.toLowerCase()
        .match(/^\s*(.*?)\s*(?:;.*)?$/)![1]!;
    return isSwfCore(details.url, mimeType);
}
function onHeadersReceived(
    details:
        | chrome.webRequest.WebResponseHeadersDetails
        | browser.webRequest._OnHeadersReceivedDetails,
) {
    if (isSwf(details)) {
        const baseUrl = utils.runtime.getURL("player.html");
        return {
            redirectUrl: `${baseUrl}?url=${encodeURIComponent(details.url)}`,
        };
    }
    return undefined;
}
    (chrome || browser).webRequest.onHeadersReceived.addListener(
        onHeadersReceived,
        {
            urls: ["<all_urls>"],
            types: ["main_frame", "sub_frame"],
        },
        ["blocking", "responseHeaders"],
    );

The right check here for declarativeNetRequest would be checking if either the Content-Type equals, case insensitively, one of "application/x-shockwave-flash", "application/futuresplash", "application/x-shockwave-flash2-preview", "application/vnd.adobe.flash.movie" or the URL ends in .swf or .spl and the Content-Type header is not specified or case insensitively matches "binary/octet-stream" or "application/octet-stream".

danielhjacobs commented 3 weeks ago

If there was a condition to check the headers, that could be done with the following rules:

        const rules = [
            {
                id: 1,
                action: {
                    type: (chrome || browser).declarativeNetRequest.RuleActionType.REDIRECT,
                    redirect: { regexSubstitution: (chrome || browser).runtime.getURL("/player.html") + "#\\0" }
                },
                condition: {
                    regexFilter: "^.*$",
                    headers: {
                        "Content-Type": "^application\/(?:x-shockwave-flash|futuresplash|x-shockwave-flash2-preview|vnd\.adobe\.flash\.movie)$"
                    },
                    resourceTypes: [
                        (chrome || browser).declarativeNetRequest.ResourceType.MAIN_FRAME
                    ]
                }
            },
            {
                id: 2,
                action: {
                    type: (chrome || browser).declarativeNetRequest.RuleActionType.REDIRECT,
                    redirect: { regexSubstitution: (chrome || browser).runtime.getURL("/player.html") + "#\\0" }
                },
                condition: {
                    regexFilter: "^.*\\.s(?:wf|pl)(\\?.*|#.*|)$",
                    headers: {
                        "Content-Type": "^(?:(?:application|binary)\/octet-stream|)$"
                    },
                    resourceTypes: [
                        (chrome || browser).declarativeNetRequest.ResourceType.MAIN_FRAME
                    ]
                }
            }
        ];
tophf commented 3 weeks ago

Sounds like #460 and it's already implemented in Chrome Canary, see the internal documentation (it'll be published in readable form once the API goes stable, I guess).

danielhjacobs commented 3 weeks ago

Yeah, I suppose it sounds like this can be done with the following rules according to that documentation.

                    regexFilter: "^.*$",
                    responseHeaders: [
                        { header: "content-type", values: [ "application/x-shockwave-flash", "application/futuresplash", "application/x-shockwave-flash2-preview", "application/vnd.adobe.flash.movie" ] }
                    ],

                    regexFilter: "^.*\\.s(?:wf|pl)(\\?.*|#.*|)$",
                    responseHeaders: [
                        { header: "content-type", values: [ "application/octet-stream", "application/binary-stream", "" ] }
                    ],