using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
namespace JJServiceManager.Remote
{
/// 비밀번호 처리
public static class RemoteSecurity
{
private const int Iterations = 120_000;
/// 대상 PC: 접속 비밀번호를 해시로 저장
public static (string hash, string salt) HashPassword(string password)
{
var salt = RandomNumberGenerator.GetBytes(16);
var hash = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, Iterations, HashAlgorithmName.SHA256, 32);
return (Convert.ToBase64String(hash), Convert.ToBase64String(salt));
}
public static bool Verify(string password, string hashB64, string saltB64)
{
if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(hashB64) || string.IsNullOrEmpty(saltB64)) return false;
try
{
var salt = Convert.FromBase64String(saltB64);
var expected = Convert.FromBase64String(hashB64);
var actual = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, Iterations, HashAlgorithmName.SHA256, expected.Length);
return CryptographicOperations.FixedTimeEquals(actual, expected);
}
catch
{
return false;
}
}
/// 관리 PC: 대상 PC 비밀번호를 현재 Windows 사용자로 암호화해 저장
public static string Protect(string password)
{
if (string.IsNullOrEmpty(password)) return "";
var data = ProtectedData.Protect(Encoding.UTF8.GetBytes(password), null, DataProtectionScope.CurrentUser);
return Convert.ToBase64String(data);
}
public static string Unprotect(string protectedB64)
{
if (string.IsNullOrEmpty(protectedB64)) return "";
try
{
var data = ProtectedData.Unprotect(Convert.FromBase64String(protectedB64), null, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(data);
}
catch
{
return "";
}
}
}
///
/// 한 줄에 JSON 하나씩 주고받는 연결 (SSH 채널 / TLS 스트림 공통)
///
public sealed class JsonLineConnection : IDisposable
{
private const int MaxLineChars = 16 * 1024 * 1024;
private readonly Stream _stream;
private readonly StreamReader _reader;
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
private readonly IDisposable[] _owned;
private int _disposed;
public JsonLineConnection(Stream stream, params IDisposable[] owned)
{
_stream = stream;
_reader = new StreamReader(stream, new UTF8Encoding(false), false, 16 * 1024, leaveOpen: true);
_owned = owned ?? Array.Empty();
}
public bool IsDisposed => Volatile.Read(ref _disposed) == 1;
private long _bytesSent, _charsReceived;
/// 보낸 바이트 수 (연결 기록 통계용)
public long BytesSent => Interlocked.Read(ref _bytesSent);
/// 받은 양 (문자 수 ≈ 바이트, 연결 기록 통계용)
public long BytesReceived => Interlocked.Read(ref _charsReceived);
private readonly char[] _buf = new char[64 * 1024];
private int _bufPos, _bufLen;
private readonly StringBuilder _line = new StringBuilder();
/// 다음 메시지. 연결이 끊기면 null (한 번에 한 곳에서만 호출)
public async Task ReadAsync(CancellationToken ct)
{
_line.Clear();
while (true)
{
if (_bufPos >= _bufLen)
{
_bufLen = await _reader.ReadAsync(_buf.AsMemory(), ct).ConfigureAwait(false);
_bufPos = 0;
if (_bufLen <= 0) return null;
Interlocked.Add(ref _charsReceived, _bufLen);
}
int idx = Array.IndexOf(_buf, '\n', _bufPos, _bufLen - _bufPos);
if (idx < 0)
{
_line.Append(_buf, _bufPos, _bufLen - _bufPos);
_bufPos = _bufLen;
if (_line.Length > MaxLineChars) throw new IOException("메시지가 너무 큽니다.");
continue;
}
_line.Append(_buf, _bufPos, idx - _bufPos);
_bufPos = idx + 1;
var text = _line.ToString().Trim();
_line.Clear();
if (text.Length == 0) continue;
try
{
return JsonNode.Parse(text) as JsonObject ?? new JsonObject();
}
catch (JsonException)
{
throw new IOException("잘못된 메시지를 받았습니다.");
}
}
}
public async Task SendAsync(JsonObject msg, CancellationToken ct = default)
{
var bytes = Encoding.UTF8.GetBytes(msg.ToJsonString() + "\n");
await _writeLock.WaitAsync(ct).ConfigureAwait(false);
try
{
await _stream.WriteAsync(bytes, ct).ConfigureAwait(false);
await _stream.FlushAsync(ct).ConfigureAwait(false);
Interlocked.Add(ref _bytesSent, bytes.Length);
}
finally
{
_writeLock.Release();
}
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 1) return;
try { _reader.Dispose(); } catch { }
try { _stream.Dispose(); } catch { }
foreach (var d in _owned)
{
try { d?.Dispose(); } catch { }
}
}
}
/// 원격으로 받은 서비스 정보
public sealed class RemoteServiceInfo
{
public string ServiceName { get; set; } = "";
public string DisplayName { get; set; } = "";
/// System.ServiceProcess.ServiceControllerStatus 값, 서비스가 없으면 null
public int? Status { get; set; }
public string LogPath { get; set; } = "";
public bool LogIsFolder { get; set; }
}
/// 원격 명령 실패
public sealed class RemoteException : Exception
{
/// 오류가 아닌 안내 (이미 실행 중 등)
public bool IsInfo { get; }
public RemoteException(string message, bool isInfo = false) : base(message) { IsInfo = isInfo; }
}
public static class NetInfo
{
/// 이 PC의 IPv4 주소 목록 (설정 화면 안내용)
public static List LocalIPv4()
{
var list = new List();
try
{
foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
{
if (ni.OperationalStatus != OperationalStatus.Up) continue;
if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue;
foreach (var ua in ni.GetIPProperties().UnicastAddresses)
if (ua.Address.AddressFamily == AddressFamily.InterNetwork)
list.Add(ua.Address.ToString());
}
}
catch { }
return list.Distinct().ToList();
}
}
///
/// Windows 방화벽 규칙
/// - "JJServiceManager Remote" : 대상 PC의 Active 수신 포트
/// - "JJServiceManager Passive" : 관리 PC의 Passive 수신 포트
/// (+ 테스트 인스턴스 꼬리표)
///
public static class FirewallManager
{
public const string ActiveRule = "JJServiceManager Remote";
public const string PassiveRule = "JJServiceManager Passive";
/// enabled면 해당 포트를 열고, 아니면 규칙을 지움
public static (bool ok, string message) Apply(string rule, bool enabled, int port)
{
var name = rule + AppInstance.Suffix;
// 기존 규칙은 지우고 다시 만듦 (포트가 바뀌었을 수 있음)
ConsoleRunner.Run("netsh.exe", $"advfirewall firewall delete rule name=\"{name}\"");
if (!enabled) return (true, $"방화벽 규칙 삭제 ({name})");
var r = ConsoleRunner.Run("netsh.exe", $"advfirewall firewall add rule name=\"{name}\" dir=in action=allow protocol=TCP localport={port} profile=any");
return r.exitCode == 0
? (true, $"방화벽 TCP {port} 허용 ({name})")
: (false, $"방화벽 규칙 추가 실패 ({name}): {r.output}");
}
}
public static class Json
{
public static string Str(JsonObject o, string key)
{
try { return o?[key]?.GetValue() ?? ""; }
catch { return o?[key]?.ToString() ?? ""; }
}
public static int Int(JsonObject o, string key, int def = 0)
{
try { return o?[key]?.GetValue() ?? def; }
catch { return def; }
}
public static bool Bool(JsonObject o, string key)
{
try { return o?[key]?.GetValue() ?? false; }
catch { return false; }
}
}
}