Skip to content

Commit 61ced13

Browse files
itai1357ipeleg
and
ipeleg
authored
adding request_wrapper for overcoming ConnectionErrors (bridgecrewio#2907)
* adding request_wrapper for avoiding error of 'Connection aborted.', ConnectionResetError(104, 'Connection reset by peer') * adding the prefix "CHECKOV_" to the env-vars and casting them to the required number values * adding comment to the new change * adding while loop and make the request_max_tries request_sleep_between_tries defined when creating instance (for better controlling them during UTs) * adding UTs * fix PRs comments * fix lint * add request_wrapper in twistcli + UTs * create global request_wrapper in http_utils.py + passing the UTs * adding test for post result * adding test for post result * own review * fix small incorresponding * fix lint * invoke tests again * adding the flag should_call_raise_for_status for not breaking the existing behavior Co-authored-by: ipeleg <[email protected]>
1 parent b67f338 commit 61ced13

File tree

7 files changed

+271
-63
lines changed

7 files changed

+271
-63
lines changed

checkov/common/bridgecrew/vulnerability_scanning/integrations/twistcli.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import logging
22
import platform
33
import stat
4-
import time
54
from abc import abstractmethod, ABC
65
from pathlib import Path
76
from typing import Dict, Any, List
87
from datetime import datetime, timedelta
98

109
import aiohttp
11-
import requests
1210
from checkov.common.bridgecrew.platform_integration import bc_integration, BcPlatformIntegration
1311
from checkov.common.util.data_structures_utils import merge_dicts
14-
from checkov.common.util.http_utils import get_default_post_headers
12+
from checkov.common.util.http_utils import get_default_post_headers, request_wrapper
1513

1614

1715
class TwistcliIntegration(ABC):
@@ -31,10 +29,9 @@ def download_twistcli(self, cli_file_name: Path) -> None:
3129

3230
os_type = platform.system().lower()
3331

34-
response = requests.request(
35-
"GET", f"{self.twictcli_base_url}?os={os_type}", headers=bc_integration.get_default_headers("GET")
36-
)
37-
response.raise_for_status()
32+
response = request_wrapper("GET", f"{self.twictcli_base_url}?os={os_type}",
33+
headers=bc_integration.get_default_headers("GET"),
34+
should_call_raise_for_status=True)
3835

3936
cli_file_name_path.write_bytes(response.content)
4037
cli_file_name_path.chmod(cli_file_name_path.stat().st_mode | stat.S_IEXEC)
@@ -50,18 +47,9 @@ def report_results(self, twistcli_scan_result: Dict[str, Any], file_path: Path,
5047
**kwargs,
5148
)
5249

53-
for i in range(2):
54-
response = requests.request("POST", self.vulnerabilities_save_results_url,
55-
headers=bc_integration.get_default_headers("POST"), json=payload)
56-
try:
57-
response.raise_for_status()
58-
break
59-
except requests.exceptions.HTTPError as ex:
60-
logging.error(f"HTTP error on request {self.vulnerabilities_save_results_url}, payload:\n{payload}")
61-
if ex.response.status_code >= 500 and i != 1:
62-
time.sleep(2)
63-
continue
64-
raise ex
50+
request_wrapper("POST", self.vulnerabilities_save_results_url,
51+
headers=bc_integration.get_default_headers("POST"),
52+
json=payload, should_call_raise_for_status=True)
6553

6654
async def report_results_async(
6755
self,
@@ -132,4 +120,4 @@ def get_packages_for_report(scan_results: Dict[str, Any]) -> List[Dict[str, Any]
132120
"licenses": package.get("licenses") or [],
133121
}
134122
for package in scan_results.get("packages") or []
135-
]
123+
]

checkov/common/util/http_utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import json
22
import requests
33
import logging
4+
import time
5+
import os
6+
from typing import Optional, Any
47

58
from checkov.common.bridgecrew.bc_source import SourceType
69
from checkov.common.util.consts import DEV_API_GET_HEADERS, DEV_API_POST_HEADERS
@@ -53,3 +56,38 @@ def get_default_get_headers(client: SourceType, client_version: str):
5356

5457
def get_default_post_headers(client: SourceType, client_version: str):
5558
return merge_dicts(DEV_API_POST_HEADERS, get_version_headers(client.name, client_version), get_user_agent_header())
59+
60+
61+
def request_wrapper(method: str, url: str, headers: Any, data: Optional[Any] = None, json: Optional[Any] = None,
62+
should_call_raise_for_status: bool = False):
63+
# using of "retry" mechanism for 'requests.request' due to unpredictable 'ConnectionError' and 'HttpError'
64+
# instances that appears from time to time.
65+
# 'ConnectionError' instances that appeared:
66+
# * 'Connection aborted.', ConnectionResetError(104, 'Connection reset by peer').
67+
# * 'Connection aborted.', OSError(107, 'Socket not connected').
68+
# 'ConnectionError' instances that appeared:
69+
# * 403 Client Error: Forbidden for url.
70+
# * 504 Server Error: Gateway Time-out for url.
71+
72+
request_max_tries = int(os.getenv('REQUEST_MAX_TRIES', 3))
73+
sleep_between_request_tries = float(os.getenv('SLEEP_BETWEEN_REQUEST_TRIES', 1))
74+
75+
for i in range(request_max_tries):
76+
try:
77+
response = requests.request(method, url, headers=headers, data=data, json=json)
78+
if should_call_raise_for_status:
79+
response.raise_for_status()
80+
return response
81+
except requests.exceptions.ConnectionError as connection_error:
82+
logging.error(f"Connection error on request {method}:{url},\ndata:\n{data}\njson:{json}\nheaders:{headers}")
83+
if i != request_max_tries - 1:
84+
time.sleep(sleep_between_request_tries * (i + 1))
85+
continue
86+
raise connection_error
87+
except requests.exceptions.HTTPError as http_error:
88+
logging.error(f"HTTP error on request {method}:{url},\ndata:\n{data}\njson:{json}\nheaders:{headers}")
89+
status_code = http_error.response.status_code
90+
if (status_code >= 500 or status_code == 403) and i != request_max_tries - 1:
91+
time.sleep(sleep_between_request_tries * (i + 1))
92+
continue
93+
raise http_error

checkov/sca_package/scanner.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111

1212
from checkov.common.bridgecrew.platform_integration import bc_integration
1313
from checkov.common.util.file_utils import compress_file_gzip_base64, decompress_file_gzip_base64
14+
from checkov.common.util.http_utils import request_wrapper
1415

1516
SLEEP_DURATION = 2
1617
MAX_SLEEP_DURATION = 60
1718

1819

1920
class Scanner:
2021
def __init__(self) -> None:
21-
self.base_url = bc_integration.api_url
22+
self._base_url = bc_integration.api_url
2223

2324
def scan(self, input_paths: "Iterable[Path]") \
2425
-> "Sequence[Dict[str, Any]]":
@@ -53,13 +54,13 @@ async def run_scan(self, input_path: Path) -> dict:
5354
"fileName": input_path.name
5455
}
5556

56-
response = requests.request(
57-
"POST", f"{self.base_url}/api/v1/vulnerabilities/scan",
58-
headers=bc_integration.get_default_headers("GET"),
59-
data=request_body
57+
response = request_wrapper(
58+
"POST", f"{self._base_url}/api/v1/vulnerabilities/scan",
59+
headers=bc_integration.get_default_headers("POST"),
60+
data=request_body,
61+
should_call_raise_for_status=True
6062
)
6163

62-
response.raise_for_status()
6364
response_json = response.json()
6465

6566
if response_json["status"] == "already_exist":
@@ -74,8 +75,8 @@ def run_scan_busy_wait(self, input_path: Path, scan_id: str) -> dict:
7475
response = requests.Response()
7576

7677
while current_state != desired_state:
77-
response = requests.request(
78-
"GET", f"{self.base_url}/api/v1/vulnerabilities/scan-results/{scan_id}",
78+
response = request_wrapper(
79+
"GET", f"{self._base_url}/api/v1/vulnerabilities/scan-results/{scan_id}",
7980
headers=bc_integration.get_default_headers("GET")
8081
)
8182
response_json = response.json()

tests/common/utils/conftest.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Dict, Any
2+
3+
import pytest
4+
5+
from checkov.common.bridgecrew.bc_source import SourceType
6+
from checkov.common.bridgecrew.platform_integration import BcPlatformIntegration, bc_integration
7+
8+
9+
@pytest.fixture()
10+
def mock_bc_integration() -> BcPlatformIntegration:
11+
bc_integration.bc_api_key = "abcd1234-abcd-1234-abcd-1234abcd1234"
12+
bc_integration.setup_bridgecrew_credentials(
13+
repo_id="bridgecrewio/checkov",
14+
skip_fixes=True,
15+
skip_download=True,
16+
source=SourceType("Github", False),
17+
source_version="1.0",
18+
repo_branch="master",
19+
)
20+
return bc_integration
21+
22+
23+
@pytest.fixture()
24+
def scan_result_success_response() -> Dict[str, Any]:
25+
return {'outputType': 'Result',
26+
'outputData': "H4sIAN22X2IC/8WY23LbOBKGX6VLN5tUWRQp"
27+
"+SCrZi88drL2VqKkLMUzNZu5gEjIQkwSXAKUrU3l3fdvgDofnNS4MheJKbIJdAP"
28+
"/193g10YpC22U1eWs0aNGy2ZFq5SmSq3B3/9WqpSZzK0J7JNtHFGjEMbIBKa2rKT7HT"
29+
"+Ie2lw5z9fG3ZWSB6mmNmJztk+F5m7k3wR+b3mO1NZGoWHuBkFbT+mnTw"
30+
"/+bcjOjTBOBXmYWP8MDh9sfHZXhprNqZoB+3TIPyBWf6EaayzIlUij"
31+
"+WNMZVbvLxK07UnV8rYUo0q6yf62ohLZVUsUvwIYTlR95P6MpOJqrL6R6of6yurrbf+xi5XaS5LMVIpRplvl"
32+
"+KNbFzevWm2w+i0eRp13XoZK2xl3KKqJ5mQyikKzoPuEf50g+iYbeKpYYvTIHLrEUNAfqzBoNcJwtbFXa"
33+
"/furjsvWt9vMXVp5vebWvQu2zxnRv8u+j1eZhEGsRV1EE2LkttTBNylFTfz+/p1e"
34+
"+DwWtaDWDGLtmJpESZTBlzOYG45K1MhZXJh9EXuPNRF1VB4yqPeWy2j3XOC9oSSabyFseo4vrHF1NfrA3x"
35+
"TuuHqjDBF8OvXzkB00iOdSnrhThy6/K0vOkWSeQJLqNw9UEUlnFEIsXuGIIeNCIU1gIeSImsxgw8JYlypGw"
36+
"pyhk9ylG9BqRLuh6+f0dTJcgvNXs01emUl6fKjRhLqgwwJD2mN6mTW6ByrBe/F7g9lRAtVo5XudbLkt7+Fq"
37+
"T1g7stVlOVP/DPibWF6bVa+TQJckg1uNfTFu9RK5FWqLS1papSmYe3wnnP6mtcuPDJKV4+wbUesXZhWT/xoQ"
38+
"INaR916dh+81SkWlmCfY3itTAElfLlexcWLSJl0lSGSLCjdSB+5l/+6bav4yyKapQqM5HJFXbeEc1Oh91meD"
39+
"KMTnon570w/MNJVZlYY+jnLeHPQROXaTbYOzsOowPsRWEN38kKfGfByffC1wd8nwBfH/Bd74FvCKBiaF5JKgS"
40+
"WC9qKdSL3iP+k1vmG/tnRx4nMoUd4D/IEOZwflZ3Qv7S+TyVd5CKdgT5zdIiI0YxrDWaBA1bmCYa7HNy+paKE"
41+
"qac6kzHIRw6AMfbdunSxRMgHY7b071LnT1L/fF9fQv0rWr9GCN+v9OMDSo/CZtgZRt3nlb7DclPpmybbSj9vR"
42+
"ufd4+M9Uod4uaCgqAbnLPgI6362oniXX7cVHx1Q/HWt+Osdit+UtZuOp19IuvaEpd5Z3nVuzrUr4lhXuSUrHi"
43+
"SvWkAXZCplxSiFBEsxxqaQzKAKEkmCxsDQKzsRlpQhdAciZbFD5S6nuYRuZPn58z/Mxks8UEmxMBK9l8gNPMmE"
44+
"wwBJ/1OuHKvgoWQZlOY1PeoqTbyXPP4cLscW6itqBOBkxqCvBAgCIDx7ALsY2pVXjB9DL86jeZwBvfqQ4xHaiH"
45+
"sxr61snMtHDJJKOGg4NkyDOZKdMxhkBqwOTPjNUt4jdNabn2kt7uD1Jr6LRuinILwi2Jdg+LJ2fgnvCtj7OZ6js"
46+
"I9jeNluRt1hdN5DnTnE8U7LdY63TbY4bkfNTqcddvZy3A7QHYHMIGp7nturbeMxQ/Usx9ebHPf3VK51jv1sR+vA"
47+
"ekc8x+3VB3CUJlh/oIC6kluFvUnQsrNrMyaNm33c4+bLyyuoO8nA9YyJjk1AAyvGY9SjbMTVK3bkVUwq5D2U0Ad"
48+
"W98rp6k6Bkyn/B/WDLsiEjRz9En0/07wsYWOVooDRRZIoDhYwz45IjekVR+IYUuPXvhWWY4EjBy2cIlvPaxDfVC"
49+
"JikB1XxupM/Q/bhJIpChw2Yk9xAj2muqiLr0gN8J0gc7AHrnfmI8wRT5VTrm0N8JrnOKz4F/n22nt1Llr6IZ9w6p"
50+
"RJQDdIXzAviVOEHx/bgvSxaw90ZY1K3BLxFPMAqdRwaP7Cror/EzveVTJeuOhvNri4dStjzuNrh6MDaaQGcU8Wge"
51+
"shd6tcwA9nkd2Wa1lkh8l2N9BFxxCGpyf7jp0os+2g89db3v6eBoBb3o+ASkKiH0vtjmDuQwLVR3z61ecJ74jTs1"
52+
"A5Z4vL3ziCHt1kaEiBDZRcQP93IlWJJ2rrxOpHdrU/RkH23wj4wTtR4uwmMtdHaJdHWPj+SAdQjOJeAqU14f4AeC"
53+
"Yy5zwFU9TLqYplQMMJoPE1nrmWYl7opT8zoR2RLoldzPsA+D0FTYb+PfjQJzgt2BeVx7pkkghMa56wHns9HD+Fyx"
54+
"VLqjf2LaD+h+GbHiLG65mYEcsoFQUtS2uI/e92vrdDX3zj2Ya1/tTzo9V9TYB7cN0B5ZUeHGzIl0Rvsr6fzVrr+y"
55+
"p8l4+R7ZCLcic6WOF3Wq5X+G2TnZ263529x9Lw54L51uGzaNZDrhKoupKXEtWsR1UOrfufqwSxflFS3KLjnd5ueu"
56+
"anz3q7neGie2cS8HcBin/BL8U8U/AL0XOSX+jt75P82r6+RIV6DoZDXW14oKMNz5rR2TA6fr6j3WG52dFumrjvsG"
57+
"sp7dAH12j5wbWz+sG1vfOD6+m3b/8HQd/FwVgXAAA=",
58+
'compressionMethod': 'gzip'}

tests/common/utils/test_http_utils.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import os
2+
import responses
3+
import requests
4+
from unittest import mock
5+
6+
from checkov.common.util.http_utils import request_wrapper
7+
8+
9+
@responses.activate
10+
@mock.patch.dict(os.environ, {"REQUEST_MAX_TRIES": "5", "SLEEP_BETWEEN_REQUEST_TRIES": "0.01"})
11+
def test_request_wrapper_all_fail_with_connection_error_for_get_scan_result(mock_bc_integration):
12+
# given
13+
mock_url = mock_bc_integration.bc_api_url + "/api/v1/vulnerabilities/scan-results/2e97f5afea42664309f492a1e2083b43479c2936"
14+
responses.add(
15+
method=responses.GET,
16+
url=mock_url,
17+
body=requests.exceptions.ConnectionError()
18+
)
19+
try:
20+
request_wrapper("GET", mock_url, {})
21+
assert False, "\'request_wrapper\' is expected to fail in this scenario"
22+
except requests.exceptions.ConnectionError:
23+
responses.assert_call_count(mock_url, 5)
24+
25+
26+
@responses.activate
27+
@mock.patch.dict(os.environ, {"REQUEST_MAX_TRIES": "5", "SLEEP_BETWEEN_REQUEST_TRIES": "0.01"})
28+
def test_request_wrapper_all_fail_with_connection_error_for_post_scan(mock_bc_integration):
29+
# given
30+
mock_url = mock_bc_integration.bc_api_url + "/api/v1/vulnerabilities/scan"
31+
responses.add(
32+
method=responses.POST,
33+
url=mock_url,
34+
body=requests.exceptions.ConnectionError()
35+
)
36+
try:
37+
request_wrapper("POST", mock_url, {}, data={'mocked_key': 'mocked_value'})
38+
assert False, "\'request_wrapper\' is expected to fail in this scenario"
39+
except requests.exceptions.ConnectionError:
40+
responses.assert_call_count(mock_url, 5)
41+
42+
43+
@responses.activate
44+
@mock.patch.dict(os.environ, {"REQUEST_MAX_TRIES": "5", "SLEEP_BETWEEN_REQUEST_TRIES": "0.01"})
45+
def test_request_wrapper_all_fail_with_http_error(mock_bc_integration):
46+
# given
47+
mock_url = mock_bc_integration.bc_api_url + "/api/v1/vulnerabilities/twistcli?os=linux"
48+
responses.add(
49+
method=responses.GET,
50+
url=mock_url,
51+
json={'error': "mocked client error"},
52+
status=403
53+
)
54+
request_wrapper("GET", mock_url, {})
55+
responses.assert_call_count(mock_url, 1)
56+
57+
58+
@responses.activate
59+
@mock.patch.dict(os.environ, {"REQUEST_MAX_TRIES": "5", "SLEEP_BETWEEN_REQUEST_TRIES": "0.01"})
60+
def test_request_wrapper_all_fail_with_http_error_should_call_raise_for_status(mock_bc_integration):
61+
# given
62+
mock_url = mock_bc_integration.bc_api_url + "/api/v1/vulnerabilities/twistcli?os=linux"
63+
responses.add(
64+
method=responses.GET,
65+
url=mock_url,
66+
json={'error': "mocked client error"},
67+
status=403
68+
)
69+
try:
70+
request_wrapper("GET", mock_url, {}, should_call_raise_for_status=True)
71+
assert False, "\'request_wrapper\' is expected to fail in this scenario"
72+
except requests.exceptions.HTTPError:
73+
responses.assert_call_count(mock_url, 5)
74+
75+
76+
@responses.activate
77+
@mock.patch.dict(os.environ, {"REQUEST_MAX_TRIES": "3", "SLEEP_BETWEEN_REQUEST_TRIES": "0.01"})
78+
def test_request_wrapper_with_success_for_get_scan_result(mock_bc_integration, scan_result_success_response):
79+
# given
80+
mock_url = mock_bc_integration.bc_api_url + "/api/v1/vulnerabilities/scan-results/2e97f5afea42664309f492a1e2083b43479c2936"
81+
responses.add(
82+
method=responses.GET,
83+
url=mock_url,
84+
json=scan_result_success_response,
85+
status=200
86+
)
87+
request_wrapper("GET", mock_url, {})
88+
responses.assert_call_count(mock_url, 1)
89+
90+
91+
@responses.activate
92+
@mock.patch.dict(os.environ, {"REQUEST_MAX_TRIES": "3", "SLEEP_BETWEEN_REQUEST_TRIES": "0.01"})
93+
def test_request_wrapper_with_success_for_download_twistcli(mock_bc_integration):
94+
# given
95+
mock_url = mock_bc_integration.bc_api_url + "/api/v1/vulnerabilities/twistcli?os=linux"
96+
responses.add(
97+
method=responses.GET,
98+
url=mock_url,
99+
json={},
100+
status=200
101+
)
102+
request_wrapper("GET", mock_url, {})
103+
responses.assert_call_count(mock_url, 1)
104+
105+
106+
@responses.activate
107+
@mock.patch.dict(os.environ, {"REQUEST_MAX_TRIES": "3", "SLEEP_BETWEEN_REQUEST_TRIES": "0.01"})
108+
def test_request_wrapper_with_success_for_post_scan(mock_bc_integration, scan_result_success_response):
109+
# given
110+
mock_url = mock_bc_integration.bc_api_url + "/api/v1/vulnerabilities/scan"
111+
responses.add(
112+
method=responses.POST,
113+
url=mock_url,
114+
json=scan_result_success_response,
115+
status=200
116+
)
117+
request_wrapper("POST", mock_url, {}, data={'mocked_key': 'mocked_value'})
118+
responses.assert_call_count(mock_url, 1)

tests/sca_package/conftest.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,3 +1589,41 @@ def scan_result() -> List[Dict[str, Any]]:
15891589
"vulnerabilityDistribution": {"critical": 0, "high": 2, "medium": 0, "low": 0, "total": 2},
15901590
},
15911591
]
1592+
1593+
1594+
@pytest.fixture()
1595+
def scan_result_success_response() -> Dict[str, Any]:
1596+
return {'outputType': 'Result',
1597+
'outputData': "H4sIAN22X2IC/8WY23LbOBKGX6VLN5tUWRQp"
1598+
"+SCrZi88drL2VqKkLMUzNZu5gEjIQkwSXAKUrU3l3fdvgDofnNS4MheJKbIJdAP"
1599+
"/193g10YpC22U1eWs0aNGy2ZFq5SmSq3B3/9WqpSZzK0J7JNtHFGjEMbIBKa2rKT7HT"
1600+
"+Ie2lw5z9fG3ZWSB6mmNmJztk+F5m7k3wR+b3mO1NZGoWHuBkFbT+mnTw"
1601+
"/+bcjOjTBOBXmYWP8MDh9sfHZXhprNqZoB+3TIPyBWf6EaayzIlUij"
1602+
"+WNMZVbvLxK07UnV8rYUo0q6yf62ohLZVUsUvwIYTlR95P6MpOJqrL6R6of6yurrbf+xi5XaS5LMVIpRplvl"
1603+
"+KNbFzevWm2w+i0eRp13XoZK2xl3KKqJ5mQyikKzoPuEf50g+iYbeKpYYvTIHLrEUNAfqzBoNcJwtbFXa"
1604+
"/furjsvWt9vMXVp5vebWvQu2zxnRv8u+j1eZhEGsRV1EE2LkttTBNylFTfz+/p1e"
1605+
"+DwWtaDWDGLtmJpESZTBlzOYG45K1MhZXJh9EXuPNRF1VB4yqPeWy2j3XOC9oSSabyFseo4vrHF1NfrA3x"
1606+
"TuuHqjDBF8OvXzkB00iOdSnrhThy6/K0vOkWSeQJLqNw9UEUlnFEIsXuGIIeNCIU1gIeSImsxgw8JYlypGw"
1607+
"pyhk9ylG9BqRLuh6+f0dTJcgvNXs01emUl6fKjRhLqgwwJD2mN6mTW6ByrBe/F7g9lRAtVo5XudbLkt7+Fq"
1608+
"T1g7stVlOVP/DPibWF6bVa+TQJckg1uNfTFu9RK5FWqLS1papSmYe3wnnP6mtcuPDJKV4+wbUesXZhWT/xoQ"
1609+
"INaR916dh+81SkWlmCfY3itTAElfLlexcWLSJl0lSGSLCjdSB+5l/+6bav4yyKapQqM5HJFXbeEc1Oh91meD"
1610+
"KMTnon570w/MNJVZlYY+jnLeHPQROXaTbYOzsOowPsRWEN38kKfGfByffC1wd8nwBfH/Bd74FvCKBiaF5JKgS"
1611+
"WC9qKdSL3iP+k1vmG/tnRx4nMoUd4D/IEOZwflZ3Qv7S+TyVd5CKdgT5zdIiI0YxrDWaBA1bmCYa7HNy+paKE"
1612+
"qac6kzHIRw6AMfbdunSxRMgHY7b071LnT1L/fF9fQv0rWr9GCN+v9OMDSo/CZtgZRt3nlb7DclPpmybbSj9vR"
1613+
"ufd4+M9Uod4uaCgqAbnLPgI6362oniXX7cVHx1Q/HWt+Osdit+UtZuOp19IuvaEpd5Z3nVuzrUr4lhXuSUrHi"
1614+
"SvWkAXZCplxSiFBEsxxqaQzKAKEkmCxsDQKzsRlpQhdAciZbFD5S6nuYRuZPn58z/Mxks8UEmxMBK9l8gNPMmE"
1615+
"wwBJ/1OuHKvgoWQZlOY1PeoqTbyXPP4cLscW6itqBOBkxqCvBAgCIDx7ALsY2pVXjB9DL86jeZwBvfqQ4xHaiH"
1616+
"sxr61snMtHDJJKOGg4NkyDOZKdMxhkBqwOTPjNUt4jdNabn2kt7uD1Jr6LRuinILwi2Jdg+LJ2fgnvCtj7OZ6js"
1617+
"I9jeNluRt1hdN5DnTnE8U7LdY63TbY4bkfNTqcddvZy3A7QHYHMIGp7nturbeMxQ/Usx9ebHPf3VK51jv1sR+vA"
1618+
"ekc8x+3VB3CUJlh/oIC6kluFvUnQsrNrMyaNm33c4+bLyyuoO8nA9YyJjk1AAyvGY9SjbMTVK3bkVUwq5D2U0Ad"
1619+
"W98rp6k6Bkyn/B/WDLsiEjRz9En0/07wsYWOVooDRRZIoDhYwz45IjekVR+IYUuPXvhWWY4EjBy2cIlvPaxDfVC"
1620+
"JikB1XxupM/Q/bhJIpChw2Yk9xAj2muqiLr0gN8J0gc7AHrnfmI8wRT5VTrm0N8JrnOKz4F/n22nt1Llr6IZ9w6p"
1621+
"RJQDdIXzAviVOEHx/bgvSxaw90ZY1K3BLxFPMAqdRwaP7Cror/EzveVTJeuOhvNri4dStjzuNrh6MDaaQGcU8Wge"
1622+
"shd6tcwA9nkd2Wa1lkh8l2N9BFxxCGpyf7jp0os+2g89db3v6eBoBb3o+ASkKiH0vtjmDuQwLVR3z61ecJ74jTs1"
1623+
"A5Z4vL3ziCHt1kaEiBDZRcQP93IlWJJ2rrxOpHdrU/RkH23wj4wTtR4uwmMtdHaJdHWPj+SAdQjOJeAqU14f4AeC"
1624+
"Yy5zwFU9TLqYplQMMJoPE1nrmWYl7opT8zoR2RLoldzPsA+D0FTYb+PfjQJzgt2BeVx7pkkghMa56wHns9HD+Fyx"
1625+
"VLqjf2LaD+h+GbHiLG65mYEcsoFQUtS2uI/e92vrdDX3zj2Ya1/tTzo9V9TYB7cN0B5ZUeHGzIl0Rvsr6fzVrr+y"
1626+
"p8l4+R7ZCLcic6WOF3Wq5X+G2TnZ263529x9Lw54L51uGzaNZDrhKoupKXEtWsR1UOrfufqwSxflFS3KLjnd5ueu"
1627+
"anz3q7neGie2cS8HcBin/BL8U8U/AL0XOSX+jt75P82r6+RIV6DoZDXW14oKMNz5rR2TA6fr6j3WG52dFumrjvsG"
1628+
"sp7dAH12j5wbWz+sG1vfOD6+m3b/8HQd/FwVgXAAA=",
1629+
'compressionMethod': 'gzip'}

0 commit comments

Comments
 (0)