[desktop] Fix desktop app direct uploads when self-hosting

It seems that Backblaze returns `null` as the ACAO response if the scheme is not
http(s), even if we have set allowedOrigins to "*". The desktop app has a custom
scheme, "ente://app", which is thus causing B2 to return null.

    # Works:
    curl -v -X OPTIONS -H 'Origin: http://example.org' -H 'Access-Control-Request-Method: PUT' 'https://xxx.s3.eu-central-003.backblazeb2.com/yyy'

    # Fails:
    curl -v -X OPTIONS -H 'Origin: ente://example.org' -H 'Access-Control-Request-Method: PUT' 'https://xxx.s3.eu-central-003.backblazeb2.com/yyy'

Ref: https://github.com/ente-io/ente/discussions/2461#discussioncomment-10125881
This commit is contained in:
Manav Rathi
2024-07-23 19:30:34 +05:30
parent 088b4b9cff
commit 7f28d2bfe9

View File

@@ -501,28 +501,31 @@ const allowExternalLinks = (webContents: WebContents) =>
});
/**
* Allow uploading to arbitrary S3 buckets.
* Allow connecting to arbitrary S3 buckets.
*
* The files in the desktop app are served over the ente:// protocol. When that
* is returned as the CORS allowed origin, "Access-Control-Allow-Origin:
* ente://app", CORS requests fail.
* The embedded web app within in the desktop app is served over the ente://
* protocol. When pages in that web app make requests, their originate from this
* "ente://app" origin, which thus serves as the value for the
* "Access-Control-Allow-Origin" header in the CORS preflight requests.
*
* Further, during testing or self-hosting, file uploads involve a redirection
* (This doesn't affect our production systems since we upload via a worker,
* See: [Note: Passing credentials for self-hosted file fetches]).
* Some S3 providers (B2 is the motivating example for this workaround) do not
* allow whitelisting custom URI schemes. That is, even if we set
* "`allowedOrigin: ["*"]` in our B2 bucket CORS configuration, when the web
* code makes a CORS request with ACAO "ente://app", it gets back
* "Access-Control-Allow-Origin" set to `null` in the response, and thus the
* request fails (since it does not match the origin we sent).
*
* In some cases, we might be using a S3 bucket that does not allow whitelisting
* a custom URI scheme. Echoing back the value of `Origin` (even if the bucket
* would allow us to) would also not work, since the browser sends `null` as the
* `Origin` for the redirected request (this is as per the CORS spec). So the
* only way in such cases would be to require the bucket to set an
* "Access-Control-Allow-Origin: *".
* This is not an issue for production apps since they upload via a worker
* instead of directly touching an S3 provider. However, this impacts people who
* are self hosting (or when we ourselves are trying to test things by with an
* arbitrary S3 bucket without going via a worker).
*
* To avoid these issues, we intercepting the ACAO header and set it to `*`.
* To avoid these issues, we intercept the ACAO header and set it to `*`.
*
* However, that cause problems with requests that use credentials since "*" is
* not a valid value in such cases. One such example is the HCaptcha requests
* made by Stripe when we initiate a payment within the desktop app:
* However, an unconditional interception causes problems with requests that use
* credentials, since "*" is not a valid value in such cases. One such example
* is the HCaptcha requests made by Stripe when we initiate a payment within the
* desktop app:
*
* > Access to XMLHttpRequest at 'https://api2.hcaptcha.com/getcaptcha/xxx' from
* > origin 'https://newassets.hcaptcha.com' has been blocked by CORS policy:
@@ -532,7 +535,8 @@ const allowExternalLinks = (webContents: WebContents) =>
* > controlled by the withCredentials attribute.
*
* So we only do this workaround if there was either no ACAO specified in the
* response, or if the ACAO was "ente://app".
* response, or if the ACAO in the response was "null" (the string serialization
* of `null`).
*/
const allowAllCORSOrigins = (webContents: WebContents) =>
webContents.session.webRequest.onHeadersReceived(
@@ -543,7 +547,7 @@ const allowAllCORSOrigins = (webContents: WebContents) =>
for (const [key, value] of Object.entries(responseHeaders ?? {}))
if (key.toLowerCase() == "access-control-allow-origin") {
headers["Access-Control-Allow-Origin"] =
value[0] == rendererURL ? ["*"] : value;
value[0] == "null" ? ["*"] : value;
} else {
headers[key] = value;
}