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:
parent
db7b802803
commit
de45740129
@ -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())
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
});
|
||||
|
@ -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'),
|
||||
});
|
||||
|
@ -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, '');
|
||||
|
@ -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,
|
||||
|
@ -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']),
|
||||
};
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user