mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
fix(media): 🐛 Don't proxy media from trusted origins, use new ProxiedUrl class
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 6s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 11s
Build Docker Images / tests (push) Failing after 27s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 6s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 6s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 11s
Build Docker Images / tests (push) Failing after 27s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 6s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
This commit is contained in:
parent
411fcd8af5
commit
dc1ddb758d
14 changed files with 114 additions and 140 deletions
|
|
@ -1,5 +1,4 @@
|
|||
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
|
||||
import { proxyUrl } from "@/response";
|
||||
import type { CustomEmoji } from "@versia/client/schemas";
|
||||
import type { CustomEmojiExtension } from "@versia/federation/types";
|
||||
import { type Instance, Media, db } from "@versia/kit/db";
|
||||
|
|
@ -179,8 +178,8 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
|||
return {
|
||||
id: this.id,
|
||||
shortcode: this.data.shortcode,
|
||||
static_url: proxyUrl(this.media.getUrl()).toString(),
|
||||
url: proxyUrl(this.media.getUrl()).toString(),
|
||||
static_url: this.media.getUrl().proxied,
|
||||
url: this.media.getUrl().proxied,
|
||||
visible_in_picker: this.data.visibleInPicker,
|
||||
category: this.data.category,
|
||||
global: this.data.ownerId === null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { join } from "node:path";
|
||||
import { mimeLookup } from "@/content_types.ts";
|
||||
import { proxyUrl } from "@/response";
|
||||
import type { Attachment as AttachmentSchema } from "@versia/client/schemas";
|
||||
import type { ContentFormat } from "@versia/federation/types";
|
||||
import { db } from "@versia/kit/db";
|
||||
|
|
@ -20,6 +19,7 @@ import { MediaBackendType } from "~/classes/config/schema.ts";
|
|||
import { config } from "~/config.ts";
|
||||
import { ApiError } from "../errors/api-error.ts";
|
||||
import { getMediaHash } from "../media/media-hasher.ts";
|
||||
import { ProxiableUrl } from "../media/url.ts";
|
||||
import { MediaJobType, mediaQueue } from "../queues/media.ts";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
|
||||
|
|
@ -369,10 +369,10 @@ export class Media extends BaseInterface<typeof Medias> {
|
|||
throw new Error("Unknown media backend");
|
||||
}
|
||||
|
||||
public getUrl(): URL {
|
||||
public getUrl(): ProxiableUrl {
|
||||
const type = this.getPreferredMimeType();
|
||||
|
||||
return new URL(this.data.content[type]?.content ?? "");
|
||||
return new ProxiableUrl(this.data.content[type]?.content ?? "");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -510,10 +510,10 @@ export class Media extends BaseInterface<typeof Medias> {
|
|||
return {
|
||||
id: this.data.id,
|
||||
type: this.getMastodonType(),
|
||||
url: proxyUrl(new URL(data.content)).toString(),
|
||||
url: this.getUrl().proxied,
|
||||
remote_url: null,
|
||||
preview_url: thumbnailData?.content
|
||||
? proxyUrl(new URL(thumbnailData.content)).toString()
|
||||
? new ProxiableUrl(thumbnailData.content).proxied
|
||||
: null,
|
||||
meta: this.toApiMeta(),
|
||||
description: data.description || null,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { proxyUrl } from "@/response";
|
||||
import type { Role as RoleSchema } from "@versia/client/schemas";
|
||||
import type { RolePermission } from "@versia/client/schemas";
|
||||
import { db } from "@versia/kit/db";
|
||||
|
|
@ -14,11 +13,30 @@ import {
|
|||
} from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { config } from "~/config.ts";
|
||||
import { ProxiableUrl } from "../media/url.ts";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
type RoleType = InferSelectModel<typeof Roles>;
|
||||
|
||||
export class Role extends BaseInterface<typeof Roles> {
|
||||
public static $type: RoleType;
|
||||
public static defaultRole = new Role({
|
||||
id: "default",
|
||||
name: "Default",
|
||||
permissions: config.permissions.default,
|
||||
priority: 0,
|
||||
description: "Default role for all users",
|
||||
visible: false,
|
||||
icon: null,
|
||||
});
|
||||
public static adminRole = new Role({
|
||||
id: "admin",
|
||||
name: "Admin",
|
||||
permissions: config.permissions.admin,
|
||||
priority: 2 ** 31 - 1,
|
||||
description: "Default role for all administrators",
|
||||
visible: false,
|
||||
icon: null,
|
||||
});
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Role.fromId(this.data.id);
|
||||
|
|
@ -59,24 +77,8 @@ export class Role extends BaseInterface<typeof Roles> {
|
|||
|
||||
public static async getAll(): Promise<Role[]> {
|
||||
return (await Role.manyFromSql(undefined)).concat(
|
||||
new Role({
|
||||
id: "default",
|
||||
name: "Default",
|
||||
permissions: config.permissions.default,
|
||||
priority: 0,
|
||||
description: "Default role for all users",
|
||||
visible: false,
|
||||
icon: null,
|
||||
}),
|
||||
new Role({
|
||||
id: "admin",
|
||||
name: "Admin",
|
||||
permissions: config.permissions.admin,
|
||||
priority: 2 ** 31 - 1,
|
||||
description: "Default role for all administrators",
|
||||
visible: false,
|
||||
icon: null,
|
||||
}),
|
||||
Role.defaultRole,
|
||||
Role.adminRole,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +217,7 @@ export class Role extends BaseInterface<typeof Roles> {
|
|||
description: this.data.description ?? undefined,
|
||||
visible: this.data.visible,
|
||||
icon: this.data.icon
|
||||
? proxyUrl(new URL(this.data.icon)).toString()
|
||||
? new ProxiableUrl(this.data.icon).proxied
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { idValidator } from "@/api";
|
||||
import { getBestContentType } from "@/content_types";
|
||||
import { randomString } from "@/math";
|
||||
import { proxyUrl } from "@/response";
|
||||
import { sentry } from "@/sentry";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
import type {
|
||||
|
|
@ -56,6 +55,7 @@ import { findManyUsers } from "~/classes/functions/user";
|
|||
import { searchManager } from "~/classes/search/search-manager";
|
||||
import { config } from "~/config.ts";
|
||||
import type { KnownEntity } from "~/types/api.ts";
|
||||
import { ProxiableUrl } from "../media/url.ts";
|
||||
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
|
||||
import { PushJobType, pushQueue } from "../queues/push.ts";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
|
|
@ -180,19 +180,14 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
}
|
||||
|
||||
public getAllPermissions(): RolePermission[] {
|
||||
return (
|
||||
this.data.roles
|
||||
.flatMap((role) => role.permissions)
|
||||
return Array.from(
|
||||
new Set([
|
||||
...this.data.roles.flatMap((role) => role.permissions),
|
||||
// Add default permissions
|
||||
.concat(config.permissions.default)
|
||||
...config.permissions.default,
|
||||
// If admin, add admin permissions
|
||||
.concat(this.data.isAdmin ? config.permissions.admin : [])
|
||||
.reduce((acc, permission) => {
|
||||
if (!acc.includes(permission)) {
|
||||
acc.push(permission);
|
||||
}
|
||||
return acc;
|
||||
}, [] as RolePermission[])
|
||||
...(this.data.isAdmin ? config.permissions.admin : []),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -415,7 +410,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
icon?: string;
|
||||
icon?: ProxiableUrl;
|
||||
}[],
|
||||
): Promise<
|
||||
{
|
||||
|
|
@ -445,9 +440,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
id: issuer.id,
|
||||
name: issuer.name,
|
||||
url: issuer.url,
|
||||
icon: issuer.icon
|
||||
? proxyUrl(new URL(issuer.icon)).toString()
|
||||
: undefined,
|
||||
icon: issuer.icon?.proxied,
|
||||
server_id: account.serverId,
|
||||
};
|
||||
})
|
||||
|
|
@ -831,11 +824,11 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
* Get the user's avatar in raw URL format
|
||||
* @returns The raw URL for the user's avatar
|
||||
*/
|
||||
public getAvatarUrl(): URL {
|
||||
public getAvatarUrl(): ProxiableUrl {
|
||||
if (!this.avatar) {
|
||||
return (
|
||||
config.defaults.avatar ||
|
||||
new URL(
|
||||
new ProxiableUrl(
|
||||
`https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.data.username}`,
|
||||
)
|
||||
);
|
||||
|
|
@ -928,10 +921,11 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
* Get the user's header in raw URL format
|
||||
* @returns The raw URL for the user's header
|
||||
*/
|
||||
public getHeaderUrl(): URL | null {
|
||||
public getHeaderUrl(): ProxiableUrl | null {
|
||||
if (!this.header) {
|
||||
return config.defaults.header ?? null;
|
||||
}
|
||||
|
||||
return this.header.getUrl();
|
||||
}
|
||||
|
||||
|
|
@ -974,7 +968,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
newUser.isBot ||
|
||||
newUser.isLocked ||
|
||||
newUser.endpoints ||
|
||||
newUser.isDiscoverable)
|
||||
newUser.isDiscoverable ||
|
||||
newUser.isIndexable)
|
||||
) {
|
||||
await this.federateToFollowers(this.toVersia());
|
||||
}
|
||||
|
|
@ -1050,6 +1045,19 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
return new FederationRequester(signatureConstructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all remote followers of the user
|
||||
* @returns The remote followers
|
||||
*/
|
||||
private getRemoteFollowers(): Promise<User[]> {
|
||||
return User.manyFromSql(
|
||||
and(
|
||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${this.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
||||
isNotNull(Users.instanceId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Federates an entity to all followers of the user
|
||||
*
|
||||
|
|
@ -1057,12 +1065,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
*/
|
||||
public async federateToFollowers(entity: KnownEntity): Promise<void> {
|
||||
// Get followers
|
||||
const followers = await User.manyFromSql(
|
||||
and(
|
||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${this.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
||||
isNotNull(Users.instanceId),
|
||||
),
|
||||
);
|
||||
const followers = await this.getRemoteFollowers();
|
||||
|
||||
await deliveryQueue.addBulk(
|
||||
followers.map((follower) => ({
|
||||
|
|
@ -1130,10 +1133,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
url:
|
||||
user.uri ||
|
||||
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
||||
avatar: proxyUrl(this.getAvatarUrl()).toString(),
|
||||
header: this.getHeaderUrl()
|
||||
? proxyUrl(this.getHeaderUrl() as URL).toString()
|
||||
: "",
|
||||
avatar: this.getAvatarUrl().proxied,
|
||||
header: this.getHeaderUrl()?.proxied ?? "",
|
||||
locked: user.isLocked,
|
||||
created_at: new Date(user.createdAt).toISOString(),
|
||||
followers_count:
|
||||
|
|
@ -1154,10 +1155,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
bot: user.isBot,
|
||||
source: isOwnAccount ? user.source : undefined,
|
||||
// TODO: Add static avatar and header
|
||||
avatar_static: proxyUrl(this.getAvatarUrl()).toString(),
|
||||
header_static: this.getHeaderUrl()
|
||||
? proxyUrl(this.getHeaderUrl() as URL).toString()
|
||||
: "",
|
||||
avatar_static: this.getAvatarUrl().proxied,
|
||||
header_static: this.getHeaderUrl()?.proxied ?? "",
|
||||
acct: this.getAcct(),
|
||||
// TODO: Add these fields
|
||||
limited: false,
|
||||
|
|
@ -1168,33 +1167,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
mute_expires_at: null,
|
||||
roles: user.roles
|
||||
.map((role) => new Role(role))
|
||||
.concat(
|
||||
new Role({
|
||||
id: "default",
|
||||
name: "Default",
|
||||
permissions: config.permissions.default,
|
||||
priority: 0,
|
||||
description: "Default role for all users",
|
||||
visible: false,
|
||||
icon: null,
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
user.isAdmin
|
||||
? [
|
||||
new Role({
|
||||
id: "admin",
|
||||
name: "Admin",
|
||||
permissions: config.permissions.admin,
|
||||
priority: 2 ** 31 - 1,
|
||||
description:
|
||||
"Default role for all administrators",
|
||||
visible: false,
|
||||
icon: null,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
)
|
||||
.concat(Role.defaultRole)
|
||||
.concat(user.isAdmin ? Role.adminRole : [])
|
||||
.map((r) => r.toApi()),
|
||||
group: false,
|
||||
// TODO
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue