-
Notifications
You must be signed in to change notification settings - Fork 0
fix: trying to fix ssl certification issue while downloading on some macs #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # Package marker for Temoa Web GUI Backend |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,10 @@ | |
| from datetime import datetime | ||
| from pathlib import Path | ||
| from typing import List, Optional | ||
| import urllib.request | ||
| import shutil | ||
|
|
||
| from .utils import create_secure_ssl_context | ||
|
|
||
| from fastapi import ( | ||
| FastAPI, | ||
|
|
@@ -152,6 +156,36 @@ def list_files(path: str = "."): | |
| raise HTTPException(status_code=500, detail=str(e)) | ||
|
|
||
|
|
||
| @app.post("/api/download_tutorial") | ||
| def download_tutorial(): | ||
| """Downloads the tutorial database from the main repo.""" | ||
| assets_path = Path("assets") | ||
| assets_path.mkdir(parents=True, exist_ok=True) | ||
| target_path = assets_path / "tutorial_database.sqlite" | ||
| temp_path = target_path.with_suffix(".tmp") | ||
|
|
||
| try: | ||
| url = "https://raw.githubusercontent.com/TemoaProject/temoa-web-gui/main/assets/tutorial_database.sqlite" | ||
| ctx = create_secure_ssl_context() | ||
|
|
||
| with urllib.request.urlopen(url, context=ctx, timeout=10) as response: | ||
| with open(temp_path, "wb") as out_file: | ||
| shutil.copyfileobj(response, out_file) | ||
|
|
||
| # Atomic replace | ||
| temp_path.replace(target_path) | ||
| return {"status": "ok", "path": str(target_path.absolute())} | ||
| except Exception as e: | ||
| logging.exception("Failed to download tutorial") | ||
| raise HTTPException(status_code=500, detail=f"Download failed: {str(e)}") from e | ||
|
Comment on lines
+179
to
+180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider using a named logger for consistency with The endpoint uses ♻️ Proposed fixAdd at module level: logger = logging.getLogger(__name__)Then replace: - logging.exception("Failed to download tutorial")
+ logger.exception("Failed to download tutorial")🧰 Tools🪛 Ruff (0.14.14)[warning] 179-179: (LOG015) [warning] 180-180: Use explicit conversion flag Replace with conversion flag (RUF010) 🤖 Prompt for AI Agents |
||
| finally: | ||
| if temp_path.exists(): | ||
| try: | ||
| temp_path.unlink() | ||
| except Exception: | ||
| pass | ||
|
|
||
|
|
||
| @app.get("/api/solvers") | ||
| def list_solvers(): | ||
| """Detect available solvers on the local system.""" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import pytest | ||
| from unittest.mock import patch, MagicMock | ||
| from fastapi.testclient import TestClient | ||
| from backend.main import app | ||
| import ssl | ||
|
|
||
| client = TestClient(app) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| @pytest.mark.parametrize("skip_verify", ["0", "1"]) | ||
| def test_download_tutorial_ssl_context(skip_verify, monkeypatch): | ||
| """ | ||
| Test that the SSL context is correctly configured based on TEMOA_SKIP_CERT_VERIFY. | ||
| This test is parametrized to ensure deterministic behavior. | ||
| """ | ||
| monkeypatch.setenv("TEMOA_SKIP_CERT_VERIFY", skip_verify) | ||
|
|
||
| # Patch targets must be on the module that USES the functions | ||
| with patch("backend.main.urllib.request.urlopen") as mock_urlopen, patch( | ||
| "backend.main.shutil.copyfileobj" | ||
| ), patch("backend.main.open", new_callable=MagicMock), patch( | ||
| "backend.main.Path.replace" | ||
| ) as mock_replace, patch("backend.main.Path.unlink"), patch( | ||
| "backend.main.Path.exists", return_value=True | ||
| ): | ||
| # Configure the mock response | ||
| mock_response = MagicMock() | ||
| mock_urlopen.return_value.__enter__.return_value = mock_response | ||
|
|
||
| response = client.post("/api/download_tutorial") | ||
|
|
||
| assert response.status_code == 200 | ||
| assert response.json()["status"] == "ok" | ||
|
|
||
| # Verify SSL context and timeout | ||
| _, kwargs = mock_urlopen.call_args | ||
| assert kwargs.get("timeout") == 10 | ||
| assert "context" in kwargs | ||
| ctx = kwargs["context"] | ||
| assert isinstance(ctx, ssl.SSLContext) | ||
|
|
||
| if skip_verify == "1": | ||
| assert ctx.check_hostname is False | ||
| assert ctx.verify_mode == ssl.CERT_NONE | ||
| else: | ||
| assert ctx.check_hostname is True | ||
| assert ctx.verify_mode == ssl.CERT_REQUIRED | ||
|
|
||
| # Verify atomic move was attempted | ||
| assert mock_replace.called | ||
|
|
||
|
|
||
| def test_download_tutorial_failure(): | ||
| # Patch on the actual module to ensure it's intercepted | ||
| with patch( | ||
| "backend.main.urllib.request.urlopen", side_effect=Exception("Network error") | ||
| ): | ||
| response = client.post("/api/download_tutorial") | ||
| assert response.status_code == 500 | ||
| assert "Download failed" in response.json()["detail"] | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import ssl | ||
| import certifi | ||
| import os | ||
| import logging | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def create_secure_ssl_context(): | ||
| """ | ||
| Creates a secure SSL context using certifi's CA bundle. | ||
| Allows bypassing verification ONLY if TEMOA_SKIP_CERT_VERIFY is set to '1'. | ||
| """ | ||
| skip_verify = os.environ.get("TEMOA_SKIP_CERT_VERIFY") == "1" | ||
|
|
||
| if skip_verify: | ||
| logger.warning( | ||
| "SSL certificate verification is DISABLED via TEMOA_SKIP_CERT_VERIFY." | ||
| ) | ||
| ctx = ssl.create_default_context() | ||
| ctx.check_hostname = False | ||
| ctx.verify_mode = ssl.CERT_NONE | ||
| return ctx | ||
|
|
||
| # Secure default using certifi | ||
| ctx = ssl.create_default_context(cafile=certifi.where()) | ||
| return ctx | ||
|
coderabbitai[bot] marked this conversation as resolved.
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ dependencies = [ | |
| "tomlkit", | ||
| "datasette", | ||
| "websockets", | ||
| "certifi", | ||
| ] | ||
|
|
||
| [tool.pytest.ini_options] | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.