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; } } } }