Skip to content

Commit 14a485c

Browse files
fix: trying to fix ssl certification issue while downloading on some macs
1 parent e26fa31 commit 14a485c

7 files changed

Lines changed: 140 additions & 1571 deletions

File tree

backend/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Package marker for Temoa Web GUI Backend

backend/main.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
from datetime import datetime
1616
from pathlib import Path
1717
from typing import List, Optional
18+
import urllib.request
19+
import shutil
20+
21+
from .utils import create_secure_ssl_context
1822

1923
from fastapi import (
2024
FastAPI,
@@ -152,6 +156,27 @@ def list_files(path: str = "."):
152156
raise HTTPException(status_code=500, detail=str(e))
153157

154158

159+
@app.post("/api/download_tutorial")
160+
def download_tutorial():
161+
"""Downloads the tutorial database from the main repo."""
162+
try:
163+
url = "https://raw.githubusercontent.com/TemoaProject/temoa-web-gui/main/assets/tutorial_database.sqlite"
164+
assets_path = Path("assets")
165+
assets_path.mkdir(exist_ok=True)
166+
target_path = assets_path / "tutorial_database.sqlite"
167+
168+
ctx = create_secure_ssl_context()
169+
170+
with urllib.request.urlopen(url, context=ctx) as response:
171+
with open(target_path, "wb") as out_file:
172+
shutil.copyfileobj(response, out_file)
173+
174+
return {"status": "ok", "path": str(target_path.absolute())}
175+
except Exception as e:
176+
logging.exception("Failed to download tutorial")
177+
raise HTTPException(status_code=500, detail=f"Download failed: {str(e)}") from e
178+
179+
155180
@app.get("/api/solvers")
156181
def list_solvers():
157182
"""Detect available solvers on the local system."""

backend/tests/test_download.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pytest
2+
from unittest.mock import patch, MagicMock
3+
from fastapi.testclient import TestClient
4+
from backend.main import app
5+
import ssl
6+
7+
client = TestClient(app)
8+
9+
10+
@pytest.mark.parametrize("skip_verify", ["0", "1"])
11+
def test_download_tutorial_ssl_context(skip_verify, monkeypatch):
12+
"""
13+
Test that the SSL context is correctly configured based on TEMOA_SKIP_CERT_VERIFY.
14+
This test is parametrized to ensure deterministic behavior.
15+
"""
16+
monkeypatch.setenv("TEMOA_SKIP_CERT_VERIFY", skip_verify)
17+
18+
# Patch targets must be on the module that USES the functions
19+
with patch("backend.main.urllib.request.urlopen") as mock_urlopen, patch(
20+
"backend.main.shutil.copyfileobj"
21+
), patch("backend.main.open", new_callable=MagicMock):
22+
# Configure the mock response
23+
mock_response = MagicMock()
24+
mock_urlopen.return_value.__enter__.return_value = mock_response
25+
26+
response = client.post("/api/download_tutorial")
27+
28+
assert response.status_code == 200
29+
assert response.json()["status"] == "ok"
30+
31+
# Verify SSL context
32+
_, kwargs = mock_urlopen.call_args
33+
assert "context" in kwargs
34+
ctx = kwargs["context"]
35+
assert isinstance(ctx, ssl.SSLContext)
36+
37+
if skip_verify == "1":
38+
assert ctx.check_hostname is False
39+
assert ctx.verify_mode == ssl.CERT_NONE
40+
else:
41+
assert ctx.check_hostname is True
42+
assert ctx.verify_mode == ssl.CERT_REQUIRED
43+
44+
45+
def test_download_tutorial_failure():
46+
# Patch on the actual module to ensure it's intercepted
47+
with patch(
48+
"backend.main.urllib.request.urlopen", side_effect=Exception("Network error")
49+
):
50+
response = client.post("/api/download_tutorial")
51+
assert response.status_code == 500
52+
assert "Download failed" in response.json()["detail"]

backend/utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import ssl
2+
import certifi
3+
import os
4+
import logging
5+
6+
logger = logging.getLogger(__name__)
7+
8+
9+
def create_secure_ssl_context():
10+
"""
11+
Creates a secure SSL context using certifi's CA bundle.
12+
Allows bypassing verification ONLY if TEMOA_SKIP_CERT_VERIFY is set to '1'.
13+
"""
14+
skip_verify = os.environ.get("TEMOA_SKIP_CERT_VERIFY") == "1"
15+
16+
if skip_verify:
17+
logger.warning(
18+
"SSL certificate verification is DISABLED via TEMOA_SKIP_CERT_VERIFY."
19+
)
20+
ctx = ssl.create_default_context()
21+
ctx.check_hostname = False
22+
ctx.verify_mode = ssl.CERT_NONE
23+
return ctx
24+
25+
# Secure default using certifi
26+
ctx = ssl.create_default_context(cafile=certifi.where())
27+
return ctx

pyproject.toml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,6 @@ version = "0.1.0"
44
description = "Web interface for Temoa Energy System Model"
55
readme = "README.md"
66
requires-python = ">=3.12"
7-
dependencies = [
8-
"temoa>=4.0.0a1",
9-
"fastapi",
10-
"uvicorn[standard]",
11-
"tomlkit",
12-
"datasette",
13-
"websockets",
14-
]
157

168
[tool.pytest.ini_options]
179
pythonpath = ["."]

temoa_runner.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,21 @@
77
# "tomlkit",
88
# "websockets",
99
# "datasette",
10+
# "certifi",
1011
# ]
1112
# ///
1213

1314
import asyncio
1415
import logging
1516
import sys
17+
import shutil
1618
from datetime import datetime
1719
from pathlib import Path
1820
from typing import List, Optional
1921
import urllib.request
22+
import os
23+
import certifi
24+
import ssl
2025

2126
from fastapi import (
2227
FastAPI,
@@ -29,6 +34,28 @@
2934
from fastapi.staticfiles import StaticFiles
3035
from pydantic import BaseModel
3136

37+
38+
def create_secure_ssl_context():
39+
"""
40+
Creates a secure SSL context using certifi's CA bundle.
41+
Allows bypassing verification ONLY if TEMOA_SKIP_CERT_VERIFY is set to '1'.
42+
"""
43+
skip_verify = os.environ.get("TEMOA_SKIP_CERT_VERIFY") == "1"
44+
45+
if skip_verify:
46+
logging.warning(
47+
"SSL certificate verification is DISABLED via TEMOA_SKIP_CERT_VERIFY."
48+
)
49+
ctx = ssl.create_default_context()
50+
ctx.check_hostname = False
51+
ctx.verify_mode = ssl.CERT_NONE
52+
return ctx
53+
54+
# Secure default using certifi
55+
ctx = ssl.create_default_context(cafile=certifi.where())
56+
return ctx
57+
58+
3259
# --- Temoa Imports ---
3360
# We assume temoa is installed in the same environment
3461
try:
@@ -119,12 +146,19 @@ def ensure_assets():
119146
assets_dir.mkdir(exist_ok=True)
120147

121148
files = ["tutorial_database.sqlite", "tutorial_config.toml"]
149+
150+
ctx = create_secure_ssl_context()
151+
122152
for f in files:
123153
target = assets_dir / f
124154
if not target.exists():
125155
print(f"Downloading missing asset: {f}...")
126156
try:
127-
urllib.request.urlretrieve(base_url + f, target)
157+
# Use urlopen with context instead of urlretrieve
158+
url = base_url + f
159+
with urllib.request.urlopen(url, context=ctx) as response:
160+
with open(target, "wb") as out_file:
161+
shutil.copyfileobj(response, out_file)
128162
except Exception as e:
129163
print(f"Failed to download {f}: {e}")
130164

0 commit comments

Comments
 (0)