|
54 | 54 | from rich.tree import Tree |
55 | 55 | from typer.core import TyperGroup |
56 | 56 |
|
| 57 | +from ._download_security import read_response_limited |
57 | 58 | from .integration_runtime import ( |
58 | 59 | invoke_separator_for_integration as _invoke_separator_for_integration, |
59 | 60 | resolve_integration_options as _resolve_integration_options_impl, |
@@ -1772,7 +1773,13 @@ def _fetch_latest_release_tag() -> tuple[str | None, str | None]: |
1772 | 1773 | req.add_header("Authorization", f"Bearer {token}") |
1773 | 1774 | try: |
1774 | 1775 | with urllib.request.urlopen(req, timeout=5) as resp: |
1775 | | - payload = json.loads(resp.read().decode("utf-8")) |
| 1776 | + payload = json.loads( |
| 1777 | + read_response_limited( |
| 1778 | + resp, |
| 1779 | + max_bytes=1024 * 1024, |
| 1780 | + label="GitHub latest release", |
| 1781 | + ).decode("utf-8") |
| 1782 | + ) |
1776 | 1783 | tag = payload.get("tag_name") |
1777 | 1784 | if not isinstance(tag, str) or not tag: |
1778 | 1785 | raise ValueError("GitHub API response missing valid tag_name") |
@@ -3376,8 +3383,10 @@ def preset_add( |
3376 | 3383 | zip_path = Path(tmpdir) / "preset.zip" |
3377 | 3384 | try: |
3378 | 3385 | with urllib.request.urlopen(from_url, timeout=60) as response: |
3379 | | - zip_path.write_bytes(response.read()) |
3380 | | - except urllib.error.URLError as e: |
| 3386 | + zip_path.write_bytes( |
| 3387 | + read_response_limited(response, label=f"preset {from_url}") |
| 3388 | + ) |
| 3389 | + except (urllib.error.URLError, ValueError) as e: |
3381 | 3390 | console.print(f"[red]Error:[/red] Failed to download: {e}") |
3382 | 3391 | raise typer.Exit(1) |
3383 | 3392 |
|
@@ -4280,12 +4289,15 @@ def extension_add( |
4280 | 4289 |
|
4281 | 4290 | try: |
4282 | 4291 | with urllib.request.urlopen(from_url, timeout=60) as response: |
4283 | | - zip_data = response.read() |
| 4292 | + zip_data = read_response_limited( |
| 4293 | + response, |
| 4294 | + label=f"extension {from_url}", |
| 4295 | + ) |
4284 | 4296 | zip_path.write_bytes(zip_data) |
4285 | 4297 |
|
4286 | 4298 | # Install from downloaded ZIP |
4287 | 4299 | manifest = manager.install_from_zip(zip_path, speckit_version, priority=priority) |
4288 | | - except urllib.error.URLError as e: |
| 4300 | + except (urllib.error.URLError, ValueError) as e: |
4289 | 4301 | console.print(f"[red]Error:[/red] Failed to download from {from_url}: {e}") |
4290 | 4302 | raise typer.Exit(1) |
4291 | 4303 | finally: |
@@ -5526,7 +5538,7 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None: |
5526 | 5538 | console.print(f"[red]Error:[/red] URL redirected to non-HTTPS: {final_url}") |
5527 | 5539 | raise typer.Exit(1) |
5528 | 5540 | with tempfile.NamedTemporaryFile(suffix=".yml", delete=False) as tmp: |
5529 | | - tmp.write(resp.read()) |
| 5541 | + tmp.write(read_response_limited(resp, label=f"workflow {source}")) |
5530 | 5542 | tmp_path = Path(tmp.name) |
5531 | 5543 | except typer.Exit: |
5532 | 5544 | raise |
@@ -5630,7 +5642,9 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None: |
5630 | 5642 | f"[red]Error:[/red] Workflow '{source}' redirected to non-HTTPS URL: {final_url}" |
5631 | 5643 | ) |
5632 | 5644 | raise typer.Exit(1) |
5633 | | - workflow_file.write_bytes(response.read()) |
| 5645 | + workflow_file.write_bytes( |
| 5646 | + read_response_limited(response, label=f"workflow {source}") |
| 5647 | + ) |
5634 | 5648 | except Exception as exc: |
5635 | 5649 | if workflow_dir.exists(): |
5636 | 5650 | import shutil |
|
0 commit comments