mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +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
|
|
@ -69,6 +69,7 @@ In the case that you've been running secret instances in the shadows, let us kno
|
||||||
- 🐛 All media content-type is now correctly fetched, instead of guessed from the file extension as before.
|
- 🐛 All media content-type is now correctly fetched, instead of guessed from the file extension as before.
|
||||||
- 🐛 Fixed OpenAPI schema generation and `/docs` endpoint.
|
- 🐛 Fixed OpenAPI schema generation and `/docs` endpoint.
|
||||||
- 🐛 Logs folder is now automatically created if it doesn't exist.
|
- 🐛 Logs folder is now automatically created if it doesn't exist.
|
||||||
|
- 🐛 Media hosted on the configured S3 bucket and on the local filesystem is no longer unnecessarily proxied.
|
||||||
|
|
||||||
# `0.7.0` • The Auth and APIs Update
|
# `0.7.0` • The Auth and APIs Update
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ describe("/api/v1/emojis", () => {
|
||||||
|
|
||||||
expect(ok).toBe(true);
|
expect(ok).toBe(true);
|
||||||
expect(data.shortcode).toBe("test1");
|
expect(data.shortcode).toBe("test1");
|
||||||
expect(data.url).toContain("/media/proxy");
|
expect(data.url).toContain("/media/");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should try to upload a non-image", async () => {
|
test("should try to upload a non-image", async () => {
|
||||||
|
|
@ -116,7 +116,7 @@ describe("/api/v1/emojis", () => {
|
||||||
|
|
||||||
expect(ok).toBe(true);
|
expect(ok).toBe(true);
|
||||||
expect(data.shortcode).toBe("test4");
|
expect(data.shortcode).toBe("test4");
|
||||||
expect(data.url).toContain("/media/proxy");
|
expect(data.url).toContain("/media/");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should fail when uploading an already existing global emoji", async () => {
|
test("should fail when uploading an already existing global emoji", async () => {
|
||||||
|
|
@ -141,7 +141,7 @@ describe("/api/v1/emojis", () => {
|
||||||
|
|
||||||
expect(ok).toBe(true);
|
expect(ok).toBe(true);
|
||||||
expect(data.shortcode).toBe("test4");
|
expect(data.shortcode).toBe("test4");
|
||||||
expect(data.url).toContain("/media/proxy/");
|
expect(data.url).toContain("/media/");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { apiRoute } from "@/api";
|
import { apiRoute } from "@/api";
|
||||||
import { proxyUrl } from "@/response";
|
|
||||||
import { InstanceV1 as InstanceV1Schema } from "@versia/client/schemas";
|
import { InstanceV1 as InstanceV1Schema } from "@versia/client/schemas";
|
||||||
import { Instance, Note, User } from "@versia/kit/db";
|
import { Instance, Note, User } from "@versia/kit/db";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
|
|
@ -8,6 +7,7 @@ import { describeRoute } from "hono-openapi";
|
||||||
import { resolver } from "hono-openapi/zod";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { markdownParse } from "~/classes/functions/status";
|
import { markdownParse } from "~/classes/functions/status";
|
||||||
|
import type { ProxiableUrl } from "~/classes/media/url";
|
||||||
import { config } from "~/config.ts";
|
import { config } from "~/config.ts";
|
||||||
import manifest from "~/package.json";
|
import manifest from "~/package.json";
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ export default apiRoute((app) =>
|
||||||
providers?: {
|
providers?: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon?: ProxiableUrl;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
@ -114,9 +114,7 @@ export default apiRoute((app) =>
|
||||||
status_count: statusCount,
|
status_count: statusCount,
|
||||||
user_count: userCount,
|
user_count: userCount,
|
||||||
},
|
},
|
||||||
thumbnail: config.instance.branding.logo
|
thumbnail: config.instance.branding.logo?.proxied ?? null,
|
||||||
? proxyUrl(config.instance.branding.logo).toString()
|
|
||||||
: null,
|
|
||||||
title: config.instance.name,
|
title: config.instance.name,
|
||||||
uri: config.http.base_url.host,
|
uri: config.http.base_url.host,
|
||||||
urls: {
|
urls: {
|
||||||
|
|
@ -131,9 +129,7 @@ export default apiRoute((app) =>
|
||||||
providers:
|
providers:
|
||||||
oidcConfig?.providers?.map((p) => ({
|
oidcConfig?.providers?.map((p) => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
icon: p.icon
|
icon: p.icon?.proxied,
|
||||||
? proxyUrl(new URL(p.icon)).toString()
|
|
||||||
: undefined,
|
|
||||||
id: p.id,
|
id: p.id,
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { apiRoute } from "@/api";
|
import { apiRoute } from "@/api";
|
||||||
import { proxyUrl } from "@/response";
|
|
||||||
import { Instance as InstanceSchema } from "@versia/client/schemas";
|
import { Instance as InstanceSchema } from "@versia/client/schemas";
|
||||||
import { User } from "@versia/kit/db";
|
import { User } from "@versia/kit/db";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { resolver } from "hono-openapi/zod";
|
import { resolver } from "hono-openapi/zod";
|
||||||
|
import type { ProxiableUrl } from "~/classes/media/url";
|
||||||
import { config } from "~/config.ts";
|
import { config } from "~/config.ts";
|
||||||
import pkg from "~/package.json";
|
import pkg from "~/package.json";
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ export default apiRoute((app) =>
|
||||||
providers?: {
|
providers?: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon?: ProxiableUrl;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
@ -69,14 +69,10 @@ export default apiRoute((app) =>
|
||||||
mastodon: 1,
|
mastodon: 1,
|
||||||
},
|
},
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: config.instance.branding.logo
|
url: config.instance.branding.logo?.proxied ?? pkg.icon,
|
||||||
? proxyUrl(config.instance.branding.logo).toString()
|
|
||||||
: pkg.icon,
|
|
||||||
},
|
},
|
||||||
banner: {
|
banner: {
|
||||||
url: config.instance.branding.banner
|
url: config.instance.branding.banner?.proxied ?? null,
|
||||||
? proxyUrl(config.instance.branding.banner).toString()
|
|
||||||
: null,
|
|
||||||
},
|
},
|
||||||
icon: [],
|
icon: [],
|
||||||
languages: config.instance.languages,
|
languages: config.instance.languages,
|
||||||
|
|
@ -172,7 +168,7 @@ export default apiRoute((app) =>
|
||||||
providers:
|
providers:
|
||||||
oidcConfig?.providers?.map((p) => ({
|
oidcConfig?.providers?.map((p) => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
icon: p.icon ? proxyUrl(new URL(p.icon)) : "",
|
icon: p.icon?.proxied,
|
||||||
id: p.id,
|
id: p.id,
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { generateVAPIDKeys } from "web-push";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
import { ProxiableUrl } from "~/classes/media/url.ts";
|
||||||
import { RolePermission } from "~/packages/client/schemas/permissions.ts";
|
import { RolePermission } from "~/packages/client/schemas/permissions.ts";
|
||||||
|
|
||||||
export const DEFAULT_ROLES = [
|
export const DEFAULT_ROLES = [
|
||||||
|
|
@ -70,21 +71,21 @@ export enum MediaBackendType {
|
||||||
// Need to declare this here instead of importing it otherwise we get cyclical import errors
|
// Need to declare this here instead of importing it otherwise we get cyclical import errors
|
||||||
export const iso631 = z.enum(ISO6391.getAllCodes() as [string, ...string[]]);
|
export const iso631 = z.enum(ISO6391.getAllCodes() as [string, ...string[]]);
|
||||||
|
|
||||||
const urlPath = z
|
export const urlPath = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
// Remove trailing slashes, but keep the root slash
|
// Remove trailing slashes, but keep the root slash
|
||||||
.transform((arg) => (arg === "/" ? arg : arg.replace(/\/$/, "")));
|
.transform((arg) => (arg === "/" ? arg : arg.replace(/\/$/, "")));
|
||||||
|
|
||||||
const url = z
|
export const url = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.refine((arg) => URL.canParse(arg), "Invalid url")
|
.refine((arg) => URL.canParse(arg), "Invalid url")
|
||||||
.transform((arg) => new URL(arg));
|
.transform((arg) => new ProxiableUrl(arg));
|
||||||
|
|
||||||
const unixPort = z
|
export const unixPort = z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
|
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
|
||||||
import { proxyUrl } from "@/response";
|
|
||||||
import type { CustomEmoji } from "@versia/client/schemas";
|
import type { CustomEmoji } from "@versia/client/schemas";
|
||||||
import type { CustomEmojiExtension } from "@versia/federation/types";
|
import type { CustomEmojiExtension } from "@versia/federation/types";
|
||||||
import { type Instance, Media, db } from "@versia/kit/db";
|
import { type Instance, Media, db } from "@versia/kit/db";
|
||||||
|
|
@ -179,8 +178,8 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
shortcode: this.data.shortcode,
|
shortcode: this.data.shortcode,
|
||||||
static_url: proxyUrl(this.media.getUrl()).toString(),
|
static_url: this.media.getUrl().proxied,
|
||||||
url: proxyUrl(this.media.getUrl()).toString(),
|
url: this.media.getUrl().proxied,
|
||||||
visible_in_picker: this.data.visibleInPicker,
|
visible_in_picker: this.data.visibleInPicker,
|
||||||
category: this.data.category,
|
category: this.data.category,
|
||||||
global: this.data.ownerId === null,
|
global: this.data.ownerId === null,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { mimeLookup } from "@/content_types.ts";
|
import { mimeLookup } from "@/content_types.ts";
|
||||||
import { proxyUrl } from "@/response";
|
|
||||||
import type { Attachment as AttachmentSchema } from "@versia/client/schemas";
|
import type { Attachment as AttachmentSchema } from "@versia/client/schemas";
|
||||||
import type { ContentFormat } from "@versia/federation/types";
|
import type { ContentFormat } from "@versia/federation/types";
|
||||||
import { db } from "@versia/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
|
|
@ -20,6 +19,7 @@ import { MediaBackendType } from "~/classes/config/schema.ts";
|
||||||
import { config } from "~/config.ts";
|
import { config } from "~/config.ts";
|
||||||
import { ApiError } from "../errors/api-error.ts";
|
import { ApiError } from "../errors/api-error.ts";
|
||||||
import { getMediaHash } from "../media/media-hasher.ts";
|
import { getMediaHash } from "../media/media-hasher.ts";
|
||||||
|
import { ProxiableUrl } from "../media/url.ts";
|
||||||
import { MediaJobType, mediaQueue } from "../queues/media.ts";
|
import { MediaJobType, mediaQueue } from "../queues/media.ts";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
|
|
||||||
|
|
@ -369,10 +369,10 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
throw new Error("Unknown media backend");
|
throw new Error("Unknown media backend");
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUrl(): URL {
|
public getUrl(): ProxiableUrl {
|
||||||
const type = this.getPreferredMimeType();
|
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 {
|
return {
|
||||||
id: this.data.id,
|
id: this.data.id,
|
||||||
type: this.getMastodonType(),
|
type: this.getMastodonType(),
|
||||||
url: proxyUrl(new URL(data.content)).toString(),
|
url: this.getUrl().proxied,
|
||||||
remote_url: null,
|
remote_url: null,
|
||||||
preview_url: thumbnailData?.content
|
preview_url: thumbnailData?.content
|
||||||
? proxyUrl(new URL(thumbnailData.content)).toString()
|
? new ProxiableUrl(thumbnailData.content).proxied
|
||||||
: null,
|
: null,
|
||||||
meta: this.toApiMeta(),
|
meta: this.toApiMeta(),
|
||||||
description: data.description || null,
|
description: data.description || null,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { proxyUrl } from "@/response";
|
|
||||||
import type { Role as RoleSchema } from "@versia/client/schemas";
|
import type { Role as RoleSchema } from "@versia/client/schemas";
|
||||||
import type { RolePermission } from "@versia/client/schemas";
|
import type { RolePermission } from "@versia/client/schemas";
|
||||||
import { db } from "@versia/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
|
|
@ -14,11 +13,30 @@ import {
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { config } from "~/config.ts";
|
import { config } from "~/config.ts";
|
||||||
|
import { ProxiableUrl } from "../media/url.ts";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
type RoleType = InferSelectModel<typeof Roles>;
|
type RoleType = InferSelectModel<typeof Roles>;
|
||||||
|
|
||||||
export class Role extends BaseInterface<typeof Roles> {
|
export class Role extends BaseInterface<typeof Roles> {
|
||||||
public static $type: RoleType;
|
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> {
|
public async reload(): Promise<void> {
|
||||||
const reloaded = await Role.fromId(this.data.id);
|
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[]> {
|
public static async getAll(): Promise<Role[]> {
|
||||||
return (await Role.manyFromSql(undefined)).concat(
|
return (await Role.manyFromSql(undefined)).concat(
|
||||||
new Role({
|
Role.defaultRole,
|
||||||
id: "default",
|
Role.adminRole,
|
||||||
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,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,7 +217,7 @@ export class Role extends BaseInterface<typeof Roles> {
|
||||||
description: this.data.description ?? undefined,
|
description: this.data.description ?? undefined,
|
||||||
visible: this.data.visible,
|
visible: this.data.visible,
|
||||||
icon: this.data.icon
|
icon: this.data.icon
|
||||||
? proxyUrl(new URL(this.data.icon)).toString()
|
? new ProxiableUrl(this.data.icon).proxied
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { idValidator } from "@/api";
|
import { idValidator } from "@/api";
|
||||||
import { getBestContentType } from "@/content_types";
|
import { getBestContentType } from "@/content_types";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { proxyUrl } from "@/response";
|
|
||||||
import { sentry } from "@/sentry";
|
import { sentry } from "@/sentry";
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -56,6 +55,7 @@ import { findManyUsers } from "~/classes/functions/user";
|
||||||
import { searchManager } from "~/classes/search/search-manager";
|
import { searchManager } from "~/classes/search/search-manager";
|
||||||
import { config } from "~/config.ts";
|
import { config } from "~/config.ts";
|
||||||
import type { KnownEntity } from "~/types/api.ts";
|
import type { KnownEntity } from "~/types/api.ts";
|
||||||
|
import { ProxiableUrl } from "../media/url.ts";
|
||||||
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
|
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
|
||||||
import { PushJobType, pushQueue } from "../queues/push.ts";
|
import { PushJobType, pushQueue } from "../queues/push.ts";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
|
|
@ -180,19 +180,14 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllPermissions(): RolePermission[] {
|
public getAllPermissions(): RolePermission[] {
|
||||||
return (
|
return Array.from(
|
||||||
this.data.roles
|
new Set([
|
||||||
.flatMap((role) => role.permissions)
|
...this.data.roles.flatMap((role) => role.permissions),
|
||||||
// Add default permissions
|
// Add default permissions
|
||||||
.concat(config.permissions.default)
|
...config.permissions.default,
|
||||||
// If admin, add admin permissions
|
// If admin, add admin permissions
|
||||||
.concat(this.data.isAdmin ? config.permissions.admin : [])
|
...(this.data.isAdmin ? config.permissions.admin : []),
|
||||||
.reduce((acc, permission) => {
|
]),
|
||||||
if (!acc.includes(permission)) {
|
|
||||||
acc.push(permission);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [] as RolePermission[])
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -415,7 +410,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon?: string;
|
icon?: ProxiableUrl;
|
||||||
}[],
|
}[],
|
||||||
): Promise<
|
): Promise<
|
||||||
{
|
{
|
||||||
|
|
@ -445,9 +440,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
id: issuer.id,
|
id: issuer.id,
|
||||||
name: issuer.name,
|
name: issuer.name,
|
||||||
url: issuer.url,
|
url: issuer.url,
|
||||||
icon: issuer.icon
|
icon: issuer.icon?.proxied,
|
||||||
? proxyUrl(new URL(issuer.icon)).toString()
|
|
||||||
: undefined,
|
|
||||||
server_id: account.serverId,
|
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
|
* Get the user's avatar in raw URL format
|
||||||
* @returns The raw URL for the user's avatar
|
* @returns The raw URL for the user's avatar
|
||||||
*/
|
*/
|
||||||
public getAvatarUrl(): URL {
|
public getAvatarUrl(): ProxiableUrl {
|
||||||
if (!this.avatar) {
|
if (!this.avatar) {
|
||||||
return (
|
return (
|
||||||
config.defaults.avatar ||
|
config.defaults.avatar ||
|
||||||
new URL(
|
new ProxiableUrl(
|
||||||
`https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.data.username}`,
|
`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
|
* Get the user's header in raw URL format
|
||||||
* @returns The raw URL for the user's header
|
* @returns The raw URL for the user's header
|
||||||
*/
|
*/
|
||||||
public getHeaderUrl(): URL | null {
|
public getHeaderUrl(): ProxiableUrl | null {
|
||||||
if (!this.header) {
|
if (!this.header) {
|
||||||
return config.defaults.header ?? null;
|
return config.defaults.header ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.header.getUrl();
|
return this.header.getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -974,7 +968,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
newUser.isBot ||
|
newUser.isBot ||
|
||||||
newUser.isLocked ||
|
newUser.isLocked ||
|
||||||
newUser.endpoints ||
|
newUser.endpoints ||
|
||||||
newUser.isDiscoverable)
|
newUser.isDiscoverable ||
|
||||||
|
newUser.isIndexable)
|
||||||
) {
|
) {
|
||||||
await this.federateToFollowers(this.toVersia());
|
await this.federateToFollowers(this.toVersia());
|
||||||
}
|
}
|
||||||
|
|
@ -1050,6 +1045,19 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
return new FederationRequester(signatureConstructor);
|
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
|
* 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> {
|
public async federateToFollowers(entity: KnownEntity): Promise<void> {
|
||||||
// Get followers
|
// Get followers
|
||||||
const followers = await User.manyFromSql(
|
const followers = await this.getRemoteFollowers();
|
||||||
and(
|
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${this.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
|
||||||
isNotNull(Users.instanceId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await deliveryQueue.addBulk(
|
await deliveryQueue.addBulk(
|
||||||
followers.map((follower) => ({
|
followers.map((follower) => ({
|
||||||
|
|
@ -1130,10 +1133,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
url:
|
url:
|
||||||
user.uri ||
|
user.uri ||
|
||||||
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
||||||
avatar: proxyUrl(this.getAvatarUrl()).toString(),
|
avatar: this.getAvatarUrl().proxied,
|
||||||
header: this.getHeaderUrl()
|
header: this.getHeaderUrl()?.proxied ?? "",
|
||||||
? proxyUrl(this.getHeaderUrl() as URL).toString()
|
|
||||||
: "",
|
|
||||||
locked: user.isLocked,
|
locked: user.isLocked,
|
||||||
created_at: new Date(user.createdAt).toISOString(),
|
created_at: new Date(user.createdAt).toISOString(),
|
||||||
followers_count:
|
followers_count:
|
||||||
|
|
@ -1154,10 +1155,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
bot: user.isBot,
|
bot: user.isBot,
|
||||||
source: isOwnAccount ? user.source : undefined,
|
source: isOwnAccount ? user.source : undefined,
|
||||||
// TODO: Add static avatar and header
|
// TODO: Add static avatar and header
|
||||||
avatar_static: proxyUrl(this.getAvatarUrl()).toString(),
|
avatar_static: this.getAvatarUrl().proxied,
|
||||||
header_static: this.getHeaderUrl()
|
header_static: this.getHeaderUrl()?.proxied ?? "",
|
||||||
? proxyUrl(this.getHeaderUrl() as URL).toString()
|
|
||||||
: "",
|
|
||||||
acct: this.getAcct(),
|
acct: this.getAcct(),
|
||||||
// TODO: Add these fields
|
// TODO: Add these fields
|
||||||
limited: false,
|
limited: false,
|
||||||
|
|
@ -1168,33 +1167,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
mute_expires_at: null,
|
mute_expires_at: null,
|
||||||
roles: user.roles
|
roles: user.roles
|
||||||
.map((role) => new Role(role))
|
.map((role) => new Role(role))
|
||||||
.concat(
|
.concat(Role.defaultRole)
|
||||||
new Role({
|
.concat(user.isAdmin ? Role.adminRole : [])
|
||||||
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,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
)
|
|
||||||
.map((r) => r.toApi()),
|
.map((r) => r.toApi()),
|
||||||
group: false,
|
group: false,
|
||||||
// TODO
|
// TODO
|
||||||
|
|
|
||||||
25
classes/media/url.ts
Normal file
25
classes/media/url.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
|
export class ProxiableUrl extends URL {
|
||||||
|
private isAllowedOrigin(): boolean {
|
||||||
|
const allowedOrigins: URL[] = [config.http.base_url].concat(
|
||||||
|
config.s3?.public_url ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
return allowedOrigins.some((origin) =>
|
||||||
|
this.hostname.endsWith(origin.hostname),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get proxied(): string {
|
||||||
|
// Don't proxy from CDN and self, since those sources are trusted
|
||||||
|
if (this.isAllowedOrigin()) {
|
||||||
|
return this.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlAsBase64Url = Buffer.from(this.href).toString("base64url");
|
||||||
|
|
||||||
|
return new URL(`/media/proxy/${urlAsBase64Url}`, config.http.base_url)
|
||||||
|
.href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import { getCookie } from "hono/cookie";
|
||||||
import { jwtVerify } from "jose";
|
import { jwtVerify } from "jose";
|
||||||
import { JOSEError, JWTExpired } from "jose/errors";
|
import { JOSEError, JWTExpired } from "jose/errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { keyPair, sensitiveString } from "~/classes/config/schema.ts";
|
import { url, keyPair, sensitiveString } from "~/classes/config/schema.ts";
|
||||||
import { ApiError } from "~/classes/errors/api-error.ts";
|
import { ApiError } from "~/classes/errors/api-error.ts";
|
||||||
import authorizeRoute from "./routes/authorize.ts";
|
import authorizeRoute from "./routes/authorize.ts";
|
||||||
import jwksRoute from "./routes/jwks.ts";
|
import jwksRoute from "./routes/jwks.ts";
|
||||||
|
|
@ -27,7 +27,7 @@ const configSchema = z.object({
|
||||||
url: z.string().min(1),
|
url: z.string().min(1),
|
||||||
client_id: z.string().min(1),
|
client_id: z.string().min(1),
|
||||||
client_secret: sensitiveString,
|
client_secret: sensitiveString,
|
||||||
icon: z.string().min(1).optional(),
|
icon: url.optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.default([]),
|
.default([]),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { auth, handleZodError } from "@/api";
|
import { auth, handleZodError } from "@/api";
|
||||||
import { proxyUrl } from "@/response";
|
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { db } from "@versia/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
import { type SQL, eq } from "@versia/kit/drizzle";
|
import { type SQL, eq } from "@versia/kit/drizzle";
|
||||||
|
|
@ -77,9 +76,7 @@ export default (plugin: PluginType): void => {
|
||||||
{
|
{
|
||||||
id: issuer.id,
|
id: issuer.id,
|
||||||
name: issuer.name,
|
name: issuer.name,
|
||||||
icon: issuer.icon
|
icon: issuer.icon?.proxied,
|
||||||
? proxyUrl(new URL(issuer.icon))
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
export type Json =
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
| Json[]
|
|
||||||
| { [key: string]: Json };
|
|
||||||
|
|
||||||
export const proxyUrl = (url: URL): URL => {
|
|
||||||
const urlAsBase64Url = Buffer.from(url.toString() || "").toString(
|
|
||||||
"base64url",
|
|
||||||
);
|
|
||||||
return new URL(`/media/proxy/${urlAsBase64Url}`, config.http.base_url);
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { stringifyEntitiesLight } from "stringify-entities";
|
import { stringifyEntitiesLight } from "stringify-entities";
|
||||||
import xss, { type IFilterXSSOptions } from "xss";
|
import xss, { type IFilterXSSOptions } from "xss";
|
||||||
import { proxyUrl } from "./response.ts";
|
import { ProxiableUrl } from "~/classes/media/url.ts";
|
||||||
|
|
||||||
export const sanitizedHtmlStrip = (html: string): Promise<string> => {
|
export const sanitizedHtmlStrip = (html: string): Promise<string> => {
|
||||||
return sanitizeHtml(html, {
|
return sanitizeHtml(html, {
|
||||||
|
|
@ -137,9 +137,9 @@ export const sanitizeHtml = async (
|
||||||
element.setAttribute(
|
element.setAttribute(
|
||||||
"src",
|
"src",
|
||||||
element.getAttribute("src")
|
element.getAttribute("src")
|
||||||
? proxyUrl(
|
? new ProxiableUrl(
|
||||||
new URL(element.getAttribute("src") as string),
|
element.getAttribute("src") as string,
|
||||||
).toString()
|
).proxied
|
||||||
: "",
|
: "",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue