diff --git a/build.py b/build.py index 41bc0d0..c61c91e 100644 --- a/build.py +++ b/build.py @@ -117,6 +117,14 @@ def build(build: bool = True, copy: bool = True, test: bool = False, dest: str | name_used = f"{base_name}_{ts}" print(f"Previous artifact appears locked; building as {name_used} to avoid PermissionError") + # Ensure cert.pem is included for test builds as well (if present) + cert_path = project_root / "cert.pem" + if cert_path.exists(): + arg = f"{str(cert_path)};." + # Avoid adding duplicate entries + if arg not in add_data_args: + add_data_args += ["--add-data", arg] + PyInstaller.__main__.run([ str(project_root / "nice.py"), "--onefile", diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..3b36397 --- /dev/null +++ b/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhzCCAm+gAwIBAgIQHLnU4uDoKaJAaqa5Yi/NoTANBgkqhkiG9w0BAQUFADBW +MQswCQYDVQQGEwJHQjERMA8GA1UECBMIQ29ybndhbGwxFTATBgNVBAoTDENvcm53 +YWxsIE5IUzEdMBsGA1UEAxMUQ29ybndhbGwgSUNUIFJvb3QgQ0EwHhcNMTMwNjI2 +MDkwNjExWhcNMzMwNjI2MDkxNjA1WjBWMQswCQYDVQQGEwJHQjERMA8GA1UECBMI +Q29ybndhbGwxFTATBgNVBAoTDENvcm53YWxsIE5IUzEdMBsGA1UEAxMUQ29ybndh +bGwgSUNUIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCm +m8jF64E3wkxhf2BLfD3lT3v9RebGhJ684jtv0A6qL2BTe/Co4SJ/obYiDw82K4Vw +1O+jwIg77hVrH/YaCsjPiefxxwL/T0bSURlXq5Q8pCQaHjd1iH1JNY1WfW0Ywkni +a7xbm6/2+zwsyTHfhuSU+zmSQxejOX+Ffz6MWZSb+JjppOH6OBKTHGw69JIXcAUu +wT3GSAdYyYKgP6i6jfsEeJkq+s/hd6cVxFWPrWJJaGtfIvLZT38vZeitYDYT9Ad0 +gIQPDyLH35tuNszFWkU5DA/U6xXvWkAV7Mq/EaGSVHckGeQiGAQNmV74KugqpcC/ +9reOFKEbc6qvDdGaASjZAgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBShR/WOiN4mvXi2tYLGtbeAaCXE7DAQBgkrBgEEAYI3 +FQEEAwIBADANBgkqhkiG9w0BAQUFAAOCAQEAEMca1BLnKJIXRZhJUTfLrSWtQDgV +T1jGnHFKngPxWUB1u0fVwpnqeuHSWQ2qkNvEZoBQFbJhEny83r8thLamJB1UHEyj +yhWrtjif7dne3LwzLtmiMZIKPjeyCa+zzl5YW+lUenQErl48PC7sJc5gq4lZajOo +fsEtw5zZLWFqXE/KJNt4vrGhQ0SplphbOzau7VrFIFciiKHHHw3rnV5byjqYDy/Z +IJytye488Pi75lBKq8pWW6YbUFmKly5bqF6Q8oicd9y0/9GGmTZGrzp1WbspYF9Y +lc2p6bkhfQ1f7Q+8/sF0D/sGfjfo099OlRhc881Mwt5p8mEZtJTxk3SZlQ== +-----END CERTIFICATE----- diff --git a/nice.py b/nice.py index 250dd1e..9fac182 100644 --- a/nice.py +++ b/nice.py @@ -14,6 +14,7 @@ from datetime import datetime from loguru import logger import sys +import os from anonymiser import Anonymizer import pydicom @@ -41,7 +42,11 @@ import importlib.util # traceback was previously imported for debugging but is unused -# TODO fix ssl +# SSL verification setting. Can be a boolean (True/False) or a path to +# a CA bundle (PEM file) used by requests' 'verify' parameter. +# Default is True. It can be overridden via CLI options (--ca-bundle, +# --insecure) or environment variables: NICE_UPLOADER_CA_BUNDLE, +# REQUESTS_CA_BUNDLE or SSL_CERT_FILE. VERIFY_SSL = True SOCKET_PATH = "tcp://localhost:9976" @@ -79,8 +84,14 @@ logging.getLogger('niceGUI').setLevel(logging.INFO) # application is frozen into a single executable (PyInstaller --onefile). try: if getattr(sys, "frozen", False): - # When frozen, place logs next to the executable - base_path = Path(sys.executable).resolve().parent + # When frozen by PyInstaller, data files are extracted to a temporary + # folder available as sys._MEIPASS. Fall back to the executable + # directory when _MEIPASS is not present. + meipass = getattr(sys, "_MEIPASS", None) + if meipass: + base_path = Path(meipass) + else: + base_path = Path(sys.executable).resolve().parent else: base_path = Path(__file__).resolve().parent except Exception: @@ -707,7 +718,11 @@ def launch_app( work_dir: Path = typer.Option(WORK_DIR, help="Working directory"), nng: bool = typer.Option(True, help="Use nng"), native_mode: bool = typer.Option(False, help="Use native mode"), - debug: bool = typer.Option(False, help="Debug mode") + debug: bool = typer.Option(False, help="Debug mode"), + ca_bundle: Optional[Path] = typer.Option( + None, help="Path to a CA bundle (PEM file) to verify HTTPS connections" + ), + insecure: bool = typer.Option(False, help="Disable SSL verification (insecure)"), ): global WORK_DIR, EXPORT_PATH, PROCESS_PATH, ANON_PATH, DEBUG @@ -719,6 +734,52 @@ def launch_app( logger.debug(f"Work dir: {WORK_DIR}") + # Configure SSL verification behaviour. VERIFY_SSL can be: + # - True (default): use system certs + # - False: disable verification (insecure) + # - path (str): path to CA bundle PEM file used by requests + global VERIFY_SSL + + env_ca = ( + os.environ.get("NICE_UPLOADER_CA_BUNDLE") + or os.environ.get("REQUESTS_CA_BUNDLE") + or os.environ.get("SSL_CERT_FILE") + ) + + if insecure: + VERIFY_SSL = False + logger.warning( + "SSL verification disabled (insecure). Only use for troubleshooting in trusted networks." + ) + elif ca_bundle is not None: + VERIFY_SSL = str(ca_bundle) + logger.info(f"Using CA bundle from CLI: {VERIFY_SSL}") + elif env_ca: + VERIFY_SSL = env_ca + logger.info(f"Using CA bundle from environment: {VERIFY_SSL}") + else: + VERIFY_SSL = True + + # If verification is disabled, suppress urllib3 insecure warnings to keep logs clean + if VERIFY_SSL is False: + try: + import urllib3 + + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + except Exception: + pass + + # If a bundled cert.pem was included in the frozen app, prefer that + # when the user hasn't explicitly provided a CA bundle or disabled + # verification. This lets builders ship a corporate CA with the app. + try: + bundled = base_path / "cert.pem" + if bundled.exists() and VERIFY_SSL is True: + VERIFY_SSL = str(bundled) + logger.info(f"Using bundled CA bundle at {VERIFY_SSL}") + except Exception: + pass + # When running as a frozen executable, the native/webview backend # (which depends on pythonnet) can fail to load inside the bundled # runtime. Disable native mode automatically for frozen builds so the diff --git a/nice_uploader.spec b/nice_uploader.spec index 4a82542..bcafa78 100644 --- a/nice_uploader.spec +++ b/nice_uploader.spec @@ -8,7 +8,12 @@ here = Path(__file__).resolve().parent project_root = here # Prepare datas and binaries lists; include icon resources -datas = [(str(project_root / 'icon'), 'icon'), (str(project_root / 'icon' / 'icon1.png'), 'icon')] +datas = [ + (str(project_root / 'icon'), 'icon'), + (str(project_root / 'icon' / 'icon1.png'), 'icon'), + # include user-supplied CA bundle if present so frozen app can use it + (str(project_root / 'cert.pem'), '.') +] binaries = [] # Try to include pythonnet runtime DLLs so Python.Runtime.dll is available