aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-03-14 17:59:22 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-03-15 03:00:05 +0100
commitd2213e7eba2ec8b478c879397dc0de64d293f367 (patch)
tree5e32ece325fa11f13117b2c9e5966d7142826af4 /packages/main
parentfeat(renderer): Back and forward mouse buttons (diff)
downloadsophie-d2213e7eba2ec8b478c879397dc0de64d293f367.tar.gz
sophie-d2213e7eba2ec8b478c879397dc0de64d293f367.tar.zst
sophie-d2213e7eba2ec8b478c879397dc0de64d293f367.zip
feat: Temporary certificate acceptance backend
We use the 'certificate-error' event of webContents to detect certificate verification errors and display a message to manually trust the certificate. Certificates are trusted per profile and only until Sophie is restarted. We still need to build the associated UI, the current one is just a rough prototype for debugging. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages/main')
-rw-r--r--packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts27
-rw-r--r--packages/main/src/stores/Profile.ts8
-rw-r--r--packages/main/src/stores/Service.ts178
3 files changed, 154 insertions, 59 deletions
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts
index d90ff19..edcf758 100644
--- a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts
+++ b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts
@@ -93,6 +93,33 @@ export default class ElectronServiceView implements ServiceView {
93 }, 93 },
94 ); 94 );
95 95
96 /**
97 * We use the `'certificate-error'` event instead of `session.setCertificateVerifyProc`
98 * because:
99 *
100 * 1. `'certificate-error'` is bound to the `webContents`, so we can display the certificate
101 * in the place of the correct service. Note that chromium still manages certificate trust
102 * per session, so we can't have different trusted certificates for each service of a
103 * profile.
104 * 2. The results of `'certificate-error'` are _not_ cached, so we can initially reject
105 * the certificate but we can still accept it once the user trusts it temporarily.
106 */
107 webContents.on(
108 'certificate-error',
109 (event, url, error, certificate, callback, isMainFrame) => {
110 if (service.isCertificateTemporarilyTrusted(certificate)) {
111 event.preventDefault();
112 callback(true);
113 return;
114 }
115 if (isMainFrame) {
116 setLocation(url);
117 service.setCertificateError(error, certificate);
118 }
119 callback(false);
120 },
121 );
122
96 webContents.on('page-title-updated', (_event, title) => { 123 webContents.on('page-title-updated', (_event, title) => {
97 service.setTitle(title); 124 service.setTitle(title);
98 }); 125 });
diff --git a/packages/main/src/stores/Profile.ts b/packages/main/src/stores/Profile.ts
index 836f4a8..405a5d4 100644
--- a/packages/main/src/stores/Profile.ts
+++ b/packages/main/src/stores/Profile.ts
@@ -18,8 +18,8 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { Profile as ProfileBase } from '@sophie/shared'; 21import { Certificate, Profile as ProfileBase } from '@sophie/shared';
22import { getSnapshot, Instance } from 'mobx-state-tree'; 22import { clone, getSnapshot, Instance } from 'mobx-state-tree';
23 23
24import type ProfileConfig from './config/ProfileConfig'; 24import type ProfileConfig from './config/ProfileConfig';
25 25
@@ -28,6 +28,10 @@ const Profile = ProfileBase.views((self) => ({
28 const { id, settings } = self; 28 const { id, settings } = self;
29 return { ...getSnapshot(settings), id }; 29 return { ...getSnapshot(settings), id };
30 }, 30 },
31})).actions((self) => ({
32 temporarilyTrustCertificate(certificate: Certificate): void {
33 self.temporarilyTrustedCertificates.push(clone(certificate));
34 },
31})); 35}));
32 36
33/* 37/*
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts
index d98e52e..9b2bf1e 100644
--- a/packages/main/src/stores/Service.ts
+++ b/packages/main/src/stores/Service.ts
@@ -19,8 +19,13 @@
19 */ 19 */
20 20
21import type { UnreadCount } from '@sophie/service-shared'; 21import type { UnreadCount } from '@sophie/service-shared';
22import { defineServiceModel, ServiceAction } from '@sophie/shared'; 22import {
23import { Instance, getSnapshot } from 'mobx-state-tree'; 23 CertificateSnapshotIn,
24 defineServiceModel,
25 ServiceAction,
26 ServiceStateSnapshotIn,
27} from '@sophie/shared';
28import { Instance, getSnapshot, cast } from 'mobx-state-tree';
24 29
25import type { ServiceView } from '../infrastructure/electron/types'; 30import type { ServiceView } from '../infrastructure/electron/types';
26import { getLogger } from '../utils/log'; 31import { getLogger } from '../utils/log';
@@ -31,6 +36,13 @@ import type ServiceConfig from './config/ServiceConfig';
31const log = getLogger('Service'); 36const log = getLogger('Service');
32 37
33const Service = defineServiceModel(ServiceSettings) 38const Service = defineServiceModel(ServiceSettings)
39 .volatile(
40 (): {
41 serviceView: ServiceView | undefined;
42 } => ({
43 serviceView: undefined,
44 }),
45 )
34 .views((self) => ({ 46 .views((self) => ({
35 get config(): ServiceConfig { 47 get config(): ServiceConfig {
36 const { id, settings } = self; 48 const { id, settings } = self;
@@ -42,18 +54,16 @@ const Service = defineServiceModel(ServiceSettings)
42 get shouldBeLoaded(): boolean { 54 get shouldBeLoaded(): boolean {
43 return !self.crashed; 55 return !self.crashed;
44 }, 56 },
57 }))
58 .views((self) => ({
45 get shouldBeVisible(): boolean { 59 get shouldBeVisible(): boolean {
46 return this.shouldBeLoaded && !self.failed; 60 return self.shouldBeLoaded && !self.hasError;
47 }, 61 },
48 })) 62 }))
49 .volatile(
50 (): {
51 serviceView: ServiceView | undefined;
52 } => ({
53 serviceView: undefined,
54 }),
55 )
56 .actions((self) => ({ 63 .actions((self) => ({
64 setServiceView(serviceView: ServiceView | undefined): void {
65 self.serviceView = serviceView;
66 },
57 setLocation({ 67 setLocation({
58 url, 68 url,
59 canGoBack, 69 canGoBack,
@@ -70,21 +80,6 @@ const Service = defineServiceModel(ServiceSettings)
70 setTitle(title: string): void { 80 setTitle(title: string): void {
71 self.title = title; 81 self.title = title;
72 }, 82 },
73 startLoading(): void {
74 self.state = { type: 'loading' };
75 },
76 finishLoading(): void {
77 if (self.loading) {
78 // Do not overwrite crashed state if the service haven't been reloaded yet.
79 self.state = { type: 'loaded' };
80 }
81 },
82 setFailed(errorCode: number, errorDesc: string): void {
83 self.state = { type: 'failed', errorCode, errorDesc };
84 },
85 setCrashed(reason: string, exitCode: number): void {
86 self.state = { type: 'crashed', reason, exitCode };
87 },
88 setUnreadCount({ direct, indirect }: UnreadCount): void { 83 setUnreadCount({ direct, indirect }: UnreadCount): void {
89 if (direct !== undefined) { 84 if (direct !== undefined) {
90 self.directMessageCount = direct; 85 self.directMessageCount = direct;
@@ -93,55 +88,124 @@ const Service = defineServiceModel(ServiceSettings)
93 self.indirectMessageCount = indirect; 88 self.indirectMessageCount = indirect;
94 } 89 }
95 }, 90 },
96 setServiceView(serviceView: ServiceView | undefined): void { 91 }))
97 self.serviceView = serviceView; 92 .actions((self) => {
98 }, 93 function setState(state: ServiceStateSnapshotIn): void {
99 goBack(): void { 94 self.state = cast(state);
100 self.serviceView?.goBack(); 95 }
101 }, 96
102 goForward(): void { 97 return {
103 self.serviceView?.goForward(); 98 startLoading(): void {
99 setState({ type: 'loading' });
100 },
101 finishLoading(): void {
102 if (self.loading) {
103 // Do not overwrite any error state state if the service haven't been reloaded yet.
104 setState({ type: 'loaded' });
105 }
106 },
107 setFailed(errorCode: number, errorDesc: string): void {
108 if (!self.hasError) {
109 setState({
110 type: 'failed',
111 errorCode,
112 errorDesc,
113 });
114 }
115 },
116 setCertificateError(
117 errorCode: string,
118 certificate: CertificateSnapshotIn,
119 ): void {
120 if (!self.crashed && self.state.type !== 'certificateError') {
121 setState({
122 type: 'certificateError',
123 errorCode,
124 certificate,
125 });
126 }
127 },
128 setCrashed(reason: string, exitCode: number): void {
129 if (!self.crashed) {
130 setState({
131 type: 'crashed',
132 reason,
133 exitCode,
134 });
135 }
136 },
137 goBack(): void {
138 self.serviceView?.goBack();
139 },
140 goForward(): void {
141 self.serviceView?.goForward();
142 },
143 reload(ignoreCache = false): void {
144 if (self.serviceView === undefined) {
145 setState({ type: 'initializing' });
146 } else {
147 self.serviceView?.reload(ignoreCache);
148 }
149 },
150 stop(): void {
151 self.serviceView?.stop();
152 },
153 go(url: string): void {
154 if (self.serviceView === undefined) {
155 self.currentUrl = url;
156 setState({ type: 'initializing' });
157 } else {
158 self.serviceView?.loadURL(url);
159 }
160 },
161 };
162 })
163 .actions((self) => ({
164 goHome(): void {
165 self.go(self.settings.url);
104 }, 166 },
105 reload(ignoreCache = false): void { 167 temporarilyTrustCurrentCertificate(fingerprint: string): void {
106 if (self.serviceView === undefined) { 168 if (self.state.type !== 'certificateError') {
107 self.state = { type: 'initializing' }; 169 log.error('Tried to trust certificate without any certificate error');
108 } else { 170 return;
109 self.serviceView?.reload(ignoreCache);
110 } 171 }
111 }, 172 if (self.state.certificate.fingerprint !== fingerprint) {
112 stop(): void { 173 log.error(
113 self.serviceView?.stop(); 174 'Tried to trust certificate',
114 }, 175 fingerprint,
115 go(url: string): void { 176 'but the currently pending fingerprint is',
116 if (self.serviceView === undefined) { 177 self.state.certificate.fingerprint,
117 self.currentUrl = url; 178 );
118 self.state = { type: 'initializing' }; 179 return;
119 } else {
120 self.serviceView?.loadURL(url);
121 } 180 }
181 self.settings.profile.temporarilyTrustCertificate(self.state.certificate);
182 self.state.trust = 'accepted';
183 self.reload();
122 }, 184 },
123 goHome(): void { 185 }))
124 this.go(self.settings.url); 186 .actions((self) => ({
125 },
126 dispatch(action: ServiceAction): void { 187 dispatch(action: ServiceAction): void {
127 switch (action.action) { 188 switch (action.action) {
128 case 'back': 189 case 'back':
129 this.goBack(); 190 self.goBack();
130 break; 191 break;
131 case 'forward': 192 case 'forward':
132 this.goForward(); 193 self.goForward();
133 break; 194 break;
134 case 'reload': 195 case 'reload':
135 this.reload(action.ignoreCache); 196 self.reload(action.ignoreCache);
136 break; 197 break;
137 case 'stop': 198 case 'stop':
138 this.stop(); 199 self.stop();
139 break; 200 break;
140 case 'go-home': 201 case 'go-home':
141 this.goHome(); 202 self.goHome();
142 break; 203 break;
143 case 'go': 204 case 'go':
144 this.go(action.url); 205 self.go(action.url);
206 break;
207 case 'temporarily-trust-current-certificate':
208 self.temporarilyTrustCurrentCertificate(action.fingerprint);
145 break; 209 break;
146 default: 210 default:
147 log.error('Unknown action to dispatch', action); 211 log.error('Unknown action to dispatch', action);