diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..b99af496 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/libs/contract/commands/hosts/create.command.ts b/libs/contract/commands/hosts/create.command.ts index c886dcf5..634292d1 100644 --- a/libs/contract/commands/hosts/create.command.ts +++ b/libs/contract/commands/hosts/create.command.ts @@ -63,19 +63,23 @@ export namespace CreateHostCommand { }) .nullable(), ), - tag: z + tags: z .optional( z - .string() - .regex( - /^[A-Z0-9_:]+$/, - 'Tag can only contain uppercase letters, numbers, underscores and colons', + .array( + z + .string() + .regex( + /^[A-Z0-9_:]+$/, + 'Tag can only contain uppercase letters, numbers, underscores and colons', + ) + .max(32, 'Tag must be less than 32 characters'), ) - .max(32, 'Tag must be less than 32 characters') - .nullable(), + .max(10, 'Maximum 10 tags allowed') + .default([]), ) .describe( - 'Optional. Host tag for categorization. Max 32 characters, uppercase letters, numbers, underscores and colons are allowed.', + 'Optional. Host tags for categorization. Max 10 tags, each max 32 characters, uppercase letters, numbers, underscores and colons are allowed.', ), isHidden: z.optional(z.boolean().default(false)), overrideSniFromAddress: z.optional(z.boolean().default(false)), diff --git a/libs/contract/commands/hosts/update.command.ts b/libs/contract/commands/hosts/update.command.ts index c1b8eb93..709d1197 100644 --- a/libs/contract/commands/hosts/update.command.ts +++ b/libs/contract/commands/hosts/update.command.ts @@ -67,19 +67,22 @@ export namespace UpdateHostCommand { }) .nullable(), ), - tag: z + tags: z .optional( z - .string() - .regex( - /^[A-Z0-9_:]+$/, - 'Tag can only contain uppercase letters, numbers, underscores and colons', + .array( + z + .string() + .regex( + /^[A-Z0-9_:]+$/, + 'Tag can only contain uppercase letters, numbers, underscores and colons', + ) + .max(32, 'Tag must be less than 32 characters'), ) - .max(32, 'Tag must be less than 32 characters') - .nullable(), + .max(10, 'Maximum 10 tags allowed'), ) .describe( - 'Optional. Host tag for categorization. Max 32 characters, uppercase letters, numbers, underscores and colons are allowed.', + 'Optional. Host tags for categorization. Max 10 tags, each max 32 characters, uppercase letters, numbers, underscores and colons are allowed.', ), isHidden: z.optional(z.boolean()), overrideSniFromAddress: z.optional(z.boolean()), diff --git a/libs/contract/models/hosts.schema.ts b/libs/contract/models/hosts.schema.ts index 1a8420f8..d621c12f 100644 --- a/libs/contract/models/hosts.schema.ts +++ b/libs/contract/models/hosts.schema.ts @@ -27,7 +27,7 @@ export const HostsSchema = z.object({ }), serverDescription: z.string().max(30).nullable(), - tag: z.string().nullable(), + tags: z.array(z.string()).default([]), isHidden: z.boolean().default(false), overrideSniFromAddress: z.boolean().default(false), keepSniBlank: z.boolean().default(false), diff --git a/libs/contract/models/resolved-proxy-config.schema.ts b/libs/contract/models/resolved-proxy-config.schema.ts index d30fed71..7ca11ee8 100644 --- a/libs/contract/models/resolved-proxy-config.schema.ts +++ b/libs/contract/models/resolved-proxy-config.schema.ts @@ -203,7 +203,7 @@ export const SecurityVariantSchema = z.discriminatedUnion('security', [ export const ProxyEntryMetadataSchema = z.object({ uuid: z.string().uuid(), - tag: z.string().nullable(), + tags: z.array(z.string()).default([]), excludeFromSubscriptionTypes: z.array(z.nativeEnum(SUBSCRIPTION_TEMPLATE_TYPE)), inboundTag: z.string(), configProfileUuid: z.string().uuid().nullable(), diff --git a/prisma/migrations/20260408120000_migrate_host_tag_to_tags/migration.sql b/prisma/migrations/20260408120000_migrate_host_tag_to_tags/migration.sql new file mode 100644 index 00000000..c1d2da2f --- /dev/null +++ b/prisma/migrations/20260408120000_migrate_host_tag_to_tags/migration.sql @@ -0,0 +1,10 @@ +-- AlterTable: migrate host tag (single string) to tags (array of strings) + +-- Step 1: Add new tags column with default empty array +ALTER TABLE "hosts" ADD COLUMN "tags" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[]; + +-- Step 2: Migrate existing tag data to tags array +UPDATE "hosts" SET "tags" = ARRAY["tag"] WHERE "tag" IS NOT NULL; + +-- Step 3: Drop old tag column +ALTER TABLE "hosts" DROP COLUMN "tag"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7dd5b28b..e250eeab 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -282,7 +282,7 @@ model Hosts { keepSniBlank Boolean @default(false) @map("keep_sni_blank") excludeFromSubscriptionTypes String[] @default([]) @map("exclude_from_subscription_types") - tag String? @map("tag") + tags String[] @default([]) @map("tags") isHidden Boolean @default(false) @map("is_hidden") overrideSniFromAddress Boolean @default(false) @map("override_sni_from_address") diff --git a/src/modules/hosts/entities/hosts.entity.ts b/src/modules/hosts/entities/hosts.entity.ts index d08fd91f..8a9c1686 100644 --- a/src/modules/hosts/entities/hosts.entity.ts +++ b/src/modules/hosts/entities/hosts.entity.ts @@ -26,7 +26,7 @@ export class HostsEntity implements Hosts { serverDescription: null | string; allowInsecure: boolean; - tag: null | string; + tags: string[]; isHidden: boolean; overrideSniFromAddress: boolean; diff --git a/src/modules/hosts/hosts.converter.ts b/src/modules/hosts/hosts.converter.ts index 62c87db9..69e9e66d 100644 --- a/src/modules/hosts/hosts.converter.ts +++ b/src/modules/hosts/hosts.converter.ts @@ -34,7 +34,7 @@ const entityToModel = (entity: HostsEntity): Hosts => { shuffleHost: entity.shuffleHost, mihomoX25519: entity.mihomoX25519, - tag: entity.tag, + tags: entity.tags, isHidden: entity.isHidden, overrideSniFromAddress: entity.overrideSniFromAddress, diff --git a/src/modules/hosts/models/host.response.model.ts b/src/modules/hosts/models/host.response.model.ts index b9d95403..d72c06b8 100644 --- a/src/modules/hosts/models/host.response.model.ts +++ b/src/modules/hosts/models/host.response.model.ts @@ -26,7 +26,7 @@ export class HostResponseModel { public shuffleHost: boolean; public mihomoX25519: boolean; - public tag: null | string; + public tags: string[]; public isHidden: boolean; public overrideSniFromAddress: boolean; @@ -69,7 +69,7 @@ export class HostResponseModel { this.shuffleHost = data.shuffleHost; this.mihomoX25519 = data.mihomoX25519; - this.tag = data.tag; + this.tags = data.tags; this.isHidden = data.isHidden; this.overrideSniFromAddress = data.overrideSniFromAddress; diff --git a/src/modules/hosts/repositories/hosts.repository.ts b/src/modules/hosts/repositories/hosts.repository.ts index 0f6acbc8..213235c3 100644 --- a/src/modules/hosts/repositories/hosts.repository.ts +++ b/src/modules/hosts/repositories/hosts.repository.ts @@ -1,4 +1,5 @@ import { Prisma } from '@prisma/client'; +import { sql } from 'kysely'; import { IReorderHost } from 'src/modules/hosts/interfaces/reorder-host.interface'; @@ -93,6 +94,7 @@ export class HostsRepository implements ICrud { | 'excludedInternalSquads' | 'excludeFromSubscriptionTypes' | 'finalMask' + | 'tags' >, ): Promise { const list = await this.prisma.tx.hosts.findMany({ @@ -245,14 +247,15 @@ export class HostsRepository implements ICrud { } public async getAllHostTags(): Promise { - const result = await this.prisma.tx.hosts.findMany({ - select: { - tag: true, - }, - distinct: ['tag'], - }); + const result = await this.qb.kysely + .selectFrom('hosts') + .select(sql`unnest(tags)`.as('tag')) + .distinct() + .where('tags', 'is not', null) + .orderBy('tag') + .execute(); - return result.map((host) => host.tag).filter((tag) => tag !== null); + return result.map((value) => value.tag); } public async addNodesToHost(hostUuid: string, nodes: string[]): Promise { diff --git a/src/modules/subscription-template/generators/xray-json.generator.service.ts b/src/modules/subscription-template/generators/xray-json.generator.service.ts index 7b60ed03..128ca733 100644 --- a/src/modules/subscription-template/generators/xray-json.generator.service.ts +++ b/src/modules/subscription-template/generators/xray-json.generator.service.ts @@ -351,7 +351,9 @@ export class XrayJsonGeneratorService { } if (useHostTagAsTag) { - return hosts.map((h) => this.buildOutbound(h, h.metadata.tag || h.finalRemark)); + return hosts.map((h) => + this.buildOutbound(h, h.metadata.tags[0] || h.finalRemark), + ); } const proxyTag = tagPrefix ?? 'proxy'; @@ -404,13 +406,17 @@ export class XrayJsonGeneratorService { case 'sameTagAsRecipient': return candidates.filter( (h) => - h.metadata.tag && host.metadata.tag && h.metadata.tag === host.metadata.tag, + h.metadata.tags.length > 0 && + host.metadata.tags.length > 0 && + h.metadata.tags.some((t) => host.metadata.tags.includes(t)), ); case 'tagRegex': { const regex = this.parseRegex(selector.pattern); if (!regex) return []; - return candidates.filter((h) => h.metadata.tag && regex.test(h.metadata.tag)); + return candidates.filter( + (h) => h.metadata.tags.length > 0 && h.metadata.tags.some((t) => regex.test(t)), + ); } } } diff --git a/src/modules/subscription-template/resolve-proxy/interfaces/resolved-proxy-config.interface.ts b/src/modules/subscription-template/resolve-proxy/interfaces/resolved-proxy-config.interface.ts index 38066f6a..cd39f6f5 100644 --- a/src/modules/subscription-template/resolve-proxy/interfaces/resolved-proxy-config.interface.ts +++ b/src/modules/subscription-template/resolve-proxy/interfaces/resolved-proxy-config.interface.ts @@ -186,7 +186,7 @@ export type SecurityVariant = TlsSecurity | RealitySecurity | NoneSecurity; export interface IProxyEntryMetadata { uuid: string; - tag: string | null; + tags: string[]; excludeFromSubscriptionTypes: TSubscriptionTemplateType[]; inboundTag: string; configProfileUuid: string | null; diff --git a/src/modules/subscription-template/resolve-proxy/resolve-proxy-config.service.ts b/src/modules/subscription-template/resolve-proxy/resolve-proxy-config.service.ts index f1a49a6b..2e7cf5b4 100644 --- a/src/modules/subscription-template/resolve-proxy/resolve-proxy-config.service.ts +++ b/src/modules/subscription-template/resolve-proxy/resolve-proxy-config.service.ts @@ -564,7 +564,7 @@ export class ResolveProxyConfigService { }, metadata: { uuid: inputHost.uuid, - tag: inputHost.tag, + tags: inputHost.tags, excludeFromSubscriptionTypes: inputHost.excludeFromSubscriptionTypes, inboundTag: inputHost.inboundTag, configProfileUuid: inputHost.configProfileUuid, @@ -676,7 +676,7 @@ export class ResolveProxyConfigService { }, metadata: { uuid: '00000000-0000-0000-0000-000000000000', - tag: null, + tags: [], excludeFromSubscriptionTypes: [], inboundTag: '', configProfileUuid: null,