Skip to content

Commit 0f0e19d

Browse files
authored
Merge pull request #128 from ramusbucket/fix-proxy-cert
fix: Refactor HTTP client usage to utilize truststore for SSL context
2 parents 21b3dbf + 78e6c99 commit 0f0e19d

File tree

2 files changed

+32
-21
lines changed

2 files changed

+32
-21
lines changed

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "specify-cli"
3-
version = "0.0.2"
3+
version = "0.0.3"
44
description = "Setup tool for Specify spec-driven development projects"
55
requires-python = ">=3.11"
66
dependencies = [
@@ -9,6 +9,7 @@ dependencies = [
99
"httpx",
1010
"platformdirs",
1111
"readchar",
12+
"truststore>=0.10.4",
1213
]
1314

1415
[project.scripts]
@@ -19,4 +20,4 @@ requires = ["hatchling"]
1920
build-backend = "hatchling.build"
2021

2122
[tool.hatch.build.targets.wheel]
22-
packages = ["src/specify_cli"]
23+
packages = ["src/specify_cli"]

src/specify_cli/__init__.py

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646

4747
# For cross-platform keyboard input
4848
import readchar
49+
import ssl
50+
import truststore
51+
52+
ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
53+
client = httpx.Client(verify=ssl_context)
4954

5055
# Constants
5156
AI_CHOICES = {
@@ -385,19 +390,18 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
385390
os.chdir(original_cwd)
386391

387392

388-
def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True):
389-
"""Download the latest template release from GitHub using HTTP requests.
390-
Returns (zip_path, metadata_dict)
391-
"""
393+
def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True, client: httpx.Client = None):
392394
repo_owner = "github"
393395
repo_name = "spec-kit"
396+
if client is None:
397+
client = httpx.Client(verify=ssl_context)
394398

395399
if verbose:
396400
console.print("[cyan]Fetching latest release information...[/cyan]")
397401
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
398402

399403
try:
400-
response = httpx.get(api_url, timeout=30, follow_redirects=True)
404+
response = client.get(api_url, timeout=30, follow_redirects=True)
401405
response.raise_for_status()
402406
release_data = response.json()
403407
except httpx.RequestError as e:
@@ -437,18 +441,15 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
437441
console.print(f"[cyan]Downloading template...[/cyan]")
438442

439443
try:
440-
with httpx.stream("GET", download_url, timeout=30, follow_redirects=True) as response:
444+
with client.stream("GET", download_url, timeout=30, follow_redirects=True) as response:
441445
response.raise_for_status()
442446
total_size = int(response.headers.get('content-length', 0))
443-
444447
with open(zip_path, 'wb') as f:
445448
if total_size == 0:
446-
# No content-length header, download without progress
447449
for chunk in response.iter_bytes(chunk_size=8192):
448450
f.write(chunk)
449451
else:
450452
if show_progress:
451-
# Show progress bar
452453
with Progress(
453454
SpinnerColumn(),
454455
TextColumn("[progress.description]{task.description}"),
@@ -462,10 +463,8 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
462463
downloaded += len(chunk)
463464
progress.update(task, completed=downloaded)
464465
else:
465-
# Silent download loop
466466
for chunk in response.iter_bytes(chunk_size=8192):
467467
f.write(chunk)
468-
469468
except httpx.RequestError as e:
470469
if verbose:
471470
console.print(f"[red]Error downloading template:[/red] {e}")
@@ -483,7 +482,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
483482
return zip_path, metadata
484483

485484

486-
def download_and_extract_template(project_path: Path, ai_assistant: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None) -> Path:
485+
def download_and_extract_template(project_path: Path, ai_assistant: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None) -> Path:
487486
"""Download the latest release and extract it to create a new project.
488487
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
489488
"""
@@ -497,12 +496,13 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
497496
ai_assistant,
498497
current_dir,
499498
verbose=verbose and tracker is None,
500-
show_progress=(tracker is None)
499+
show_progress=(tracker is None),
500+
client=client
501501
)
502502
if tracker:
503503
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
504504
tracker.add("download", "Download template")
505-
tracker.complete("download", meta['filename']) # already downloaded inside helper
505+
tracker.complete("download", meta['filename'])
506506
except Exception as e:
507507
if tracker:
508508
tracker.error("fetch", str(e))
@@ -642,6 +642,7 @@ def init(
642642
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
643643
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
644644
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
645+
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
645646
):
646647
"""
647648
Initialize a new Specify project from the latest template.
@@ -770,7 +771,12 @@ def init(
770771
with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
771772
tracker.attach_refresh(lambda: live.update(tracker.render()))
772773
try:
773-
download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker)
774+
# Create a httpx client with verify based on skip_tls
775+
verify = not skip_tls
776+
local_ssl_context = ssl_context if verify else False
777+
local_client = httpx.Client(verify=local_ssl_context)
778+
779+
download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker, client=local_client)
774780

775781
# Git step
776782
if not no_git:
@@ -835,21 +841,25 @@ def init(
835841
# Removed farewell line per user request
836842

837843

844+
# Add skip_tls option to check
838845
@app.command()
839-
def check():
846+
def check(skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)")):
840847
"""Check that all required tools are installed."""
841848
show_banner()
842849
console.print("[bold]Checking Specify requirements...[/bold]\n")
843-
850+
844851
# Check if we have internet connectivity by trying to reach GitHub API
845852
console.print("[cyan]Checking internet connectivity...[/cyan]")
853+
verify = not skip_tls
854+
local_ssl_context = ssl_context if verify else False
855+
local_client = httpx.Client(verify=local_ssl_context)
846856
try:
847-
response = httpx.get("https://api.github.com", timeout=5, follow_redirects=True)
857+
response = local_client.get("https://api.github.com", timeout=5, follow_redirects=True)
848858
console.print("[green]✓[/green] Internet connection available")
849859
except httpx.RequestError:
850860
console.print("[red]✗[/red] No internet connection - required for downloading templates")
851861
console.print("[yellow]Please check your internet connection[/yellow]")
852-
862+
853863
console.print("\n[cyan]Optional tools:[/cyan]")
854864
git_ok = check_tool("git", "https://git-scm.com/downloads")
855865

0 commit comments

Comments
 (0)