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 = 4 * 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; /// 다음 메시지. 연결이 끊기면 null public async Task ReadAsync(CancellationToken ct) { var sb = new StringBuilder(); var buf = new char[1]; while (true) { int n = await _reader.ReadAsync(buf.AsMemory(0, 1), ct).ConfigureAwait(false); if (n == 0) return null; char c = buf[0]; if (c == '\n') { var line = sb.ToString().Trim(); sb.Clear(); if (line.Length == 0) continue; try { return JsonNode.Parse(line) as JsonObject ?? new JsonObject(); } catch (JsonException) { throw new IOException("잘못된 메시지를 받았습니다."); } } sb.Append(c); if (sb.Length > MaxLineChars) 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); } 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" (+ 테스트 인스턴스 꼬리표) /// public static class FirewallManager { private static string RuleName => "JJServiceManager Remote" + AppInstance.Suffix; /// enabled면 해당 포트를 열고, 아니면 규칙을 지움 public static (bool ok, string message) Apply(bool enabled, int port) { // 기존 규칙은 지우고 다시 만듦 (포트가 바뀌었을 수 있음) Run($"advfirewall firewall delete rule name=\"{RuleName}\""); if (!enabled) return (true, "방화벽 규칙 삭제"); var r = Run($"advfirewall firewall add rule name=\"{RuleName}\" dir=in action=allow protocol=TCP localport={port} profile=any"); return r.exit == 0 ? (true, $"방화벽 TCP {port} 허용") : (false, "방화벽 규칙 추가 실패: " + r.output); } private static (int exit, string output) Run(string args) { try { var psi = new ProcessStartInfo("netsh.exe", args) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = OemEncoding(), StandardErrorEncoding = OemEncoding() }; using var p = Process.Start(psi); var o = p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd(); p.WaitForExit(15000); return (p.ExitCode, o.Trim()); } catch (Exception ex) { return (-1, ex.Message); } } private static Encoding OemEncoding() { try { return Encoding.GetEncoding(System.Globalization.CultureInfo.CurrentCulture.TextInfo.OEMCodePage); } catch { return Encoding.UTF8; } } } 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; } } } }