Add SSL verification options to updater configuration and enhance UI elements in dialogs
- Introduced `ssl_verify` and `ca_bundle_path` settings in AppController and UpdateManager for improved SSL handling. - Implemented helper functions to parse and build SSL verification options. - Updated the settings dialog and history dialog to display version information and author details more clearly. - Increased the height of the notification dialog for better content visibility.
This commit is contained in:
parent
501b4e3af6
commit
e67e7a5572
|
|
@ -222,6 +222,8 @@ class AppController:
|
||||||
self.settings['update'].setdefault('check_interval_hours', 1)
|
self.settings['update'].setdefault('check_interval_hours', 1)
|
||||||
self.settings['update'].setdefault('program_id', 'voc_monitor')
|
self.settings['update'].setdefault('program_id', 'voc_monitor')
|
||||||
self.settings['update'].setdefault('version_table', 'program_versions')
|
self.settings['update'].setdefault('version_table', 'program_versions')
|
||||||
|
self.settings['update'].setdefault('ssl_verify', True)
|
||||||
|
self.settings['update'].setdefault('ca_bundle_path', '')
|
||||||
self.logger.info("설정 파일 로드 완료")
|
self.logger.info("설정 파일 로드 완료")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# 기본 설정 생성
|
# 기본 설정 생성
|
||||||
|
|
@ -247,7 +249,9 @@ class AppController:
|
||||||
"environment": "main",
|
"environment": "main",
|
||||||
"check_interval_hours": 1,
|
"check_interval_hours": 1,
|
||||||
"program_id": "voc_monitor",
|
"program_id": "voc_monitor",
|
||||||
"version_table": "program_versions"
|
"version_table": "program_versions",
|
||||||
|
"ssl_verify": True,
|
||||||
|
"ca_bundle_path": ""
|
||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
"output_path": str(get_data_dir() / "reports")
|
"output_path": str(get_data_dir() / "reports")
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,38 @@ class ConfigError(UpdateError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_ssl_verify(value: Any, default: bool = True) -> bool:
|
||||||
|
"""ssl_verify 값을 bool로 정규화"""
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
lowered = value.strip().lower()
|
||||||
|
if lowered in {"true", "1", "yes", "on"}:
|
||||||
|
return True
|
||||||
|
if lowered in {"false", "0", "no", "off"}:
|
||||||
|
return False
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return bool(value)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _build_requests_verify_option(ssl_verify: bool, ca_bundle_path: str) -> bool | str:
|
||||||
|
"""requests.verify 옵션 생성 (bool 또는 CA 번들 경로)"""
|
||||||
|
if not ssl_verify:
|
||||||
|
return False
|
||||||
|
|
||||||
|
bundle_path = str(ca_bundle_path or "").strip()
|
||||||
|
if not bundle_path:
|
||||||
|
return True
|
||||||
|
|
||||||
|
bundle = Path(bundle_path).expanduser()
|
||||||
|
if bundle.exists():
|
||||||
|
return str(bundle)
|
||||||
|
|
||||||
|
logger.warning(f"CA 번들 경로를 찾을 수 없습니다. 기본 인증서 검증 사용: {bundle}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _strip_json_comments(content: str) -> str:
|
def _strip_json_comments(content: str) -> str:
|
||||||
"""JSON 문자열의 주석(//, /* */) 제거"""
|
"""JSON 문자열의 주석(//, /* */) 제거"""
|
||||||
result = []
|
result = []
|
||||||
|
|
@ -176,6 +208,8 @@ def load_updater_connection_config(
|
||||||
fallback = local_config.get("fallback", {})
|
fallback = local_config.get("fallback", {})
|
||||||
default_env = local_config.get("default_environment", "main")
|
default_env = local_config.get("default_environment", "main")
|
||||||
config_url = local_config.get("config_url", "").strip()
|
config_url = local_config.get("config_url", "").strip()
|
||||||
|
local_ssl_verify = _parse_ssl_verify(local_config.get("ssl_verify", True), default=True)
|
||||||
|
local_ca_bundle = str(local_config.get("ca_bundle_path", "")).strip()
|
||||||
|
|
||||||
# 1) 원격 설정 시도
|
# 1) 원격 설정 시도
|
||||||
if config_url:
|
if config_url:
|
||||||
|
|
@ -185,7 +219,12 @@ def load_updater_connection_config(
|
||||||
"Pragma": "no-cache",
|
"Pragma": "no-cache",
|
||||||
"Expires": "0",
|
"Expires": "0",
|
||||||
}
|
}
|
||||||
response = requests.get(config_url, timeout=timeout, headers=headers)
|
response = requests.get(
|
||||||
|
config_url,
|
||||||
|
timeout=timeout,
|
||||||
|
headers=headers,
|
||||||
|
verify=_build_requests_verify_option(local_ssl_verify, local_ca_bundle),
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
remote_raw = _strip_json_comments(response.text)
|
remote_raw = _strip_json_comments(response.text)
|
||||||
remote = json.loads(remote_raw)
|
remote = json.loads(remote_raw)
|
||||||
|
|
@ -201,9 +240,15 @@ def load_updater_connection_config(
|
||||||
url = env_config.get("supabaseUrl", "").strip()
|
url = env_config.get("supabaseUrl", "").strip()
|
||||||
key = env_config.get("anonKey", "").strip()
|
key = env_config.get("anonKey", "").strip()
|
||||||
if url and key:
|
if url and key:
|
||||||
|
env_ssl_verify = _parse_ssl_verify(
|
||||||
|
env_config.get("sslVerify", local_ssl_verify), default=local_ssl_verify
|
||||||
|
)
|
||||||
|
env_ca_bundle = str(env_config.get("caBundlePath", local_ca_bundle)).strip()
|
||||||
return {
|
return {
|
||||||
"supabase_url": url,
|
"supabase_url": url,
|
||||||
"supabase_key": key,
|
"supabase_key": key,
|
||||||
|
"ssl_verify": env_ssl_verify,
|
||||||
|
"ca_bundle_path": env_ca_bundle,
|
||||||
"environment": env_name,
|
"environment": env_name,
|
||||||
"source": "remote",
|
"source": "remote",
|
||||||
}
|
}
|
||||||
|
|
@ -213,6 +258,10 @@ def load_updater_connection_config(
|
||||||
# 2) 로컬 fallback
|
# 2) 로컬 fallback
|
||||||
url = str(fallback.get("supabase_url", "")).strip()
|
url = str(fallback.get("supabase_url", "")).strip()
|
||||||
key = str(fallback.get("anon_key", "")).strip()
|
key = str(fallback.get("anon_key", "")).strip()
|
||||||
|
fallback_ssl_verify = _parse_ssl_verify(
|
||||||
|
fallback.get("ssl_verify", local_ssl_verify), default=local_ssl_verify
|
||||||
|
)
|
||||||
|
fallback_ca_bundle = str(fallback.get("ca_bundle_path", local_ca_bundle)).strip()
|
||||||
env = environment or default_env
|
env = environment or default_env
|
||||||
|
|
||||||
if not url or not key:
|
if not url or not key:
|
||||||
|
|
@ -221,6 +270,8 @@ def load_updater_connection_config(
|
||||||
return {
|
return {
|
||||||
"supabase_url": url,
|
"supabase_url": url,
|
||||||
"supabase_key": key,
|
"supabase_key": key,
|
||||||
|
"ssl_verify": fallback_ssl_verify,
|
||||||
|
"ca_bundle_path": fallback_ca_bundle,
|
||||||
"environment": env,
|
"environment": env,
|
||||||
"source": "fallback",
|
"source": "fallback",
|
||||||
}
|
}
|
||||||
|
|
@ -327,6 +378,8 @@ class UpdateManager:
|
||||||
current_version: str = VERSION,
|
current_version: str = VERSION,
|
||||||
check_interval: int = 1,
|
check_interval: int = 1,
|
||||||
version_table: str = "program_version",
|
version_table: str = "program_version",
|
||||||
|
ssl_verify: bool = True,
|
||||||
|
ca_bundle_path: str = "",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
UpdateManager 초기화
|
UpdateManager 초기화
|
||||||
|
|
@ -344,6 +397,8 @@ class UpdateManager:
|
||||||
self.supabase_key = supabase_key
|
self.supabase_key = supabase_key
|
||||||
self.check_interval = check_interval
|
self.check_interval = check_interval
|
||||||
self.version_table = version_table
|
self.version_table = version_table
|
||||||
|
self.ssl_verify = ssl_verify
|
||||||
|
self.ca_bundle_path = ca_bundle_path
|
||||||
|
|
||||||
self._latest_version: Optional[VersionInfo] = None
|
self._latest_version: Optional[VersionInfo] = None
|
||||||
self._stop_flag = threading.Event()
|
self._stop_flag = threading.Event()
|
||||||
|
|
@ -407,9 +462,16 @@ class UpdateManager:
|
||||||
table_candidates.append("program_versions")
|
table_candidates.append("program_versions")
|
||||||
|
|
||||||
response = None
|
response = None
|
||||||
|
verify_option = _build_requests_verify_option(self.ssl_verify, self.ca_bundle_path)
|
||||||
for table_name in table_candidates:
|
for table_name in table_candidates:
|
||||||
url = f"{self.supabase_url}/rest/v1/{table_name}"
|
url = f"{self.supabase_url}/rest/v1/{table_name}"
|
||||||
response = requests.get(url, headers=headers, params=params, timeout=10)
|
response = requests.get(
|
||||||
|
url,
|
||||||
|
headers=headers,
|
||||||
|
params=params,
|
||||||
|
timeout=10,
|
||||||
|
verify=verify_option,
|
||||||
|
)
|
||||||
if response.status_code == 404 and table_name != table_candidates[-1]:
|
if response.status_code == 404 and table_name != table_candidates[-1]:
|
||||||
logger.warning(f"버전 테이블 미존재: {table_name}, 다음 후보 시도")
|
logger.warning(f"버전 테이블 미존재: {table_name}, 다음 후보 시도")
|
||||||
continue
|
continue
|
||||||
|
|
@ -449,6 +511,11 @@ class UpdateManager:
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
logger.warning("업데이트 서버 연결 시간 초과")
|
logger.warning("업데이트 서버 연결 시간 초과")
|
||||||
raise NetworkError("서버 연결 시간 초과")
|
raise NetworkError("서버 연결 시간 초과")
|
||||||
|
except requests.exceptions.SSLError as e:
|
||||||
|
logger.warning(f"업데이트 SSL 검증 실패: {e}")
|
||||||
|
raise NetworkError(
|
||||||
|
"SSL 인증서 검증에 실패했습니다. 내부망 인증서 사용 시 update.ssl_verify 또는 update.ca_bundle_path 설정을 확인하세요."
|
||||||
|
)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logger.warning(f"업데이트 확인 실패: {e}")
|
logger.warning(f"업데이트 확인 실패: {e}")
|
||||||
raise NetworkError(f"네트워크 오류: {e}")
|
raise NetworkError(f"네트워크 오류: {e}")
|
||||||
|
|
@ -623,6 +690,8 @@ def create_update_manager_from_settings(settings: dict) -> UpdateManager:
|
||||||
# settings.json에서 직접 지정 시 우선 사용, 없으면 updater/config.json 사용
|
# settings.json에서 직접 지정 시 우선 사용, 없으면 updater/config.json 사용
|
||||||
supabase_url = str(update_settings.get("supabase_url", "")).strip()
|
supabase_url = str(update_settings.get("supabase_url", "")).strip()
|
||||||
supabase_key = str(update_settings.get("supabase_key", "")).strip()
|
supabase_key = str(update_settings.get("supabase_key", "")).strip()
|
||||||
|
ssl_verify_raw = update_settings.get("ssl_verify", None)
|
||||||
|
ca_bundle_path = str(update_settings.get("ca_bundle_path", "")).strip()
|
||||||
|
|
||||||
if not supabase_url or not supabase_key:
|
if not supabase_url or not supabase_key:
|
||||||
config_path_raw = update_settings.get("connection_config_path", "")
|
config_path_raw = update_settings.get("connection_config_path", "")
|
||||||
|
|
@ -631,10 +700,16 @@ def create_update_manager_from_settings(settings: dict) -> UpdateManager:
|
||||||
conn = load_updater_connection_config(config_path=config_path, environment=env_name)
|
conn = load_updater_connection_config(config_path=config_path, environment=env_name)
|
||||||
supabase_url = conn["supabase_url"]
|
supabase_url = conn["supabase_url"]
|
||||||
supabase_key = conn["supabase_key"]
|
supabase_key = conn["supabase_key"]
|
||||||
|
if ssl_verify_raw is None:
|
||||||
|
ssl_verify_raw = conn.get("ssl_verify", True)
|
||||||
|
if not ca_bundle_path:
|
||||||
|
ca_bundle_path = str(conn.get("ca_bundle_path", "")).strip()
|
||||||
logger.info(
|
logger.info(
|
||||||
f"업데이터 연결 설정 로드: source={conn.get('source')} / environment={conn.get('environment')}"
|
f"업데이터 연결 설정 로드: source={conn.get('source')} / environment={conn.get('environment')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ssl_verify = _parse_ssl_verify(ssl_verify_raw, default=True)
|
||||||
|
|
||||||
return UpdateManager(
|
return UpdateManager(
|
||||||
supabase_url=supabase_url,
|
supabase_url=supabase_url,
|
||||||
supabase_key=supabase_key,
|
supabase_key=supabase_key,
|
||||||
|
|
@ -642,4 +717,6 @@ def create_update_manager_from_settings(settings: dict) -> UpdateManager:
|
||||||
current_version=update_settings.get("current_version", VERSION),
|
current_version=update_settings.get("current_version", VERSION),
|
||||||
check_interval=update_settings.get("check_interval_hours", 1),
|
check_interval=update_settings.get("check_interval_hours", 1),
|
||||||
version_table=update_settings.get("version_table", "program_version"),
|
version_table=update_settings.get("version_table", "program_version"),
|
||||||
|
ssl_verify=ssl_verify,
|
||||||
|
ca_bundle_path=ca_bundle_path,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@ import customtkinter as ctk
|
||||||
from view.theme import theme_manager
|
from view.theme import theme_manager
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from view.components.date_range_selector import DateRangeSelector
|
from view.components.date_range_selector import DateRangeSelector
|
||||||
|
from updater.__version__ import VERSION
|
||||||
|
|
||||||
class HistoryDialog(ctk.CTkToplevel):
|
class HistoryDialog(ctk.CTkToplevel):
|
||||||
def __init__(self, controller, data_rows):
|
def __init__(self, controller, data_rows):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.author = " by KH.Choi"
|
self.author = "KH.Choi"
|
||||||
self.title("VOC 수집 내역" + self.author)
|
self.title("VOC 수집 내역")
|
||||||
self.geometry("1200x700") # 너비 확장
|
self.geometry("1200x700") # 너비 확장
|
||||||
self.all_data = data_rows # 필터링용 원본 데이터
|
self.all_data = data_rows # 필터링용 원본 데이터
|
||||||
|
|
||||||
|
|
@ -35,6 +36,14 @@ class HistoryDialog(ctk.CTkToplevel):
|
||||||
self.btn_settings = ctk.CTkButton(self.toolbar, text="⚙️ 설정", width=100, command=self.open_settings, font=theme_manager.get_font(12))
|
self.btn_settings = ctk.CTkButton(self.toolbar, text="⚙️ 설정", width=100, command=self.open_settings, font=theme_manager.get_font(12))
|
||||||
self.btn_settings.pack(side="right", padx=5)
|
self.btn_settings.pack(side="right", padx=5)
|
||||||
|
|
||||||
|
self.meta_label = ctk.CTkLabel(
|
||||||
|
self.toolbar,
|
||||||
|
text=f"v{VERSION} | 제작 {self.author}",
|
||||||
|
font=theme_manager.get_font(11),
|
||||||
|
text_color=("gray45", "gray65")
|
||||||
|
)
|
||||||
|
self.meta_label.pack(side="right", padx=(0, 10))
|
||||||
|
|
||||||
self.btn_theme = ctk.CTkButton(self.toolbar, text="테마 변경", width=100, command=self.toggle_theme, font=theme_manager.get_font(12))
|
self.btn_theme = ctk.CTkButton(self.toolbar, text="테마 변경", width=100, command=self.toggle_theme, font=theme_manager.get_font(12))
|
||||||
self.btn_theme.pack(side="right", padx=5)
|
self.btn_theme.pack(side="right", padx=5)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class NotificationDialog(ctk.CTkToplevel):
|
||||||
|
|
||||||
# UI 구성
|
# UI 구성
|
||||||
self.width = 460
|
self.width = 460
|
||||||
self.height = 200
|
self.height = 400
|
||||||
self._center_window()
|
self._center_window()
|
||||||
|
|
||||||
# 프레임
|
# 프레임
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import customtkinter as ctk
|
||||||
from tkinter import filedialog
|
from tkinter import filedialog
|
||||||
from view.theme import theme_manager
|
from view.theme import theme_manager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from updater.__version__ import VERSION, APP_NAME
|
||||||
|
|
||||||
|
|
||||||
class HoverTooltip:
|
class HoverTooltip:
|
||||||
|
|
@ -69,8 +70,8 @@ class SettingsDialog(ctk.CTkToplevel):
|
||||||
def __init__(self, controller, parent=None):
|
def __init__(self, controller, parent=None):
|
||||||
super().__init__(master=parent)
|
super().__init__(master=parent)
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.author = " by KH.CHOI"
|
self.author = "KH.Choi"
|
||||||
self.title("VOC 모니터링 설정" + self.author)
|
self.title(f"{APP_NAME} 설정")
|
||||||
self.geometry("500x500")
|
self.geometry("500x500")
|
||||||
|
|
||||||
# parent가 있으면 transient 설정 (Z-order 문제 해결)
|
# parent가 있으면 transient 설정 (Z-order 문제 해결)
|
||||||
|
|
@ -95,6 +96,14 @@ class SettingsDialog(ctk.CTkToplevel):
|
||||||
self._init_system_tab()
|
self._init_system_tab()
|
||||||
self._init_notification_tab()
|
self._init_notification_tab()
|
||||||
|
|
||||||
|
self.meta_label = ctk.CTkLabel(
|
||||||
|
self,
|
||||||
|
text=f"버전 v{VERSION} | 제작 {self.author}",
|
||||||
|
font=theme_manager.get_font(11),
|
||||||
|
text_color=("gray45", "gray65")
|
||||||
|
)
|
||||||
|
self.meta_label.pack(pady=(0, 10))
|
||||||
|
|
||||||
def _init_login_tab(self):
|
def _init_login_tab(self):
|
||||||
font = theme_manager.get_font(12)
|
font = theme_manager.get_font(12)
|
||||||
ctk.CTkLabel(self.tab_login, text="사번 (ID)", font=font).pack(pady=5)
|
ctk.CTkLabel(self.tab_login, text="사번 (ID)", font=font).pack(pady=5)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue