aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-04-20 01:17:32 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:55:01 +0200
commitbf2dfbc65e1f702c64fd7d76356339b9b37ef5f7 (patch)
tree4eede1c4153e84c02ca19e5a7a5c292d4ce859ac
parentfeat: Always show location bar on error (diff)
downloadsophie-bf2dfbc65e1f702c64fd7d76356339b9b37ef5f7.tar.gz
sophie-bf2dfbc65e1f702c64fd7d76356339b9b37ef5f7.tar.zst
sophie-bf2dfbc65e1f702c64fd7d76356339b9b37ef5f7.zip
feat(renderer): Insecure connection warning
Show a more prominent warning for insecure connections with a button to try connecting over HTTPS if possible. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--locales/en/translation.json9
-rw-r--r--packages/renderer/src/components/App.tsx4
-rw-r--r--packages/renderer/src/components/banner/InsecureConnectionBanner.tsx92
-rw-r--r--packages/renderer/src/components/banner/NewWindowBanner.tsx (renamed from packages/renderer/src/components/NewWindowBanner.tsx)2
-rw-r--r--packages/renderer/src/components/banner/NotificationBanner.tsx (renamed from packages/renderer/src/components/NotificationBanner.tsx)21
-rw-r--r--packages/renderer/src/stores/Service.ts190
6 files changed, 225 insertions, 93 deletions
diff --git a/locales/en/translation.json b/locales/en/translation.json
index 74320f3..a669d28 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -10,6 +10,11 @@
10 "messageSingleLink": "{{name}} wants to open <2>{{url}}</2> in a new window", 10 "messageSingleLink": "{{name}} wants to open <2>{{url}}</2> in a new window",
11 "messageMultipleLinks_one": "{{name}} wants to open <2>{{url}}</2> and <5>{{count}}</5> other link in new windows", 11 "messageMultipleLinks_one": "{{name}} wants to open <2>{{url}}</2> and <5>{{count}}</5> other link in new windows",
12 "messageMultipleLinks_other": "{{name}} wants to open <2>{{url}}</2> and <5>{{count}}</5> other links in new windows" 12 "messageMultipleLinks_other": "{{name}} wants to open <2>{{url}}</2> and <5>{{count}}</5> other links in new windows"
13 },
14 "insecureConnection": {
15 "message": "Your connection to this server is not secure. Attackers might be able to access any passwords or personal data you enter!",
16 "home": "$t(error.home, { \"serviceName\": \"{{serviceName}}\" })",
17 "reconnectSecurely": "Try connecting securely"
13 } 18 }
14 }, 19 },
15 "menu": { 20 "menu": {
@@ -34,7 +39,7 @@
34 "unspecific": "Unknown error", 39 "unspecific": "Unknown error",
35 "notSecureConnection": "Not secure", 40 "notSecureConnection": "Not secure",
36 "secureConnection": "Secure connection", 41 "secureConnection": "Secure connection",
37 "certificateError": "Certificate error", 42 "certificateError": "$t(securityLabel.notSecureConnection)",
38 "invalidURL": "Invalid URL" 43 "invalidURL": "Invalid URL"
39 }, 44 },
40 "service": { 45 "service": {
@@ -67,7 +72,7 @@
67 }, 72 },
68 "error": { 73 "error": {
69 "errorCode": "Error code: {{errorCode}}", 74 "errorCode": "Error code: {{errorCode}}",
70 "home": "Got to {{serviceName}} home", 75 "home": "Go to {{serviceName}} home",
71 "reload": "$t(toolbar.reload)", 76 "reload": "$t(toolbar.reload)",
72 "failed": { 77 "failed": {
73 "unspecific": { 78 "unspecific": {
diff --git a/packages/renderer/src/components/App.tsx b/packages/renderer/src/components/App.tsx
index 4a3a5cf..d381abf 100644
--- a/packages/renderer/src/components/App.tsx
+++ b/packages/renderer/src/components/App.tsx
@@ -24,8 +24,9 @@ import React, { useCallback, useEffect } from 'react';
24import { useTranslation } from 'react-i18next'; 24import { useTranslation } from 'react-i18next';
25 25
26import BrowserViewPlaceholder from './BrowserViewPlaceholder'; 26import BrowserViewPlaceholder from './BrowserViewPlaceholder';
27import NewWindowBanner from './NewWindowBanner';
28import { useStore } from './StoreProvider'; 27import { useStore } from './StoreProvider';
28import InsecureConnectionBanner from './banner/InsecureConnectionBanner';
29import NewWindowBanner from './banner/NewWindowBanner';
29import ErrorPage from './errorPage/ErrorPage'; 30import ErrorPage from './errorPage/ErrorPage';
30import LocationBar from './locationBar/LocationBar'; 31import LocationBar from './locationBar/LocationBar';
31import Sidebar from './sidebar/Sidebar'; 32import Sidebar from './sidebar/Sidebar';
@@ -103,6 +104,7 @@ function App({ devMode }: { devMode: boolean }): JSX.Element {
103 }} 104 }}
104 > 105 >
105 <LocationBar /> 106 <LocationBar />
107 <InsecureConnectionBanner service={selectedService} />
106 <NewWindowBanner service={selectedService} /> 108 <NewWindowBanner service={selectedService} />
107 <BrowserViewPlaceholder> 109 <BrowserViewPlaceholder>
108 <ErrorPage service={selectedService} /> 110 <ErrorPage service={selectedService} />
diff --git a/packages/renderer/src/components/banner/InsecureConnectionBanner.tsx b/packages/renderer/src/components/banner/InsecureConnectionBanner.tsx
new file mode 100644
index 0000000..7a03fce
--- /dev/null
+++ b/packages/renderer/src/components/banner/InsecureConnectionBanner.tsx
@@ -0,0 +1,92 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import CableIcon from '@mui/icons-material/Cable';
22import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
23import HomeIcon from '@mui/icons-material/HomeOutlined';
24import Button from '@mui/material/Button';
25import { SecurityLabelKind } from '@sophie/shared';
26import { observer } from 'mobx-react-lite';
27import React from 'react';
28import { useTranslation } from 'react-i18next';
29
30import type Service from '../../stores/Service';
31
32import NotificationBanner from './NotificationBanner';
33
34function InsecureConnectionBanner({
35 service,
36}: {
37 service: Service | undefined;
38}): JSX.Element | null {
39 const { t } = useTranslation(undefined, {
40 keyPrefix: 'banner.insecureConnection',
41 });
42
43 if (service === undefined) {
44 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering.
45 return null;
46 }
47
48 const {
49 canReconnectSecurely,
50 hasError,
51 securityLabel,
52 settings: { name: serviceName },
53 } = service;
54
55 if (securityLabel !== SecurityLabelKind.NotSecureConnection || hasError) {
56 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering.
57 return null;
58 }
59
60 return (
61 <NotificationBanner
62 severity="error"
63 icon={<ErrorOutlineIcon fontSize="inherit" />}
64 buttons={
65 <>
66 <Button
67 onClick={() => service.goHome()}
68 color="inherit"
69 size="small"
70 startIcon={<HomeIcon />}
71 >
72 {t('home', { serviceName })}
73 </Button>
74 {canReconnectSecurely && (
75 <Button
76 onClick={() => service.reconnectSecurely()}
77 color="inherit"
78 size="small"
79 startIcon={<CableIcon />}
80 >
81 {t('reconnectSecurely')}
82 </Button>
83 )}
84 </>
85 }
86 >
87 {t('message')}
88 </NotificationBanner>
89 );
90}
91
92export default observer(InsecureConnectionBanner);
diff --git a/packages/renderer/src/components/NewWindowBanner.tsx b/packages/renderer/src/components/banner/NewWindowBanner.tsx
index 9aa6121..478de8e 100644
--- a/packages/renderer/src/components/NewWindowBanner.tsx
+++ b/packages/renderer/src/components/banner/NewWindowBanner.tsx
@@ -25,7 +25,7 @@ import { observer } from 'mobx-react-lite';
25import React from 'react'; 25import React from 'react';
26import { Trans, useTranslation } from 'react-i18next'; 26import { Trans, useTranslation } from 'react-i18next';
27 27
28import type Service from '../stores/Service'; 28import type Service from '../../stores/Service';
29 29
30import NotificationBanner from './NotificationBanner'; 30import NotificationBanner from './NotificationBanner';
31 31
diff --git a/packages/renderer/src/components/NotificationBanner.tsx b/packages/renderer/src/components/banner/NotificationBanner.tsx
index c759d46..818f498 100644
--- a/packages/renderer/src/components/NotificationBanner.tsx
+++ b/packages/renderer/src/components/banner/NotificationBanner.tsx
@@ -26,8 +26,7 @@ import React, { ReactNode } from 'react';
26import { useTranslation } from 'react-i18next'; 26import { useTranslation } from 'react-i18next';
27 27
28const NotificationBannerRoot = styled(Alert)(({ theme }) => ({ 28const NotificationBannerRoot = styled(Alert)(({ theme }) => ({
29 paddingTop: 7, 29 padding: `7px ${theme.spacing(1)} 6px ${theme.spacing(2)}`,
30 paddingBottom: 6,
31 // Match the height of the location bar. 30 // Match the height of the location bar.
32 minHeight: 53, 31 minHeight: 53,
33 borderRadius: 0, 32 borderRadius: 0,
@@ -42,6 +41,7 @@ const NotificationBannerRoot = styled(Alert)(({ theme }) => ({
42 }, 41 },
43 '.MuiAlert-action': { 42 '.MuiAlert-action': {
44 paddingLeft: 0, 43 paddingLeft: 0,
44 paddingRight: theme.spacing(1),
45 }, 45 },
46})); 46}));
47 47
@@ -52,10 +52,12 @@ const NotificationBannerText = styled(Typography)(({ theme }) => ({
52 flexGrow: 9999, 52 flexGrow: 9999,
53})); 53}));
54 54
55const NotificationBannerButtons = styled(Box)(({ theme }) => ({ 55const NotificationBannerButtons = styled(Box, {
56 shouldForwardProp: (prop) => prop !== 'hasCloseButton',
57})<{ hasCloseButton: boolean }>(({ theme, hasCloseButton }) => ({
56 fontSize: 'inherit', 58 fontSize: 'inherit',
57 paddingTop: theme.spacing(0.5), 59 paddingTop: theme.spacing(0.5),
58 paddingRight: theme.spacing(0.5), 60 paddingRight: hasCloseButton ? theme.spacing(0.5) : 0,
59 display: 'flex', 61 display: 'flex',
60 flexWrap: 'wrap', 62 flexWrap: 'wrap',
61 flexGrow: 1, 63 flexGrow: 1,
@@ -74,22 +76,26 @@ export default function NotificationBanner({
74}: { 76}: {
75 severity?: AlertColor; 77 severity?: AlertColor;
76 icon?: ReactNode; 78 icon?: ReactNode;
77 onClose: () => void; 79 onClose?: () => void;
78 buttons?: ReactNode; 80 buttons?: ReactNode;
79 children?: ReactNode; 81 children?: ReactNode;
80}): JSX.Element { 82}): JSX.Element {
81 const { t } = useTranslation(); 83 const { t } = useTranslation();
82 84
83 return ( 85 return (
86 /* eslint-disable react/jsx-props-no-spreading -- Conditionally set the onClose prop. */
84 <NotificationBannerRoot 87 <NotificationBannerRoot
85 severity={severity ?? 'success'} 88 severity={severity ?? 'success'}
86 icon={icon ?? false} 89 icon={icon ?? false}
87 onClose={onClose} 90 {...(onClose === undefined ? {} : { onClose })}
88 closeText={t<string>('banner.close')} 91 closeText={t<string>('banner.close')}
89 > 92 >
93 {/* eslint-enable react/jsx-props-no-spreading */}
90 <NotificationBannerText>{children}</NotificationBannerText> 94 <NotificationBannerText>{children}</NotificationBannerText>
91 {buttons && ( 95 {buttons && (
92 <NotificationBannerButtons>{buttons}</NotificationBannerButtons> 96 <NotificationBannerButtons hasCloseButton={onClose !== undefined}>
97 {buttons}
98 </NotificationBannerButtons>
93 )} 99 )}
94 </NotificationBannerRoot> 100 </NotificationBannerRoot>
95 ); 101 );
@@ -98,6 +104,7 @@ export default function NotificationBanner({
98NotificationBanner.defaultProps = { 104NotificationBanner.defaultProps = {
99 severity: 'success', 105 severity: 'success',
100 icon: false, 106 icon: false,
107 onClose: undefined,
101 buttons: undefined, 108 buttons: undefined,
102 children: undefined, 109 children: undefined,
103}; 110};
diff --git a/packages/renderer/src/stores/Service.ts b/packages/renderer/src/stores/Service.ts
index e14d80b..4510ec0 100644
--- a/packages/renderer/src/stores/Service.ts
+++ b/packages/renderer/src/stores/Service.ts
@@ -24,92 +24,118 @@ import { Instance } from 'mobx-state-tree';
24import { getEnv } from './RendererEnv'; 24import { getEnv } from './RendererEnv';
25import ServiceSettings from './ServiceSettings'; 25import ServiceSettings from './ServiceSettings';
26 26
27const Service = defineServiceModel(ServiceSettings).actions((self) => { 27const Service = defineServiceModel(ServiceSettings)
28 function dispatch(serviceAction: ServiceAction): void { 28 .views((self) => ({
29 getEnv(self).dispatchMainAction({ 29 get canReconnectSecurely(): boolean {
30 action: 'dispatch-service-action', 30 const { currentUrl } = self;
31 serviceId: self.id, 31 if (currentUrl === undefined) {
32 serviceAction, 32 return false;
33 }); 33 }
34 } 34 try {
35 35 const { protocol } = new URL(currentUrl);
36 return { 36 return protocol === 'http:';
37 goBack(): void { 37 } catch {
38 dispatch({ 38 return false;
39 action: 'back',
40 });
41 },
42 goForward(): void {
43 dispatch({
44 action: 'forward',
45 });
46 },
47 reload(ignoreCache = false): void {
48 dispatch({
49 action: 'reload',
50 ignoreCache,
51 });
52 },
53 stop(): void {
54 dispatch({
55 action: 'stop',
56 });
57 },
58 go(url: string): void {
59 dispatch({
60 action: 'go',
61 url,
62 });
63 },
64 goHome(): void {
65 dispatch({
66 action: 'go-home',
67 });
68 },
69 temporarilyTrustCurrentCertificate(): void {
70 if (self.state.type !== 'certificateError') {
71 throw new Error('No certificate to accept');
72 } 39 }
73 dispatch({
74 action: 'temporarily-trust-current-certificate',
75 fingerprint: self.state.certificate.fingerprint,
76 });
77 },
78 openCurrentURLInExternalBrowser(): void {
79 dispatch({
80 action: 'open-current-url-in-external-browser',
81 });
82 },
83 followPopup(url: string): void {
84 dispatch({
85 action: 'follow-popup',
86 url,
87 });
88 },
89 openPopupInExternalBrowser(url: string): void {
90 dispatch({
91 action: 'open-popup-in-external-browser',
92 url,
93 });
94 },
95 openAllPopupsInExternalBrowser(): void {
96 dispatch({
97 action: 'open-all-popups-in-external-browser',
98 });
99 },
100 dismissPopup(url: string): void {
101 dispatch({
102 action: 'dismiss-popup',
103 url,
104 });
105 }, 40 },
106 dismissAllPopups(): void { 41 }))
107 dispatch({ 42 .actions((self) => {
108 action: 'dismiss-all-popups', 43 function dispatch(serviceAction: ServiceAction): void {
44 getEnv(self).dispatchMainAction({
45 action: 'dispatch-service-action',
46 serviceId: self.id,
47 serviceAction,
109 }); 48 });
49 }
50
51 return {
52 goBack(): void {
53 dispatch({
54 action: 'back',
55 });
56 },
57 goForward(): void {
58 dispatch({
59 action: 'forward',
60 });
61 },
62 reload(ignoreCache = false): void {
63 dispatch({
64 action: 'reload',
65 ignoreCache,
66 });
67 },
68 stop(): void {
69 dispatch({
70 action: 'stop',
71 });
72 },
73 go(url: string): void {
74 dispatch({
75 action: 'go',
76 url,
77 });
78 },
79 goHome(): void {
80 dispatch({
81 action: 'go-home',
82 });
83 },
84 temporarilyTrustCurrentCertificate(): void {
85 if (self.state.type !== 'certificateError') {
86 throw new Error('No certificate to accept');
87 }
88 dispatch({
89 action: 'temporarily-trust-current-certificate',
90 fingerprint: self.state.certificate.fingerprint,
91 });
92 },
93 openCurrentURLInExternalBrowser(): void {
94 dispatch({
95 action: 'open-current-url-in-external-browser',
96 });
97 },
98 followPopup(url: string): void {
99 dispatch({
100 action: 'follow-popup',
101 url,
102 });
103 },
104 openPopupInExternalBrowser(url: string): void {
105 dispatch({
106 action: 'open-popup-in-external-browser',
107 url,
108 });
109 },
110 openAllPopupsInExternalBrowser(): void {
111 dispatch({
112 action: 'open-all-popups-in-external-browser',
113 });
114 },
115 dismissPopup(url: string): void {
116 dispatch({
117 action: 'dismiss-popup',
118 url,
119 });
120 },
121 dismissAllPopups(): void {
122 dispatch({
123 action: 'dismiss-all-popups',
124 });
125 },
126 };
127 })
128 .actions((self) => ({
129 reconnectSecurely(): void {
130 const { currentUrl, canReconnectSecurely } = self;
131 if (currentUrl === undefined || !canReconnectSecurely) {
132 return;
133 }
134 const url = new URL(currentUrl);
135 url.protocol = 'https:';
136 self.go(url.toString());
110 }, 137 },
111 }; 138 }));
112});
113 139
114/* 140/*
115 eslint-disable-next-line @typescript-eslint/no-redeclare -- 141 eslint-disable-next-line @typescript-eslint/no-redeclare --