diff --git a/desktop/src/@batch-flask/compiler/dev-translations-loader.ts b/desktop/src/@batch-flask/compiler/dev-translations-loader.ts index fc3fc5a914..1b9b2e351f 100644 --- a/desktop/src/@batch-flask/compiler/dev-translations-loader.ts +++ b/desktop/src/@batch-flask/compiler/dev-translations-loader.ts @@ -16,7 +16,7 @@ export class DevTranslationsLoader { this.translations.clear(); if (!this.translationFiles) { this.translationFiles = await glob("**/*.i18n.yml", { - ignore: "node_modules/**/*", + ignore: ["node_modules/**/*", "release/**/*"], }); } await this._processFiles(this.translationFiles, duplicateCallback); diff --git a/desktop/src/client/core/aad/auth-provider.ts b/desktop/src/client/core/aad/auth-provider.ts index 979e321782..8cefc8625a 100644 --- a/desktop/src/client/core/aad/auth-provider.ts +++ b/desktop/src/client/core/aad/auth-provider.ts @@ -13,6 +13,9 @@ import MSALCachePlugin from "./msal-cache-plugin"; import { AuthObserver } from "./auth-observer"; import { shell } from "electron"; import { AuthLoopbackClient } from "./auth-loopback-client"; +import { ProxyNetworkClient } from "./proxy-network-client"; + +import "global-agent/bootstrap"; const MSAL_SCOPES = ["user_impersonation"]; @@ -245,10 +248,13 @@ export default class AuthProvider { private async _createClient(tenantId: string): Promise { const proxyUrl = await this._loadProxyUrl(); + let networkClient; if (proxyUrl) { log.info(`[${tenantId}] Proxying auth endpoints through ` + proxyUrl); + process.env.GLOBAL_AGENT_HTTP_PROXY = proxyUrl; + networkClient = new ProxyNetworkClient(proxyUrl); } const authority = @@ -256,7 +262,7 @@ export default class AuthProvider { return new PublicClientApplication({ system: { - proxyUrl + networkClient }, auth: { clientId: this.config.clientId, diff --git a/desktop/src/client/core/aad/proxy-network-client.ts b/desktop/src/client/core/aad/proxy-network-client.ts new file mode 100644 index 0000000000..a8904f3fa0 --- /dev/null +++ b/desktop/src/client/core/aad/proxy-network-client.ts @@ -0,0 +1,55 @@ +import type { INetworkModule, NetworkRequestOptions, NetworkResponse } from "@azure/msal-node"; +import * as HttpsProxyAgent from "https-proxy-agent"; +import fetch from "node-fetch"; + +/** + * Placeholder for msal-node's network module which uses node-fetch to support + * HTTP proxy configurations with authorization + * + * @see https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/6527#issuecomment-2077953882 + */ +export class ProxyNetworkClient implements INetworkModule { + private proxyAgent: HttpsProxyAgent; + constructor(proxyUrl: string) { + this.proxyAgent = new HttpsProxyAgent(proxyUrl); + } + + sendGetRequestAsync(url: string, options?: NetworkRequestOptions): Promise> { + return this.sendRequestAsync(url, "GET", options); + } + sendPostRequestAsync(url: string, options?: NetworkRequestOptions): Promise> { + return this.sendRequestAsync(url, "POST", options); + } + + private async sendRequestAsync( + url: string, + method: "GET" | "POST", + options: NetworkRequestOptions = {}, + ): Promise> { + try { + const requestOptions = { + method: method, + headers: options.headers, + body: method === "POST" ? options.body : undefined, + agent: this.proxyAgent, + }; + + const response = await fetch(url, requestOptions); + const data = await response.json() as any; + + const headersObj: Record = {}; + response.headers.forEach((value, key) => { + headersObj[key] = value; + }); + + return { + headers: headersObj, + body: data, + status: response.status, + }; + } catch (err) { + console.error("Proxy request error", err); + throw err; + } + } +} diff --git a/desktop/src/client/core/batch-explorer-application.ts b/desktop/src/client/core/batch-explorer-application.ts index 54cd9292a3..d5aacf6bea 100644 --- a/desktop/src/client/core/batch-explorer-application.ts +++ b/desktop/src/client/core/batch-explorer-application.ts @@ -86,6 +86,7 @@ export class BatchExplorerApplication { this._setupProcessEvents(); this._registerFileProtocol(); await this.proxySettings.init(); + this._applyProxySettings(); this.storageBlobAdapter.init(); } @@ -365,4 +366,44 @@ export class BatchExplorerApplication { callback({ cancel: false, requestHeaders: details.requestHeaders }); }); } + + private async _trustedDomains(): Promise { + return [ + "https://raw.githubusercontent.com", + "https://batch.azure.com", // Public data-plane API calls + this.properties.azureEnvironment.aadUrl, + this.properties.azureEnvironment.arm, + this.properties.azureEnvironment.batch, + this.properties.azureEnvironment.msGraph, + this.properties.azureEnvironment.storageEndpoint + ].map(url => { + try { + // Ensure the URL has a protocol (default to "https://") + const normalized = url.startsWith("http") ? url : `https://${url}`; + return new URL(normalized).hostname; + } catch (error) { + console.error(`Invalid URL: ${url}`, error); + return null; // Handle invalid URLs gracefully + } + }).filter(Boolean); + } + + private async _applyProxySettings() { + const settings = await this.proxySettings.settings; + + const conf = settings.http || settings.https; + const proxyUrl = `${conf.protocol}://${conf.host}:${conf.port}`; + + session.defaultSession.setProxy({ proxyRules: proxyUrl }); + + const trustedDomains = await this._trustedDomains(); + session.defaultSession.setCertificateVerifyProc((request, verifyCert) => { + if (trustedDomains.some(host => request.hostname.includes(host))) { + verifyCert(0); // trust the certificate + } else { + console.error("Untrusted certificate", request.hostname); + verifyCert(-3); + } + }); + } } diff --git a/desktop/src/client/core/i18n/client-translations-loader.service.ts b/desktop/src/client/core/i18n/client-translations-loader.service.ts index 54030afa68..e61e46d79b 100644 --- a/desktop/src/client/core/i18n/client-translations-loader.service.ts +++ b/desktop/src/client/core/i18n/client-translations-loader.service.ts @@ -35,7 +35,7 @@ export class ClientTranslationsLoaderService extends TranslationsLoaderService { private async _loadDevelopementTranslations() { this.translations = await this.devTranslationsService.load((key, source) => { - log.error(`Translation with key ${key} already exists. ${source} is redefining it`); + log.warn(`Translation with key ${key} already exists. ${source} is redefining it`); }); await this._loadLocaleTranslations(); }