1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-23 18:53:36 +02:00

Server: Load shared user content from correct domain

This commit is contained in:
Laurent Cozic 2021-06-06 19:14:12 +02:00
parent db7b802803
commit de45740129
14 changed files with 106 additions and 35 deletions

View File

@ -1,6 +1,5 @@
import { utils as pluginUtils, PluginStates } from '@joplin/lib/services/plugins/reducer';
import CommandService from '@joplin/lib/services/CommandService';
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import eventManager from '@joplin/lib/eventManager';
import InteropService from '@joplin/lib/services/interop/InteropService';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
@ -134,7 +133,7 @@ export default class NoteListUtils {
})
);
if (Setting.value('sync.target') === SyncTargetJoplinServer.id()) {
if ([9, 10].includes(Setting.value('sync.target'))) {
menu.append(
new MenuItem(
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice())

View File

@ -766,7 +766,9 @@ export default class BaseApplication {
}
if (Setting.value('env') === Env.Dev) {
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
Setting.setValue('sync.10.path', 'http://api-joplincloud.local:22300');
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
}
// For now always disable fuzzy search due to performance issues:

View File

@ -10,6 +10,7 @@ const logger = Logger.create('JoplinServerApi');
interface Options {
baseUrl(): string;
userContentBaseUrl(): string;
username(): string;
password(): string;
env?: Env;
@ -55,6 +56,10 @@ export default class JoplinServerApi {
return rtrimSlashes(this.options_.baseUrl());
}
public userContentBaseUrl() {
return this.options_.userContentBaseUrl() || this.baseUrl();
}
private async session() {
if (this.session_) return this.session_;
@ -165,7 +170,7 @@ export default class JoplinServerApi {
const responseText = await response.text();
if (this.debugRequests_) {
logger.debug('Response', responseText);
logger.debug('Response', options.responseFormat, responseText);
}
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier

View File

@ -7,6 +7,7 @@ import SyncTargetJoplinServer, { initFileApi } from './SyncTargetJoplinServer';
interface FileApiOptions {
path(): string;
userContentPath(): string;
username(): string;
password(): string;
}
@ -46,6 +47,7 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
protected async initFileApi() {
return initFileApi(this.logger(), {
path: () => Setting.value('sync.10.path'),
userContentPath: () => Setting.value('sync.10.userContentPath'),
username: () => Setting.value('sync.10.username'),
password: () => Setting.value('sync.10.password'),
});

View File

@ -9,6 +9,7 @@ import Logger from './Logger';
interface FileApiOptions {
path(): string;
userContentPath(): string;
username(): string;
password(): string;
}
@ -16,6 +17,7 @@ interface FileApiOptions {
export async function newFileApi(id: number, options: FileApiOptions) {
const apiOptions = {
baseUrl: () => options.path(),
userContentBaseUrl: () => options.userContentPath(),
username: () => options.username(),
password: () => options.password(),
env: Setting.value('env'),
@ -87,6 +89,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
protected async initFileApi() {
return initFileApi(this.logger(), {
path: () => Setting.value('sync.9.path'),
userContentPath: () => Setting.value('sync.9.userContentPath'),
username: () => Setting.value('sync.9.username'),
password: () => Setting.value('sync.9.password'),
});

View File

@ -80,7 +80,19 @@ export default class FileApiDriverJoplinServer {
const response = await this.api().exec('GET', `${this.apiFilePath_(path)}/delta`, query);
const stats = response.items
.filter((item: any) => {
return item.item_name.indexOf('locks/') !== 0 && item.item_name.indexOf('temp/') !== 0;
// We don't need to know about lock changes, since this
// is handled by the LockHandler.
if (item.item_name.indexOf('locks/') === 0) return false;
// We don't need to sync what's in the temp folder
if (item.item_name.indexOf('temp/') === 0) return false;
// Although we sync the content of .resource, whether we
// fetch or upload data to it is driven by the
// associated resource item (.md) file. So at this point
// we don't want to automatically fetch from it.
if (item.item_name.indexOf('.resource/') === 0) return false;
return true;
})
.map((item: any) => {
return this.metadataToStat_(item, item.item_name, item.type === 3, '');

View File

@ -476,6 +476,12 @@ class Setting extends BaseModel {
description: () => emptyDirWarning,
storage: SettingStorage.File,
},
'sync.9.userContentPath': {
value: '',
type: SettingItemType.String,
public: false,
storage: SettingStorage.Database,
},
'sync.9.username': {
value: '',
type: SettingItemType.String,
@ -509,6 +515,12 @@ class Setting extends BaseModel {
public: false,
storage: SettingStorage.Database,
},
'sync.10.userContentPath': {
value: 'https://joplinusercontent.com',
type: SettingItemType.String,
public: false,
storage: SettingStorage.Database,
},
'sync.10.username': {
value: '',
type: SettingItemType.String,

View File

@ -77,6 +77,6 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
folderIsShareRootAndOwnedByUser: commandFolder ? isRootSharedFolder(commandFolder) && isSharedFolderOwner(state, commandFolder.id) : false,
folderIsShared: commandFolder ? !!commandFolder.share_id : false,
joplinServerConnected: state.settings['sync.target'] === 9,
joplinServerConnected: [9, 10].includes(state.settings['sync.target']),
};
}

View File

@ -40,6 +40,7 @@ export default class ShareService {
this.api_ = new JoplinServerApi({
baseUrl: () => Setting.value(`sync.${syncTargetId}.path`),
userContentBaseUrl: () => Setting.value(`sync.${syncTargetId}.userContentPath`),
username: () => Setting.value(`sync.${syncTargetId}.username`),
password: () => Setting.value(`sync.${syncTargetId}.password`),
});
@ -136,7 +137,7 @@ export default class ShareService {
}
public shareUrl(share: StateShare): string {
return `${this.api().baseUrl()}/shares/${share.id}`;
return `${this.api().userContentBaseUrl()}/shares/${share.id}`;
}
public get shares() {

View File

@ -576,6 +576,7 @@ async function initFileApi() {
// read/write from multiple processes at the same time.
const api = new JoplinServerApi({
baseUrl: () => 'http://localhost:22300',
userContentBaseUrl: () => '',
username: () => 'admin@localhost',
password: () => 'admin',
});

View File

@ -25,7 +25,7 @@ shimInit();
const env: Env = argv.env as Env || Env.Prod;
const envVariables: Record<Env, EnvVariables> = {
const defaultEnvVariables: Record<Env, EnvVariables> = {
dev: {
SQLITE_DATABASE: `${sqliteDefaultDir}/db-dev.sqlite`,
},
@ -46,28 +46,6 @@ function appLogger(): LoggerWrapper {
return appLogger_;
}
const app = new Koa();
// Note: the order of middlewares is important. For example, ownerHandler
// loads the user, which is then used by notificationHandler. And finally
// routeHandler uses data from both previous middlewares. It would be good to
// layout these dependencies in code but not clear how to do this.
const corsAllowedDomains = ['https://joplinapp.org'];
app.use(cors({
// https://github.com/koajs/cors/issues/52#issuecomment-413887382
origin: (ctx: AppContext) => {
if (corsAllowedDomains.indexOf(ctx.request.header.origin) !== -1) {
return ctx.request.header.origin;
}
// we can't return void, so let's return one of the valid domains
return corsAllowedDomains[0];
},
}));
app.use(ownerHandler);
app.use(notificationHandler);
app.use(routeHandler);
function markPasswords(o: Record<string, any>): Record<string, any> {
const output: Record<string, any> = {};
@ -97,12 +75,64 @@ async function main() {
if (envFilePath) nodeEnvFile(envFilePath);
if (!envVariables[env]) throw new Error(`Invalid env: ${env}`);
if (!defaultEnvVariables[env]) throw new Error(`Invalid env: ${env}`);
await initConfig(env, {
...envVariables[env],
const envVariables: EnvVariables = {
...defaultEnvVariables[env],
...process.env,
});
};
const app = new Koa();
// Note: the order of middlewares is important. For example, ownerHandler
// loads the user, which is then used by notificationHandler. And finally
// routeHandler uses data from both previous middlewares. It would be good to
// layout these dependencies in code but not clear how to do this.
const corsAllowedDomains = [
'https://joplinapp.org',
];
function acceptOrigin(origin: string): boolean {
const hostname = (new URL(origin)).hostname;
const userContentDomain = envVariables.USER_CONTENT_BASE_URL ? (new URL(envVariables.USER_CONTENT_BASE_URL)).hostname : '';
if (hostname === userContentDomain) return true;
const hostnameNoSub = hostname.split('.').slice(1).join('.');
if (hostnameNoSub === userContentDomain) return true;
if (corsAllowedDomains.indexOf(origin) === 0) return true;
return false;
}
app.use(cors({
// https://github.com/koajs/cors/issues/52#issuecomment-413887382
origin: (ctx: AppContext) => {
const origin = ctx.request.header.origin;
if (acceptOrigin(origin)) {
return origin;
} else {
// we can't return void, so let's return one of the valid domains
return corsAllowedDomains[0];
}
// const requestOrigin = ctx.request.header.origin;
// if (hostname === envVariables.USER_CONTENT_BASE_URL) return
// if (corsAllowedDomains.indexOf(requestOrigin) === 0) {
// return requestOrigin;
// }
},
}));
app.use(ownerHandler);
app.use(notificationHandler);
app.use(routeHandler);
await initConfig(env, envVariables);
await fs.mkdirp(config().logDir);
await fs.mkdirp(config().tempDir);

View File

@ -64,6 +64,10 @@ export default abstract class BaseModel<T> {
return this.config_.baseUrl;
}
protected get userContentUrl(): string {
return this.config_.userContentBaseUrl;
}
protected get appName(): string {
return this.config_.appName;
}

View File

@ -80,7 +80,7 @@ export default class ShareModel extends BaseModel<Share> {
}
public shareUrl(id: Uuid, query: any = null): string {
return setQueryParameters(`${this.baseUrl}/shares/${id}`, query);
return setQueryParameters(`${this.userContentUrl}/shares/${id}`, query);
}
public async byItemId(itemId: Uuid): Promise<Share | null> {

View File

@ -40,6 +40,6 @@ router.get('shares/:id', async (path: SubPath, ctx: AppContext) => {
ctx.response.set('Content-Type', result.mime);
ctx.response.set('Content-Length', result.size.toString());
return new Response(ResponseType.KoaResponse, ctx.response);
});
}, RouteType.UserContent);
export default router;