Skip to content

Commit b0aa40d

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

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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from unittest.mock import patch, MagicMock
2+
from fastapi.testclient import TestClient
3+
from backend.main import app
4+
import ssl
5+
6+
client = TestClient(app)
7+
8+
9+
def test_download_tutorial_success():
10+
# Mock urllib.request.urlopen and shutil.copyfileobj
11+
with patch("urllib.request.urlopen") as mock_urlopen, patch(
12+
"shutil.copyfileobj"
13+
), patch("builtins.open", new_callable=MagicMock):
14+
# Configure the mock response
15+
mock_response = MagicMock()
16+
mock_urlopen.return_value.__enter__.return_value = mock_response
17+
18+
response = client.post("/api/download_tutorial")
19+
20+
assert response.status_code == 200
21+
assert response.json()["status"] == "ok"
22+
assert "tutorial_database.sqlite" in response.json()["path"]
23+
24+
# Verify URL and SSL context
25+
args, kwargs = mock_urlopen.call_args
26+
assert (
27+
args[0]
28+
== "https://raw.githubusercontent.com/TemoaProject/temoa-web-gui/main/assets/tutorial_database.sqlite"
29+
)
30+
assert "context" in kwargs
31+
ctx = kwargs["context"]
32+
assert isinstance(ctx, ssl.SSLContext)
33+
34+
# The SSL context should be secure by default (using certifi)
35+
# unless TEMOA_SKIP_CERT_VERIFY is set.
36+
import os
37+
38+
if os.environ.get("TEMOA_SKIP_CERT_VERIFY") == "1":
39+
assert ctx.check_hostname is False
40+
assert ctx.verify_mode == ssl.CERT_NONE
41+
else:
42+
# By default it should be secure
43+
assert ctx.check_hostname is True
44+
assert ctx.verify_mode == ssl.CERT_REQUIRED
45+
46+
47+
def test_download_tutorial_failure():
48+
# Patch on the actual module to ensure it's intercepted
49+
with patch(
50+
"backend.main.urllib.request.urlopen", side_effect=Exception("Network error")
51+
):
52+
response = client.post("/api/download_tutorial")
53+
assert response.status_code == 500
54+
assert "Download failed" in response.json()["detail"]

backend/utils.py

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