842 lines
42 KiB
C#
842 lines
42 KiB
C#
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using JJServiceManager.Remote;
|
|
|
|
namespace JJServiceManager
|
|
{
|
|
/// <summary>설정 화면 (탭: 일반 / 원격 접속 허용 / 관리 대상 PC)</summary>
|
|
public sealed class SettingsWindow : Window
|
|
{
|
|
private readonly AppSettings _s;
|
|
|
|
// 일반
|
|
private readonly CheckBox _startup, _hotkey, _opLog, _shutdownButtons, _confirmLocal, _confirmRemote;
|
|
private readonly TextBox _hotkeyBox;
|
|
private readonly ComboBox _logMode, _esc;
|
|
private ModifierKeys _mods;
|
|
private Key _key;
|
|
|
|
// 원격 접속 허용
|
|
private readonly CheckBox _server, _activeListen, _allowView, _allowControl, _requireApproval;
|
|
private readonly ComboBox _serverMode;
|
|
private readonly TextBox _serverPort;
|
|
private readonly TextBlock _serverPortHint, _pwState, _serverInfo;
|
|
private readonly PasswordBox _pwd1, _pwd2;
|
|
private int _sshPort, _tcpPort;
|
|
private int _shownMode = -1;
|
|
|
|
// Passive 접속 (대상 PC 쪽)
|
|
private readonly ListBox _pmList;
|
|
private readonly TextBox _pmAddr, _pmCode, _pmPort;
|
|
private readonly ComboBox _pmMode;
|
|
private readonly TextBlock _pmResult;
|
|
private readonly ObservableCollection<PassiveManager> _pms = new ObservableCollection<PassiveManager>();
|
|
|
|
// 관리 대상 PC
|
|
private readonly CheckBox _manage;
|
|
private readonly ComboBox _passiveMode;
|
|
private readonly TextBox _passivePort;
|
|
private int _passiveSshPort, _passiveTcpPort;
|
|
private int _shownPassiveMode = -1;
|
|
private readonly ListBox _targets;
|
|
private readonly TextBox _addr, _hostname, _port, _code;
|
|
private readonly ComboBox _mode, _direction;
|
|
private readonly PasswordBox _pwdTarget;
|
|
private readonly TextBlock _pwTargetState, _test, _lblAddr;
|
|
private readonly FrameworkElement _lblMode, _lblPort, _lblCode, _pnlCode;
|
|
private readonly Button _btnTest;
|
|
private readonly ObservableCollection<TargetItem> _items = new ObservableCollection<TargetItem>();
|
|
|
|
// 중계 서버 (대상 PC: 등록)
|
|
private readonly CheckBox _relayAgent;
|
|
private readonly TextBox _relayAgentUrl;
|
|
private readonly TextBlock _relayAgentState;
|
|
private string _relayDeviceId, _relayKeyProt, _relayPub, _relayDeviceName;
|
|
|
|
// 중계 서버 (관리 PC: 로그인)
|
|
private readonly RelayManager _relay;
|
|
private readonly CheckBox _relayManage, _relaySavePw;
|
|
private readonly TextBox _relayManageUrl, _relayUser;
|
|
private readonly PasswordBox _relayPw;
|
|
private readonly TextBlock _relayLoginState;
|
|
private readonly Button _btnRelayLogin;
|
|
private string _relayTokenProt, _relayPwProt;
|
|
private string _loggedUrl, _loggedUser; // 이 토큰을 받은 주소·아이디
|
|
|
|
/// <summary>목록 표시용</summary>
|
|
private sealed class TargetItem : ObservableObject
|
|
{
|
|
public RemoteTarget T { get; }
|
|
public TargetItem(RemoteTarget t) => T = t;
|
|
public string Label => T.Label;
|
|
public string ModeText => T.Direction == ConnectDirection.Passive
|
|
? "Passive · 코드 " + T.AccessCode
|
|
: "Active · " + (T.Mode == RemoteMode.Ssh ? "SSH" : "지정 포트") + " " + T.Port;
|
|
public void Changed()
|
|
{
|
|
OnPropertyChanged(nameof(Label));
|
|
OnPropertyChanged(nameof(ModeText));
|
|
}
|
|
}
|
|
|
|
public SettingsWindow(AppSettings current, RelayManager relay = null)
|
|
{
|
|
_s = current;
|
|
_relay = relay;
|
|
|
|
Title = "설정" + AppInstance.TitleTag;
|
|
Width = 800;
|
|
Height = 720;
|
|
MinHeight = 560;
|
|
ResizeMode = ResizeMode.CanResize;
|
|
FontFamily = new FontFamily("Malgun Gothic");
|
|
FontSize = 13;
|
|
Background = Brushes2.Make("#FAFAFA");
|
|
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
|
ShowInTaskbar = false;
|
|
AppIcon.Apply(this);
|
|
|
|
var root = XamlLoader.LoadElement("Settings.xaml");
|
|
Content = root;
|
|
|
|
// ───────── 일반 ─────────
|
|
_startup = root.Find<CheckBox>("chkStartup");
|
|
_hotkey = root.Find<CheckBox>("chkHotkey");
|
|
_opLog = root.Find<CheckBox>("chkOpLog");
|
|
_shutdownButtons = root.Find<CheckBox>("chkShutdownButtons");
|
|
_confirmLocal = root.Find<CheckBox>("chkConfirmLocal");
|
|
_confirmRemote = root.Find<CheckBox>("chkConfirmRemote");
|
|
_hotkeyBox = root.Find<TextBox>("txtHotkey");
|
|
_logMode = root.Find<ComboBox>("cmbLogMode");
|
|
_esc = root.Find<ComboBox>("cmbEsc");
|
|
root.Find<TextBlock>("txtDataPath").Text = "설정 파일: " + SettingsStore.SettingsPath
|
|
+ "\n작업 로그 파일: " + OperationLogFile.Folder;
|
|
|
|
_startup.IsChecked = current.RunAtStartup;
|
|
_hotkey.IsChecked = current.HotkeyEnabled;
|
|
_opLog.IsChecked = current.ShowOperationLog;
|
|
_shutdownButtons.IsChecked = current.ShowShutdownButtons;
|
|
_confirmLocal.IsChecked = current.ConfirmLocalStartStop;
|
|
_confirmRemote.IsChecked = current.ConfirmRemoteStartStop;
|
|
_logMode.SelectedIndex = (int)current.LogWindowMode;
|
|
_esc.SelectedIndex = (int)current.EscAction;
|
|
_mods = (ModifierKeys)current.HotkeyModifiers;
|
|
_key = (Key)current.HotkeyKey;
|
|
ShowHotkey();
|
|
|
|
// 단축키: 클릭 후 직접 눌러서 지정
|
|
_hotkeyBox.GotKeyboardFocus += (s, e) => _hotkeyBox.Background = Brushes2.Make("#FFF8E1");
|
|
_hotkeyBox.LostKeyboardFocus += (s, e) => { _hotkeyBox.Background = Brushes2.Make("#FAFAFA"); ShowHotkey(); };
|
|
_hotkeyBox.PreviewKeyDown += OnHotkeyKeyDown;
|
|
|
|
// ───────── 원격 접속 허용 ─────────
|
|
_server = root.Find<CheckBox>("chkServer");
|
|
_activeListen = root.Find<CheckBox>("chkActiveListen");
|
|
_allowView = root.Find<CheckBox>("chkAllowView");
|
|
_allowControl = root.Find<CheckBox>("chkAllowControl");
|
|
_requireApproval = root.Find<CheckBox>("chkRequireApproval");
|
|
_serverMode = root.Find<ComboBox>("cmbServerMode");
|
|
_serverPort = root.Find<TextBox>("txtServerPort");
|
|
_serverPortHint = root.Find<TextBlock>("txtServerPortHint");
|
|
_pwd1 = root.Find<PasswordBox>("pwdServer");
|
|
_pwd2 = root.Find<PasswordBox>("pwdServer2");
|
|
_pwState = root.Find<TextBlock>("txtPwState");
|
|
_serverInfo = root.Find<TextBlock>("txtServerInfo");
|
|
|
|
_sshPort = current.RemoteServerSshPort;
|
|
_tcpPort = current.RemoteServerTcpPort;
|
|
_server.IsChecked = current.RemoteServerEnabled;
|
|
_activeListen.IsChecked = current.RemoteServerActiveListen;
|
|
_allowView.IsChecked = current.RemoteAllowView;
|
|
_allowControl.IsChecked = current.RemoteAllowControl;
|
|
_requireApproval.IsChecked = current.RemoteRequireApproval;
|
|
_serverMode.SelectionChanged += (s, e) => OnServerModeChanged();
|
|
_serverMode.SelectedIndex = (int)current.RemoteServerMode;
|
|
_pwState.Text = string.IsNullOrEmpty(current.RemotePasswordHash)
|
|
? "비밀번호가 설정되어 있지 않습니다. 원격 접속을 허용하려면 입력하세요."
|
|
: "비밀번호가 설정되어 있습니다. 바꿀 때만 입력하세요.";
|
|
|
|
// 제어 허용은 화면 보기가 켜져 있어야 함
|
|
_allowView.Checked += (s, e) => _allowControl.IsEnabled = true;
|
|
_allowView.Unchecked += (s, e) => { _allowControl.IsChecked = false; _allowControl.IsEnabled = false; };
|
|
_allowControl.IsEnabled = _allowView.IsChecked == true;
|
|
UpdateServerInfo();
|
|
|
|
// Passive 접속 (이 PC → 관리 PC)
|
|
_pmList = root.Find<ListBox>("lstPassive");
|
|
_pmAddr = root.Find<TextBox>("txtPmAddr");
|
|
_pmCode = root.Find<TextBox>("txtPmCode");
|
|
_pmPort = root.Find<TextBox>("txtPmPort");
|
|
_pmMode = root.Find<ComboBox>("cmbPmMode");
|
|
_pmResult = root.Find<TextBlock>("txtPmResult");
|
|
foreach (var m in current.PassiveManagers) _pms.Add(m.Clone());
|
|
_pmList.ItemsSource = _pms;
|
|
_pmList.SelectionChanged += (s, e) => ShowPm(_pmList.SelectedItem as PassiveManager);
|
|
_pmMode.SelectionChanged += (s, e) =>
|
|
{
|
|
if (_pmPort.Text == "" || _pmPort.Text == "9990" || _pmPort.Text == "2224")
|
|
_pmPort.Text = _pmMode.SelectedIndex == 0 ? "2224" : "9990";
|
|
};
|
|
ClearPmForm();
|
|
root.Find<Button>("btnPmNew").Click += (s, e) => { _pmList.SelectedItem = null; ClearPmForm(); };
|
|
root.Find<Button>("btnPmAdd").Click += (s, e) => AddPm();
|
|
root.Find<Button>("btnPmUpdate").Click += (s, e) => UpdatePm();
|
|
root.Find<Button>("btnPmDelete").Click += (s, e) => DeletePm();
|
|
|
|
// ───────── 관리 대상 PC ─────────
|
|
_manage = root.Find<CheckBox>("chkManage");
|
|
_passiveMode = root.Find<ComboBox>("cmbPassiveMode");
|
|
_passivePort = root.Find<TextBox>("txtPassivePort");
|
|
_targets = root.Find<ListBox>("lstTargets");
|
|
_addr = root.Find<TextBox>("txtAddr");
|
|
_hostname = root.Find<TextBox>("txtHostname");
|
|
_port = root.Find<TextBox>("txtPort");
|
|
_code = root.Find<TextBox>("txtCode");
|
|
_mode = root.Find<ComboBox>("cmbMode");
|
|
_direction = root.Find<ComboBox>("cmbDirection");
|
|
_pwdTarget = root.Find<PasswordBox>("pwdTarget");
|
|
_pwTargetState = root.Find<TextBlock>("txtPwTarget");
|
|
_test = root.Find<TextBlock>("txtTest");
|
|
_lblAddr = root.Find<TextBlock>("lblAddr");
|
|
_lblMode = root.Find<FrameworkElement>("lblMode");
|
|
_lblPort = root.Find<FrameworkElement>("lblPort");
|
|
_lblCode = root.Find<FrameworkElement>("lblCode");
|
|
_pnlCode = root.Find<FrameworkElement>("pnlCode");
|
|
_btnTest = root.Find<Button>("btnTest");
|
|
|
|
_manage.IsChecked = current.RemoteManageEnabled;
|
|
_passiveSshPort = current.PassiveListenSshPort;
|
|
_passiveTcpPort = current.PassiveListenTcpPort;
|
|
_passiveMode.SelectionChanged += (s, e) =>
|
|
{
|
|
if (_shownPassiveMode == 0 && int.TryParse(_passivePort.Text, out var a)) _passiveSshPort = a;
|
|
if (_shownPassiveMode == 1 && int.TryParse(_passivePort.Text, out var b)) _passiveTcpPort = b;
|
|
_shownPassiveMode = _passiveMode.SelectedIndex;
|
|
_passivePort.Text = (_shownPassiveMode == 0 ? _passiveSshPort : _passiveTcpPort).ToString();
|
|
};
|
|
_passiveMode.SelectedIndex = (int)current.PassiveListenMode;
|
|
|
|
foreach (var t in current.Targets) _items.Add(new TargetItem(t.Clone()));
|
|
_targets.ItemsSource = _items;
|
|
_targets.SelectionChanged += (s, e) => ShowTarget(_targets.SelectedItem as TargetItem);
|
|
_mode.SelectionChanged += (s, e) =>
|
|
{
|
|
// 방식을 바꾸면 기본 포트로 (직접 바꾼 포트가 아닐 때)
|
|
if (_port.Text == "" || _port.Text == "9988" || _port.Text == "2222")
|
|
_port.Text = _mode.SelectedIndex == 0 ? "2222" : "9988";
|
|
};
|
|
_direction.SelectionChanged += (s, e) => OnDirectionChanged();
|
|
root.Find<Button>("btnNewCode").Click += (s, e) => _code.Text = RemoteTarget.NewAccessCode();
|
|
ClearForm();
|
|
|
|
root.Find<Button>("btnNew").Click += (s, e) => { _targets.SelectedItem = null; ClearForm(); };
|
|
root.Find<Button>("btnAddTarget").Click += (s, e) => AddTarget();
|
|
root.Find<Button>("btnUpdateTarget").Click += (s, e) => UpdateTarget();
|
|
root.Find<Button>("btnDeleteTarget").Click += (s, e) => DeleteTarget();
|
|
_btnTest.Click += async (s, e) => await TestTarget();
|
|
|
|
// ───────── 중계 서버 (대상 PC: 등록) ─────────
|
|
_relayAgent = root.Find<CheckBox>("chkRelayAgent");
|
|
_relayAgentUrl = root.Find<TextBox>("txtRelayAgentUrl");
|
|
_relayAgentState = root.Find<TextBlock>("txtRelayAgentState");
|
|
_relayAgent.IsChecked = current.RelayAgentEnabled;
|
|
_relayAgentUrl.Text = current.RelayAgentUrl;
|
|
_relayDeviceId = current.RelayDeviceId;
|
|
_relayKeyProt = current.RelayDeviceKeyProtected;
|
|
_relayPub = current.RelayServerPublicKey;
|
|
_relayDeviceName = current.RelayDeviceName;
|
|
ShowRelayAgentState(null, true);
|
|
root.Find<Button>("btnRelayEnroll").Click += async (s, e) => await EnrollAsync((Button)s);
|
|
root.Find<Button>("btnRelayUnenroll").Click += (s, e) => Unenroll();
|
|
|
|
// ───────── 중계 서버 (관리 PC: 로그인) ─────────
|
|
_relayManage = root.Find<CheckBox>("chkRelayManage");
|
|
_relayManageUrl = root.Find<TextBox>("txtRelayManageUrl");
|
|
_relayUser = root.Find<TextBox>("txtRelayUser");
|
|
_relayPw = root.Find<PasswordBox>("pwdRelay");
|
|
_relaySavePw = root.Find<CheckBox>("chkRelaySavePw");
|
|
_relayLoginState = root.Find<TextBlock>("txtRelayLoginState");
|
|
_btnRelayLogin = root.Find<Button>("btnRelayLogin");
|
|
_relayManage.IsChecked = current.RelayManageEnabled;
|
|
_relayManageUrl.Text = current.RelayManageUrl;
|
|
_relayUser.Text = current.RelayUsername;
|
|
_relayTokenProt = current.RelayTokenProtected;
|
|
_relayPwProt = current.RelayPasswordProtected;
|
|
_relaySavePw.IsChecked = !string.IsNullOrEmpty(current.RelayPasswordProtected) || string.IsNullOrEmpty(current.RelayTokenProtected);
|
|
_loggedUrl = current.RelayManageUrl;
|
|
_loggedUser = current.RelayUsername;
|
|
ShowRelayLoginState();
|
|
_btnRelayLogin.Click += async (s, e) => await RelayLoginAsync();
|
|
root.Find<Button>("btnRelayLogout").Click += async (s, e) => await RelayLogoutAsync();
|
|
|
|
// [서버 관리] 탭: 중계 서버에 관리자 계정으로 로그인되어 있을 때만
|
|
if (relay != null && relay.IsLoggedIn && relay.IsAdmin)
|
|
{
|
|
var tabs = root.Find<TabControl>("tabs");
|
|
tabs.Items.Add(new TabItem { Header = " 서버 관리 ", Content = new ServerAdminPanel(relay, this) });
|
|
Width = 980;
|
|
}
|
|
|
|
// ───────── 확인 / 취소 ─────────
|
|
root.Find<Button>("btnOk").Click += (s, e) => Save();
|
|
root.Find<Button>("btnCancel").Click += (s, e) => Close();
|
|
PreviewKeyDown += (s, e) =>
|
|
{
|
|
if (e.Key == Key.Escape && !_hotkeyBox.IsKeyboardFocused) Close();
|
|
};
|
|
}
|
|
|
|
// ───────────────────────── 단축키 ─────────────────────────
|
|
|
|
private void OnHotkeyKeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
e.Handled = true;
|
|
var key = e.Key == Key.System ? e.SystemKey : e.Key;
|
|
var mods = Keyboard.Modifiers;
|
|
if (Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin)) mods |= ModifierKeys.Windows;
|
|
|
|
// 보조키만 누른 상태: 진행 중 표시
|
|
if (key is Key.LeftCtrl or Key.RightCtrl or Key.LeftAlt or Key.RightAlt
|
|
or Key.LeftShift or Key.RightShift or Key.LWin or Key.RWin)
|
|
{
|
|
_hotkeyBox.Text = HotkeyManager.Format(mods, Key.None) + "…";
|
|
return;
|
|
}
|
|
|
|
// Tab은 포커스 이동에 사용
|
|
if (key == Key.Tab && mods == ModifierKeys.None)
|
|
{
|
|
e.Handled = false;
|
|
return;
|
|
}
|
|
|
|
if ((mods & (ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Windows)) == 0)
|
|
{
|
|
_hotkeyBox.Text = "Ctrl / Alt / Win 중 하나와 함께 누르세요";
|
|
return;
|
|
}
|
|
|
|
_mods = mods;
|
|
_key = key;
|
|
ShowHotkey();
|
|
}
|
|
|
|
private void ShowHotkey() => _hotkeyBox.Text = HotkeyManager.Format(_mods, _key);
|
|
|
|
// ───────────────────────── 원격 접속 허용 ─────────────────────────
|
|
|
|
private void OnServerModeChanged()
|
|
{
|
|
// 방식마다 포트를 따로 기억
|
|
if (_shownMode == 0 && int.TryParse(_serverPort.Text, out var a)) _sshPort = a;
|
|
if (_shownMode == 1 && int.TryParse(_serverPort.Text, out var b)) _tcpPort = b;
|
|
_shownMode = _serverMode.SelectedIndex;
|
|
_serverPort.Text = (_shownMode == 0 ? _sshPort : _tcpPort).ToString();
|
|
_serverPortHint.Text = _shownMode == 0 ? "기본 2222" : "기본 9988";
|
|
UpdateServerInfo();
|
|
}
|
|
|
|
private void UpdateServerInfo()
|
|
{
|
|
var ips = NetInfo.LocalIPv4();
|
|
_serverInfo.Text =
|
|
$"이 PC: {Environment.MachineName} IP: {(ips.Count == 0 ? "(확인 안 됨)" : string.Join(", ", ips))}\n" +
|
|
"Active: 관리 PC의 [관리 대상 PC]에 위 IP, 같은 통신 방식과 포트, 접속 비밀번호를 입력합니다.\n" +
|
|
"Passive: 관리 PC에서 받은 접속 코드를 위 [Passive 접속]에 입력하고, 관리 PC에는 접속 비밀번호를 입력합니다.\n" +
|
|
"Active 수신을 켜면 Windows 방화벽에 해당 TCP 포트를 자동으로 열고, 끄면 규칙을 지웁니다.\n" +
|
|
"원격에서는 이 PC에 등록된 서비스만 보고 Start / Stop 할 수 있습니다.";
|
|
}
|
|
|
|
private void ClearPmForm()
|
|
{
|
|
_pmAddr.Text = "";
|
|
_pmCode.Text = "";
|
|
_pmMode.SelectedIndex = 1;
|
|
_pmPort.Text = "9990";
|
|
_pmResult.Text = "";
|
|
}
|
|
|
|
private void ShowPm(PassiveManager m)
|
|
{
|
|
if (m == null) return;
|
|
_pmAddr.Text = m.Address;
|
|
_pmCode.Text = m.AccessCode;
|
|
_pmMode.SelectedIndex = (int)m.Mode;
|
|
_pmPort.Text = m.Port.ToString();
|
|
_pmResult.Text = "";
|
|
}
|
|
|
|
private string ReadPm(out PassiveManager m)
|
|
{
|
|
m = new PassiveManager
|
|
{
|
|
Address = _pmAddr.Text.Trim(),
|
|
AccessCode = _pmCode.Text.Trim().ToUpperInvariant(),
|
|
Mode = (RemoteMode)Math.Max(0, _pmMode.SelectedIndex)
|
|
};
|
|
if (m.Address.Length == 0) return "관리 PC 주소를 입력하세요.";
|
|
if (m.AccessCode.Length < 4) return "관리 PC에서 받은 접속 코드를 입력하세요.";
|
|
if (!int.TryParse(_pmPort.Text.Trim(), out var p) || p < 1 || p > 65535) return "포트는 1~65535 사이 숫자로 입력하세요.";
|
|
m.Port = p;
|
|
return null;
|
|
}
|
|
|
|
private void AddPm()
|
|
{
|
|
var err = ReadPm(out var m);
|
|
if (err != null) { ShowResult(_pmResult, err, false); return; }
|
|
_pms.Add(m);
|
|
_pmList.SelectedItem = m;
|
|
ShowResult(_pmResult, "추가했습니다. [확인]을 누르면 저장됩니다.", true);
|
|
}
|
|
|
|
private void UpdatePm()
|
|
{
|
|
if (_pmList.SelectedItem is not PassiveManager sel) { ShowResult(_pmResult, "수정할 항목을 목록에서 고르세요.", false); return; }
|
|
var err = ReadPm(out var m);
|
|
if (err != null) { ShowResult(_pmResult, err, false); return; }
|
|
int i = _pms.IndexOf(sel);
|
|
m.Id = sel.Id;
|
|
_pms[i] = m;
|
|
_pmList.SelectedItem = m;
|
|
ShowResult(_pmResult, "수정했습니다. [확인]을 누르면 저장됩니다.", true);
|
|
}
|
|
|
|
private void DeletePm()
|
|
{
|
|
if (_pmList.SelectedItem is not PassiveManager sel) { ShowResult(_pmResult, "삭제할 항목을 목록에서 고르세요.", false); return; }
|
|
_pms.Remove(sel);
|
|
ClearPmForm();
|
|
ShowResult(_pmResult, "삭제했습니다. [확인]을 누르면 저장됩니다.", true);
|
|
}
|
|
|
|
// ───────────────────────── 관리 대상 PC ─────────────────────────
|
|
|
|
private bool FormIsPassive => _direction.SelectedIndex == 1;
|
|
|
|
private void OnDirectionChanged()
|
|
{
|
|
bool passive = FormIsPassive;
|
|
// Active: IP·방식·포트 입력 / Passive: 접속 코드
|
|
_lblAddr.Text = passive ? "대상 IP (자동)" : "대상 IP";
|
|
_addr.IsEnabled = !passive;
|
|
_mode.IsEnabled = !passive;
|
|
_port.IsEnabled = !passive;
|
|
_lblMode.Opacity = _lblPort.Opacity = passive ? 0.4 : 1;
|
|
_lblCode.Visibility = _pnlCode.Visibility = passive ? Visibility.Visible : Visibility.Collapsed;
|
|
_btnTest.IsEnabled = !passive;
|
|
if (passive && string.IsNullOrEmpty(_code.Text)) _code.Text = RemoteTarget.NewAccessCode();
|
|
}
|
|
|
|
private void ClearForm()
|
|
{
|
|
_direction.SelectedIndex = 0;
|
|
_addr.Text = "";
|
|
_hostname.Text = "";
|
|
_code.Text = "";
|
|
_mode.SelectedIndex = 1;
|
|
_port.Text = "9988";
|
|
_pwdTarget.Password = "";
|
|
_pwTargetState.Text = "대상 PC의 접속 비밀번호";
|
|
_test.Text = "";
|
|
OnDirectionChanged();
|
|
}
|
|
|
|
private void ShowTarget(TargetItem item)
|
|
{
|
|
if (item == null) return;
|
|
var t = item.T;
|
|
_direction.SelectedIndex = (int)t.Direction;
|
|
_addr.Text = t.Direction == ConnectDirection.Passive ? t.LastAddress : t.Address;
|
|
_hostname.Text = t.Hostname;
|
|
_code.Text = t.AccessCode;
|
|
_mode.SelectedIndex = (int)t.Mode;
|
|
_port.Text = t.Port.ToString();
|
|
_pwdTarget.Password = "";
|
|
_pwTargetState.Text = string.IsNullOrEmpty(t.ProtectedPassword) ? "비밀번호 없음 - 입력하세요" : "저장됨 (바꿀 때만 입력)";
|
|
_test.Text = "";
|
|
OnDirectionChanged();
|
|
}
|
|
|
|
/// <summary>입력값 검사. 문제가 있으면 메시지</summary>
|
|
private string ReadForm(out string addr, out RemoteMode mode, out int port)
|
|
{
|
|
addr = _addr.Text.Trim();
|
|
mode = (RemoteMode)Math.Max(0, _mode.SelectedIndex);
|
|
port = 0;
|
|
if (FormIsPassive)
|
|
{
|
|
if (_code.Text.Trim().Length < 4) return "[새 코드]를 눌러 접속 코드를 만드세요.";
|
|
int.TryParse(_port.Text.Trim(), out port);
|
|
return null;
|
|
}
|
|
if (addr.Length == 0) return "대상 IP를 입력하세요.";
|
|
if (addr.Contains(' ')) return "대상 IP에 공백이 있습니다.";
|
|
if (!int.TryParse(_port.Text.Trim(), out port) || port < 1 || port > 65535) return "포트는 1~65535 사이 숫자로 입력하세요.";
|
|
return null;
|
|
}
|
|
|
|
private void AddTarget()
|
|
{
|
|
var err = ReadForm(out var addr, out var mode, out var port);
|
|
if (err == null && _pwdTarget.Password.Length == 0) err = "비밀번호를 입력하세요.";
|
|
if (err == null && !FormIsPassive && _items.Any(i => i.T.Direction == ConnectDirection.Active && string.Equals(i.T.Address, addr, StringComparison.OrdinalIgnoreCase) && i.T.Port == port))
|
|
err = "같은 IP와 포트가 이미 등록되어 있습니다.";
|
|
if (err == null && FormIsPassive && _items.Any(i => string.Equals(i.T.AccessCode, _code.Text.Trim(), StringComparison.OrdinalIgnoreCase)))
|
|
err = "같은 접속 코드가 이미 있습니다. [새 코드]를 누르세요.";
|
|
if (err != null) { ShowTest(err, false); return; }
|
|
|
|
var t = new RemoteTarget
|
|
{
|
|
Direction = FormIsPassive ? ConnectDirection.Passive : ConnectDirection.Active,
|
|
AccessCode = FormIsPassive ? _code.Text.Trim() : "",
|
|
Address = FormIsPassive ? "" : addr,
|
|
Mode = mode,
|
|
Port = port > 0 ? port : 9988,
|
|
Hostname = _hostname.Text.Trim(),
|
|
ProtectedPassword = RemoteSecurity.Protect(_pwdTarget.Password)
|
|
};
|
|
var item = new TargetItem(t);
|
|
_items.Add(item);
|
|
_targets.SelectedItem = item;
|
|
ShowTest(FormIsPassive
|
|
? $"추가했습니다. 대상 PC의 [원격 접속 허용 → Passive 접속]에 이 PC 주소와 코드 {t.AccessCode}를 입력하세요."
|
|
: "추가했습니다. [확인]을 누르면 저장됩니다.", true);
|
|
}
|
|
|
|
private void UpdateTarget()
|
|
{
|
|
if (_targets.SelectedItem is not TargetItem item) { ShowTest("수정할 PC를 목록에서 고르세요.", false); return; }
|
|
var err = ReadForm(out var addr, out var mode, out var port);
|
|
if (err != null) { ShowTest(err, false); return; }
|
|
|
|
var t = item.T;
|
|
var newDir = FormIsPassive ? ConnectDirection.Passive : ConnectDirection.Active;
|
|
if (newDir != t.Direction || (newDir == ConnectDirection.Active && !string.Equals(t.Address, addr, StringComparison.OrdinalIgnoreCase)))
|
|
t.Hostname = ""; // 방향이나 IP가 바뀌면 hostname 다시 확인
|
|
t.Direction = newDir;
|
|
if (newDir == ConnectDirection.Passive)
|
|
{
|
|
t.AccessCode = _code.Text.Trim();
|
|
}
|
|
else
|
|
{
|
|
t.Address = addr;
|
|
t.Mode = mode;
|
|
t.Port = port;
|
|
}
|
|
if (_hostname.Text.Trim().Length > 0 && t.Hostname.Length == 0) t.Hostname = _hostname.Text.Trim();
|
|
if (_pwdTarget.Password.Length > 0) t.ProtectedPassword = RemoteSecurity.Protect(_pwdTarget.Password);
|
|
item.Changed();
|
|
ShowTarget(item);
|
|
ShowTest("수정했습니다. [확인]을 누르면 저장됩니다.", true);
|
|
}
|
|
|
|
private void DeleteTarget()
|
|
{
|
|
if (_targets.SelectedItem is not TargetItem item) { ShowTest("삭제할 PC를 목록에서 고르세요.", false); return; }
|
|
if (!Dialogs.Confirm(this, $"{item.Label} 을(를) 관리 대상에서 삭제할까요?", "삭제")) return;
|
|
_items.Remove(item);
|
|
ClearForm();
|
|
ShowTest("삭제했습니다. [확인]을 누르면 저장됩니다.", true);
|
|
}
|
|
|
|
private async System.Threading.Tasks.Task TestTarget()
|
|
{
|
|
var err = ReadForm(out var addr, out var mode, out var port);
|
|
if (err != null) { ShowTest(err, false); return; }
|
|
|
|
// 입력한 비밀번호가 없으면 저장된 비밀번호 사용
|
|
var pw = _pwdTarget.Password;
|
|
if (pw.Length == 0 && _targets.SelectedItem is TargetItem sel) pw = RemoteSecurity.Unprotect(sel.T.ProtectedPassword);
|
|
if (pw.Length == 0) { ShowTest("비밀번호를 입력하세요.", false); return; }
|
|
|
|
_btnTest.IsEnabled = false;
|
|
ShowTest($"{addr}:{port} 연결 중…", true);
|
|
try
|
|
{
|
|
var host = await RemoteClient.TestAsync(new RemoteTarget { Address = addr, Mode = mode, Port = port }, pw);
|
|
_hostname.Text = host;
|
|
if (_targets.SelectedItem is TargetItem item
|
|
&& string.Equals(item.T.Address, addr, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
item.T.Hostname = host;
|
|
item.Changed();
|
|
}
|
|
ShowTest($"연결 성공: {host}", true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ShowTest("연결 실패: " + RemoteClient.Friendly(ex), false);
|
|
}
|
|
finally
|
|
{
|
|
_btnTest.IsEnabled = !FormIsPassive;
|
|
}
|
|
}
|
|
|
|
private void ShowTest(string msg, bool ok) => ShowResult(_test, msg, ok);
|
|
|
|
private static void ShowResult(TextBlock tb, string msg, bool ok)
|
|
{
|
|
tb.Text = msg;
|
|
tb.Foreground = ok ? Brushes2.Running : Brushes2.Stopped;
|
|
}
|
|
|
|
// ───────────────────────── 중계 서버: 대상 PC 등록 ─────────────────────────
|
|
|
|
private void ShowRelayAgentState(string msg, bool ok)
|
|
{
|
|
if (msg != null) { ShowResult(_relayAgentState, msg, ok); return; }
|
|
_relayAgentState.Foreground = Brushes2.Make("#37474F");
|
|
_relayAgentState.Text = string.IsNullOrEmpty(_relayDeviceId)
|
|
? "등록되지 않음"
|
|
: $"등록됨: {(_relayDeviceName.Length > 0 ? _relayDeviceName : Environment.MachineName)} (기기 ID {_relayDeviceId.Substring(0, Math.Min(8, _relayDeviceId.Length))}…)";
|
|
}
|
|
|
|
private async System.Threading.Tasks.Task EnrollAsync(Button btn)
|
|
{
|
|
var url = _relayAgentUrl.Text.Trim();
|
|
if (url.Length == 0) { ShowRelayAgentState("서버 주소를 입력하세요.", false); return; }
|
|
if (!string.IsNullOrEmpty(_relayDeviceId) &&
|
|
!Dialogs.Confirm(this, "이미 등록되어 있습니다. 새 등록 코드로 다시 등록할까요?\n(서버의 이전 기기 항목은 관리자가 정리해야 합니다)", "다시 등록"))
|
|
return;
|
|
var code = TextPrompt.Ask(this, "등록 코드", "중계 서버 관리자에게 받은 등록 코드를 입력하세요. (예: AB12-CD34)", "", upper: true);
|
|
if (string.IsNullOrWhiteSpace(code)) return;
|
|
|
|
// 지금 입력 중인 Active 수신 정보 (직접 연결 후보)
|
|
System.Text.Json.Nodes.JsonObject active = null;
|
|
if (_server.IsChecked == true && _activeListen.IsChecked == true && int.TryParse(_serverPort.Text.Trim(), out var port))
|
|
active = new System.Text.Json.Nodes.JsonObject { ["mode"] = _serverMode.SelectedIndex == 0 ? "Ssh" : "Port", ["port"] = port };
|
|
|
|
btn.IsEnabled = false;
|
|
ShowRelayAgentState("등록 중…", true);
|
|
try
|
|
{
|
|
var r = await RelayAgent.EnrollAsync(url, code.Trim(), active, System.Threading.CancellationToken.None);
|
|
_relayDeviceId = r.DeviceId;
|
|
_relayKeyProt = RemoteSecurity.Protect(r.DeviceKey);
|
|
_relayPub = r.ServerPublicKey;
|
|
_relayDeviceName = r.Name ?? "";
|
|
_relayAgent.IsChecked = true;
|
|
ShowRelayAgentState($"등록했습니다: {_relayDeviceName}. [확인]을 누르면 중계 서버에 접속합니다.", true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ShowRelayAgentState("등록 실패: " + RelayUrl.Friendly(ex), false);
|
|
}
|
|
finally
|
|
{
|
|
btn.IsEnabled = true;
|
|
}
|
|
}
|
|
|
|
private void Unenroll()
|
|
{
|
|
if (string.IsNullOrEmpty(_relayDeviceId)) { ShowRelayAgentState("등록되어 있지 않습니다.", false); return; }
|
|
if (!Dialogs.Confirm(this, "이 PC의 중계 서버 등록 정보를 지울까요?\n다시 쓰려면 새 등록 코드가 필요합니다.\n(서버의 기기 항목은 서버 관리자가 '삭제'로 정리합니다)", "등록 해제")) return;
|
|
_relayDeviceId = "";
|
|
_relayKeyProt = "";
|
|
_relayPub = "";
|
|
_relayDeviceName = "";
|
|
_relayAgent.IsChecked = false;
|
|
ShowRelayAgentState("등록 정보를 지웠습니다. [확인]을 누르면 저장됩니다.", true);
|
|
}
|
|
|
|
// ───────────────────────── 중계 서버: 관리 PC 로그인 ─────────────────────────
|
|
|
|
private void ShowRelayLoginState(string msg = null, bool ok = true)
|
|
{
|
|
if (msg != null) { ShowResult(_relayLoginState, msg, ok); return; }
|
|
_relayLoginState.Foreground = Brushes2.Make("#37474F");
|
|
if (_relay != null && _relay.IsLoggedIn)
|
|
_relayLoginState.Text = _relay.StatusText + $" · 기기 {_relay.Devices.Count}대";
|
|
else if (!string.IsNullOrEmpty(_relayTokenProt) || !string.IsNullOrEmpty(_relayPwProt))
|
|
_relayLoginState.Text = _relay != null && _relay.StatusText.Length > 0 ? _relay.StatusText : "로그인 정보 저장됨";
|
|
else
|
|
_relayLoginState.Text = "로그인하지 않음";
|
|
}
|
|
|
|
private async System.Threading.Tasks.Task RelayLoginAsync()
|
|
{
|
|
var url = _relayManageUrl.Text.Trim();
|
|
var user = _relayUser.Text.Trim();
|
|
var pw = _relayPw.Password;
|
|
if (url.Length == 0) { ShowRelayLoginState("서버 주소를 입력하세요.", false); return; }
|
|
if (user.Length == 0 || pw.Length == 0) { ShowRelayLoginState("아이디와 비밀번호를 입력하세요.", false); return; }
|
|
|
|
_btnRelayLogin.IsEnabled = false;
|
|
ShowRelayLoginState("로그인 중…");
|
|
try
|
|
{
|
|
var ct = System.Threading.CancellationToken.None;
|
|
var (token, mustChange, isAdmin) = await RelayManager.LoginAsync(url, user, pw, ct);
|
|
if (mustChange)
|
|
{
|
|
// 첫 로그인 / 비밀번호 초기화 후: 새 비밀번호로 바꿔야 함
|
|
var np = ChangePasswordWindow.Ask(this, $"{user}: 처음 로그인했거나 비밀번호가 초기화되었습니다.\n새 비밀번호를 정하세요.", askOld: false);
|
|
if (np == null) { ShowRelayLoginState("비밀번호를 바꿔야 로그인할 수 있습니다.", false); return; }
|
|
if (np.Value.newPw == pw) { ShowRelayLoginState("지금과 다른 비밀번호를 정하세요.", false); return; }
|
|
await RelayManager.ChangePasswordAsync(url, token, pw, np.Value.newPw, ct);
|
|
pw = np.Value.newPw;
|
|
(token, _, isAdmin) = await RelayManager.LoginAsync(url, user, pw, ct);
|
|
}
|
|
_relayTokenProt = RemoteSecurity.Protect(token);
|
|
_relayPwProt = _relaySavePw.IsChecked == true ? RemoteSecurity.Protect(pw) : "";
|
|
_loggedUrl = url;
|
|
_loggedUser = user;
|
|
_relayPw.Password = "";
|
|
_relayManage.IsChecked = true;
|
|
ShowRelayLoginState($"로그인 성공: {user}{(isAdmin ? " (관리자)" : "")}. [확인]을 누르면 권한 있는 PC가 초기화면에 나타납니다."
|
|
+ (isAdmin && (_relay == null || !_relay.IsAdmin) ? " [서버 관리] 탭은 다음에 설정을 열 때 보입니다." : ""), true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ShowRelayLoginState("로그인 실패: " + RelayUrl.Friendly(ex), false);
|
|
}
|
|
finally
|
|
{
|
|
_btnRelayLogin.IsEnabled = true;
|
|
}
|
|
}
|
|
|
|
private async System.Threading.Tasks.Task RelayLogoutAsync()
|
|
{
|
|
if (string.IsNullOrEmpty(_relayTokenProt) && string.IsNullOrEmpty(_relayPwProt)) { ShowRelayLoginState("로그인되어 있지 않습니다.", false); return; }
|
|
if (_relay != null) await _relay.LogoutAsync();
|
|
_relayTokenProt = "";
|
|
_relayPwProt = "";
|
|
ShowRelayLoginState("로그아웃했습니다. [확인]을 누르면 저장됩니다.", true);
|
|
}
|
|
|
|
// ───────────────────────── 저장 ─────────────────────────
|
|
|
|
private void Save()
|
|
{
|
|
// 원격 접속 허용 검사
|
|
if (!int.TryParse(_serverPort.Text.Trim(), out var sp) || sp < 1 || sp > 65535)
|
|
{
|
|
Fail(1, "Active 수신 포트는 1~65535 사이 숫자로 입력하세요.");
|
|
return;
|
|
}
|
|
if (_serverMode.SelectedIndex == 0) _sshPort = sp; else _tcpPort = sp;
|
|
|
|
if (!int.TryParse(_passivePort.Text.Trim(), out var pp) || pp < 1 || pp > 65535)
|
|
{
|
|
Fail(2, "Passive 수신 포트는 1~65535 사이 숫자로 입력하세요.");
|
|
return;
|
|
}
|
|
if (_passiveMode.SelectedIndex == 0) _passiveSshPort = pp; else _passiveTcpPort = pp;
|
|
|
|
string newHash = null, newSalt = null;
|
|
if (_pwd1.Password.Length > 0 || _pwd2.Password.Length > 0)
|
|
{
|
|
if (_pwd1.Password != _pwd2.Password) { Fail(1, "접속 비밀번호와 확인이 다릅니다."); return; }
|
|
if (_pwd1.Password.Length < 4) { Fail(1, "접속 비밀번호는 4자 이상으로 입력하세요."); return; }
|
|
(newHash, newSalt) = RemoteSecurity.HashPassword(_pwd1.Password);
|
|
}
|
|
bool hasPassword = newHash != null || !string.IsNullOrEmpty(_s.RemotePasswordHash);
|
|
if (_server.IsChecked == true && !hasPassword)
|
|
{
|
|
Fail(1, "원격 접속을 허용하려면 접속 비밀번호를 정하세요.");
|
|
return;
|
|
}
|
|
bool relayAgentOn = _relayAgent.IsChecked == true;
|
|
if (relayAgentOn && string.IsNullOrEmpty(_relayDeviceId))
|
|
{
|
|
Fail(1, "중계 서버를 사용하려면 먼저 [등록 코드로 등록]을 하세요.");
|
|
return;
|
|
}
|
|
if (relayAgentOn && _server.IsChecked != true)
|
|
{
|
|
Fail(1, "중계 서버 접속은 [원격 접속 허용]이 켜져 있어야 동작합니다.");
|
|
return;
|
|
}
|
|
if (_server.IsChecked == true && _activeListen.IsChecked != true && _pms.Count == 0 && !relayAgentOn)
|
|
{
|
|
Fail(1, "원격 접속 허용을 켜려면 Active 수신을 켜거나, Passive 접속을 추가하거나, 중계 서버를 사용하세요.");
|
|
return;
|
|
}
|
|
|
|
bool relayManageOn = _relayManage.IsChecked == true;
|
|
if (relayManageOn && string.IsNullOrEmpty(_relayTokenProt) && string.IsNullOrEmpty(_relayPwProt))
|
|
{
|
|
Fail(2, "중계 서버를 사용하려면 [로그인]을 먼저 하세요.");
|
|
return;
|
|
}
|
|
if (relayManageOn && (!string.Equals(_relayManageUrl.Text.Trim(), _loggedUrl, StringComparison.OrdinalIgnoreCase)
|
|
|| !string.Equals(_relayUser.Text.Trim(), _loggedUser, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
Fail(2, "중계 서버 주소나 아이디를 바꿨으면 [로그인]을 다시 누르세요.");
|
|
return;
|
|
}
|
|
if (relayManageOn && _manage.IsChecked != true)
|
|
{
|
|
Fail(2, "중계 서버 로그인은 위의 [다른 PC 관리]가 켜져 있어야 동작합니다.");
|
|
return;
|
|
}
|
|
|
|
// 일반
|
|
_s.RunAtStartup = _startup.IsChecked == true;
|
|
_s.HotkeyEnabled = _hotkey.IsChecked == true;
|
|
_s.HotkeyModifiers = (int)_mods;
|
|
_s.HotkeyKey = (int)_key;
|
|
_s.ShowOperationLog = _opLog.IsChecked == true;
|
|
_s.ShowShutdownButtons = _shutdownButtons.IsChecked == true;
|
|
_s.ConfirmLocalStartStop = _confirmLocal.IsChecked == true;
|
|
_s.ConfirmRemoteStartStop = _confirmRemote.IsChecked == true;
|
|
_s.LogWindowMode = (LogWindowMode)Math.Max(0, _logMode.SelectedIndex);
|
|
_s.EscAction = (EscAction)Math.Max(0, _esc.SelectedIndex);
|
|
|
|
// 원격 접속 허용
|
|
_s.RemoteServerEnabled = _server.IsChecked == true;
|
|
_s.RemoteServerActiveListen = _activeListen.IsChecked == true;
|
|
_s.RemoteServerMode = (RemoteMode)Math.Max(0, _serverMode.SelectedIndex);
|
|
_s.RemoteServerSshPort = _sshPort;
|
|
_s.RemoteServerTcpPort = _tcpPort;
|
|
if (newHash != null)
|
|
{
|
|
_s.RemotePasswordHash = newHash;
|
|
_s.RemotePasswordSalt = newSalt;
|
|
}
|
|
_s.PassiveManagers = _pms.Select(m => m.Clone()).ToList();
|
|
_s.RemoteAllowView = _allowView.IsChecked == true;
|
|
_s.RemoteAllowControl = _allowView.IsChecked == true && _allowControl.IsChecked == true;
|
|
_s.RemoteRequireApproval = _requireApproval.IsChecked == true;
|
|
|
|
// 관리 대상 PC
|
|
_s.RemoteManageEnabled = _manage.IsChecked == true;
|
|
_s.PassiveListenMode = (RemoteMode)Math.Max(0, _passiveMode.SelectedIndex);
|
|
_s.PassiveListenSshPort = _passiveSshPort;
|
|
_s.PassiveListenTcpPort = _passiveTcpPort;
|
|
_s.Targets = _items.Select(i => i.T.Clone()).ToList();
|
|
|
|
// 중계 서버
|
|
_s.RelayAgentEnabled = relayAgentOn;
|
|
_s.RelayAgentUrl = _relayAgentUrl.Text.Trim();
|
|
_s.RelayDeviceId = _relayDeviceId ?? "";
|
|
_s.RelayDeviceKeyProtected = _relayKeyProt ?? "";
|
|
_s.RelayServerPublicKey = _relayPub ?? "";
|
|
_s.RelayDeviceName = _relayDeviceName ?? "";
|
|
_s.RelayManageEnabled = relayManageOn;
|
|
_s.RelayManageUrl = _relayManageUrl.Text.Trim();
|
|
_s.RelayUsername = _relayUser.Text.Trim();
|
|
_s.RelayTokenProtected = _relayTokenProt ?? "";
|
|
_s.RelayPasswordProtected = _relaySavePw.IsChecked == true ? (_relayPwProt ?? "") : "";
|
|
|
|
DialogResult = true;
|
|
}
|
|
|
|
private void Fail(int tabIndex, string msg)
|
|
{
|
|
if (Content is FrameworkElement root && root.FindName("tabs") is TabControl tc) tc.SelectedIndex = tabIndex;
|
|
Dialogs.Error(this, msg, "설정");
|
|
}
|
|
}
|
|
}
|