aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-07-10 16:07:45 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-07-11 17:47:53 +0200
commitfa1a7037b47f2e0114d8abc5a99d29239bd3637b (patch)
tree83404acf711aa8976dce47950edcca64836e0cd8 /src
parent6.0.0-nightly.96 [skip ci] (diff)
downloadferdium-app-fa1a7037b47f2e0114d8abc5a99d29239bd3637b.tar.gz
ferdium-app-fa1a7037b47f2e0114d8abc5a99d29239bd3637b.tar.zst
ferdium-app-fa1a7037b47f2e0114d8abc5a99d29239bd3637b.zip
refactor: local server import/export
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'src')
-rw-r--r--src/@types/stores.types.ts3
-rw-r--r--src/api/apiBase.ts17
-rw-r--r--src/api/server/ServerApi.ts4
-rw-r--r--src/api/utils/auth.ts21
-rw-r--r--src/components/settings/settings/EditSettingsForm.jsx25
-rw-r--r--src/containers/settings/EditSettingsScreen.tsx2
-rw-r--r--src/electron/ipc-api/localServer.ts35
-rw-r--r--src/environment.ts4
-rw-r--r--src/i18n/locales/en-US.json2
-rw-r--r--src/internal-server/app/Controllers/Http/ImageController.js16
-rw-r--r--src/internal-server/config/app.js2
-rw-r--r--src/internal-server/config/session.js2
-rw-r--r--src/internal-server/config/shield.js2
-rw-r--r--src/internal-server/start.ts41
-rw-r--r--src/internal-server/start/routes.js68
-rw-r--r--src/internal-server/test.ts2
-rw-r--r--src/lib/Menu.js4
-rw-r--r--src/models/Service.ts16
-rw-r--r--src/stores/RequestStore.ts11
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
168interface RequestsStore extends TypedStore { 168interface 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
37export function needsToken(): boolean {
38 return (window as any).ferdium.stores.settings.all.app.server === LOCAL_SERVER;
39}
40
41export function localServerToken(): string | undefined {
42 return needsToken()
43 ? (window as any).ferdium.stores.requests.localServerToken
44 : undefined;
45}
46
47export function importExportURL() {
48 const base = apiBase(false);
49 return needsToken()
50 ? `${base}/token/${localServerToken()}`
51 : base;
52}
53
37export function serverBase() { 54export 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';
25import { userDataRecipesPath, userDataPath } from '../../environment-remote'; 25import { userDataRecipesPath, userDataPath } from '../../environment-remote';
26import { asarRecipesPath } from '../../helpers/asar-helpers'; 26import { asarRecipesPath } from '../../helpers/asar-helpers';
27import apiBase from '../apiBase'; 27import apiBase from '../apiBase';
28import { prepareAuthRequest, sendAuthRequest } from '../utils/auth'; 28import { prepareAuthRequest, prepareLocalToken, sendAuthRequest } from '../utils/auth';
29 29
30import { 30import {
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 @@
1import localStorage from 'mobx-localstorage'; 1import localStorage from 'mobx-localstorage';
2import { when } from 'mobx';
3import { localServerToken, needsToken } from '../apiBase';
2import { ferdiumLocale, ferdiumVersion } from '../../environment-remote'; 4import { ferdiumLocale, ferdiumVersion } from '../../environment-remote';
3 5
4export const prepareAuthRequest = ( 6export const prepareAuthRequest = (
@@ -29,10 +31,23 @@ export const prepareAuthRequest = (
29 return request; 31 return request;
30}; 32};
31 33
32export const sendAuthRequest = ( 34export 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
44export 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';
33import { openPath } from '../../../helpers/url-helpers'; 33import { openExternalUrl, openPath } from '../../../helpers/url-helpers';
34import globalMessages from '../../../i18n/globalMessages'; 34import globalMessages from '../../../i18n/globalMessages';
35import Icon from '../../ui/icon'; 35import Icon from '../../ui/icon';
36import Slider from '../../ui/Slider'; 36import 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
33import ErrorBoundary from '../../components/util/ErrorBoundary'; 33import ErrorBoundary from '../../components/util/ErrorBoundary';
34 34
35import globalMessages from '../../i18n/globalMessages'; 35import globalMessages from '../../i18n/globalMessages';
36import { importExportURL } from '../../api/apiBase';
36 37
37const debug = require('../../preload-safe-debug')('Ferdium:EditSettingsScreen'); 38const 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 @@
1import { randomBytes } from 'crypto';
1import { ipcMain, BrowserWindow } from 'electron'; 2import { ipcMain, BrowserWindow } from 'electron';
2import { createServer } from 'net'; 3import { createServer } from 'net';
3import { LOCAL_HOSTNAME, LOCAL_PORT } from '../../config'; 4import { LOCAL_HOSTNAME, LOCAL_PORT } from '../../config';
4import { userDataPath } from '../../environment-remote'; 5import { userDataPath } from '../../environment-remote';
5import { server } from '../../internal-server/start'; 6import { server } from '../../internal-server/start';
6 7
8const debug = require('../../preload-safe-debug')('Ferdium:LocalServer');
9
7const portInUse = (port: number): Promise<boolean> => 10const 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
24let localServerStarted = false; 27let localServerStarted = false;
28let port = LOCAL_PORT;
29let token = '';
25 30
26export default (params: { mainWindow: BrowserWindow }) => { 31export 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';
6export const isWindows = process.platform === 'win32'; 6export const isWindows = process.platform === 'win32';
7export const isLinux = process.platform === 'linux'; 7export const isLinux = process.platform === 'linux';
8 8
9export const electronVersion: string = process.versions.electron; 9export const electronVersion: string = process.versions.electron ?? '';
10export const chromeVersion: string = process.versions.chrome; 10export const chromeVersion: string = process.versions.chrome ?? '';
11export const nodeVersion: string = process.versions.node; 11export const nodeVersion: string = process.versions.node;
12 12
13export const osArch: string = arch(); 13export 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
3const path = require('path'); 3const path = require('path');
4const fs = require('fs-extra'); 4const fs = require('fs-extra');
5const sanitize = require('sanitize-filename');
5 6
6class ImageController { 7class 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
18import fold from '@adonisjs/fold'; 18import fold from '@adonisjs/fold';
19import { Ignitor } from '@adonisjs/ignitor'; 19import { Ignitor, hooks } from '@adonisjs/ignitor';
20import { existsSync, readFile, statSync, chmodSync, writeFile } from 'fs-extra'; 20import { readFile, stat, chmod, writeFile } from 'fs-extra';
21import { join } from 'path'; 21import { join } from 'path';
22import { LOCAL_HOSTNAME } from '../config'; 22import { LOCAL_HOSTNAME } from '../config';
23import { isWindows } from '../environment'; 23import { isWindows } from '../environment';
24 24
25process.env.ENV_PATH = join(__dirname, 'env.ini'); 25process.env.ENV_PATH = join(__dirname, 'env.ini');
26 26
27export const server = async (userPath: string, port: number) => { 27async 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
47export 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
8const { timingSafeEqual } = require('crypto');
9
8/** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ 10/** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */
9const Route = use('Route'); 11const Route = use('Route');
10 12
@@ -14,14 +16,38 @@ const migrate = require('./migrate');
14 16
15migrate(); 17migrate();
16 18
19async 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
17const OnlyAllowFerdium = async ({ request, response }, next) => { 31const 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
41const RequireTokenInQS = async ({ request, response }, next) => {
42 const clientToken = request.get().token;
43 return validateToken(clientToken, response, next);
44}
45
46const FERDIUM_LOCAL_TOKEN_COOKIE = 'ferdium-local-token';
47
48const 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
68Route.group(() => { 94Route.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 100Route.group(() => {
73Route.post('import', 'UserController.import'); 101 // Franz account import
74Route.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
77Route.get('export', 'UserController.export'); 111 Route.get('/', ({ view }) => view.render('index'));
78Route.post('transfer', 'UserController.importFerdium'); 112}).middleware(RequireAuthenticatedBrowser);
79Route.get('transfer', ({ view }) => view.render('transfer'));
80 113
81// Index 114Route.get('token/:token', ({ params: { token }, response }) => {
82Route.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
7ensureDirSync(dummyUserFolder); 7ensureDirSync(dummyUserFolder);
8 8
9server(dummyUserFolder, 46_568); 9server(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';
38import { todoActions } from '../features/todos/actions'; 38import { todoActions } from '../features/todos/actions';
39import workspaceActions from '../features/workspaces/actions'; 39import workspaceActions from '../features/workspaces/actions';
40import { workspaceStore } from '../features/workspaces/index'; 40import { workspaceStore } from '../features/workspaces/index';
41import apiBase, { serverBase, serverName } from '../api/apiBase'; 41import { importExportURL, serverBase, serverName } from '../api/apiBase';
42import { openExternalUrl } from '../helpers/url-helpers'; 42import { openExternalUrl } from '../helpers/url-helpers';
43import globalMessages from '../i18n/globalMessages'; 43import 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';
11import { DEFAULT_SERVICE_ORDER } from '../config'; 11import { DEFAULT_SERVICE_ORDER } from '../config';
12import { ifUndefined } from '../jsUtils'; 12import { ifUndefined } from '../jsUtils';
13import { IRecipe } from './Recipe'; 13import { IRecipe } from './Recipe';
14import { needsToken } from '../api/apiBase';
14 15
15const debug = require('../preload-safe-debug')('Ferdium:Service'); 16const 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';
6import { ApiInterface } from '../api'; 6import { ApiInterface } from '../api';
7import { Stores } from '../@types/stores.types'; 7import { Stores } from '../@types/stores.types';
8import CachedRequest from './lib/CachedRequest'; 8import CachedRequest from './lib/CachedRequest';
9import { LOCAL_PORT } from '../config'; 9import { LOCAL_HOSTNAME, LOCAL_PORT } from '../config';
10 10
11import TypedStore from './lib/TypedStore'; 11import 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();