diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/@types/stores.types.ts | 3 | ||||
-rw-r--r-- | src/api/apiBase.ts | 17 | ||||
-rw-r--r-- | src/api/server/ServerApi.ts | 4 | ||||
-rw-r--r-- | src/api/utils/auth.ts | 21 | ||||
-rw-r--r-- | src/components/settings/settings/EditSettingsForm.jsx | 25 | ||||
-rw-r--r-- | src/containers/settings/EditSettingsScreen.tsx | 2 | ||||
-rw-r--r-- | src/electron/ipc-api/localServer.ts | 35 | ||||
-rw-r--r-- | src/environment.ts | 4 | ||||
-rw-r--r-- | src/i18n/locales/en-US.json | 2 | ||||
-rw-r--r-- | src/internal-server/app/Controllers/Http/ImageController.js | 16 | ||||
-rw-r--r-- | src/internal-server/config/app.js | 2 | ||||
-rw-r--r-- | src/internal-server/config/session.js | 2 | ||||
-rw-r--r-- | src/internal-server/config/shield.js | 2 | ||||
-rw-r--r-- | src/internal-server/start.ts | 41 | ||||
-rw-r--r-- | src/internal-server/start/routes.js | 68 | ||||
-rw-r--r-- | src/internal-server/test.ts | 2 | ||||
-rw-r--r-- | src/lib/Menu.js | 4 | ||||
-rw-r--r-- | src/models/Service.ts | 16 | ||||
-rw-r--r-- | src/stores/RequestStore.ts | 11 |
19 files changed, 225 insertions, 52 deletions
diff --git a/src/@types/stores.types.ts b/src/@types/stores.types.ts index 37b906239..eec18c11e 100644 --- a/src/@types/stores.types.ts +++ b/src/@types/stores.types.ts | |||
@@ -166,7 +166,8 @@ interface RecipeStore extends TypedStore { | |||
166 | } | 166 | } |
167 | 167 | ||
168 | interface RequestsStore extends TypedStore { | 168 | interface RequestsStore extends TypedStore { |
169 | localServerPort: () => void; | 169 | localServerPort: number; |
170 | localServerToken: string | undefined; | ||
170 | retries: number; | 171 | retries: number; |
171 | retryDelay: number; | 172 | retryDelay: number; |
172 | servicesRequest: () => void; | 173 | servicesRequest: () => void; |
diff --git a/src/api/apiBase.ts b/src/api/apiBase.ts index 701919785..974d513a1 100644 --- a/src/api/apiBase.ts +++ b/src/api/apiBase.ts | |||
@@ -34,6 +34,23 @@ export default function apiBase(withVersion = true) { | |||
34 | return fixUrl(withVersion ? `${url}/${API_VERSION}` : url); | 34 | return fixUrl(withVersion ? `${url}/${API_VERSION}` : url); |
35 | }; | 35 | }; |
36 | 36 | ||
37 | export function needsToken(): boolean { | ||
38 | return (window as any).ferdium.stores.settings.all.app.server === LOCAL_SERVER; | ||
39 | } | ||
40 | |||
41 | export function localServerToken(): string | undefined { | ||
42 | return needsToken() | ||
43 | ? (window as any).ferdium.stores.requests.localServerToken | ||
44 | : undefined; | ||
45 | } | ||
46 | |||
47 | export function importExportURL() { | ||
48 | const base = apiBase(false); | ||
49 | return needsToken() | ||
50 | ? `${base}/token/${localServerToken()}` | ||
51 | : base; | ||
52 | } | ||
53 | |||
37 | export function serverBase() { | 54 | export function serverBase() { |
38 | const serverType = (window as any).ferdium.stores.settings.all.app.server; | 55 | const serverType = (window as any).ferdium.stores.settings.all.app.server; |
39 | const noServer = 'You are using Ferdium without a server'; | 56 | const noServer = 'You are using Ferdium without a server'; |
diff --git a/src/api/server/ServerApi.ts b/src/api/server/ServerApi.ts index 77a759b3e..8b551ade2 100644 --- a/src/api/server/ServerApi.ts +++ b/src/api/server/ServerApi.ts | |||
@@ -25,7 +25,7 @@ import { SERVER_NOT_LOADED } from '../../config'; | |||
25 | import { userDataRecipesPath, userDataPath } from '../../environment-remote'; | 25 | import { userDataRecipesPath, userDataPath } from '../../environment-remote'; |
26 | import { asarRecipesPath } from '../../helpers/asar-helpers'; | 26 | import { asarRecipesPath } from '../../helpers/asar-helpers'; |
27 | import apiBase from '../apiBase'; | 27 | import apiBase from '../apiBase'; |
28 | import { prepareAuthRequest, sendAuthRequest } from '../utils/auth'; | 28 | import { prepareAuthRequest, prepareLocalToken, sendAuthRequest } from '../utils/auth'; |
29 | 29 | ||
30 | import { | 30 | import { |
31 | getRecipeDirectory, | 31 | getRecipeDirectory, |
@@ -246,6 +246,8 @@ export default class ServerApi { | |||
246 | 246 | ||
247 | delete requestData.headers['Content-Type']; | 247 | delete requestData.headers['Content-Type']; |
248 | 248 | ||
249 | await prepareLocalToken(requestData); | ||
250 | |||
249 | const request = await window.fetch( | 251 | const request = await window.fetch( |
250 | `${apiBase()}/service/${serviceId}`, | 252 | `${apiBase()}/service/${serviceId}`, |
251 | // @ts-expect-error Argument of type '{ method: string; } & { mode: string; headers: any; }' is not assignable to parameter of type 'RequestInit | undefined'. | 253 | // @ts-expect-error Argument of type '{ method: string; } & { mode: string; headers: any; }' is not assignable to parameter of type 'RequestInit | undefined'. |
diff --git a/src/api/utils/auth.ts b/src/api/utils/auth.ts index a7a73309d..282d00459 100644 --- a/src/api/utils/auth.ts +++ b/src/api/utils/auth.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import localStorage from 'mobx-localstorage'; | 1 | import localStorage from 'mobx-localstorage'; |
2 | import { when } from 'mobx'; | ||
3 | import { localServerToken, needsToken } from '../apiBase'; | ||
2 | import { ferdiumLocale, ferdiumVersion } from '../../environment-remote'; | 4 | import { ferdiumLocale, ferdiumVersion } from '../../environment-remote'; |
3 | 5 | ||
4 | export const prepareAuthRequest = ( | 6 | export const prepareAuthRequest = ( |
@@ -29,10 +31,23 @@ export const prepareAuthRequest = ( | |||
29 | return request; | 31 | return request; |
30 | }; | 32 | }; |
31 | 33 | ||
32 | export const sendAuthRequest = ( | 34 | export const prepareLocalToken = async ( |
35 | requestData: { method: string; headers?: any; body?: any }, | ||
36 | ) => { | ||
37 | await when(() => !needsToken() || !!localServerToken(), { timeout: 2000 }); | ||
38 | const token = localServerToken(); | ||
39 | if (token) { | ||
40 | requestData.headers['X-Ferdium-Local-Token'] = token; | ||
41 | } | ||
42 | } | ||
43 | |||
44 | export const sendAuthRequest = async ( | ||
33 | url: RequestInfo, | 45 | url: RequestInfo, |
34 | options?: { method: string; headers?: any; body?: any }, | 46 | options?: { method: string; headers?: any; body?: any }, |
35 | auth?: boolean, | 47 | auth?: boolean, |
36 | ) => | 48 | ) => { |
49 | const request = prepareAuthRequest(options, auth); | ||
50 | await prepareLocalToken(request); | ||
37 | // @ts-expect-error Argument of type '{ method: string; } & { mode: string; headers: any; }' is not assignable to parameter of type 'RequestInit | undefined'. | 51 | // @ts-expect-error Argument of type '{ method: string; } & { mode: string; headers: any; }' is not assignable to parameter of type 'RequestInit | undefined'. |
38 | window.fetch(url, prepareAuthRequest(options, auth)); | 52 | return window.fetch(url, request); |
53 | }; | ||
diff --git a/src/components/settings/settings/EditSettingsForm.jsx b/src/components/settings/settings/EditSettingsForm.jsx index 4ae431adb..e77f1b25b 100644 --- a/src/components/settings/settings/EditSettingsForm.jsx +++ b/src/components/settings/settings/EditSettingsForm.jsx | |||
@@ -30,7 +30,7 @@ import { | |||
30 | userDataPath, | 30 | userDataPath, |
31 | userDataRecipesPath, | 31 | userDataRecipesPath, |
32 | } from '../../../environment-remote'; | 32 | } from '../../../environment-remote'; |
33 | import { openPath } from '../../../helpers/url-helpers'; | 33 | import { openExternalUrl, openPath } from '../../../helpers/url-helpers'; |
34 | import globalMessages from '../../../i18n/globalMessages'; | 34 | import globalMessages from '../../../i18n/globalMessages'; |
35 | import Icon from '../../ui/icon'; | 35 | import Icon from '../../ui/icon'; |
36 | import Slider from '../../ui/Slider'; | 36 | import Slider from '../../ui/Slider'; |
@@ -197,6 +197,14 @@ const messages = defineMessages({ | |||
197 | id: 'settings.app.buttonOpenFerdiumServiceRecipesFolder', | 197 | id: 'settings.app.buttonOpenFerdiumServiceRecipesFolder', |
198 | defaultMessage: 'Open Service Recipes folder', | 198 | defaultMessage: 'Open Service Recipes folder', |
199 | }, | 199 | }, |
200 | buttonOpenImportExport: { | ||
201 | id: 'settings.app.buttonOpenImportExport', | ||
202 | defaultMessage: 'Import / Export', | ||
203 | }, | ||
204 | serverHelp: { | ||
205 | id: 'settings.app.serverHelp', | ||
206 | defaultMessage: 'Connected to server at {serverURL}', | ||
207 | }, | ||
200 | buttonSearchForUpdate: { | 208 | buttonSearchForUpdate: { |
201 | id: 'settings.app.buttonSearchForUpdate', | 209 | id: 'settings.app.buttonSearchForUpdate', |
202 | defaultMessage: 'Check for updates', | 210 | defaultMessage: 'Check for updates', |
@@ -274,6 +282,7 @@ class EditSettingsForm extends Component { | |||
274 | openProcessManager: PropTypes.func.isRequired, | 282 | openProcessManager: PropTypes.func.isRequired, |
275 | isSplitModeEnabled: PropTypes.bool.isRequired, | 283 | isSplitModeEnabled: PropTypes.bool.isRequired, |
276 | isOnline: PropTypes.bool.isRequired, | 284 | isOnline: PropTypes.bool.isRequired, |
285 | serverURL: PropTypes.string.isRequired, | ||
277 | }; | 286 | }; |
278 | 287 | ||
279 | constructor(props) { | 288 | constructor(props) { |
@@ -336,6 +345,7 @@ class EditSettingsForm extends Component { | |||
336 | openProcessManager, | 345 | openProcessManager, |
337 | isTodosActivated, | 346 | isTodosActivated, |
338 | isOnline, | 347 | isOnline, |
348 | serverURL, | ||
339 | } = this.props; | 349 | } = this.props; |
340 | const { intl } = this.props; | 350 | const { intl } = this.props; |
341 | 351 | ||
@@ -939,8 +949,21 @@ class EditSettingsForm extends Component { | |||
939 | className="settings__open-settings-file-button" | 949 | className="settings__open-settings-file-button" |
940 | onClick={() => openPath(recipeFolder)} | 950 | onClick={() => openPath(recipeFolder)} |
941 | /> | 951 | /> |
952 | <Button | ||
953 | buttonType="secondary" | ||
954 | label={intl.formatMessage( | ||
955 | messages.buttonOpenImportExport, | ||
956 | )} | ||
957 | className="settings__open-settings-file-button" | ||
958 | onClick={() => openExternalUrl(serverURL, true)} | ||
959 | /> | ||
942 | </div> | 960 | </div> |
943 | </p> | 961 | </p> |
962 | <p className="settings__help"> | ||
963 | {intl.formatMessage(messages.serverHelp, { | ||
964 | serverURL, | ||
965 | })} | ||
966 | </p> | ||
944 | </div> | 967 | </div> |
945 | </div> | 968 | </div> |
946 | )} | 969 | )} |
diff --git a/src/containers/settings/EditSettingsScreen.tsx b/src/containers/settings/EditSettingsScreen.tsx index 8f0884737..0ab3b63cf 100644 --- a/src/containers/settings/EditSettingsScreen.tsx +++ b/src/containers/settings/EditSettingsScreen.tsx | |||
@@ -33,6 +33,7 @@ import EditSettingsForm from '../../components/settings/settings/EditSettingsFor | |||
33 | import ErrorBoundary from '../../components/util/ErrorBoundary'; | 33 | import ErrorBoundary from '../../components/util/ErrorBoundary'; |
34 | 34 | ||
35 | import globalMessages from '../../i18n/globalMessages'; | 35 | import globalMessages from '../../i18n/globalMessages'; |
36 | import { importExportURL } from '../../api/apiBase'; | ||
36 | 37 | ||
37 | const debug = require('../../preload-safe-debug')('Ferdium:EditSettingsScreen'); | 38 | const debug = require('../../preload-safe-debug')('Ferdium:EditSettingsScreen'); |
38 | 39 | ||
@@ -894,6 +895,7 @@ class EditSettingsScreen extends Component<EditSettingsScreenProps> { | |||
894 | } | 895 | } |
895 | openProcessManager={() => this.openProcessManager()} | 896 | openProcessManager={() => this.openProcessManager()} |
896 | isOnline={app.isOnline} | 897 | isOnline={app.isOnline} |
898 | serverURL={importExportURL()} | ||
897 | /> | 899 | /> |
898 | </ErrorBoundary> | 900 | </ErrorBoundary> |
899 | ); | 901 | ); |
diff --git a/src/electron/ipc-api/localServer.ts b/src/electron/ipc-api/localServer.ts index 04ddc976a..71a3003d1 100644 --- a/src/electron/ipc-api/localServer.ts +++ b/src/electron/ipc-api/localServer.ts | |||
@@ -1,9 +1,12 @@ | |||
1 | import { randomBytes } from 'crypto'; | ||
1 | import { ipcMain, BrowserWindow } from 'electron'; | 2 | import { ipcMain, BrowserWindow } from 'electron'; |
2 | import { createServer } from 'net'; | 3 | import { createServer } from 'net'; |
3 | import { LOCAL_HOSTNAME, LOCAL_PORT } from '../../config'; | 4 | import { LOCAL_HOSTNAME, LOCAL_PORT } from '../../config'; |
4 | import { userDataPath } from '../../environment-remote'; | 5 | import { userDataPath } from '../../environment-remote'; |
5 | import { server } from '../../internal-server/start'; | 6 | import { server } from '../../internal-server/start'; |
6 | 7 | ||
8 | const debug = require('../../preload-safe-debug')('Ferdium:LocalServer'); | ||
9 | |||
7 | const portInUse = (port: number): Promise<boolean> => | 10 | const portInUse = (port: number): Promise<boolean> => |
8 | new Promise(resolve => { | 11 | new Promise(resolve => { |
9 | const server = createServer(socket => { | 12 | const server = createServer(socket => { |
@@ -22,26 +25,32 @@ const portInUse = (port: number): Promise<boolean> => | |||
22 | }); | 25 | }); |
23 | 26 | ||
24 | let localServerStarted = false; | 27 | let localServerStarted = false; |
28 | let port = LOCAL_PORT; | ||
29 | let token = ''; | ||
25 | 30 | ||
26 | export default (params: { mainWindow: BrowserWindow }) => { | 31 | export default (params: { mainWindow: BrowserWindow }) => { |
27 | ipcMain.on('startLocalServer', () => { | 32 | ipcMain.on('startLocalServer', () => { |
28 | if (!localServerStarted) { | 33 | (async () => { |
29 | // Find next unused port for server | 34 | if (!localServerStarted) { |
30 | let port = LOCAL_PORT; | 35 | // Find next unused port for server |
31 | (async () => { | 36 | port = LOCAL_PORT; |
32 | // eslint-disable-next-line no-await-in-loop | 37 | // eslint-disable-next-line no-await-in-loop |
33 | while ((await portInUse(port)) && port < LOCAL_PORT + 10) { | 38 | while ((await portInUse(port)) && port < LOCAL_PORT + 10) { |
34 | port += 1; | 39 | port += 1; |
35 | } | 40 | } |
36 | console.log('Starting local server on port', port); | 41 | token = randomBytes(256 / 8).toString('base64url'); |
37 | 42 | debug('Starting local server at', `http://localhost:${port}/token/${token}`); | |
38 | server(userDataPath(), port); | 43 | await server(userDataPath(), port, token); |
44 | localServerStarted = true; | ||
45 | } | ||
39 | 46 | ||
40 | params.mainWindow.webContents.send('localServerPort', { | 47 | // Send local server parameters to the renderer even if the server is already running. |
41 | port, | 48 | params.mainWindow.webContents.send('localServerPort', { |
42 | }); | 49 | port, |
43 | })(); | 50 | token, |
44 | localServerStarted = true; | 51 | }); |
45 | } | 52 | })().catch((error) => { |
53 | console.error('Error while starting local server', error); | ||
54 | }); | ||
46 | }); | 55 | }); |
47 | }; | 56 | }; |
diff --git a/src/environment.ts b/src/environment.ts index 271bc7571..b1b4cb8b8 100644 --- a/src/environment.ts +++ b/src/environment.ts | |||
@@ -6,8 +6,8 @@ export const isMac = process.platform === 'darwin'; | |||
6 | export const isWindows = process.platform === 'win32'; | 6 | export const isWindows = process.platform === 'win32'; |
7 | export const isLinux = process.platform === 'linux'; | 7 | export const isLinux = process.platform === 'linux'; |
8 | 8 | ||
9 | export const electronVersion: string = process.versions.electron; | 9 | export const electronVersion: string = process.versions.electron ?? ''; |
10 | export const chromeVersion: string = process.versions.chrome; | 10 | export const chromeVersion: string = process.versions.chrome ?? ''; |
11 | export const nodeVersion: string = process.versions.node; | 11 | export const nodeVersion: string = process.versions.node; |
12 | 12 | ||
13 | export const osArch: string = arch(); | 13 | export const osArch: string = arch(); |
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 27d825657..f25d59098 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json | |||
@@ -192,6 +192,7 @@ | |||
192 | "settings.app.buttonInstallUpdate": "Restart & install update", | 192 | "settings.app.buttonInstallUpdate": "Restart & install update", |
193 | "settings.app.buttonOpenFerdiumProfileFolder": "Open Profile folder", | 193 | "settings.app.buttonOpenFerdiumProfileFolder": "Open Profile folder", |
194 | "settings.app.buttonOpenFerdiumServiceRecipesFolder": "Open Service Recipes folder", | 194 | "settings.app.buttonOpenFerdiumServiceRecipesFolder": "Open Service Recipes folder", |
195 | "settings.app.buttonOpenImportExport": "Import / Export", | ||
195 | "settings.app.buttonSearchForUpdate": "Check for updates", | 196 | "settings.app.buttonSearchForUpdate": "Check for updates", |
196 | "settings.app.cacheInfo": "Ferdium cache is currently using {size} of disk space.", | 197 | "settings.app.cacheInfo": "Ferdium cache is currently using {size} of disk space.", |
197 | "settings.app.cacheNotCleared": "Couldn't clear all cache", | 198 | "settings.app.cacheNotCleared": "Couldn't clear all cache", |
@@ -287,6 +288,7 @@ | |||
287 | "settings.app.sectionServiceIconsSettings": "Service Icons Settings", | 288 | "settings.app.sectionServiceIconsSettings": "Service Icons Settings", |
288 | "settings.app.sectionSidebarSettings": "Sidebar Settings", | 289 | "settings.app.sectionSidebarSettings": "Sidebar Settings", |
289 | "settings.app.sectionUpdates": "App Updates Settings", | 290 | "settings.app.sectionUpdates": "App Updates Settings", |
291 | "settings.app.serverHelp": "Connected to server at {serverURL}", | ||
290 | "settings.app.spellCheckerLanguageInfo": "Ferdium uses your Mac's build-in spellchecker to check for typos. If you want to change the languages the spellchecker checks for, you can do so in your Mac's System Preferences.", | 292 | "settings.app.spellCheckerLanguageInfo": "Ferdium uses your Mac's build-in spellchecker to check for typos. If you want to change the languages the spellchecker checks for, you can do so in your Mac's System Preferences.", |
291 | "settings.app.subheadlineCache": "Cache", | 293 | "settings.app.subheadlineCache": "Cache", |
292 | "settings.app.subheadlineFerdiumProfile": "Ferdium Profile", | 294 | "settings.app.subheadlineFerdiumProfile": "Ferdium Profile", |
diff --git a/src/internal-server/app/Controllers/Http/ImageController.js b/src/internal-server/app/Controllers/Http/ImageController.js index 9b11783c7..731f181e0 100644 --- a/src/internal-server/app/Controllers/Http/ImageController.js +++ b/src/internal-server/app/Controllers/Http/ImageController.js | |||
@@ -2,13 +2,25 @@ const Env = use('Env'); | |||
2 | 2 | ||
3 | const path = require('path'); | 3 | const path = require('path'); |
4 | const fs = require('fs-extra'); | 4 | const fs = require('fs-extra'); |
5 | const sanitize = require('sanitize-filename'); | ||
5 | 6 | ||
6 | class ImageController { | 7 | class ImageController { |
7 | async icon({ params, response }) { | 8 | async icon({ params, response }) { |
8 | const { id } = params; | 9 | let { id } = params; |
10 | |||
11 | id = sanitize(id); | ||
12 | if (id === '') { | ||
13 | return response.status(404).send({ | ||
14 | status: "Icon doesn't exist", | ||
15 | }); | ||
16 | } | ||
9 | 17 | ||
10 | const iconPath = path.join(Env.get('USER_PATH'), 'icons', id); | 18 | const iconPath = path.join(Env.get('USER_PATH'), 'icons', id); |
11 | if (!fs.existsSync(iconPath)) { | 19 | |
20 | try { | ||
21 | await fs.access(iconPath); | ||
22 | } catch { | ||
23 | // File not available. | ||
12 | return response.status(404).send({ | 24 | return response.status(404).send({ |
13 | status: "Icon doesn't exist", | 25 | status: "Icon doesn't exist", |
14 | }); | 26 | }); |
diff --git a/src/internal-server/config/app.js b/src/internal-server/config/app.js index 303e1290c..e8b52af18 100644 --- a/src/internal-server/config/app.js +++ b/src/internal-server/config/app.js | |||
@@ -232,7 +232,7 @@ module.exports = { | |||
232 | */ | 232 | */ |
233 | cookie: { | 233 | cookie: { |
234 | httpOnly: true, | 234 | httpOnly: true, |
235 | sameSite: false, | 235 | sameSite: true, |
236 | path: '/', | 236 | path: '/', |
237 | maxAge: 7200, | 237 | maxAge: 7200, |
238 | }, | 238 | }, |
diff --git a/src/internal-server/config/session.js b/src/internal-server/config/session.js index 3ce3cc4da..dbe007e2b 100644 --- a/src/internal-server/config/session.js +++ b/src/internal-server/config/session.js | |||
@@ -63,7 +63,7 @@ module.exports = { | |||
63 | cookie: { | 63 | cookie: { |
64 | httpOnly: true, | 64 | httpOnly: true, |
65 | path: '/', | 65 | path: '/', |
66 | sameSite: false, | 66 | sameSite: true, |
67 | }, | 67 | }, |
68 | 68 | ||
69 | /* | 69 | /* |
diff --git a/src/internal-server/config/shield.js b/src/internal-server/config/shield.js index 4ff22c3f9..55029faa4 100644 --- a/src/internal-server/config/shield.js +++ b/src/internal-server/config/shield.js | |||
@@ -133,7 +133,7 @@ module.exports = { | |||
133 | methods: ['POST', 'PUT', 'DELETE'], | 133 | methods: ['POST', 'PUT', 'DELETE'], |
134 | filterUris: [], | 134 | filterUris: [], |
135 | cookieOptions: { | 135 | cookieOptions: { |
136 | httpOnly: false, | 136 | httpOnly: true, |
137 | sameSite: true, | 137 | sameSite: true, |
138 | path: '/', | 138 | path: '/', |
139 | maxAge: 7200, | 139 | maxAge: 7200, |
diff --git a/src/internal-server/start.ts b/src/internal-server/start.ts index 62311b21e..ae28e3313 100644 --- a/src/internal-server/start.ts +++ b/src/internal-server/start.ts | |||
@@ -16,36 +16,59 @@ | |||
16 | */ | 16 | */ |
17 | 17 | ||
18 | import fold from '@adonisjs/fold'; | 18 | import fold from '@adonisjs/fold'; |
19 | import { Ignitor } from '@adonisjs/ignitor'; | 19 | import { Ignitor, hooks } from '@adonisjs/ignitor'; |
20 | import { existsSync, readFile, statSync, chmodSync, writeFile } from 'fs-extra'; | 20 | import { readFile, stat, chmod, writeFile } from 'fs-extra'; |
21 | import { join } from 'path'; | 21 | import { join } from 'path'; |
22 | import { LOCAL_HOSTNAME } from '../config'; | 22 | import { LOCAL_HOSTNAME } from '../config'; |
23 | import { isWindows } from '../environment'; | 23 | import { isWindows } from '../environment'; |
24 | 24 | ||
25 | process.env.ENV_PATH = join(__dirname, 'env.ini'); | 25 | process.env.ENV_PATH = join(__dirname, 'env.ini'); |
26 | 26 | ||
27 | export const server = async (userPath: string, port: number) => { | 27 | async function ensureDB(dbPath: string): Promise<void> { |
28 | const dbPath = join(userPath, 'server.sqlite'); | 28 | try { |
29 | const dbTemplatePath = join(__dirname, 'database', 'template.sqlite'); | 29 | await stat(dbPath); |
30 | 30 | } catch { | |
31 | if (!existsSync(dbPath)) { | 31 | // Database does not exist. |
32 | // Manually copy file | 32 | // Manually copy file |
33 | // We can't use copyFile here as it will cause the file to be readonly on Windows | 33 | // We can't use copyFile here as it will cause the file to be readonly on Windows |
34 | const dbTemplatePath = join(__dirname, 'database', 'template.sqlite'); | ||
34 | const dbTemplate = await readFile(dbTemplatePath); | 35 | const dbTemplate = await readFile(dbTemplatePath); |
35 | await writeFile(dbPath, dbTemplate); | 36 | await writeFile(dbPath, dbTemplate); |
36 | 37 | ||
37 | // Change permissions to ensure to file is not read-only | 38 | // Change permissions to ensure to file is not read-only |
38 | if (isWindows) { | 39 | if (isWindows) { |
40 | const stats = await stat(dbPath); | ||
39 | // eslint-disable-next-line no-bitwise | 41 | // eslint-disable-next-line no-bitwise |
40 | chmodSync(dbPath, statSync(dbPath).mode | 146); | 42 | await chmod(dbPath, stats.mode | 146); |
41 | } | 43 | } |
42 | } | 44 | } |
45 | } | ||
46 | |||
47 | export const server = async (userPath: string, port: number, token: string) => { | ||
48 | const dbPath = join(userPath, 'server.sqlite'); | ||
49 | await ensureDB(dbPath); | ||
43 | 50 | ||
44 | // Note: These env vars are used by adonis as env vars | 51 | // Note: These env vars are used by adonis as env vars |
45 | process.env.DB_PATH = dbPath; | 52 | process.env.DB_PATH = dbPath; |
46 | process.env.USER_PATH = userPath; | 53 | process.env.USER_PATH = userPath; |
47 | process.env.HOST = LOCAL_HOSTNAME; | 54 | process.env.HOST = LOCAL_HOSTNAME; |
48 | process.env.PORT = port.toString(); | 55 | process.env.PORT = port.toString(); |
56 | process.env.FERDIUM_LOCAL_TOKEN = token; | ||
49 | 57 | ||
50 | new Ignitor(fold).appRoot(__dirname).fireHttpServer().catch(console.error); | 58 | return new Promise<void>((resolve, reject) => { |
59 | let returned = false; | ||
60 | hooks.after.httpServer(() => { | ||
61 | if (!returned) { | ||
62 | resolve(); | ||
63 | returned = true; | ||
64 | } | ||
65 | }); | ||
66 | new Ignitor(fold).appRoot(__dirname).fireHttpServer().catch((error) => { | ||
67 | console.error(error); | ||
68 | if (!returned) { | ||
69 | returned = true; | ||
70 | reject(error); | ||
71 | } | ||
72 | }); | ||
73 | }); | ||
51 | }; | 74 | }; |
diff --git a/src/internal-server/start/routes.js b/src/internal-server/start/routes.js index 79c809f5f..736796bb8 100644 --- a/src/internal-server/start/routes.js +++ b/src/internal-server/start/routes.js | |||
@@ -5,6 +5,8 @@ | |||
5 | | | 5 | | |
6 | */ | 6 | */ |
7 | 7 | ||
8 | const { timingSafeEqual } = require('crypto'); | ||
9 | |||
8 | /** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ | 10 | /** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ |
9 | const Route = use('Route'); | 11 | const Route = use('Route'); |
10 | 12 | ||
@@ -14,14 +16,38 @@ const migrate = require('./migrate'); | |||
14 | 16 | ||
15 | migrate(); | 17 | migrate(); |
16 | 18 | ||
19 | async function validateToken(clientToken, response, next) { | ||
20 | const serverToken = process.env.FERDIUM_LOCAL_TOKEN; | ||
21 | const valid = serverToken && | ||
22 | clientToken && | ||
23 | timingSafeEqual(Buffer.from(clientToken, 'utf8'), Buffer.from(serverToken, 'utf8')); | ||
24 | if (valid) { | ||
25 | await next(); | ||
26 | return true; | ||
27 | } | ||
28 | return response.forbidden(); | ||
29 | } | ||
30 | |||
17 | const OnlyAllowFerdium = async ({ request, response }, next) => { | 31 | const OnlyAllowFerdium = async ({ request, response }, next) => { |
18 | const version = request.header('X-Franz-Version'); | 32 | const version = request.header('X-Franz-Version'); |
19 | if (!version) { | 33 | if (!version) { |
20 | return response.status(403).redirect('/'); | 34 | return response.forbidden(); |
21 | } | 35 | } |
22 | 36 | ||
23 | await next(); | 37 | const clientToken = request.header('X-Ferdium-Local-Token'); |
24 | return true; | 38 | return validateToken(clientToken, response, next); |
39 | }; | ||
40 | |||
41 | const RequireTokenInQS = async ({ request, response }, next) => { | ||
42 | const clientToken = request.get().token; | ||
43 | return validateToken(clientToken, response, next); | ||
44 | } | ||
45 | |||
46 | const FERDIUM_LOCAL_TOKEN_COOKIE = 'ferdium-local-token'; | ||
47 | |||
48 | const RequireAuthenticatedBrowser = async({ request, response }, next) => { | ||
49 | const clientToken = request.cookie(FERDIUM_LOCAL_TOKEN_COOKIE); | ||
50 | return validateToken(clientToken, response, next); | ||
25 | }; | 51 | }; |
26 | 52 | ||
27 | // Health: Returning if all systems function correctly | 53 | // Health: Returning if all systems function correctly |
@@ -67,16 +93,32 @@ Route.group(() => { | |||
67 | 93 | ||
68 | Route.group(() => { | 94 | Route.group(() => { |
69 | Route.get('icon/:id', 'ImageController.icon'); | 95 | Route.get('icon/:id', 'ImageController.icon'); |
70 | }).prefix(API_VERSION); | 96 | }) |
97 | .prefix(API_VERSION) | ||
98 | .middleware(RequireTokenInQS); | ||
71 | 99 | ||
72 | // Franz account import | 100 | Route.group(() => { |
73 | Route.post('import', 'UserController.import'); | 101 | // Franz account import |
74 | Route.get('import', ({ view }) => view.render('import')); | 102 | Route.post('import', 'UserController.import'); |
103 | Route.get('import', ({ view }) => view.render('import')); | ||
104 | |||
105 | // Account transfer | ||
106 | Route.get('export', 'UserController.export'); | ||
107 | Route.post('transfer', 'UserController.importFerdium'); | ||
108 | Route.get('transfer', ({ view }) => view.render('transfer')); | ||
75 | 109 | ||
76 | // Account transfer | 110 | // Index |
77 | Route.get('export', 'UserController.export'); | 111 | Route.get('/', ({ view }) => view.render('index')); |
78 | Route.post('transfer', 'UserController.importFerdium'); | 112 | }).middleware(RequireAuthenticatedBrowser); |
79 | Route.get('transfer', ({ view }) => view.render('transfer')); | ||
80 | 113 | ||
81 | // Index | 114 | Route.get('token/:token', ({ params: { token }, response }) => { |
82 | Route.get('/', ({ view }) => view.render('index')); | 115 | if (validateToken(token)) { |
116 | response.cookie(FERDIUM_LOCAL_TOKEN_COOKIE, token, { | ||
117 | httpOnly: true, | ||
118 | sameSite: true, | ||
119 | path: '/', | ||
120 | }); | ||
121 | return response.redirect('/'); | ||
122 | } | ||
123 | return response.forbidden(); | ||
124 | }); | ||
diff --git a/src/internal-server/test.ts b/src/internal-server/test.ts index 87ed57848..5bb1f2b36 100644 --- a/src/internal-server/test.ts +++ b/src/internal-server/test.ts | |||
@@ -6,4 +6,4 @@ const dummyUserFolder = join(__dirname, 'user_data'); | |||
6 | 6 | ||
7 | ensureDirSync(dummyUserFolder); | 7 | ensureDirSync(dummyUserFolder); |
8 | 8 | ||
9 | server(dummyUserFolder, 46_568); | 9 | server(dummyUserFolder, 46_568, 'test').catch(console.log); |
diff --git a/src/lib/Menu.js b/src/lib/Menu.js index a3a8f4566..8904919f9 100644 --- a/src/lib/Menu.js +++ b/src/lib/Menu.js | |||
@@ -38,7 +38,7 @@ import { ferdiumVersion } from '../environment-remote'; | |||
38 | import { todoActions } from '../features/todos/actions'; | 38 | import { todoActions } from '../features/todos/actions'; |
39 | import workspaceActions from '../features/workspaces/actions'; | 39 | import workspaceActions from '../features/workspaces/actions'; |
40 | import { workspaceStore } from '../features/workspaces/index'; | 40 | import { workspaceStore } from '../features/workspaces/index'; |
41 | import apiBase, { serverBase, serverName } from '../api/apiBase'; | 41 | import { importExportURL, serverBase, serverName } from '../api/apiBase'; |
42 | import { openExternalUrl } from '../helpers/url-helpers'; | 42 | import { openExternalUrl } from '../helpers/url-helpers'; |
43 | import globalMessages from '../i18n/globalMessages'; | 43 | import globalMessages from '../i18n/globalMessages'; |
44 | 44 | ||
@@ -588,7 +588,7 @@ const _titleBarTemplateFactory = (intl, locked) => [ | |||
588 | { | 588 | { |
589 | label: intl.formatMessage(menuItems.importExportData), | 589 | label: intl.formatMessage(menuItems.importExportData), |
590 | click() { | 590 | click() { |
591 | openExternalUrl(apiBase(false), true); | 591 | openExternalUrl(importExportURL(), true); |
592 | }, | 592 | }, |
593 | enabled: !locked, | 593 | enabled: !locked, |
594 | }, | 594 | }, |
diff --git a/src/models/Service.ts b/src/models/Service.ts index 92b8ee64c..21fa65f69 100644 --- a/src/models/Service.ts +++ b/src/models/Service.ts | |||
@@ -11,6 +11,7 @@ import UserAgent from './UserAgent'; | |||
11 | import { DEFAULT_SERVICE_ORDER } from '../config'; | 11 | import { DEFAULT_SERVICE_ORDER } from '../config'; |
12 | import { ifUndefined } from '../jsUtils'; | 12 | import { ifUndefined } from '../jsUtils'; |
13 | import { IRecipe } from './Recipe'; | 13 | import { IRecipe } from './Recipe'; |
14 | import { needsToken } from '../api/apiBase'; | ||
14 | 15 | ||
15 | const debug = require('../preload-safe-debug')('Ferdium:Service'); | 16 | const debug = require('../preload-safe-debug')('Ferdium:Service'); |
16 | 17 | ||
@@ -295,6 +296,21 @@ export default class Service { | |||
295 | 296 | ||
296 | @computed get icon(): string { | 297 | @computed get icon(): string { |
297 | if (this.iconUrl) { | 298 | if (this.iconUrl) { |
299 | if (needsToken()) { | ||
300 | let url: URL; | ||
301 | try { | ||
302 | url = new URL(this.iconUrl); | ||
303 | } catch (error) { | ||
304 | debug('Invalid url', this.iconUrl, error); | ||
305 | return this.iconUrl; | ||
306 | } | ||
307 | const requestStore = (window as any).ferdium.stores.requests; | ||
308 | // Make sure we only pass the token to the local server. | ||
309 | if (url.origin === requestStore.localServerOrigin) { | ||
310 | url.searchParams.set('token', requestStore.localServerToken); | ||
311 | return url.toString(); | ||
312 | } | ||
313 | } | ||
298 | return this.iconUrl; | 314 | return this.iconUrl; |
299 | } | 315 | } |
300 | 316 | ||
diff --git a/src/stores/RequestStore.ts b/src/stores/RequestStore.ts index a964c5d12..59982c05a 100644 --- a/src/stores/RequestStore.ts +++ b/src/stores/RequestStore.ts | |||
@@ -6,7 +6,7 @@ import { Actions } from '../actions/lib/actions'; | |||
6 | import { ApiInterface } from '../api'; | 6 | import { ApiInterface } from '../api'; |
7 | import { Stores } from '../@types/stores.types'; | 7 | import { Stores } from '../@types/stores.types'; |
8 | import CachedRequest from './lib/CachedRequest'; | 8 | import CachedRequest from './lib/CachedRequest'; |
9 | import { LOCAL_PORT } from '../config'; | 9 | import { LOCAL_HOSTNAME, LOCAL_PORT } from '../config'; |
10 | 10 | ||
11 | import TypedStore from './lib/TypedStore'; | 11 | import TypedStore from './lib/TypedStore'; |
12 | 12 | ||
@@ -21,6 +21,8 @@ export default class RequestStore extends TypedStore { | |||
21 | 21 | ||
22 | @observable localServerPort = LOCAL_PORT; | 22 | @observable localServerPort = LOCAL_PORT; |
23 | 23 | ||
24 | @observable localServerToken: string | undefined; | ||
25 | |||
24 | retries: number = 0; | 26 | retries: number = 0; |
25 | 27 | ||
26 | retryDelay: number = ms('2s'); | 28 | retryDelay: number = ms('2s'); |
@@ -45,6 +47,9 @@ export default class RequestStore extends TypedStore { | |||
45 | if (data.port) { | 47 | if (data.port) { |
46 | this.localServerPort = data.port; | 48 | this.localServerPort = data.port; |
47 | } | 49 | } |
50 | if (data.token) { | ||
51 | this.localServerToken = data.token; | ||
52 | } | ||
48 | }); | 53 | }); |
49 | } | 54 | } |
50 | 55 | ||
@@ -56,6 +61,10 @@ export default class RequestStore extends TypedStore { | |||
56 | return this.userInfoRequest.isExecuting || this.servicesRequest.isExecuting; | 61 | return this.userInfoRequest.isExecuting || this.servicesRequest.isExecuting; |
57 | } | 62 | } |
58 | 63 | ||
64 | @computed get localServerOrigin(): string { | ||
65 | return `http://${LOCAL_HOSTNAME}:${this.localServerPort}`; | ||
66 | } | ||
67 | |||
59 | @action _retryRequiredRequests(): void { | 68 | @action _retryRequiredRequests(): void { |
60 | this.userInfoRequest.reload(); | 69 | this.userInfoRequest.reload(); |
61 | this.servicesRequest.reload(); | 70 | this.servicesRequest.reload(); |