JJServiceManagerEnt/tests/RelayIntegration/Main.cs

176 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using JJServiceManager.Remote;
namespace JJServiceManager
{
static class M
{
static int pass, fail;
static void Check(bool ok, string name, string extra = "")
{
if (ok) { pass++; Console.WriteLine($" [OK] {name}"); }
else { fail++; Console.WriteLine($" [FAIL] {name} {extra}"); }
}
static async Task<bool> Until(Func<bool> cond, int seconds = 10)
{
for (int i = 0; i < seconds * 10; i++) { if (cond()) return true; await Task.Delay(100); }
return cond();
}
static async Task<int> Main()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
const string url = "http://127.0.0.1:21900";
var log = new List<string>();
Action<string, LogLevel> L(string who) => (m, l) => { lock (log) log.Add($"[{who} {l}] {m}"); };
Console.WriteLine("== 1. 관리자 첫 로그인 + 비밀번호 변경 (설정 화면 흐름)");
var (tok0, must, isAdmin) = await RelayManager.LoginAsync(url, "admin", "firstpass1", CancellationToken.None);
Check(must && isAdmin && tok0.Length > 10, "첫 로그인: 관리자, 비밀번호 변경 필요");
await RelayManager.ChangePasswordAsync(url, tok0, "firstpass1", "AdminPass#2026", CancellationToken.None);
var (adminToken, must2, _) = await RelayManager.LoginAsync(url, "admin", "AdminPass#2026", CancellationToken.None);
Check(!must2, "새 비밀번호로 로그인");
var admin = new RelayManager(L("admin"));
string savedToken = adminToken;
admin.Start(url, "admin", () => savedToken, () => "", t => savedToken = t);
Check(await admin.WaitReadyAsync(TimeSpan.FromSeconds(10), CancellationToken.None) && admin.IsAdmin, "관리 PC 상시 연결 (토큰)", admin.StatusText);
Console.WriteLine("== 2. 대상 PC 등록 + 온라인");
var code = Json.Str(await admin.RequestAsync("admin.codes.create", new JsonObject { ["note"] = "통합 시험" }), "code");
var services = new List<ServiceEntry> { new ServiceEntry { ServiceName = "Spooler", DisplayName = "Print Spooler" } };
bool allowView = true, allowControl = true;
var srv = new RemoteServer(() => services.ToList(), L("target"), () => { }, () => new ScreenPolicy(allowView, allowView && allowControl, false),
(w, c) => Task.FromResult(true));
var (h, s) = RemoteSecurity.HashPassword("target-pw");
srv.Start(RemoteMode.Port, 19988, h, s);
JsonObject activeInfo = new JsonObject { ["mode"] = "Port", ["port"] = 19988 };
var enr = await RelayAgent.EnrollAsync(url, code, activeInfo, CancellationToken.None);
Check(enr.DeviceId.Length > 0 && enr.DeviceKey.Length > 0 && enr.ServerPublicKey.Length > 0, "등록 코드로 등록 → 기기 ID·키·서버 공개키");
try { await RelayAgent.EnrollAsync(url, code, null, CancellationToken.None); Check(false, "같은 코드 재사용 거부"); }
catch (RemoteException ex) { Check(ex.Message.Contains("이미"), "같은 코드 재사용 거부", ex.Message); }
var agent = new RelayAgent(srv, L("agent"));
agent.Start(url, enr.DeviceId, enr.DeviceKey, enr.ServerPublicKey, () => activeInfo?.DeepClone() as JsonObject);
Check(await Until(() => agent.IsOnline), "대상 PC 중계 서버 온라인", agent.StatusText);
Check(await Until(() => admin.IsDeviceOnline(enr.DeviceId)), "관리 PC 기기 목록에 온라인으로 보임");
Console.WriteLine("== 3. 직접 연결 (대상 PC Active 수신 중)");
var target = new RemoteTarget { Id = "relay-" + enr.DeviceId, Direction = ConnectDirection.Relay, RelayDeviceId = enr.DeviceId };
var c1 = new RemoteClient(target, () => "target-pw", admin);
c1.Start();
Check(await Until(() => c1.State == RemoteState.Connected, 15), "연결됨", c1.StateMessage);
Check(c1.RelayMode == "direct", "직접 연결로 연결", c1.RelayMode);
await Until(() => c1.Services.Count > 0);
Check(c1.Services.Any(x => x.ServiceName == "Spooler"), "서비스 목록 받음");
try { await c1.StartServiceAsync("Spooler"); } catch { /* Linux에는 서비스가 없음 → 실패, 감사 로그엔 남음 */ }
c1.Dispose();
await Task.Delay(800);
Console.WriteLine("== 4. 중계 연결 (대상 PC Active 수신 끔)");
srv.Stop();
activeInfo = null;
agent.SendUpdate();
await Task.Delay(500);
var c2 = new RemoteClient(target, () => "target-pw", admin);
int frames = 0;
c2.ScreenEvent += e => { if (Json.Str(e, "event") == "frame") Interlocked.Increment(ref frames); };
c2.Start();
Check(await Until(() => c2.State == RemoteState.Connected, 20), "연결됨", c2.StateMessage);
Check(c2.RelayMode == "relay", "중계로 연결", c2.RelayMode);
await Until(() => c2.Services.Count > 0);
Check(c2.Services.Count == 1, "중계로 서비스 목록 받음");
var st = await c2.RequestAsync("screenStart", new JsonObject { ["monitor"] = 1 }, TimeSpan.FromSeconds(10));
await Task.Delay(1500);
Check(frames > 0, $"중계로 원격 화면 프레임 받음 ({frames}장)");
c2.SendNoReply(new JsonObject { ["op"] = "input", ["kind"] = "key", ["vk"] = 65, ["down"] = true });
await Task.Delay(400);
Check(InputInjector.Got.Any(x => x.StartsWith("key 65")), "중계로 키 입력 전달 (관리자 = 화면 제어 권한)");
await c2.RequestAsync("screenStop", null, TimeSpan.FromSeconds(5));
c2.Dispose();
await Task.Delay(800);
Console.WriteLine("== 5. 일반 사용자 권한 (보기만) → 티켓으로 대상 PC가 거부");
await admin.RequestAsync("admin.users.create", new JsonObject { ["username"] = "bob", ["password"] = "BobPass#123", ["mustChangePassword"] = false });
var users = (await admin.RequestAsync("admin.users.list"))["users"].AsArray();
long bobId = users.First(u => Json.Str((JsonObject)u, "username") == "bob")["id"].GetValue<long>();
await admin.RequestAsync("admin.perms.set", new JsonObject { ["userId"] = bobId, ["deviceId"] = enr.DeviceId, ["view"] = true });
var (bobTok, _, _) = await RelayManager.LoginAsync(url, "bob", "BobPass#123", CancellationToken.None);
var bob = new RelayManager(L("bob"));
bob.Start(url, "bob", () => bobTok, () => "", t => bobTok = t);
Check(await bob.WaitReadyAsync(TimeSpan.FromSeconds(10), CancellationToken.None) && !bob.IsAdmin, "bob 로그인 (일반 사용자)");
Check(bob.Devices.Count == 1 && bob.Devices[0].View && !bob.Devices[0].Control, "bob 기기 목록: 보기 권한만");
var c3 = new RemoteClient(target, () => "target-pw", bob);
c3.Start();
Check(await Until(() => c3.State == RemoteState.Connected, 20), "bob 연결됨 (보기)", c3.StateMessage);
await Until(() => c3.Services.Count > 0);
Check(c3.Services.Count == 1, "bob 서비스 목록 보기 가능");
Check(!c3.AllowView, "bob에게는 화면 보기 허용 안 됨 (서버 권한 ∩ 대상 PC 설정)");
try { await c3.StopServiceAsync("Spooler"); Check(false, "bob Stop 거부"); }
catch (RemoteException ex) { Check(ex.Message.Contains("서비스 제어"), "bob Stop 거부 (대상 PC가 티켓 권한 확인)", ex.Message); }
try { await c3.RequestAsync("screenStart", new JsonObject { ["monitor"] = 1 }, TimeSpan.FromSeconds(5)); Check(false, "bob 화면 거부"); }
catch (RemoteException ex) { Check(ex.Message.Contains("화면 보기"), "bob 원격 화면 거부", ex.Message); }
Console.WriteLine("== 5-1. 연결 중에 권한을 바꾸면 → 대상 PC가 옛 티켓 연결을 바로 끊고, 새 권한으로 다시 연결");
bool sawDrop = false;
c3.StateChanged += c => { if (c.State != RemoteState.Connected) sawDrop = true; };
await admin.RequestAsync("admin.perms.set", new JsonObject { ["userId"] = bobId, ["deviceId"] = enr.DeviceId, ["view"] = true, ["control"] = true });
Check(await Until(() => sawDrop, 10), "권한 변경 → 기존 연결 끊김");
Check(await Until(() => bob.Devices.Count == 1 && bob.Devices[0].Control, 10), "bob 기기 목록에 새 권한 반영");
c3.Nudge(); // 화면(MainWindow)은 권한이 바뀌면 Nudge → 바로 다시 연결
Check(await Until(() => c3.State == RemoteState.Connected && c3.RelayPerms?.control == true, 20), "새 티켓으로 다시 연결 (서비스 제어 권한)", c3.StateMessage);
try { await c3.StopServiceAsync("Spooler"); Check(true, "bob Stop 이제 허용 (리눅스라 실행 자체는 실패해도 됨)"); }
catch (RemoteException ex) { Check(!ex.Message.Contains("서비스 제어"), "bob Stop 이제 허용", ex.Message); }
sawDrop = false;
await admin.RequestAsync("admin.users.update", new JsonObject { ["userId"] = bobId, ["enabled"] = false });
Check(await Until(() => sawDrop, 10), "계정 사용 안 함 → 대상 PC 연결도 바로 끊김");
c3.Dispose();
Console.WriteLine("== 6. 대상 PC 접속 비밀번호가 틀리면 (서버 권한이 있어도) 거부");
var c4 = new RemoteClient(target, () => "wrong-pw", admin);
c4.Start();
Check(await Until(() => c4.State == RemoteState.AuthFailed, 20), "인증 실패", c4.StateMessage);
c4.Dispose();
Console.WriteLine("== 7. 감사 로그 / 연결 기록 (대상 PC가 보고)");
await Task.Delay(1500);
var audit = (await admin.RequestAsync("admin.audit.query", new JsonObject { ["deviceId"] = enr.DeviceId, ["limit"] = 100 }))["rows"].AsArray()
.Select(x => (JsonObject)x).ToList();
string Row(JsonObject r) => $"{Json.Str(r, "username")}:{Json.Str(r, "action")}:{Json.Str(r, "target")}:{Json.Str(r, "result")}";
var rows = audit.Select(Row).ToList();
Check(rows.Any(r => r.StartsWith("admin:start:Spooler:")), "admin Start 기록 (대상 PC 보고)", string.Join(" | ", rows));
Check(rows.Contains("admin:screenStart::ok") && rows.Contains("admin:screenEnd::ok"), "원격 화면 시작·끝 기록");
Check(rows.Contains("bob:stop:Spooler:denied") && rows.Contains("bob:screenStart::denied"), "bob 거부 기록");
Check(rows.Contains("admin:authFail::denied"), "대상 PC 비밀번호 틀림 기록");
var conns = (await admin.RequestAsync("admin.connections.query", new JsonObject { ["deviceId"] = enr.DeviceId }))["rows"].AsArray()
.Select(x => (JsonObject)x).ToList();
var modes = conns.Select(x => Json.Str(x, "mode")).ToList();
Check(modes.Contains("direct") && modes.Contains("relay"), "연결 기록: 직접 / 중계", string.Join(",", modes));
var relayRow = conns.FirstOrDefault(x => Json.Str(x, "mode") == "relay" && Json.Str(x, "username") == "admin");
Check(relayRow != null && relayRow["bytesFromDevice"].GetValue<long>() > 1000, "중계 트래픽 기록", relayRow?.ToJsonString());
var directRow = conns.FirstOrDefault(x => Json.Str(x, "mode") == "direct");
Check(directRow != null && directRow["bytesFromDevice"].GetValue<long>() > 0 && Json.Str(directRow, "endedAt").Length > 0, "직접 연결 끝·트래픽 보고", directRow?.ToJsonString());
Console.WriteLine("== 8. 기기 사용 안 함 → 대상 PC에 알림");
string revoked = null;
agent.Revoked += r => revoked = r;
await admin.RequestAsync("admin.devices.disable", new JsonObject { ["deviceId"] = enr.DeviceId });
Check(await Until(() => revoked != null), "대상 PC가 사용 안 함 알림 받음", revoked);
Check(await Until(() => !admin.IsDeviceOnline(enr.DeviceId)), "관리 PC에서 오프라인");
agent.Dispose(); admin.Dispose(); bob.Dispose(); srv.Dispose();
Console.WriteLine($"\n결과: 통과 {pass} / 실패 {fail}");
if (fail > 0) { Console.WriteLine("--- 로그"); lock (log) foreach (var l in log) Console.WriteLine(l); }
return fail == 0 ? 0 : 1;
}
}
}