diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-03-14 17:59:22 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-03-15 03:00:05 +0100 |
commit | d2213e7eba2ec8b478c879397dc0de64d293f367 (patch) | |
tree | 5e32ece325fa11f13117b2c9e5966d7142826af4 /packages/main | |
parent | feat(renderer): Back and forward mouse buttons (diff) | |
download | sophie-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.ts | 27 | ||||
-rw-r--r-- | packages/main/src/stores/Profile.ts | 8 | ||||
-rw-r--r-- | packages/main/src/stores/Service.ts | 178 |
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 | ||
21 | import { Profile as ProfileBase } from '@sophie/shared'; | 21 | import { Certificate, Profile as ProfileBase } from '@sophie/shared'; |
22 | import { getSnapshot, Instance } from 'mobx-state-tree'; | 22 | import { clone, getSnapshot, Instance } from 'mobx-state-tree'; |
23 | 23 | ||
24 | import type ProfileConfig from './config/ProfileConfig'; | 24 | import 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 | ||
21 | import type { UnreadCount } from '@sophie/service-shared'; | 21 | import type { UnreadCount } from '@sophie/service-shared'; |
22 | import { defineServiceModel, ServiceAction } from '@sophie/shared'; | 22 | import { |
23 | import { Instance, getSnapshot } from 'mobx-state-tree'; | 23 | CertificateSnapshotIn, |
24 | defineServiceModel, | ||
25 | ServiceAction, | ||
26 | ServiceStateSnapshotIn, | ||
27 | } from '@sophie/shared'; | ||
28 | import { Instance, getSnapshot, cast } from 'mobx-state-tree'; | ||
24 | 29 | ||
25 | import type { ServiceView } from '../infrastructure/electron/types'; | 30 | import type { ServiceView } from '../infrastructure/electron/types'; |
26 | import { getLogger } from '../utils/log'; | 31 | import { getLogger } from '../utils/log'; |
@@ -31,6 +36,13 @@ import type ServiceConfig from './config/ServiceConfig'; | |||
31 | const log = getLogger('Service'); | 36 | const log = getLogger('Service'); |
32 | 37 | ||
33 | const Service = defineServiceModel(ServiceSettings) | 38 | const 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); |