176 lines
13 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|