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:
9700X_PC 2026-02-18 17:04:52 +09:00
parent 501b4e3af6
commit e67e7a5572
5 changed files with 107 additions and 8 deletions

View File

@ -222,6 +222,8 @@ class AppController:
self.settings['update'].setdefault('check_interval_hours', 1)
self.settings['update'].setdefault('program_id', 'voc_monitor')
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("설정 파일 로드 완료")
except FileNotFoundError:
# 기본 설정 생성
@ -247,7 +249,9 @@ class AppController:
"environment": "main",
"check_interval_hours": 1,
"program_id": "voc_monitor",
"version_table": "program_versions"
"version_table": "program_versions",
"ssl_verify": True,
"ca_bundle_path": ""
},
"report": {
"output_path": str(get_data_dir() / "reports")

View File

@ -87,6 +87,38 @@ class ConfigError(UpdateError):
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:
"""JSON 문자열의 주석(//, /* */) 제거"""
result = []
@ -176,6 +208,8 @@ def load_updater_connection_config(
fallback = local_config.get("fallback", {})
default_env = local_config.get("default_environment", "main")
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) 원격 설정 시도
if config_url:
@ -185,7 +219,12 @@ def load_updater_connection_config(
"Pragma": "no-cache",
"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()
remote_raw = _strip_json_comments(response.text)
remote = json.loads(remote_raw)
@ -201,9 +240,15 @@ def load_updater_connection_config(
url = env_config.get("supabaseUrl", "").strip()
key = env_config.get("anonKey", "").strip()
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 {
"supabase_url": url,
"supabase_key": key,
"ssl_verify": env_ssl_verify,
"ca_bundle_path": env_ca_bundle,
"environment": env_name,
"source": "remote",
}
@ -213,6 +258,10 @@ def load_updater_connection_config(
# 2) 로컬 fallback
url = str(fallback.get("supabase_url", "")).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
if not url or not key:
@ -221,6 +270,8 @@ def load_updater_connection_config(
return {
"supabase_url": url,
"supabase_key": key,
"ssl_verify": fallback_ssl_verify,
"ca_bundle_path": fallback_ca_bundle,
"environment": env,
"source": "fallback",
}
@ -327,6 +378,8 @@ class UpdateManager:
current_version: str = VERSION,
check_interval: int = 1,
version_table: str = "program_version",
ssl_verify: bool = True,
ca_bundle_path: str = "",
):
"""
UpdateManager 초기화
@ -344,6 +397,8 @@ class UpdateManager:
self.supabase_key = supabase_key
self.check_interval = check_interval
self.version_table = version_table
self.ssl_verify = ssl_verify
self.ca_bundle_path = ca_bundle_path
self._latest_version: Optional[VersionInfo] = None
self._stop_flag = threading.Event()
@ -407,9 +462,16 @@ class UpdateManager:
table_candidates.append("program_versions")
response = None
verify_option = _build_requests_verify_option(self.ssl_verify, self.ca_bundle_path)
for table_name in table_candidates:
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]:
logger.warning(f"버전 테이블 미존재: {table_name}, 다음 후보 시도")
continue
@ -449,6 +511,11 @@ class UpdateManager:
except requests.exceptions.Timeout:
logger.warning("업데이트 서버 연결 시간 초과")
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:
logger.warning(f"업데이트 확인 실패: {e}")
raise NetworkError(f"네트워크 오류: {e}")
@ -623,6 +690,8 @@ def create_update_manager_from_settings(settings: dict) -> UpdateManager:
# settings.json에서 직접 지정 시 우선 사용, 없으면 updater/config.json 사용
supabase_url = str(update_settings.get("supabase_url", "")).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:
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)
supabase_url = conn["supabase_url"]
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(
f"업데이터 연결 설정 로드: source={conn.get('source')} / environment={conn.get('environment')}"
)
ssl_verify = _parse_ssl_verify(ssl_verify_raw, default=True)
return UpdateManager(
supabase_url=supabase_url,
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),
check_interval=update_settings.get("check_interval_hours", 1),
version_table=update_settings.get("version_table", "program_version"),
ssl_verify=ssl_verify,
ca_bundle_path=ca_bundle_path,
)

View File

@ -2,13 +2,14 @@ import customtkinter as ctk
from view.theme import theme_manager
from datetime import datetime
from view.components.date_range_selector import DateRangeSelector
from updater.__version__ import VERSION
class HistoryDialog(ctk.CTkToplevel):
def __init__(self, controller, data_rows):
super().__init__()
self.controller = controller
self.author = " by KH.Choi"
self.title("VOC 수집 내역" + self.author)
self.author = "KH.Choi"
self.title("VOC 수집 내역")
self.geometry("1200x700") # 너비 확장
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.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.pack(side="right", padx=5)

View File

@ -13,7 +13,7 @@ class NotificationDialog(ctk.CTkToplevel):
# UI 구성
self.width = 460
self.height = 200
self.height = 400
self._center_window()
# 프레임

View File

@ -2,6 +2,7 @@ import customtkinter as ctk
from tkinter import filedialog
from view.theme import theme_manager
from pathlib import Path
from updater.__version__ import VERSION, APP_NAME
class HoverTooltip:
@ -69,8 +70,8 @@ class SettingsDialog(ctk.CTkToplevel):
def __init__(self, controller, parent=None):
super().__init__(master=parent)
self.controller = controller
self.author = " by KH.CHOI"
self.title("VOC 모니터링 설정" + self.author)
self.author = "KH.Choi"
self.title(f"{APP_NAME} 설정")
self.geometry("500x500")
# parent가 있으면 transient 설정 (Z-order 문제 해결)
@ -95,6 +96,14 @@ class SettingsDialog(ctk.CTkToplevel):
self._init_system_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):
font = theme_manager.get_font(12)
ctk.CTkLabel(self.tab_login, text="사번 (ID)", font=font).pack(pady=5)