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 Until(Func 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 Main() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); const string url = "http://127.0.0.1:21900"; var log = new List(); Action 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 { 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(); 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); } 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() > 1000, "중계 트래픽 기록", relayRow?.ToJsonString()); var directRow = conns.FirstOrDefault(x => Json.Str(x, "mode") == "direct"); Check(directRow != null && directRow["bytesFromDevice"].GetValue() > 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; } } }