Skip to content

Commit

Permalink
fix(ota_proxy): fix content-encoding header is not passed through in …
Browse files Browse the repository at this point in the history
…direct proxy mode (#445)

This PR fixes a bug that prevents ota_proxy properly handling request of file being gzip by remote HTTP server. The cause of bug is ota_proxy doesn't include content-encoding header in response back to client when in direct proxy passthrough mode.

The bug is fixed by using multidict when parsing/preparing headers, so multidict<7.0,>=4.5 is added to the direct dep of otaclient. Note that multidict is used by aiohttp, so it is already a indirect dep of otaclient.
  • Loading branch information
Bodong-Yang authored Dec 2, 2024
1 parent 3d3009b commit f72f22f
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 24 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
"aiohttp>=3.10.11,<3.12",
"cryptography>=43.0.1,<45",
"grpcio>=1.53.2,<1.69",
"multidict<7.0,>=4.5",
"protobuf>=4.21.12,<5.29",
"pydantic<3,>=2.6",
"pydantic-settings<3,>=2.3",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ aiofiles<25,>=24.1
aiohttp>=3.10.11,<3.12
cryptography>=43.0.1,<45
grpcio>=1.53.2,<1.69
multidict<7.0,>=4.5
protobuf>=4.21.12,<5.29
pydantic<3,>=2.6
pydantic-settings<3,>=2.3
Expand Down
2 changes: 2 additions & 0 deletions src/ota_proxy/_consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
HEADER_AUTHORIZATION = istr("authorization")
HEADER_COOKIE = istr("cookie")
HEADER_CONTENT_ENCODING = istr("content-encoding")
HEADER_CONTENT_TYPE = istr("content-type")
BHEADER_OTA_FILE_CACHE_CONTROL = OTAFileCacheControl.HEADER_LOWERCASE.encode("utf-8")
BHEADER_AUTHORIZATION = b"authorization"
BHEADER_COOKIE = b"cookie"
BHEADER_CONTENT_ENCODING = b"content-encoding"
BHEADER_CONTENT_TYPE = b"content-type"
5 changes: 3 additions & 2 deletions src/ota_proxy/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pathlib import Path
from typing import Optional

from multidict import CIMultiDict
from pydantic import SkipValidation
from simple_sqlite3_orm import (
ConstrainRepr,
Expand Down Expand Up @@ -99,12 +100,12 @@ class CacheMeta(TableSpec):
def __hash__(self) -> int:
return hash(tuple(getattr(self, attrn) for attrn in self.model_fields))

def export_headers_to_client(self) -> dict[str, str]:
def export_headers_to_client(self) -> CIMultiDict[str]:
"""Export required headers for client.
Currently includes content-type, content-encoding and ota-file-cache-control headers.
"""
res = {}
res = CIMultiDict()
if self.content_encoding:
res[HEADER_CONTENT_ENCODING] = self.content_encoding

Expand Down
28 changes: 17 additions & 11 deletions src/ota_proxy/ota_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from urllib.parse import SplitResult, quote, urlsplit

import aiohttp
from multidict import CIMultiDictProxy
from multidict import CIMultiDict, CIMultiDictProxy

from otaclient_common.common import get_backoff
from otaclient_common.typing import StrOrPath
Expand Down Expand Up @@ -409,7 +409,7 @@ async def _do_request() -> AsyncIterator[bytes]:

async def _retrieve_file_by_cache(
self, cache_identifier: str, *, retry_cache: bool
) -> Optional[Tuple[AsyncIterator[bytes], Mapping[str, str]]]:
) -> tuple[AsyncIterator[bytes], CIMultiDict[str]] | None:
"""
Returns:
A tuple of bytes iterator and headers dict for back to client.
Expand Down Expand Up @@ -458,7 +458,8 @@ async def _retrieve_file_by_cache(

async def _retrieve_file_by_external_cache(
self, client_cache_policy: OTAFileCacheControl
) -> tuple[AsyncIterator[bytes], Mapping[str, str]] | None:
) -> tuple[AsyncIterator[bytes], CIMultiDict[str]] | None:
# skip if not external cache or otaclient doesn't sent valid file_sha256
if not self._external_cache_data_dir or not client_cache_policy.file_sha256:
return

Expand All @@ -469,26 +470,31 @@ async def _retrieve_file_by_external_cache(
)

if cache_file_zst.is_file():
return read_file(cache_file_zst, executor=self._executor), {
HEADER_OTA_FILE_CACHE_CONTROL: OTAFileCacheControl.export_kwargs_as_header(
_header = CIMultiDict()
_header[HEADER_OTA_FILE_CACHE_CONTROL] = (
OTAFileCacheControl.export_kwargs_as_header(
file_sha256=cache_identifier,
file_compression_alg=cfg.EXTERNAL_CACHE_STORAGE_COMPRESS_ALG,
)
}
elif cache_file.is_file():
return read_file(cache_file, executor=self._executor), {
HEADER_OTA_FILE_CACHE_CONTROL: OTAFileCacheControl.export_kwargs_as_header(
)
return read_file(cache_file_zst, executor=self._executor), _header

if cache_file.is_file():
_header = CIMultiDict()
_header[HEADER_OTA_FILE_CACHE_CONTROL] = (
OTAFileCacheControl.export_kwargs_as_header(
file_sha256=cache_identifier
)
}
)
return read_file(cache_file, executor=self._executor), _header

# exposed API

async def retrieve_file(
self,
raw_url: str,
headers_from_client: Dict[str, str],
) -> Optional[Tuple[AsyncIterator[bytes], Mapping[str, str]]]:
) -> tuple[AsyncIterator[bytes], CIMultiDict[str] | CIMultiDictProxy[str]] | None:
"""Retrieve a file descriptor for the requested <raw_url>.
This method retrieves a file descriptor for incoming client request.
Expand Down
30 changes: 19 additions & 11 deletions src/ota_proxy/server_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,23 @@
import logging
from contextlib import asynccontextmanager
from http import HTTPStatus
from typing import Dict, List, Mapping, Tuple, Union
from typing import Dict, List, Tuple, Union
from urllib.parse import urlparse

import aiohttp
from multidict import CIMultiDict, CIMultiDictProxy

from otaclient_common.logging import BurstSuppressFilter

from ._consts import (
BHEADER_AUTHORIZATION,
BHEADER_CONTENT_ENCODING,
BHEADER_CONTENT_TYPE,
BHEADER_COOKIE,
BHEADER_OTA_FILE_CACHE_CONTROL,
HEADER_AUTHORIZATION,
HEADER_CONTENT_ENCODING,
HEADER_CONTENT_TYPE,
HEADER_COOKIE,
HEADER_OTA_FILE_CACHE_CONTROL,
METHOD_GET,
Expand Down Expand Up @@ -88,21 +91,26 @@ def parse_raw_headers(raw_headers: List[Tuple[bytes, bytes]]) -> Dict[str, str]:
return headers


def encode_headers(headers: Mapping[str, str]) -> List[Tuple[bytes, bytes]]:
def encode_headers(
headers: Union[CIMultiDict[str], CIMultiDictProxy[str]]
) -> List[Tuple[bytes, bytes]]:
"""Encode headers dict to list of bytes tuples for sending back to client.
Uvicorn requests application to pre-process headers to bytes.
Currently we only need to send content-encoding and ota-file-cache-control header
back to client.
Currently we send the following headers back to client:
1. content-encoding
2. content-type
3. ota-file-cache-control header
"""
bytes_headers: List[Tuple[bytes, bytes]] = []
for name, value in headers.items():
if name == HEADER_CONTENT_ENCODING and value:
bytes_headers.append((BHEADER_CONTENT_ENCODING, value.encode("utf-8")))
elif name == HEADER_OTA_FILE_CACHE_CONTROL and value:
bytes_headers.append(
(BHEADER_OTA_FILE_CACHE_CONTROL, value.encode("utf-8"))
)
if _encoding := headers.get(HEADER_CONTENT_ENCODING):
bytes_headers.append((BHEADER_CONTENT_ENCODING, _encoding.encode("utf-8")))
if _type := headers.get(HEADER_CONTENT_TYPE):
bytes_headers.append((BHEADER_CONTENT_TYPE, _type.encode("utf-8")))
if _cache_control := headers.get(HEADER_OTA_FILE_CACHE_CONTROL):
bytes_headers.append(
(BHEADER_OTA_FILE_CACHE_CONTROL, _cache_control.encode("utf-8"))
)
return bytes_headers


Expand Down

1 comment on commit f72f22f

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/ota_metadata/legacy
   __init__.py10100% 
   parser.py3354885%106, 170, 175, 211–212, 222–223, 226, 238, 289–291, 295–298, 324–327, 396, 399, 407–409, 422, 431–432, 435–436, 601–603, 653–654, 657, 685–686, 689–690, 692, 696, 698–699, 753, 756–758
   types.py841384%37, 40–42, 112–116, 122–125
src/ota_metadata/utils
   cert_store.py86890%58–59, 73, 87, 91, 102, 123, 127
src/ota_proxy
   __init__.py15660%48, 50, 52, 61, 71–72
   __main__.py880%16, 18–20, 22, 24–25, 27
   _consts.py170100% 
   cache_control_header.py68494%71, 91, 113, 121
   cache_streaming.py1442284%154–156, 184–186, 211, 225, 229–230, 265–266, 268, 280, 349, 355–356, 359, 367–370
   config.py200100% 
   db.py741875%110, 116, 154, 160–161, 164, 170, 172, 193–200, 202–203
   errors.py50100% 
   external_cache.py282028%31, 35, 40–42, 44–45, 48–49, 51–53, 60, 63–65, 69–72
   lru_cache_helper.py47295%84–85
   ota_cache.py2256670%71–72, 147, 150–151, 163–164, 196–197, 214, 235, 254–258, 262–264, 266, 268–275, 277–279, 282–283, 287–288, 292, 339, 347–349, 428–431, 445, 448–449, 463–464, 466–468, 472–474, 480, 482–484, 489, 516, 522, 549, 601–603
   server_app.py1413972%79, 82, 88, 107, 111, 170, 179, 221–222, 224–226, 229, 234–235, 238, 241–242, 245, 248, 251, 254, 267–268, 271–272, 274, 277, 303–306, 309, 323–325, 331–333
   utils.py140100% 
src/otaclient
   __init__.py5260%17, 19
   __main__.py110%16
   _logging.py513335%43–44, 46–47, 49–54, 56–57, 59–60, 62–65, 67, 77, 80–82, 84–86, 89–90, 92–96
   _status_monitor.py1611193%46, 48–49, 157, 160, 177, 180, 196–197, 204, 207
   _types.py750100% 
   errors.py120199%97
   main.py492940%30, 32–39, 41–42, 44–45, 47–48, 52, 54, 59, 61, 67–69, 72–73, 77–80, 82
   ota_core.py31610566%101, 128, 130–131, 135–136, 138–140, 144–145, 150–151, 157, 159, 198–201, 207, 211, 217, 356, 388–389, 391, 400, 403, 408–409, 412, 418, 420–424, 460–463, 466–477, 480–483, 519–522, 538, 540–542, 605–612, 617, 620–627, 660–661, 667, 671–672, 678, 703–705, 707, 782, 810–811, 813–814, 822, 824–830
   utils.py37294%73–74
src/otaclient/boot_control
   __init__.py40100% 
   _firmware_package.py932276%82, 86, 136, 180, 186, 209–210, 213–218, 220–221, 224–229, 231
   _grub.py41812769%214, 262–265, 271–275, 312–313, 320–325, 328–334, 337, 340–341, 346, 348–350, 359–365, 367–368, 370–372, 381–383, 385–387, 466–467, 471–472, 524, 530, 556, 578, 582–583, 598–600, 624–627, 639, 643–645, 647–649, 708–711, 736–739, 762–765, 777–778, 781–782, 817, 823, 843–844, 846, 871–873, 891–894, 919–922, 929–932, 937–945, 950–957
   _jetson_cboot.py2612610%20, 22–25, 27–29, 35–40, 42, 58–60, 62, 64–65, 71, 75, 134, 137, 139–140, 143, 150–151, 159–160, 163, 169–170, 178, 187–191, 193, 199, 202–203, 209, 212–213, 218–219, 221, 227–228, 231–232, 235–237, 239, 245, 250–252, 254–256, 261, 263–266, 268–269, 278–279, 282–283, 288–289, 292–296, 299–300, 305–306, 309, 312–316, 321–324, 327, 330–331, 334, 337–338, 341, 345–350, 354–355, 359, 362–363, 366, 369–372, 374, 377–378, 382, 385, 388–391, 393, 400, 404–405, 408–409, 415–416, 422, 424–425, 429, 431, 433–435, 438, 442, 445, 448–449, 451, 454, 462–463, 470, 480, 483, 491–492, 497–500, 502, 509, 511–513, 519–520, 524–525, 528, 532, 535, 537, 544–548, 550, 562–565, 568, 571, 573, 580, 590–592, 594, 596, 599, 602, 605, 607–608, 611–615, 619–621, 623, 631–635, 637, 640, 644, 647, 658–659, 664, 674, 677–683, 687–693, 697–706, 710–718, 722, 724, 726–728
   _jetson_common.py1724573%132, 140, 288–291, 294, 311, 319, 354, 359–364, 382, 408–409, 411–413, 417–420, 422–423, 425–429, 431, 438–439, 442–443, 453, 456–457, 460, 462, 506–507
   _jetson_uefi.py40427432%124–126, 131–132, 151–153, 158–161, 328, 446, 448–451, 455, 459–460, 462–470, 472, 484–485, 488–489, 492–493, 496–498, 502–503, 508–510, 514, 518–519, 522–523, 526–527, 531, 534–535, 537, 542–543, 547, 550–551, 556, 560–561, 564, 568–570, 572, 576–579, 581–582, 604–605, 609–610, 612, 616, 620–621, 624–625, 632, 635–637, 640, 642–643, 648–649, 652–655, 657–658, 663, 665–666, 674, 677–680, 682–683, 685, 689–690, 694, 702–706, 709–710, 712, 715–719, 722, 725–729, 733–734, 737–742, 745–746, 749–752, 754–755, 762–763, 773–776, 779, 782–785, 788–792, 795–796, 799, 802–805, 808, 810, 815–816, 819, 822–825, 827, 833, 838–839, 858–859, 862, 870–871, 878, 888, 891, 898–899, 904–907, 915–918, 926–927, 939–942, 944, 947, 950, 958, 969–971, 973–975, 977–981, 986–987, 989, 1002, 1006, 1009, 1019, 1024, 1032–1033, 1036, 1040, 1042–1044, 1050–1051, 1056, 1064–1069, 1074–1079, 1084–1092, 1097–1104, 1112–1114
   _ota_status_control.py1021189%117, 122, 127, 240, 244–245, 248, 255, 257–258, 273
   _rpi_boot.py28713353%53, 56, 120–121, 125, 133–136, 150–153, 158–159, 161–162, 167–168, 171–172, 181–182, 222, 228–232, 235, 253–255, 259–261, 266–268, 272–274, 284–285, 288, 291, 293–294, 296–297, 299–301, 307, 310–311, 321–324, 332–336, 338, 340–341, 346–347, 354, 357–362, 393, 395–398, 408–411, 415–416, 418–422, 450–453, 472–475, 501–504, 509–517, 522–529, 544–547, 554–557, 565–567
   _slot_mnt_helper.py100100% 
   configs.py510100% 
   protocol.py60100% 
   selecter.py412929%44–46, 49–50, 54–55, 58–60, 63, 65, 69, 77–79, 81–82, 84–85, 89, 91, 93–94, 96, 98–99, 101, 103
src/otaclient/configs
   __init__.py170100% 
   _cfg_configurable.py470100% 
   _cfg_consts.py47197%97
   _common.py80100% 
   _ecu_info.py56394%59, 64–65
   _proxy_info.py51590%85, 87–88, 90, 101
   cfg.py190100% 
src/otaclient/create_standby
   __init__.py13192%36
   common.py2264480%59, 62–63, 67–69, 71, 75–76, 78, 126, 174–176, 178–180, 182, 185–188, 192, 203, 279–280, 282–287, 299, 339, 367, 370–372, 388–389, 403, 407, 429–430
   interface.py70100% 
   rebuild_mode.py1151091%98–100, 119, 150–155
src/otaclient/grpc
   _otaproxy_ctx.py644135%38, 40–41, 43–45, 47, 52–56, 58, 77–78, 80, 83, 87, 100–102, 106–107, 109, 111–112, 118–120, 124, 133–134, 136–141, 143–145
src/otaclient/grpc/api_v2
   ecu_status.py1531093%75, 78, 81, 147, 171, 173, 300, 370–371, 410
   ecu_tracker.py341944%40–42, 48, 52–54, 61–63, 66, 70–74, 77–79
   servicer.py1273671%82, 171–173, 180, 191–192, 233–234, 237–241, 250–259, 266, 272, 275–278, 284–285, 292, 295, 301, 304
   types.py46295%78–79
src/otaclient_api/v2
   api_caller.py39684%45–47, 83–85
   types.py2562391%86, 89–92, 131, 209–210, 212, 259, 262–263, 506–508, 512–513, 515, 518–519, 522–523, 586
src/otaclient_common
   __init__.py341555%42–44, 61, 63, 68–77
   _io.py64198%41
   cmdhelper.py130100% 
   common.py1061189%44, 148, 151–153, 168, 175–177, 271, 275
   downloader.py1991094%107–108, 126, 153, 369, 424, 428, 516–517, 526
   linux.py611575%51–53, 59, 69, 74, 76, 108–109, 133–134, 190, 195–196, 198
   logging.py29196%55
   persist_file_handling.py1181884%113, 118, 150–152, 163, 192–193, 228–232, 242–244, 246–247
   proto_streamer.py42880%33, 48, 66–67, 72, 81–82, 100
   proto_wrapper.py3984688%87, 165, 172, 184–186, 205, 210, 221, 257, 263, 268, 299, 303, 307, 402, 462, 469, 472, 492, 499, 501, 526, 532, 535, 537, 562, 568, 571, 573, 605, 609, 611, 625, 642, 669, 672, 676, 707, 713, 762–763, 765, 803–805
   retry_task_map.py105595%158–159, 161, 181–182
   shm_status.py952177%79–80, 83–84, 105, 120–122, 134, 139, 156–160, 169–170, 172, 179, 192, 204
   typing.py31487%48, 97–98, 100
TOTAL6596169674% 

Tests Skipped Failures Errors Time
236 0 💤 0 ❌ 0 🔥 12m 6s ⏱️

Please sign in to comment.