diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 1b46c91..b50b509 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -2,7 +2,7 @@ name: Docker Build & Publish on: push: - branches: [ "main" ] + branches: [ "main", "dp-new-release" ] workflow_dispatch: env: diff --git a/README.md b/README.md index 96b79ff..7e65106 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Главная цель утилиты — сделать так, чтобы ваш трафик не выглядел одинаковым. Бот генерирует по заданному интервалу 10 подключений с разными параметрами: -- протоколы: `vless`, `vmess`, `shadowsocks`, `trojan`; +- протоколы: `vless`, `vmess`, `shadowsocks`, `hysteria2`, `trojan`; - порты: `443`, `8443` (фиксированные) и случайные из диапазона `10000-60000`; - транспорт: `tcp`, `websocket`, `grpc`, `xhttp`; - SNI берутся из белого списка доменов (whitelist); можно использовать свой список. diff --git a/client/src/api.ts b/client/src/api.ts index 8f7592a..fc8e45c 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -1,7 +1,7 @@ import axios from 'axios'; const api = axios.create({ - baseURL: `${location.protocol}//${location.hostname}:3000/api`, + baseURL: `${location.protocol}//${location.hostname}:${location.port}/api`, }); export default api; \ No newline at end of file diff --git a/client/src/pages/SubscriptionsPage.tsx b/client/src/pages/SubscriptionsPage.tsx index 2235891..bfdb1bf 100644 --- a/client/src/pages/SubscriptionsPage.tsx +++ b/client/src/pages/SubscriptionsPage.tsx @@ -18,6 +18,7 @@ interface Subscription { name: string; uuid: string; inbounds: any[]; + inboundsConfig?: any[]; } interface Tunnel { @@ -33,6 +34,7 @@ interface InboundConfigUI { type: string; port: string; sni: string; + link?: string; } interface Domain { id: number; name: string; } @@ -46,6 +48,7 @@ const CONNECTION_OPTIONS = [ 'vmess-tcp', 'shadowsocks-tcp', 'trojan-tcp-reality', + 'custom', ]; const patchLink = function (link: string, newHost: string): string { @@ -124,16 +127,16 @@ export default function SubscriptionsPage() { setEditingId(null); setName(''); setInbounds([ - { id: generateId(), type: 'hysteria2-udp', port: 'random', sni: 'random' }, - { id: generateId(), type: 'vless-xhttp-reality', port: 'random', sni: 'random' }, - { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }, - { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }, - { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }, - { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }, - { id: generateId(), type: 'vless-grpc-reality', port: 'random', sni: 'random' }, - { id: generateId(), type: 'vless-ws', port: 'random', sni: 'random' }, - { id: generateId(), type: 'vmess-tcp', port: 'random', sni: 'random' }, - { id: generateId(), type: 'shadowsocks-tcp', port: 'random', sni: 'random' }, + { id: generateId(), type: 'hysteria2-udp', port: 'random', sni: 'random', link: '' }, + { id: generateId(), type: 'vless-xhttp-reality', port: 'random', sni: 'random', link: '' }, + { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }, + { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }, + { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }, + { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }, + { id: generateId(), type: 'vless-grpc-reality', port: 'random', sni: 'random', link: '' }, + { id: generateId(), type: 'vless-ws', port: 'random', sni: 'random', link: '' }, + { id: generateId(), type: 'vmess-tcp', port: 'random', sni: 'random', link: '' }, + { id: generateId(), type: 'shadowsocks-tcp', port: 'random', sni: 'random', link: '' }, ]); setPortErrors({}); setOpen(true); @@ -143,79 +146,58 @@ export default function SubscriptionsPage() { setEditingId(sub.id); setName(sub.name); - // Пытаемся маппить существующие инбаунды, или даем дефолтные - if (sub.inbounds && sub.inbounds.length > 0) { - setInbounds(sub.inbounds.map(i => ({ + if (sub.inboundsConfig && sub.inboundsConfig.length > 0) { + setInbounds(sub.inboundsConfig.map(i => ({ id: generateId(), type: i.type || 'vless-tcp-reality', port: i.port ? i.port.toString() : 'random', - sni: i.sni || 'random' + sni: i.sni || 'random', + link: i.link || '' }))); } else { - setInbounds([{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }]); + setInbounds([{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }]); } + setPortErrors({}); setOpen(true); }; - // Проверка порта - const handlePortCheck = async (portValue: string, id: string) => { - if (portValue === 'random' || portValue.trim() === '') { - setPortErrors(prev => { const n = { ...prev }; delete n[id]; return n; }); - return; - } - const portNum = parseInt(portValue); - if (isNaN(portNum) || portNum < 1 || portNum > 65535) { - setPortErrors(prev => ({ ...prev, [id]: 'Некорректный порт' })); - return; - } - try { - const { data } = await api.get(`/tunnels/check-port/${portNum}`); - if (!data.isFree) { - setPortErrors(prev => ({ ...prev, [id]: 'Порт занят' })); - } else { - setPortErrors(prev => { const n = { ...prev }; delete n[id]; return n; }); - } - } catch (e) { - console.error('Ошибка проверки порта', e); - } - }; - const handleInboundChange = (id: string, field: keyof InboundConfigUI, value: string) => { setInbounds(prev => prev.map(inb => inb.id === id ? { ...inb, [field]: value } : inb)); - if (field === 'port') { + if (field === 'port' || (field === 'type' && value === 'custom')) { setPortErrors(prev => { const n = { ...prev }; delete n[id]; return n; }); } }; const addInbound = () => { if (inbounds.length < 20) { - setInbounds([...inbounds, { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }]); + setInbounds([...inbounds, { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }]); } }; -const removeInbound = (id?: string) => { - if (id) { - if (inbounds.length > 1) { - setInbounds(inbounds.filter(inb => inb.id !== id)); - setPortErrors(prev => { - const n = { ...prev }; - delete n[id]; - return n; - }); - } - } else { - setInbounds([ - { - id: crypto.randomUUID(), - type: 'vless-tcp-reality', - port: 'random', - sni: 'random' + const removeInbound = (id?: string) => { + if (id) { + if (inbounds.length > 1) { + setInbounds(inbounds.filter(inb => inb.id !== id)); + setPortErrors(prev => { + const n = { ...prev }; + delete n[id]; + return n; + }); } - ]); - setPortErrors({}); - } -}; + } else { + setInbounds([ + { + id: crypto.randomUUID(), + type: 'vless-tcp-reality', + port: 'random', + sni: 'random', + link: '' + } + ]); + setPortErrors({}); + } + }; const handleSave = async () => { if (Object.keys(portErrors).length > 0) { @@ -229,11 +211,16 @@ const removeInbound = (id?: string) => { const payload = { name, - inboundsConfig: inbounds.map(i => ({ - type: i.type, - port: i.port === 'random' ? 'random' : parseInt(i.port), - sni: i.sni - })) + inboundsConfig: inbounds.map(i => { + if (i.type === 'custom') { + return { type: i.type, link: i.link }; + } + return { + type: i.type, + port: i.port === 'random' ? 'random' : parseInt(i.port), + sni: i.sni + }; + }) }; try { @@ -421,31 +408,46 @@ const removeInbound = (id?: string) => { - - handleInboundChange(inb.id, 'port', e.target.value)} - onBlur={(e) => handlePortCheck(e.target.value, inb.id)} - error={!!portErrors[inb.id]} - helperText={portErrors[inb.id] || ""} - sx={{ width: 140 }} - /> - + {inb.type === 'custom' ? ( + // Поле для кастомной ссылки + + handleInboundChange(inb.id, 'link', e.target.value)} + fullWidth + /> + + ) : ( + <> + + handleInboundChange(inb.id, 'port', e.target.value)} + error={!!portErrors[inb.id]} + helperText={portErrors[inb.id] || ""} + sx={{ width: 140 }} + /> + - - SNI - - + + SNI + + + + )} (null); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const [authMethod, setAuthMethod] = useState<'password' | 'key'>('password'); const [form, setForm] = useState({ - name: '', ip: '', sshPort: 22, username: 'root', password: '', domain: '' + name: '', ip: '', sshPort: 22, username: 'root', password: '', privateKey: '', domain: '' }); useEffect(() => { loadTunnels(); }, []); @@ -39,9 +44,16 @@ export default function TunnelsPage() { }; const handleCreate = async () => { - await api.post('/tunnels', form); + const payload = { + ...form, + password: authMethod === 'password' ? form.password : null, + privateKey: authMethod === 'key' ? form.privateKey : null, + }; + + await api.post('/tunnels', payload); setOpen(false); - setForm({ name: '', ip: '', sshPort: 22, username: 'root', password: '', domain: '' }); + setForm({ name: '', ip: '', sshPort: 22, username: 'root', password: '', privateKey: '', domain: '' }); + setAuthMethod('password'); loadTunnels(); }; @@ -149,7 +161,28 @@ export default function TunnelsPage() { - + + setAuthMethod(e.target.value as 'password' | 'key')}> + } label="По паролю" /> + } label="По SSH ключу" /> + + + + {authMethod === 'password' ? ( + + ) : ( + + )} diff --git a/install.sh b/install.sh index a65da63..5b1565a 100644 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ set -euo pipefail ################################# PROJECT_DIR="/opt/3dp-manager" DOCKER_USER="denpiligrim" -DOCKER_TAG="main" +DOCKER_TAG="dp-new-release" IMAGE_SERVER="ghcr.io/${DOCKER_USER}/3dp-manager-server:${DOCKER_TAG}" IMAGE_CLIENT="ghcr.io/${DOCKER_USER}/3dp-manager-client:${DOCKER_TAG}" @@ -400,6 +400,63 @@ networks: EOF fi +################################# +# Hysteria 2 +################################# + +# Проверка установки Hysteria 2 через наличие systemd сервиса +if ! systemctl cat hysteria-server.service &> /dev/null; then + echo "Сервис Hysteria 2 не найден. Начинаем установку..." + + # Установка Hysteria 2 согласно документации + bash <(curl -fsSL https://get.hy2.sh/) + + RANDOM_FREE_PORT=$(get_random_port) + + # Генерация надежных паролей + GENERATED_PASSWORD=$(tr -dc A-Za-z0-9 /etc/hysteria/config.yaml < 0) { for (const inbound of sub.inbounds) { - await this.xuiService.deleteInbound(inbound.xuiId); + if (inbound.xuiId && inbound.xuiId > 0) { + await this.xuiService.deleteInbound(inbound.xuiId); + } await this.inboundRepo.delete(inbound.id); } } @@ -119,55 +122,105 @@ export class RotationService implements OnModuleInit { } const usedPorts = new Set(); - - const tasks = [ - () => this.inboundBuilder.buildVlessRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }), - () => this.inboundBuilder.buildVlessRealityXhttp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }), - () => this.inboundBuilder.buildVlessRealityGrpc({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }), - () => this.inboundBuilder.buildVlessWs({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains) }), - () => this.inboundBuilder.buildVlessRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }), - () => this.inboundBuilder.buildVlessRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }), - () => this.inboundBuilder.buildVlessRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }), - () => this.inboundBuilder.buildVmessTcp({ port: 0, uuid: uuidv4() }), - () => this.inboundBuilder.buildShadowsocksTcp({ port: 0, uuid: uuidv4() }), - () => this.inboundBuilder.buildTrojanRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }), - ]; - const host = await this.settingRepo.findOne({ where: { key: 'xui_host' } }); const serverAddress = host?.value || 'localhost'; const flag = await this.settingRepo.findOne({ where: { key: 'xui_geo_flag' } }); const flagEmoji = flag?.value ?? '%F0%9F%92%AF'; - for (const [index, task] of tasks.entries()) { - let config = task(); + // Получаем конфиг или пустой массив + const inboundsConfig = sub.inboundsConfig || []; + for (const config of inboundsConfig) { + const type = config.type; + const uuid = uuidv4(); + + // Определяем SNI + let sni = 'unknown'; + if (type !== 'custom' && type !== 'vmess-tcp' && type !== 'shadowsocks-tcp') { + sni = (!config.sni || config.sni === 'random') ? this.pickDomain(domains) : config.sni; + } + + // === 1. Обработка Custom === + if (type === 'custom') { + const newInbound = this.inboundRepo.create({ + xuiId: 0, // Не привязано к 3x-ui + port: 0, + protocol: 'custom', + remark: 'custom-link', + link: config.link || '', + subscription: sub + }); + await this.inboundRepo.save(newInbound); + continue; + } + + // === 2. Обработка Hysteria2 === + if (type === 'hysteria2-udp') { + const link = this.inboundBuilder.buildHysteria2Link(serverAddress, sni, 'Hysteria2'); + const newInbound = this.inboundRepo.create({ + xuiId: 0, + port: 0, // Обычно Hysteria висит на 443, фактический порт вытаскивается в билдере + protocol: 'hysteria2', + remark: 'hysteria2-udp', + link: link, + subscription: sub + }); + await this.inboundRepo.save(newInbound); + continue; + } + + // === 3. Обработка стандартных инбаундов Xray (3x-ui) === + + // Определяем порт let port = 0; - if (index === 0) port = await this.getFreePort(8443, usedPorts); - else if (index === 1) port = await this.getFreePort(443, usedPorts); - else port = await this.getFreePort(0, usedPorts); - - config.port = port; + if (config.port === 'random' || !config.port) { + port = await this.getFreePort(0, usedPorts); + } else { + // Если передан конкретный порт строкой или числом + port = typeof config.port === 'string' ? parseInt(config.port, 10) : config.port; + } usedPorts.add(port); - const xuiId = await this.xuiService.addInbound(config); + let xuiConfig: any; + + switch (type) { + case 'vless-tcp-reality': + xuiConfig = this.inboundBuilder.buildVlessRealityTcp({ port, uuid, sni, ...keys }); + break; + case 'vless-xhttp-reality': + xuiConfig = this.inboundBuilder.buildVlessRealityXhttp({ port, uuid, sni, ...keys }); + break; + case 'vless-grpc-reality': + xuiConfig = this.inboundBuilder.buildVlessRealityGrpc({ port, uuid, sni, ...keys }); + break; + case 'vless-ws': + xuiConfig = this.inboundBuilder.buildVlessWs({ port, uuid, sni }); + break; + case 'vmess-tcp': + xuiConfig = this.inboundBuilder.buildVmessTcp({ port, uuid }); + break; + case 'shadowsocks-tcp': + xuiConfig = this.inboundBuilder.buildShadowsocksTcp({ port, uuid }); + break; + case 'trojan-tcp-reality': + xuiConfig = this.inboundBuilder.buildTrojanRealityTcp({ port, uuid, sni, ...keys }); + break; + default: + this.logger.warn(`Неизвестный тип инбаунда: ${type}`); + continue; + } + + const xuiId = await this.xuiService.addInbound(xuiConfig); if (xuiId) { - const remarkParts = config.remark.split('-'); - let domainForLink = 'unknown'; - try { - const ss = JSON.parse(config.streamSettings || '{}'); - if (ss.realitySettings?.serverNames?.[0]) domainForLink = ss.realitySettings.serverNames[0]; - else if (ss.wsSettings?.headers?.Host) domainForLink = ss.wsSettings.headers.Host; - else if (ss.tcpSettings?.header?.request?.headers?.Host?.[0]) domainForLink = ss.tcpSettings.header.request.headers.Host[0]; - } catch (e) { } - const idOrPass = config.settings ? JSON.parse(config.settings).clients?.[0]?.id || JSON.parse(config.settings).clients?.[0]?.password : ""; - const fullLink = this.inboundBuilder.buildInboundLink(config, serverAddress, idOrPass, flagEmoji); + const idOrPass = xuiConfig.settings ? JSON.parse(xuiConfig.settings).clients?.[0]?.id || JSON.parse(xuiConfig.settings).clients?.[0]?.password : ""; + const fullLink = this.inboundBuilder.buildInboundLink(xuiConfig, serverAddress, idOrPass, flagEmoji); const newInbound = this.inboundRepo.create({ xuiId: xuiId, port: port, - protocol: config.protocol, - remark: config.remark, + protocol: xuiConfig.protocol, + remark: xuiConfig.remark, link: fullLink, subscription: sub }); @@ -175,6 +228,7 @@ export class RotationService implements OnModuleInit { } } } + private pickDomain(list: Domain[]): string { return list[Math.floor(Math.random() * list.length)].name; } diff --git a/server/src/settings/settings.controller.ts b/server/src/settings/settings.controller.ts index 849380e..199e8bd 100644 --- a/server/src/settings/settings.controller.ts +++ b/server/src/settings/settings.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Post, Body } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Setting } from './entities/setting.entity'; +import * as net from 'net'; import * as dns from 'dns/promises'; import { COUNTRIES } from './countries'; import { XuiService } from 'src/xui/xui.service'; @@ -32,8 +33,14 @@ export class SettingsController { try { const parsed = new URL(settings.xui_url); settings['xui_host'] = parsed.hostname; - - const { address } = await dns.lookup(parsed.hostname); + + let address = ''; + if (net.isIP(parsed.hostname) === 0) { + const result = await dns.lookup(parsed.hostname); + address = result.address; + } else { + address = parsed.hostname; + } settings['xui_ip'] = address; console.log(`Extracted host: ${parsed.hostname} from ${settings.xui_url}`); diff --git a/server/src/subscriptions/dto/create-subscription.dto.ts b/server/src/subscriptions/dto/create-subscription.dto.ts index ccb0f5a..dfb996c 100644 --- a/server/src/subscriptions/dto/create-subscription.dto.ts +++ b/server/src/subscriptions/dto/create-subscription.dto.ts @@ -11,6 +11,10 @@ export class InboundConfigDto { @IsString() @IsOptional() sni?: string | 'random'; + + @IsString() + @IsOptional() + link?: string; } export class CreateSubscriptionDto { diff --git a/server/src/subscriptions/entities/subscription.entity.ts b/server/src/subscriptions/entities/subscription.entity.ts index 737ab06..41a3eea 100644 --- a/server/src/subscriptions/entities/subscription.entity.ts +++ b/server/src/subscriptions/entities/subscription.entity.ts @@ -15,6 +15,9 @@ export class Subscription { @Column({ default: true }) isEnabled: boolean; + @Column({ type: 'simple-json', nullable: true }) + inboundsConfig: any[]; + @OneToMany(() => Inbound, (inbound) => inbound.subscription) inbounds: Inbound[]; diff --git a/server/src/subscriptions/subscriptions.controller.ts b/server/src/subscriptions/subscriptions.controller.ts index 812600f..c122a02 100644 --- a/server/src/subscriptions/subscriptions.controller.ts +++ b/server/src/subscriptions/subscriptions.controller.ts @@ -1,5 +1,6 @@ -import { Controller, Get, Post, Delete, Body, Param } from '@nestjs/common'; +import { Controller, Get, Post, Delete, Body, Param, Put } from '@nestjs/common'; import { SubscriptionsService } from './subscriptions.service'; +import { CreateSubscriptionDto } from './dto/create-subscription.dto'; @Controller('subscriptions') export class SubscriptionsController { @@ -11,8 +12,13 @@ export class SubscriptionsController { } @Post() - create(@Body('name') name: string) { - return this.subscriptionsService.create(name); + create(@Body() createSubscriptionDto: CreateSubscriptionDto) { + return this.subscriptionsService.create(createSubscriptionDto); + } + + @Put(':id') + update(@Param('id') id: string, @Body() updateSubscriptionDto: CreateSubscriptionDto) { + return this.subscriptionsService.update(id, updateSubscriptionDto); } @Delete(':id') diff --git a/server/src/subscriptions/subscriptions.service.ts b/server/src/subscriptions/subscriptions.service.ts index 0c1528a..fa7a8fe 100644 --- a/server/src/subscriptions/subscriptions.service.ts +++ b/server/src/subscriptions/subscriptions.service.ts @@ -1,9 +1,9 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Subscription } from './entities/subscription.entity'; -import { Inbound } from '../inbounds/entities/inbound.entity'; import { XuiService } from '../xui/xui.service'; +import { CreateSubscriptionDto } from './dto/create-subscription.dto'; import { v4 as uuidv4 } from 'uuid'; @Injectable() @@ -18,11 +18,32 @@ export class SubscriptionsService { return this.subRepo.find({ relations: ['inbounds'], order: { createdAt: 'DESC' } }); } - async create(name: string) { + async create(dto: CreateSubscriptionDto) { const sub = this.subRepo.create({ - name, + name: dto.name, uuid: uuidv4(), + inboundsConfig: dto.inboundsConfig || [], }); + + return this.subRepo.save(sub); + } + + async update(id: string, dto: CreateSubscriptionDto) { + const sub = await this.subRepo.findOne({ + where: { id }, + relations: ['inbounds'] + }); + + if (!sub) { + throw new NotFoundException(`Subscription with ID ${id} not found`); + } + + sub.name = dto.name; + + if (dto.inboundsConfig) { + sub.inboundsConfig = dto.inboundsConfig; + } + return this.subRepo.save(sub); } @@ -30,7 +51,7 @@ export class SubscriptionsService { const sub = await this.subRepo.findOne({ where: { id }, relations: ['inbounds'] }); if (!sub) return; - if (sub.inbounds) { + if (sub.inbounds && sub.inbounds.length > 0) { for (const inbound of sub.inbounds) { await this.xuiService.deleteInbound(inbound.xuiId); } diff --git a/server/src/tunnels/entities/tunnel.entity.ts b/server/src/tunnels/entities/tunnel.entity.ts index c8a1821..53e61e4 100644 --- a/server/src/tunnels/entities/tunnel.entity.ts +++ b/server/src/tunnels/entities/tunnel.entity.ts @@ -17,8 +17,11 @@ export class Tunnel { @Column() username: string; - @Column({ select: false }) - password: string; + @Column({ select: false, nullable: true }) + password?: string; + + @Column({ type: 'text', select: false, nullable: true }) + privateKey?: string; @Column({ nullable: true }) domain: string; diff --git a/server/src/tunnels/ssh.service.ts b/server/src/tunnels/ssh.service.ts index ec677c8..2240a3f 100644 --- a/server/src/tunnels/ssh.service.ts +++ b/server/src/tunnels/ssh.service.ts @@ -6,7 +6,7 @@ export class SshService { private readonly logger = new Logger(SshService.name); async executeCommand( - config: { host: string; port: number; username: string; password?: string }, + config: { host: string; port: number; username: string; password?: string, privateKey?: string }, command: string ): Promise { return new Promise((resolve, reject) => { @@ -42,6 +42,7 @@ export class SshService { port: config.port, username: config.username, password: config.password, + privateKey: config.privateKey, readyTimeout: 20000, }); }); diff --git a/server/src/tunnels/tunnels.service.ts b/server/src/tunnels/tunnels.service.ts index feadc2a..fed287e 100644 --- a/server/src/tunnels/tunnels.service.ts +++ b/server/src/tunnels/tunnels.service.ts @@ -31,6 +31,7 @@ export class TunnelsService { async installScript(id: number) { const tunnel = await this.tunnelRepo.createQueryBuilder('tunnel') .addSelect('tunnel.password') + .addSelect('tunnel.privateKey') .where('tunnel.id = :id', { id }) .getOne(); @@ -55,7 +56,8 @@ export class TunnelsService { host: tunnel.ip, port: tunnel.sshPort, username: tunnel.username, - password: tunnel.password + password: tunnel.password, + privateKey: tunnel.privateKey }, command); this.logger.log(`Скрипт выполнен успешно:\n${output}`);