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:
parent
b81c300907
commit
a36b13dcb4
@ -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'));
|
||||
};
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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": ""
|
||||
// }
|
||||
|
||||
|
@ -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 = {};
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user