import { Controller, Get, Param, HttpException, HttpStatus, Res, Req, Inject, Query } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import type { Response, Request } from 'express'; import * as QRCode from 'qrcode'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import type { Cache } from 'cache-manager'; import { Subscription } from '../subscriptions/entities/subscription.entity'; import { Public } from '../auth/public.decorator'; import { Tunnel } from 'src/tunnels/entities/tunnel.entity'; @Controller() export class ClientController { constructor( @InjectRepository(Subscription) private subRepo: Repository, @InjectRepository(Tunnel) private tunnelRepo: Repository, @Inject(CACHE_MANAGER) private cacheManager: Cache ) { } @Public() @Get('bus/:uuid') async getSubscription( @Param('uuid') uuid: string, @Req() req: Request, @Res() res: Response ) { const sub = await this.subRepo.findOne({ where: { uuid }, relations: ['inbounds'] }); if (!sub || !sub.isEnabled) { throw new HttpException('Subscription not found', HttpStatus.NOT_FOUND); } const links = sub.inbounds ?.map(i => i.link) .filter(l => l && l.length > 0) || []; const plainTextList = links.join('\n'); const base64Config = Buffer.from(plainTextList).toString('base64'); const userAgent = req.headers['user-agent'] || ''; const isBrowser = /Mozilla|Chrome|Safari|Firefox|Edge/.test(userAgent); if (!isBrowser) { res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.send(base64Config); } else { const currentUrl = `${req.protocol}://${req.get('host')}/bus/${uuid}`; const cacheKey = `qr_${uuid}`; let qrDataUrl = await this.cacheManager.get(cacheKey); if (!qrDataUrl) { qrDataUrl = await QRCode.toDataURL(currentUrl, { width: 300, margin: 2 }); await this.cacheManager.set(cacheKey, qrDataUrl, 86400000); } else { console.log(`Взяли QR из кэша для ${uuid}`); } const html = ` ${sub.name} | 3DP-MANAGER

Ваша подписка

Отсканируйте QR-код в приложении Happ, v2RayTun или Streisand

QR Code
Для автоматического обновления конфигов используйте эту ссылку
`; res.setHeader('Content-Type', 'text/html'); res.send(html); } } @Public() @Get('bus/:uuid/:tunnelId') async getRelaySubscription( @Param('uuid') uuid: string, @Param('tunnelId') tunnelId: string, @Query('format') format: string, @Req() req: Request, @Res() res: Response ) { const tunnel = await this.tunnelRepo.findOne({ where: { id: +tunnelId } }); if (!tunnel) { return res.status(HttpStatus.NOT_FOUND).send('Relay server not found'); } const relayHost = tunnel.domain || tunnel.ip; const sub = await this.subRepo.findOne({ where: { uuid }, relations: ['inbounds'] }); if (!sub || !sub.isEnabled) { throw new HttpException('Subscription not found', HttpStatus.NOT_FOUND); } let links = sub.inbounds ?.map(i => i.link) .filter(l => l && l.length > 0) || []; links = links.map(link => this.patchLink(link, relayHost)); const plainTextList = links.join('\n'); const base64Config = Buffer.from(plainTextList).toString('base64'); const userAgent = req.headers['user-agent'] || ''; const isBrowser = /Mozilla|Chrome|Safari|Firefox|Edge/.test(userAgent); if (!isBrowser) { res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.send(base64Config); } else { const currentUrl = `${req.protocol}://${req.get('host')}/bus/${uuid}/${tunnelId}`; const cacheKey = `qr_${uuid}_${relayHost || 'direct'}`; let qrDataUrl = await this.cacheManager.get(cacheKey); if (!qrDataUrl) { qrDataUrl = await QRCode.toDataURL(currentUrl, { width: 300, margin: 2 }); await this.cacheManager.set(cacheKey, qrDataUrl, 86400000); } else { console.log(`Взяли QR из кэша для ${uuid}`); } const html = ` ${sub.name} | 3DP-MANAGER

Ваша подписка

Отсканируйте QR-код в приложении Happ, v2RayTun или Streisand

QR Code
Для автоматического обновления конфигов используйте эту ссылку
`; res.setHeader('Content-Type', 'text/html'); res.send(html); } } private patchLink(link: string, newHost: string): string { if (link.startsWith('vmess://')) { try { const base64Part = link.substring(8); const jsonStr = Buffer.from(base64Part, 'base64').toString('utf-8'); const config = JSON.parse(jsonStr); config.add = newHost; const newJsonStr = JSON.stringify(config); const newBase64 = Buffer.from(newJsonStr).toString('base64'); return `vmess://${newBase64}`; } catch (e) { return link; } } else if (link.startsWith('vless://') || link.startsWith('trojan://')) { return link.replace(/@.*?:/, `@${newHost}:`); } else if (link.startsWith('ss://')) { if (link.includes('@')) { return link.replace(/@.*?:/, `@${newHost}:`); } return link; } return link; } }