Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/common/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './mask-string';
export * from './md5';
export * from './superjson';
export * from './truncate-header.util';
export * from './parse-int-range.util';
26 changes: 26 additions & 0 deletions src/common/utils/parse-int-range.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface IntRange {
from: number | null;
to: number | null;
}

export function parseIntRangeUtil(value: string | number | undefined): IntRange {
if (value === undefined || value === '') return { from: null, to: null };

const [left, right = left] = String(value).split('-', 2);
const from = parseIntPart(left);
const to = parseIntPart(right);

if (from !== null && to !== null && from > to) {
return { from: to, to: from };
}

return { from, to };
}

function parseIntPart(part: string): number | null {
if (part === '') return null;

const parsed = Number(part);

return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import yaml from 'yaml';

import { Injectable, Logger } from '@nestjs/common';

import { isNonEmptyObject } from '@common/utils';
import { isNonEmptyObject, parseIntRangeUtil } from '@common/utils';
import { FINGERPRINTS } from '@libs/contracts/constants';

import { SubscriptionTemplateService } from '@modules/subscription-template/subscription-template.service';
Expand Down Expand Up @@ -41,12 +41,25 @@ interface Hysteria2FinalMask {
bbrProfile?: string;
congestion?: string;
};
udp?: Array<{
type?: string;
settings?: { password?: string };
}>;
}

interface Hysteria2Mask {
type: 'salamander';
settings: {
packetSize?: string | number;
password: string;
};
}

interface Hysteria2PacketSizeFields {
'obfs-max-packet-size': number;
'obfs-min-packet-size': number;
}

type Hysteria2ObfsFields =
| ({ obfs: 'gecko'; 'obfs-password': string } & Partial<Hysteria2PacketSizeFields>)
| { obfs: 'salamander'; 'obfs-password': string };

interface ProxyNode {
[key: string]: unknown;
alpn?: string[];
Expand Down Expand Up @@ -646,13 +659,25 @@ export class MihomoGeneratorService {

private buildHysteria2ObfsFields(
finalMask: Record<string, unknown> | null,
): Record<string, unknown> {
const password = (finalMask as Hysteria2FinalMask | null)?.udp?.find(
(m) => m?.type === 'salamander',
)?.settings?.password;
): Hysteria2ObfsFields | Record<string, never> {
const mask = this.findHysteria2Mask(finalMask);

if (!password) return {};
return { obfs: 'salamander', 'obfs-password': password };
if (!mask) return {};

const { password, packetSize } = mask.settings;

if (!packetSize) {
return { obfs: 'salamander', 'obfs-password': password };
}

const { from, to } = parseIntRangeUtil(packetSize);

return {
obfs: 'gecko',
'obfs-password': password,
...(from && { 'obfs-min-packet-size': from }),
...(to && { 'obfs-max-packet-size': to }),
};
}

private buildHysteria2TlsFields(host: ResolvedProxyConfig): Record<string, unknown> {
Expand All @@ -678,4 +703,43 @@ export class MihomoGeneratorService {
}
}
}

private findHysteria2Mask(finalMask: Record<string, unknown> | null): Hysteria2Mask | null {
const udp = finalMask?.udp;

if (!Array.isArray(udp)) return null;

return udp.find((mask): mask is Hysteria2Mask => this.isHysteria2Mask(mask)) ?? null;
}

private isHysteria2Mask(value: unknown): value is Hysteria2Mask {
if (
typeof value !== 'object' ||
value === null ||
!('type' in value) ||
!('settings' in value)
) {
return false;
}

if (value.type !== 'salamander') {
return false;
}

const settings: unknown = value.settings;
if (typeof settings !== 'object' || settings === null || !('password' in settings)) {
return false;
}

if (typeof settings.password !== 'string' || settings.password.length === 0) {
return false;
}

const packetSize: unknown = 'packetSize' in settings ? settings.packetSize : undefined;
return (
packetSize === undefined ||
typeof packetSize === 'string' ||
typeof packetSize === 'number'
);
}
}