You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-23 22:36:32 +02:00
This commit is contained in:
@@ -1073,6 +1073,8 @@ packages/lib/fs-driver-base.js
|
|||||||
packages/lib/fs-driver-node.js
|
packages/lib/fs-driver-node.js
|
||||||
packages/lib/fsDriver.test.js
|
packages/lib/fsDriver.test.js
|
||||||
packages/lib/geolocation-node.js
|
packages/lib/geolocation-node.js
|
||||||
|
packages/lib/getAppName.test.js
|
||||||
|
packages/lib/getAppName.js
|
||||||
packages/lib/hooks/useAsyncEffect.js
|
packages/lib/hooks/useAsyncEffect.js
|
||||||
packages/lib/hooks/useElementSize.js
|
packages/lib/hooks/useElementSize.js
|
||||||
packages/lib/hooks/useEventListener.js
|
packages/lib/hooks/useEventListener.js
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1048,6 +1048,8 @@ packages/lib/fs-driver-base.js
|
|||||||
packages/lib/fs-driver-node.js
|
packages/lib/fs-driver-node.js
|
||||||
packages/lib/fsDriver.test.js
|
packages/lib/fsDriver.test.js
|
||||||
packages/lib/geolocation-node.js
|
packages/lib/geolocation-node.js
|
||||||
|
packages/lib/getAppName.test.js
|
||||||
|
packages/lib/getAppName.js
|
||||||
packages/lib/hooks/useAsyncEffect.js
|
packages/lib/hooks/useAsyncEffect.js
|
||||||
packages/lib/hooks/useElementSize.js
|
packages/lib/hooks/useElementSize.js
|
||||||
packages/lib/hooks/useEventListener.js
|
packages/lib/hooks/useEventListener.js
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import { clearTimeout, setTimeout } from 'timers';
|
|||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||||
import { msleep, Second } from '@joplin/utils/time';
|
import { msleep, Second } from '@joplin/utils/time';
|
||||||
|
import determineBaseAppDirs from '@joplin/lib/determineBaseAppDirs';
|
||||||
|
import getAppName from '@joplin/lib/getAppName';
|
||||||
|
|
||||||
interface RendererProcessQuitReply {
|
interface RendererProcessQuitReply {
|
||||||
canClose: boolean;
|
canClose: boolean;
|
||||||
@@ -579,7 +581,11 @@ export default class ElectronAppWrapper {
|
|||||||
|
|
||||||
if (port === null) port = this.ipcStartPort_;
|
if (port === null) port = this.ipcStartPort_;
|
||||||
|
|
||||||
return await sendMessage(port, { ...message, sourcePort: this.ipcServer_.port }, {
|
return await sendMessage(port, {
|
||||||
|
...message,
|
||||||
|
sourcePort: this.ipcServer_.port,
|
||||||
|
secretKey: this.ipcServer_.secretKey,
|
||||||
|
}, {
|
||||||
logger: this.ipcLogger_,
|
logger: this.ipcLogger_,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
@@ -631,6 +637,7 @@ export default class ElectronAppWrapper {
|
|||||||
const response = await this.sendCrossAppIpcMessage({
|
const response = await this.sendCrossAppIpcMessage({
|
||||||
action: 'ping',
|
action: 'ping',
|
||||||
data: null,
|
data: null,
|
||||||
|
secretKey: this.ipcServer_.secretKey,
|
||||||
}, message.sourcePort, {
|
}, message.sourcePort, {
|
||||||
sendToSpecificPortOnly: true,
|
sendToSpecificPortOnly: true,
|
||||||
});
|
});
|
||||||
@@ -662,7 +669,12 @@ export default class ElectronAppWrapper {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.ipcServer_ = await startServer(this.ipcStartPort_, async (message) => {
|
const defaultProfileDir = determineBaseAppDirs('', getAppName(true, this.env() === 'dev'), '').rootProfileDir;
|
||||||
|
const secretKeyFilePath = `${defaultProfileDir}/ipc_secret_key.txt`;
|
||||||
|
|
||||||
|
this.ipcLogger_.info('Starting server using secret key:', secretKeyFilePath);
|
||||||
|
|
||||||
|
this.ipcServer_ = await startServer(this.ipcStartPort_, secretKeyFilePath, async (message) => {
|
||||||
if (messageHandlers[message.action]) {
|
if (messageHandlers[message.action]) {
|
||||||
this.ipcLogger_.info('Got message:', message);
|
this.ipcLogger_.info('Got message:', message);
|
||||||
return messageHandlers[message.action](message);
|
return messageHandlers[message.action](message);
|
||||||
@@ -684,6 +696,7 @@ export default class ElectronAppWrapper {
|
|||||||
profilePath: this.profilePath_,
|
profilePath: this.profilePath_,
|
||||||
argv: process.argv,
|
argv: process.argv,
|
||||||
},
|
},
|
||||||
|
secretKey: this.ipcServer_.secretKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.sendCrossAppIpcMessage(message);
|
await this.sendCrossAppIpcMessage(message);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import processStartFlags from './utils/processStartFlags';
|
|||||||
import { setupAutoDeletion } from './services/trash/permanentlyDeleteOldItems';
|
import { setupAutoDeletion } from './services/trash/permanentlyDeleteOldItems';
|
||||||
import determineProfileAndBaseDir from './determineBaseAppDirs';
|
import determineProfileAndBaseDir from './determineBaseAppDirs';
|
||||||
import NavService from './services/NavService';
|
import NavService from './services/NavService';
|
||||||
|
import getAppName from './getAppName';
|
||||||
|
|
||||||
const appLogger: LoggerWrapper = Logger.create('App');
|
const appLogger: LoggerWrapper = Logger.create('App');
|
||||||
|
|
||||||
@@ -679,8 +680,7 @@ export default class BaseApplication {
|
|||||||
|
|
||||||
let appName = options.appName;
|
let appName = options.appName;
|
||||||
if (!appName) {
|
if (!appName) {
|
||||||
appName = initArgs.env === 'dev' ? 'joplindev' : 'joplin';
|
appName = getAppName(Setting.value('appId').indexOf('-desktop') >= 0, initArgs.env === 'dev');
|
||||||
if (Setting.value('appId').indexOf('-desktop') >= 0) appName += '-desktop';
|
|
||||||
}
|
}
|
||||||
Setting.setConstant('appName', appName);
|
Setting.setConstant('appName', appName);
|
||||||
|
|
||||||
|
|||||||
12
packages/lib/getAppName.test.ts
Normal file
12
packages/lib/getAppName.test.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import getAppName from './getAppName';
|
||||||
|
|
||||||
|
describe('getAppName', () => {
|
||||||
|
|
||||||
|
it('should get the app name', () => {
|
||||||
|
expect(getAppName(true, true)).toBe('joplindev-desktop');
|
||||||
|
expect(getAppName(true, false)).toBe('joplin-desktop');
|
||||||
|
expect(getAppName(false, false)).toBe('joplin');
|
||||||
|
expect(getAppName(false, true)).toBe('joplindev');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
5
packages/lib/getAppName.ts
Normal file
5
packages/lib/getAppName.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default (isDesktop: boolean, isDev: boolean) => {
|
||||||
|
let appName = isDev ? 'joplindev' : 'joplin';
|
||||||
|
if (isDesktop) appName += '-desktop';
|
||||||
|
return appName;
|
||||||
|
};
|
||||||
9
packages/utils/crypto.ts
Normal file
9
packages/utils/crypto.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
export const getSecureRandomString = (length: number): string => {
|
||||||
|
const bytes = randomBytes(Math.ceil(length * 2));
|
||||||
|
const randomString = bytes.toString('base64').replace(/[^a-zA-Z0-9]/g, '');
|
||||||
|
return randomString.slice(0, length);
|
||||||
|
};
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
|
||||||
import { mkdirp } from 'fs-extra';
|
import { mkdirp } from 'fs-extra';
|
||||||
import { FileLocker } from './fs';
|
import { FileLocker } from './fs';
|
||||||
import { msleep, Second } from './time';
|
import { msleep, Second } from './time';
|
||||||
|
|
||||||
const baseTempDir = `${__dirname}/../app-cli/tests/tmp`;
|
const baseTempDir = `${__dirname}/../app-cli/tests/tmp`;
|
||||||
|
|
||||||
const createTempDir = async () => {
|
export const createTempDir = async () => {
|
||||||
const p = `${baseTempDir}/${Date.now()}`;
|
const p = `${baseTempDir}/${Date.now()}`;
|
||||||
await mkdirp(p);
|
await mkdirp(p);
|
||||||
return p;
|
return p;
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import { createTempDir } from './fs.test';
|
||||||
import { newHttpError, sendMessage, startServer, stopServer } from './ipc';
|
import { newHttpError, sendMessage, startServer, stopServer } from './ipc';
|
||||||
|
|
||||||
describe('ipc', () => {
|
describe('ipc', () => {
|
||||||
|
|
||||||
it('should send and receive messages', async () => {
|
it('should send and receive messages', async () => {
|
||||||
|
const tempDir = await createTempDir();
|
||||||
|
const secretFilePath = `${tempDir}/secret.txt`;
|
||||||
const startPort = 41168;
|
const startPort = 41168;
|
||||||
|
|
||||||
const server1 = await startServer(startPort, async (request) => {
|
const server1 = await startServer(startPort, secretFilePath, async (request) => {
|
||||||
if (request.action === 'testing') {
|
if (request.action === 'testing') {
|
||||||
return {
|
return {
|
||||||
text: 'hello1',
|
text: 'hello1',
|
||||||
@@ -15,7 +19,7 @@ describe('ipc', () => {
|
|||||||
throw newHttpError(404);
|
throw newHttpError(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
const server2 = await startServer(startPort, async (request) => {
|
const server2 = await startServer(startPort, secretFilePath, async (request) => {
|
||||||
if (request.action === 'testing') {
|
if (request.action === 'testing') {
|
||||||
return {
|
return {
|
||||||
text: 'hello2',
|
text: 'hello2',
|
||||||
@@ -31,12 +35,15 @@ describe('ipc', () => {
|
|||||||
throw newHttpError(404);
|
throw newHttpError(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secretKey = await readFile(secretFilePath, 'utf-8');
|
||||||
|
|
||||||
{
|
{
|
||||||
const responses = await sendMessage(startPort, {
|
const responses = await sendMessage(startPort, {
|
||||||
action: 'testing',
|
action: 'testing',
|
||||||
data: {
|
data: {
|
||||||
test: 1234,
|
test: 1234,
|
||||||
},
|
},
|
||||||
|
secretKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(responses).toEqual([
|
expect(responses).toEqual([
|
||||||
@@ -49,6 +56,7 @@ describe('ipc', () => {
|
|||||||
const responses = await sendMessage(startPort, {
|
const responses = await sendMessage(startPort, {
|
||||||
action: 'ping',
|
action: 'ping',
|
||||||
data: null,
|
data: null,
|
||||||
|
secretKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(responses).toEqual([
|
expect(responses).toEqual([
|
||||||
@@ -63,6 +71,7 @@ describe('ipc', () => {
|
|||||||
test: 1234,
|
test: 1234,
|
||||||
},
|
},
|
||||||
sourcePort: 41168,
|
sourcePort: 41168,
|
||||||
|
secretKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(responses).toEqual([
|
expect(responses).toEqual([
|
||||||
@@ -74,4 +83,44 @@ describe('ipc', () => {
|
|||||||
await stopServer(server2);
|
await stopServer(server2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not process message if secret is invalid', async () => {
|
||||||
|
const tempDir = await createTempDir();
|
||||||
|
const secretFilePath = `${tempDir}/secret.txt`;
|
||||||
|
const startPort = 41168;
|
||||||
|
|
||||||
|
const server = await startServer(startPort, secretFilePath, async (request) => {
|
||||||
|
if (request.action === 'testing') {
|
||||||
|
return {
|
||||||
|
text: 'hello1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw newHttpError(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretKey = await readFile(secretFilePath, 'utf-8');
|
||||||
|
|
||||||
|
{
|
||||||
|
const responses = await sendMessage(startPort, {
|
||||||
|
action: 'testing',
|
||||||
|
data: null,
|
||||||
|
secretKey: 'wrong_key',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(responses.length).toBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const responses = await sendMessage(startPort, {
|
||||||
|
action: 'testing',
|
||||||
|
data: null,
|
||||||
|
secretKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(responses.length).toBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await stopServer(server);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { createServer, IncomingMessage, ServerResponse } from 'http';
|
|||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { Server } from 'http';
|
import { Server } from 'http';
|
||||||
import Logger from './Logger';
|
import Logger from './Logger';
|
||||||
|
import { pathExists } from 'fs-extra';
|
||||||
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
|
import { getSecureRandomString } from './crypto';
|
||||||
|
|
||||||
const tcpPortUsed = require('tcp-port-used');
|
const tcpPortUsed = require('tcp-port-used');
|
||||||
const maxPorts = 10;
|
const maxPorts = 10;
|
||||||
@@ -51,6 +54,7 @@ export interface Message {
|
|||||||
action: string;
|
action: string;
|
||||||
data: object|number|string|null;
|
data: object|number|string|null;
|
||||||
sourcePort?: number;
|
sourcePort?: number;
|
||||||
|
secretKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Response = string|number|object|boolean;
|
type Response = string|number|object|boolean;
|
||||||
@@ -66,21 +70,52 @@ export type IpcMessageHandler = (message: Message)=> Promise<Response|void>;
|
|||||||
export interface IpcServer {
|
export interface IpcServer {
|
||||||
port: number;
|
port: number;
|
||||||
httpServer: Server;
|
httpServer: Server;
|
||||||
|
secretKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StartServerOptions {
|
interface StartServerOptions {
|
||||||
logger?: Logger;
|
logger?: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startServer = async (startPort: number, messageHandler: IpcMessageHandler, options: StartServerOptions|null = null): Promise<IpcServer> => {
|
const getSecretKey = async (filePath: string) => {
|
||||||
|
try {
|
||||||
|
const keyLength = 64;
|
||||||
|
|
||||||
|
const writeKeyToFile = async () => {
|
||||||
|
const key = getSecureRandomString(keyLength);
|
||||||
|
await writeFile(filePath, key, 'utf-8');
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!(await pathExists(filePath))) {
|
||||||
|
return await writeKeyToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = await readFile(filePath, 'utf-8');
|
||||||
|
if (key.length !== keyLength) return await writeKeyToFile();
|
||||||
|
|
||||||
|
return key;
|
||||||
|
} catch (error) {
|
||||||
|
const e = error as NodeJS.ErrnoException;
|
||||||
|
e.message = `Could not get secret key from file: ${filePath}`;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// `secretKeyFilePath` must be the same for all the instances that can communicate with each others
|
||||||
|
export const startServer = async (startPort: number, secretKeyFilePath: string, messageHandler: IpcMessageHandler, options: StartServerOptions|null = null): Promise<IpcServer> => {
|
||||||
const port = await findAvailablePort(startPort);
|
const port = await findAvailablePort(startPort);
|
||||||
const logger = options && options.logger ? options.logger : new Logger();
|
const logger = options && options.logger ? options.logger : new Logger();
|
||||||
|
|
||||||
|
const secretKey = await getSecretKey(secretKeyFilePath);
|
||||||
|
|
||||||
return new Promise<IpcServer>((resolve, reject) => {
|
return new Promise<IpcServer>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
let message: Message|null = null;
|
||||||
try {
|
try {
|
||||||
const message = await parseJson(req) as Message;
|
message = await parseJson(req) as Message;
|
||||||
|
if (message.secretKey !== secretKey) throw newHttpError(401, 'Invalid secret key');
|
||||||
if (!message.action) throw newHttpError(400, 'Missing "action" property in message');
|
if (!message.action) throw newHttpError(400, 'Missing "action" property in message');
|
||||||
const response = await messageHandler(message);
|
const response = await messageHandler(message);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
@@ -88,6 +123,7 @@ export const startServer = async (startPort: number, messageHandler: IpcMessageH
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const httpError = error as HttpError;
|
const httpError = error as HttpError;
|
||||||
const httpCode = httpError.httpCode || 500;
|
const httpCode = httpError.httpCode || 500;
|
||||||
|
logger.error('Could not response to request:', message, 'Error', httpCode, httpError.message);
|
||||||
res.writeHead(httpCode, { 'Content-Type': 'text/plain' });
|
res.writeHead(httpCode, { 'Content-Type': 'text/plain' });
|
||||||
res.end(`Error ${httpCode}: ${httpError.message}`);
|
res.end(`Error ${httpCode}: ${httpError.message}`);
|
||||||
}
|
}
|
||||||
@@ -101,6 +137,7 @@ export const startServer = async (startPort: number, messageHandler: IpcMessageH
|
|||||||
resolve({
|
resolve({
|
||||||
httpServer: server,
|
httpServer: server,
|
||||||
port,
|
port,
|
||||||
|
secretKey,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Joplin Desktop supports multiple instances through **profile locking** and **IPC
|
|||||||
|
|
||||||
- When a message is sent, the implementation automatically discovers running IPC servers.
|
- When a message is sent, the implementation automatically discovers running IPC servers.
|
||||||
|
|
||||||
|
- Messages are secured using a secret that is shared by all applications. That secret is read from the profile directory of the main instance. It is created by the server if it doesn't exist. This ensures that, for example, a browser cannot send a valid message to the apps.
|
||||||
|
|
||||||
### Instance Differentiation
|
### Instance Differentiation
|
||||||
|
|
||||||
- The `--alt-instance-id` flag must be used to launch an alternative instance. This disables services like the Web Clipper.
|
- The `--alt-instance-id` flag must be used to launch an alternative instance. This disables services like the Web Clipper.
|
||||||
|
|||||||
Reference in New Issue
Block a user