From 8e077e0282dddb7fbd9ab578d8a6aaf31bfb85b2 Mon Sep 17 00:00:00 2001
From: Michael Clark <5285928+MikesGlitch@users.noreply.github.com>
Date: Wed, 28 Aug 2024 21:39:14 +0100
Subject: [PATCH] :electron: Desktop app to work with self signed certificates
 (#3308)

* solves the problem but creates a vulnerability

* sake...

* working but need to specify rootca.pem

* works

* being flexible on the cert names, as long as its a crt or pem

* remove console logs

* initial setup for adding cert

* caps

* comments

* fix ts strict

* rewrote it

* release notes

* remove unneeded

* https no polyfill

* removing the cert reference if it is not found

* moving full stop
---
 .../src/components/manager/ConfigServer.tsx   | 69 ++++++++++++++++---
 .../platform/server/fetch/index.electron.ts   |  2 +-
 packages/loot-core/src/server/main.ts         | 26 +++++++
 packages/loot-core/src/types/prefs.d.ts       |  1 +
 .../webpack/webpack.browser.config.js         |  1 +
 upcoming-release-notes/3308.md                |  6 ++
 6 files changed, 94 insertions(+), 11 deletions(-)
 create mode 100644 upcoming-release-notes/3308.md

diff --git a/packages/desktop-client/src/components/manager/ConfigServer.tsx b/packages/desktop-client/src/components/manager/ConfigServer.tsx
index 7ac54bc87..66e454330 100644
--- a/packages/desktop-client/src/components/manager/ConfigServer.tsx
+++ b/packages/desktop-client/src/components/manager/ConfigServer.tsx
@@ -8,11 +8,13 @@ import {
 } from 'loot-core/src/shared/environment';
 
 import { useActions } from '../../hooks/useActions';
+import { useGlobalPref } from '../../hooks/useGlobalPref';
 import { useNavigate } from '../../hooks/useNavigate';
 import { useSetThemeColor } from '../../hooks/useSetThemeColor';
 import { theme } from '../../style';
 import { Button, ButtonWithLoading } from '../common/Button2';
 import { BigInput } from '../common/Input';
+import { Link } from '../common/Link';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { useServerURL, useSetServerURL } from '../ServerContext';
@@ -32,6 +34,9 @@ export function ConfigServer() {
   }, [currentUrl]);
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
+  const [_serverSelfSignedCert, setServerSelfSignedCert] = useGlobalPref(
+    'serverSelfSignedCert',
+  );
 
   function getErrorMessage(error: string) {
     switch (error) {
@@ -83,6 +88,23 @@ export function ConfigServer() {
     setUrl(window.location.origin);
   }
 
+  async function onSelectSelfSignedCertificate() {
+    const selfSignedCertificateLocation = await window.Actual?.openFileDialog({
+      properties: ['openFile'],
+      filters: [
+        {
+          name: 'Self Signed Certificate',
+          extensions: ['crt', 'pem'],
+        },
+      ],
+    });
+
+    if (selfSignedCertificateLocation) {
+      setServerSelfSignedCert(selfSignedCertificateLocation[0]);
+      globalThis.window.Actual.relaunch(); // relaunch to use the certificate
+    }
+  }
+
   async function onSkip() {
     await setServerUrl(null);
     await loggedIn();
@@ -121,16 +143,43 @@ export function ConfigServer() {
       </Text>
 
       {error && (
-        <Text
-          style={{
-            marginTop: 20,
-            color: theme.errorText,
-            borderRadius: 4,
-            fontSize: 15,
-          }}
-        >
-          {getErrorMessage(error)}
-        </Text>
+        <>
+          <Text
+            style={{
+              marginTop: 20,
+              color: theme.errorText,
+              borderRadius: 4,
+              fontSize: 15,
+            }}
+          >
+            {getErrorMessage(error)}
+          </Text>
+          {isElectron() && (
+            <View
+              style={{ display: 'flex', flexDirection: 'row', marginTop: 20 }}
+            >
+              <Text
+                style={{
+                  color: theme.errorText,
+                  borderRadius: 4,
+                  fontSize: 15,
+                }}
+              >
+                <Trans>
+                  If the server is using a self-signed certificate{' '}
+                  <Link
+                    variant="text"
+                    style={{ fontSize: 15 }}
+                    onClick={onSelectSelfSignedCertificate}
+                  >
+                    select it here
+                  </Link>
+                  .
+                </Trans>
+              </Text>
+            </View>
+          )}
+        </>
       )}
 
       <View style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}>
diff --git a/packages/loot-core/src/platform/server/fetch/index.electron.ts b/packages/loot-core/src/platform/server/fetch/index.electron.ts
index 5aabd6dd8..63ba01462 100644
--- a/packages/loot-core/src/platform/server/fetch/index.electron.ts
+++ b/packages/loot-core/src/platform/server/fetch/index.electron.ts
@@ -1,4 +1,4 @@
-// // @ts-strict-ignore
+// @ts-strict-ignore
 import nodeFetch from 'node-fetch';
 
 export const fetch = (input: RequestInfo | URL, options?: RequestInit) => {
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index eee8d7b76..0a84bdbaf 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -1,5 +1,8 @@
 // @ts-strict-ignore
 import './polyfills';
+import https from 'https';
+import tls from 'tls';
+
 import * as injectAPI from '@actual-app/api/injected';
 import * as CRDT from '@actual-app/crdt';
 import { v4 as uuidv4 } from 'uuid';
@@ -1253,6 +1256,12 @@ handlers['save-global-prefs'] = async function (prefs) {
   if ('theme' in prefs) {
     await asyncStorage.setItem('theme', prefs.theme);
   }
+  if ('serverSelfSignedCert' in prefs) {
+    await asyncStorage.setItem(
+      'server-self-signed-cert',
+      prefs.serverSelfSignedCert,
+    );
+  }
   return 'ok';
 };
 
@@ -2126,6 +2135,23 @@ export async function initApp(isDev, socketName) {
     }
   }
 
+  const selfSignedCertPath = await asyncStorage.getItem(
+    'server-self-signed-cert',
+  );
+
+  if (selfSignedCertPath) {
+    try {
+      const selfSignedCert = await fs.readFile(selfSignedCertPath);
+      https.globalAgent.options.ca = [...tls.rootCertificates, selfSignedCert];
+    } catch (error) {
+      console.error(
+        'Unable to add the self signed certificate, removing its reference',
+        error,
+      );
+      await asyncStorage.removeItem('server-self-signed-cert');
+    }
+  }
+
   const url = await asyncStorage.getItem('server-url');
 
   if (!url) {
diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts
index c1d7054ea..29f348631 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -88,4 +88,5 @@ export type GlobalPrefs = Partial<{
   keyId?: string;
   theme: Theme;
   documentDir: string; // Electron only
+  serverSelfSignedCert: string; // Electron only
 }>;
diff --git a/packages/loot-core/webpack/webpack.browser.config.js b/packages/loot-core/webpack/webpack.browser.config.js
index 949888059..096633ea0 100644
--- a/packages/loot-core/webpack/webpack.browser.config.js
+++ b/packages/loot-core/webpack/webpack.browser.config.js
@@ -41,6 +41,7 @@ module.exports = {
       process: require.resolve('process/browser'),
       stream: require.resolve('stream-browserify'),
       tls: false,
+      https: false,
       // used by memfs in a check which we can ignore I think
       url: false,
       zlib: require.resolve('browserify-zlib'),
diff --git a/upcoming-release-notes/3308.md b/upcoming-release-notes/3308.md
new file mode 100644
index 000000000..e2b62d70c
--- /dev/null
+++ b/upcoming-release-notes/3308.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MikesGlitch]
+---
+
+Support servers with self signed certificates in the Desktop app
-- 
GitLab