From d86aaa1fd48b2927fd878c5b715fdb109b8f9d2a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 11:07:00 +0000 Subject: [PATCH] =?UTF-8?q?=EC=A4=91=EA=B3=84:=20=EA=B6=8C=ED=95=9C=C2=B7?= =?UTF-8?q?=EA=B3=84=EC=A0=95=EC=9D=B4=20=EB=B0=94=EB=80=8C=EB=A9=B4=20?= =?UTF-8?q?=EC=97=B4=EB=A6=B0=20=EC=97=B0=EA=B2=B0=EC=9D=84=20=EB=B0=94?= =?UTF-8?q?=EB=A1=9C=20=EB=81=8A=EA=B3=A0=20=EC=83=88=20=ED=8B=B0=EC=BC=93?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 대상 PC: 서버의 revokeUser 알림 → 그 사용자의 티켓 연결을 즉시 닫음 - 관리 PC: 기기 권한이 바뀌면 바로 다시 연결 (새 권한 티켓) - 서버 권한이 없어 원격 화면이 막히면 그 이유를 알림 - 통합 점검 37개 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013U497kUNL8ArhV7jkzCpKa --- Core/Remote/Relay.cs | 3 +++ Core/Remote/RemoteClient.cs | 6 ++++++ Core/Remote/RemoteServer.cs | 21 +++++++++++++++++++++ Windows/MainWindow.cs | 16 ++++++++++++++-- tests/RelayIntegration/Main.cs | 15 +++++++++++++++ tests/RelayIntegration/README.md | 2 +- 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/Core/Remote/Relay.cs b/Core/Remote/Relay.cs index 8c7d521..95e80f5 100644 --- a/Core/Remote/Relay.cs +++ b/Core/Remote/Relay.cs @@ -622,6 +622,9 @@ namespace JJServiceManager.Remote case "replaced": _log("중계 서버: " + Json.Str(e, "reason"), LogLevel.Warning); break; + case "revokeUser": + _server.DropTicketUser(e["userId"]?.GetValue() ?? 0); + break; } } diff --git a/Core/Remote/RemoteClient.cs b/Core/Remote/RemoteClient.cs index 8c486a0..a7f67ce 100644 --- a/Core/Remote/RemoteClient.cs +++ b/Core/Remote/RemoteClient.cs @@ -46,6 +46,9 @@ namespace JJServiceManager.Remote public bool IsPassive => _target.Direction == ConnectDirection.Passive; public bool IsRelay => _target.Direction == ConnectDirection.Relay; + /// Relay: 중계 서버가 준 이 기기 권한 (연결 hello에서 받음, 없으면 null) + public (bool view, bool control, bool screenView, bool screenControl)? RelayPerms { get; private set; } + /// Relay: 실제 연결 방식 ("direct" / "relay", 연결 전에는 빈 값) public string RelayMode { get; private set; } = ""; @@ -151,6 +154,9 @@ namespace JJServiceManager.Remote } AllowView = Json.Bool(hello, "allowView"); AllowControl = Json.Bool(hello, "allowControl"); + RelayPerms = hello["perms"] is JsonObject rp + ? (Json.Bool(rp, "view"), Json.Bool(rp, "control"), Json.Bool(rp, "screenView"), Json.Bool(rp, "screenControl")) + : null; SetState(RemoteState.Connected, "연결됨"); var reader = Task.Run(() => ReadLoopAsync(conn, ct)); diff --git a/Core/Remote/RemoteServer.cs b/Core/Remote/RemoteServer.cs index 28a33d7..f749148 100644 --- a/Core/Remote/RemoteServer.cs +++ b/Core/Remote/RemoteServer.cs @@ -47,6 +47,8 @@ namespace JJServiceManager.Remote // 화면 보기 중인 세션 private readonly ConcurrentDictionary _screens = new ConcurrentDictionary(); + /// 중계 서버 권한 티켓으로 열린 연결 (권한이 바뀌면 바로 끊기 위해) + private readonly ConcurrentDictionary _ticketConns = new ConcurrentDictionary(); private int _nextScreenId; public bool IsRunning => _listener != null; @@ -247,6 +249,23 @@ namespace JJServiceManager.Remote lock (_failures) _failures.Enqueue(DateTime.UtcNow); } + /// 중계 서버가 이 사용자의 권한·계정을 바꿈 → 옛 티켓으로 열린 연결을 바로 끊음 (관리 PC는 새 티켓으로 다시 연결) + public int DropTicketUser(long userId) + { + int n = 0; + foreach (var kv in _ticketConns) + { + if (kv.Value.UserId != userId) continue; + if (_ticketConns.TryRemove(kv.Key, out _)) + { + n++; + try { kv.Key.Dispose(); } catch { } + } + } + if (n > 0) _log($"중계 서버에서 사용자 권한이 바뀌어 원격 연결 {n}개를 끊었습니다.", LogLevel.Notice); + return n; + } + // ───────────────────────── 요청 처리 ───────────────────────── /// 중계 통로로 들어온 연결 (권한 티켓 필수, 세션 ID 일치) @@ -314,6 +333,7 @@ namespace JJServiceManager.Remote authed = true; } _log($"원격 접속: {who}", LogLevel.Notice); + if (ticket != null) _ticketConns[conn] = ticket; var pol = Policy(ticket); var helloResult = new JsonObject { @@ -564,6 +584,7 @@ namespace JJServiceManager.Remote foreach (var t in streams.Values) t.Dispose(); streams.Clear(); } + _ticketConns.TryRemove(conn, out _); var s = Interlocked.Exchange(ref screen, null); EndScreen(s); conn.Dispose(); diff --git a/Windows/MainWindow.cs b/Windows/MainWindow.cs index faf6a66..09403fb 100644 --- a/Windows/MainWindow.cs +++ b/Windows/MainWindow.cs @@ -600,7 +600,11 @@ namespace JJServiceManager } if (!g.Client.AllowView) { - Dialogs.Info(this, $"{g.Title} 에서 화면 보기를 허용하지 않았습니다.\n대상 PC 설정 → 원격 접속 허용 → 원격관리에서 '화면 보기 허용'을 켜야 합니다."); + // 중계 서버 권한이 없어서인지, 대상 PC 설정이 꺼져 있어서인지 구분 + if (g.Client.RelayPerms is { } rp && !rp.screenView) + Dialogs.Info(this, $"{g.Title} 의 [화면 보기] 권한이 중계 서버에서 주어지지 않았습니다.\n중계 서버 관리자에게 [서버 관리 → 권한]에서 '화면 보기'를 요청하세요."); + else + Dialogs.Info(this, $"{g.Title} 에서 화면 보기를 허용하지 않았습니다.\n대상 PC 설정 → 원격 접속 허용 → 원격관리에서 '화면 보기 허용'을 켜야 합니다."); return; } @@ -949,6 +953,9 @@ namespace JJServiceManager } /// 중계 서버의 기기 목록 → 초기화면 그룹 (권한 있는 기기만) + private readonly Dictionary _relayPermSig = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static string PermSig(RelayDevice d) => $"{d.View}{d.Control}{d.ScreenView}{d.ScreenControl}"; + private void SyncRelayGroups() { bool active = _settings.RemoteManageEnabled && _settings.RelayManageEnabled && _relayManageKey != "off" && _relayManageKey != ""; @@ -963,6 +970,7 @@ namespace JJServiceManager if (active && devices.Count == 0 && !_relayManager.IsLoggedIn) continue; RemoveRemoteGroup(g); _relayTargets.Remove(g.Id); + _relayPermSig.Remove(g.Id); if (_hostTabs.ContainsKey(g.Id)) CloseHostTab(g.Id, save: false); } @@ -980,7 +988,10 @@ namespace JJServiceManager existing.Title = t0.Label; existing.TitleChanged(); } - if (d.Online && existing.Client?.State != RemoteState.Connected) existing.Client?.Nudge(); + // 권한이 바뀌었으면: 대상 PC가 옛 티켓 연결을 끊음 → 기다리지 않고 새 티켓으로 바로 다시 연결 + bool permChanged = _relayPermSig.TryGetValue(gid, out var sig0) && sig0 != PermSig(d); + _relayPermSig[gid] = PermSig(d); + if (d.Online && (permChanged || existing.Client?.State != RemoteState.Connected)) existing.Client?.Nudge(); continue; } @@ -993,6 +1004,7 @@ namespace JJServiceManager Hostname = d.Hostname }; _relayTargets[gid] = t; + _relayPermSig[gid] = PermSig(d); var deviceId = d.Id; var client = new RemoteClient(t, () => _settings.RelayDevicePasswords.TryGetValue(deviceId, out var p) ? RemoteSecurity.Unprotect(p) : "", _relayManager); diff --git a/tests/RelayIntegration/Main.cs b/tests/RelayIntegration/Main.cs index 15a851a..27d6709 100644 --- a/tests/RelayIntegration/Main.cs +++ b/tests/RelayIntegration/Main.cs @@ -117,6 +117,21 @@ namespace JJServiceManager 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 접속 비밀번호가 틀리면 (서버 권한이 있어도) 거부"); diff --git a/tests/RelayIntegration/README.md b/tests/RelayIntegration/README.md index 040d369..4096ebc 100644 --- a/tests/RelayIntegration/README.md +++ b/tests/RelayIntegration/README.md @@ -1,6 +1,6 @@ # RelayIntegration -중계 서버(JJServiceManagerRelay)와 이 프로그램의 원격 코어(대상 PC·관리 PC)를 함께 띄워 확인하는 통합 점검 (32개 항목, Linux에서도 실행). +중계 서버(JJServiceManagerRelay)와 이 프로그램의 원격 코어(대상 PC·관리 PC)를 함께 띄워 확인하는 통합 점검 (37개 항목, Linux에서도 실행). 화면 캡처·입력은 가짜(Stubs.cs)로 대신합니다. 1. PostgreSQL + 중계 서버를 `http://127.0.0.1:21900`에 띄움 (첫 관리자 `admin` / `firstpass1`, 빈 DB)