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