1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-30 10:36:35 +02:00

Server: Handle custom user content URLs

This commit is contained in:
Laurent Cozic 2021-06-10 19:33:04 +02:00
parent b81c300907
commit a36b13dcb4
9 changed files with 55 additions and 22 deletions

View File

@ -95,7 +95,7 @@ export function ShareNoteDialog(props: Props) {
const copyLinksToClipboard = (shares: StateShare[]) => {
const links = [];
for (const share of shares) links.push(ShareService.instance().shareUrl(share));
for (const share of shares) links.push(ShareService.instance().shareUrl(ShareService.instance().userId, share));
clipboard.writeText(links.join('\n'));
};

View File

@ -25,13 +25,13 @@ if [ "$RESET_ALL" == "1" ]; then
rm -rf "$PROFILE_DIR"
echo "config keychain.supported 0" >> "$CMD_FILE"
echo "config sync.target 9" >> "$CMD_FILE"
echo "config sync.9.path http://api-joplincloud.local:22300" >> "$CMD_FILE"
echo "config sync.9.username $USER_EMAIL" >> "$CMD_FILE"
echo "config sync.9.password 123456" >> "$CMD_FILE"
echo "config sync.target 10" >> "$CMD_FILE"
echo "config sync.10.path http://api.joplincloud.local:22300" >> "$CMD_FILE"
echo "config sync.10.username $USER_EMAIL" >> "$CMD_FILE"
echo "config sync.10.password 123456" >> "$CMD_FILE"
if [ "$USER_NUM" == "1" ]; then
curl --data '{"action": "createTestUsers"}' -H 'Content-Type: application/json' http://api-joplincloud.local:22300/api/debug
curl --data '{"action": "createTestUsers"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
echo 'mkbook "shared"' >> "$CMD_FILE"
echo 'mkbook "other"' >> "$CMD_FILE"

View File

@ -768,7 +768,7 @@ export default class BaseApplication {
if (Setting.value('env') === Env.Dev) {
Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
// Setting.setValue('sync.10.path', 'http://api-joplincloud.local:22300');
// Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
// Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
}

View File

@ -56,8 +56,14 @@ export default class JoplinServerApi {
return rtrimSlashes(this.options_.baseUrl());
}
public userContentBaseUrl() {
return this.options_.userContentBaseUrl() || this.baseUrl();
public userContentBaseUrl(userId: string) {
if (this.options_.userContentBaseUrl()) {
if (!userId) throw new Error('User ID must be specified');
const url = new URL(this.options_.userContentBaseUrl());
return `${url.protocol}//${userId.substr(0, 10).toLowerCase()}.${url.host}`;
} else {
return this.baseUrl();
}
}
private async session() {

View File

@ -33,6 +33,10 @@ export default class ShareService {
return this.store.getState()[stateRootKey] as State;
}
public get userId(): string {
return this.api() ? this.api().userId : '';
}
private api(): JoplinServerApi {
if (this.api_) return this.api_;
@ -136,8 +140,8 @@ export default class ShareService {
await Note.save({ id: note.id, is_shared: 0 });
}
public shareUrl(share: StateShare): string {
return `${this.api().userContentBaseUrl()}/shares/${share.id}`;
public shareUrl(userId: string, share: StateShare): string {
return `${this.api().userContentBaseUrl(userId)}/shares/${share.id}`;
}
public get shares() {

View File

@ -577,7 +577,7 @@ async function initFileApi() {
// const joplinServerAuth = {
// "email": "admin@localhost",
// "password": "admin",
// "baseUrl": "http://api-joplincloud.local:22300",
// "baseUrl": "http://api.joplincloud.local:22300",
// "userContentBaseUrl": ""
// }

View File

@ -5,6 +5,7 @@ import { unique } from '../utils/array';
import { ErrorBadRequest, ErrorForbidden, ErrorNotFound } from '../utils/errors';
import { setQueryParameters } from '../utils/urlUtils';
import BaseModel, { AclAction, DeleteOptions, ValidateOptions } from './BaseModel';
import { userIdFromUserContentUrl } from '../utils/routeUtils';
export default class ShareModel extends BaseModel<Share> {
@ -33,6 +34,19 @@ export default class ShareModel extends BaseModel<Share> {
}
}
public checkShareUrl(share: Share, shareUrl: string) {
if (this.baseUrl === this.userContentUrl) return; // OK
const userId = userIdFromUserContentUrl(shareUrl);
const shareUserId = share.owner_id.toLowerCase();
if (userId.length >= 10 && shareUserId.indexOf(userId) === 0) {
// OK
} else {
throw new ErrorBadRequest('Invalid origin (User Content)');
}
}
protected objectToApiOutput(object: Share): Share {
const output: Share = {};

View File

@ -36,6 +36,8 @@ router.get('shares/:id', async (path: SubPath, ctx: AppContext) => {
const result = await renderItem(ctx, item, share);
ctx.models.share().checkShareUrl(share, ctx.URL.origin);
ctx.response.body = result.body;
ctx.response.set('Content-Type', result.mime);
ctx.response.set('Content-Length', result.size.toString());

View File

@ -1,5 +1,5 @@
import { baseUrl } from '../config';
import { Item, ItemAddressingType } from '../db';
import { Item, ItemAddressingType, Uuid } from '../db';
import { ErrorBadRequest, ErrorForbidden, ErrorNotFound } from './errors';
import Router from './Router';
import { AppContext, HttpMethod, RouteType } from './types';
@ -153,19 +153,26 @@ export function parseSubPath(basePath: string, p: string, rawPath: string = null
return output;
}
export function isValidOrigin(requestOrigin: string, endPointBaseUrl: string): boolean {
export function isValidOrigin(requestOrigin: string, endPointBaseUrl: string, routeType: RouteType): boolean {
const host1 = (new URL(requestOrigin)).host;
const host2 = (new URL(endPointBaseUrl)).host;
return host1 === host2;
if (routeType === RouteType.UserContent) {
if (host1 === host2) return true;
const hostNoPrefix = host1.split('.').slice(1).join('.');
return hostNoPrefix === host2;
} else {
return host1 === host2;
}
}
export function userIdFromUserContentUrl(url: string): Uuid {
const s = (new URL(url)).hostname.split('.');
return s[0].toLowerCase();
}
export function routeResponseFormat(context: AppContext): RouteResponseFormat {
// const rawPath = context.path;
// if (match && match.route.responseFormat) return match.route.responseFormat;
// let path = rawPath;
// if (match) path = match.basePath ? match.basePath : match.subPath.raw;
const path = context.path;
return path.indexOf('api') === 0 || path.indexOf('/api') === 0 ? RouteResponseFormat.Json : RouteResponseFormat.Html;
}
@ -175,7 +182,7 @@ export async function execRequest(routes: Routers, ctx: AppContext) {
if (!match) throw new ErrorNotFound();
const endPoint = match.route.findEndPoint(ctx.request.method as HttpMethod, match.subPath.schema);
if (ctx.URL && !isValidOrigin(ctx.URL.origin, baseUrl(endPoint.type))) throw new ErrorNotFound('Invalid origin', 'invalidOrigin');
if (ctx.URL && !isValidOrigin(ctx.URL.origin, baseUrl(endPoint.type), endPoint.type)) throw new ErrorNotFound('Invalid origin', 'invalidOrigin');
// This is a generic catch-all for all private end points - if we
// couldn't get a valid session, we exit now. Individual end points