Skip to content

Commit 25a23c4

Browse files
b4u-mwb-rowan
authored andcommitted
fix: pass http(s) URLs directly to devices instead of proxying
1 parent c83192d commit 25a23c4

2 files changed

Lines changed: 148 additions & 3 deletions

File tree

goosebit/updates/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,14 @@ async def generate_chunk(request: Request, device: Device) -> list[UpdateChunk]:
9898
if software is None:
9999
return []
100100

101-
# Always use the download endpoint for consistency, the endpoint
102-
# will handle both local and remote files appropriately.
103-
href = str(request.url_for("download_artifact", dev_id=device.id))
101+
# For remote http(s) URLs, pass the original URL directly to the device.
102+
# This preserves credentials in the URL and allows relative path resolution (e.g. for casync).
103+
# For s3:// or file:// URIs, use the download endpoint which handles proxying.
104+
parsed_uri = urlparse(software.uri)
105+
if parsed_uri.scheme in ("http", "https"):
106+
href = software.uri
107+
else:
108+
href = str(request.url_for("download_artifact", dev_id=device.id))
104109

105110
return [
106111
UpdateChunk(
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from typing import cast
2+
from unittest.mock import MagicMock
3+
4+
import pytest
5+
import pytest_asyncio
6+
7+
from goosebit.db.models import (
8+
Device,
9+
Hardware,
10+
Software,
11+
UpdateModeEnum,
12+
UpdateStateEnum,
13+
)
14+
from goosebit.updates import generate_chunk
15+
16+
17+
@pytest.fixture
18+
def mock_request() -> MagicMock:
19+
"""Create a mock request object."""
20+
request = MagicMock()
21+
request.url_for.return_value = "http://test/DEFAULT/controller/v1/test-device/download"
22+
return request
23+
24+
25+
@pytest_asyncio.fixture
26+
async def hardware(db: None) -> Hardware:
27+
"""Create test hardware."""
28+
return cast(Hardware, await Hardware.create(model="test-model", revision="1.0"))
29+
30+
31+
@pytest_asyncio.fixture
32+
async def device(db: None, hardware: Hardware) -> Device:
33+
"""Create test device."""
34+
return cast(
35+
Device,
36+
await Device.create(
37+
id="test-device",
38+
last_state=UpdateStateEnum.REGISTERED,
39+
update_mode=UpdateModeEnum.ASSIGNED,
40+
hardware=hardware,
41+
),
42+
)
43+
44+
45+
async def create_software_with_uri(uri: str, hardware: Hardware) -> Software:
46+
"""Helper to create software with a specific URI."""
47+
software = cast(
48+
Software,
49+
await Software.create(
50+
version="1.0.0",
51+
hash="testhash123",
52+
size=1024,
53+
uri=uri,
54+
),
55+
)
56+
await software.compatibility.add(hardware)
57+
return software
58+
59+
60+
@pytest.mark.asyncio
61+
async def test_generate_chunk_http_url_direct(mock_request: MagicMock, device: Device, hardware: Hardware) -> None:
62+
"""Test that http:// URLs are passed directly to the device."""
63+
uri = "http://example.com/firmware.swu"
64+
software = await create_software_with_uri(uri, hardware)
65+
device.assigned_software = software
66+
await device.save()
67+
68+
chunks = await generate_chunk(mock_request, device)
69+
70+
assert len(chunks) == 1
71+
assert chunks[0].artifacts[0].links["download"]["href"] == uri
72+
73+
74+
@pytest.mark.asyncio
75+
async def test_generate_chunk_https_url_direct(mock_request: MagicMock, device: Device, hardware: Hardware) -> None:
76+
"""Test that https:// URLs are passed directly to the device."""
77+
uri = "https://example.com/firmware.swu"
78+
software = await create_software_with_uri(uri, hardware)
79+
device.assigned_software = software
80+
await device.save()
81+
82+
chunks = await generate_chunk(mock_request, device)
83+
84+
assert len(chunks) == 1
85+
assert chunks[0].artifacts[0].links["download"]["href"] == uri
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_generate_chunk_https_with_credentials_direct(
90+
mock_request: MagicMock, device: Device, hardware: Hardware
91+
) -> None:
92+
"""Test that https:// URLs with credentials are passed directly, preserving credentials."""
93+
uri = "https://user:secretpass@example.com/firmware.swu"
94+
software = await create_software_with_uri(uri, hardware)
95+
device.assigned_software = software
96+
await device.save()
97+
98+
chunks = await generate_chunk(mock_request, device)
99+
100+
assert len(chunks) == 1
101+
assert chunks[0].artifacts[0].links["download"]["href"] == uri
102+
103+
104+
@pytest.mark.asyncio
105+
async def test_generate_chunk_file_url_proxied(mock_request: MagicMock, device: Device, hardware: Hardware) -> None:
106+
"""Test that file:// URLs use the proxy download endpoint."""
107+
uri = "file:///path/to/firmware.swu"
108+
software = await create_software_with_uri(uri, hardware)
109+
device.assigned_software = software
110+
await device.save()
111+
112+
chunks = await generate_chunk(mock_request, device)
113+
114+
assert len(chunks) == 1
115+
assert chunks[0].artifacts[0].links["download"]["href"] == "http://test/DEFAULT/controller/v1/test-device/download"
116+
mock_request.url_for.assert_called_with("download_artifact", dev_id=device.id)
117+
118+
119+
@pytest.mark.asyncio
120+
async def test_generate_chunk_s3_url_proxied(mock_request: MagicMock, device: Device, hardware: Hardware) -> None:
121+
"""Test that s3:// URLs use the proxy download endpoint."""
122+
uri = "s3://bucket-name/path/to/firmware.swu"
123+
software = await create_software_with_uri(uri, hardware)
124+
device.assigned_software = software
125+
await device.save()
126+
127+
chunks = await generate_chunk(mock_request, device)
128+
129+
assert len(chunks) == 1
130+
assert chunks[0].artifacts[0].links["download"]["href"] == "http://test/DEFAULT/controller/v1/test-device/download"
131+
mock_request.url_for.assert_called_with("download_artifact", dev_id=device.id)
132+
133+
134+
@pytest.mark.asyncio
135+
async def test_generate_chunk_no_software_assigned(mock_request: MagicMock, device: Device) -> None:
136+
"""Test that an empty list is returned when no software is assigned."""
137+
# Device has no assigned software and no rollout
138+
chunks = await generate_chunk(mock_request, device)
139+
140+
assert chunks == []

0 commit comments

Comments
 (0)