532 lines
20 KiB
Python
532 lines
20 KiB
Python
import os
|
|
import random
|
|
import time
|
|
from contextlib import ExitStack
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import LXMF
|
|
import pytest
|
|
import RNS
|
|
from hypothesis import HealthCheck, given, settings
|
|
from hypothesis import strategies as st
|
|
|
|
from meshchatx.meshchat import ReticulumMeshChat
|
|
from meshchatx.src.backend.interface_config_parser import InterfaceConfigParser
|
|
from meshchatx.src.backend.lxmf_message_fields import (
|
|
LxmfAudioField,
|
|
LxmfFileAttachment,
|
|
LxmfImageField,
|
|
)
|
|
from meshchatx.src.backend.telemetry_utils import Telemeter
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(data=st.binary(min_size=0, max_size=1000))
|
|
def test_telemetry_unpack_fuzzing(data):
|
|
"""Fuzz the telemetry unpacking logic with random binary data."""
|
|
try:
|
|
# This should not raise unhandled exceptions
|
|
Telemeter.from_packed(data)
|
|
except Exception:
|
|
# We expect some failures for invalid packed data, but no crashes
|
|
pass
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(config_text=st.text(min_size=0, max_size=5000))
|
|
def test_interface_config_parsing_fuzzing(config_text):
|
|
"""Fuzz the interface configuration parser with random text."""
|
|
try:
|
|
InterfaceConfigParser.parse(config_text)
|
|
except Exception as e:
|
|
pytest.fail(f"InterfaceConfigParser crashed with input: {e}")
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(identity_bytes=st.binary(min_size=0, max_size=2048))
|
|
def test_identity_parsing_fuzzing(identity_bytes):
|
|
"""Fuzz RNS.Identity loading with random bytes."""
|
|
try:
|
|
RNS.Identity.from_bytes(identity_bytes)
|
|
except Exception:
|
|
# RNS.Identity.from_bytes is expected to fail on random bytes,
|
|
# but it should not cause an unhandled crash of the process.
|
|
pass
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(path_data=st.one_of(st.none(), st.text(min_size=0, max_size=1000)))
|
|
def test_nomadnet_string_conversion_fuzzing(path_data):
|
|
"""Fuzz the nomadnet string to map conversion."""
|
|
try:
|
|
ReticulumMeshChat.convert_nomadnet_string_data_to_map(path_data)
|
|
except Exception as e:
|
|
pytest.fail(
|
|
f"convert_nomadnet_string_data_to_map crashed with data {path_data}: {e}",
|
|
)
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(
|
|
field_data=st.one_of(
|
|
st.none(), st.dictionaries(keys=st.text(), values=st.text()), st.text(),
|
|
),
|
|
)
|
|
def test_nomadnet_field_conversion_fuzzing(field_data):
|
|
"""Fuzz the nomadnet field data to map conversion."""
|
|
try:
|
|
ReticulumMeshChat.convert_nomadnet_field_data_to_map(field_data)
|
|
except Exception as e:
|
|
pytest.fail(
|
|
f"convert_nomadnet_field_data_to_map crashed with data {field_data}: {e}",
|
|
)
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(app_data_base64=st.one_of(st.none(), st.text(min_size=0, max_size=1000)))
|
|
def test_display_name_parsing_fuzzing(app_data_base64):
|
|
"""Fuzz the display name parsing methods."""
|
|
try:
|
|
ReticulumMeshChat.parse_lxmf_display_name(app_data_base64)
|
|
ReticulumMeshChat.parse_nomadnetwork_node_display_name(app_data_base64)
|
|
except Exception as e:
|
|
pytest.fail(f"Display name parsing crashed with data {app_data_base64}: {e}")
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir(tmp_path):
|
|
return str(tmp_path)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_app(temp_dir):
|
|
with ExitStack() as stack:
|
|
# Mock database and other managers to avoid heavy initialization
|
|
stack.enter_context(patch("meshchatx.meshchat.Database"))
|
|
stack.enter_context(patch("meshchatx.meshchat.ConfigManager"))
|
|
stack.enter_context(patch("meshchatx.meshchat.MessageHandler"))
|
|
stack.enter_context(patch("meshchatx.meshchat.AnnounceManager"))
|
|
stack.enter_context(patch("meshchatx.meshchat.ArchiverManager"))
|
|
stack.enter_context(patch("meshchatx.meshchat.MapManager"))
|
|
stack.enter_context(patch("meshchatx.meshchat.TelephoneManager"))
|
|
stack.enter_context(patch("meshchatx.meshchat.VoicemailManager"))
|
|
stack.enter_context(patch("meshchatx.meshchat.RingtoneManager"))
|
|
stack.enter_context(patch("meshchatx.meshchat.RNCPHandler"))
|
|
stack.enter_context(patch("meshchatx.meshchat.RNStatusHandler"))
|
|
stack.enter_context(patch("meshchatx.meshchat.RNProbeHandler"))
|
|
stack.enter_context(patch("meshchatx.meshchat.TranslatorHandler"))
|
|
mock_async_utils = stack.enter_context(patch("meshchatx.meshchat.AsyncUtils"))
|
|
stack.enter_context(patch("LXMF.LXMRouter"))
|
|
mock_identity_class = stack.enter_context(patch("RNS.Identity"))
|
|
stack.enter_context(patch("RNS.Reticulum"))
|
|
stack.enter_context(patch("RNS.Transport"))
|
|
stack.enter_context(patch("threading.Thread"))
|
|
stack.enter_context(
|
|
patch.object(ReticulumMeshChat, "announce_loop", return_value=None),
|
|
)
|
|
stack.enter_context(
|
|
patch.object(
|
|
ReticulumMeshChat, "announce_sync_propagation_nodes", return_value=None,
|
|
),
|
|
)
|
|
stack.enter_context(
|
|
patch.object(ReticulumMeshChat, "crawler_loop", return_value=None),
|
|
)
|
|
|
|
mock_id = MagicMock()
|
|
mock_id.hash = b"test_hash_32_bytes_long_01234567"
|
|
mock_id.get_private_key.return_value = b"test_private_key"
|
|
mock_identity_class.return_value = mock_id
|
|
|
|
# Make run_async a no-op that doesn't trigger coroutine warnings
|
|
mock_async_utils.run_async = MagicMock(side_effect=lambda coroutine: None)
|
|
|
|
app = ReticulumMeshChat(
|
|
identity=mock_id,
|
|
storage_dir=temp_dir,
|
|
reticulum_config_dir=temp_dir,
|
|
)
|
|
|
|
# Setup config mock to return real values to avoid JSON serialization issues
|
|
app.config = MagicMock()
|
|
app.config.display_name.get.return_value = "Test User"
|
|
app.config.auto_announce_enabled.get.return_value = True
|
|
app.config.auto_announce_interval_seconds.get.return_value = 600
|
|
app.config.last_announced_at.get.return_value = 0
|
|
app.config.theme.get.return_value = "dark"
|
|
app.config.language.get.return_value = "en"
|
|
app.config.auto_resend_failed_messages_when_announce_received.get.return_value = True
|
|
app.config.allow_auto_resending_failed_messages_with_attachments.get.return_value = False
|
|
app.config.auto_send_failed_messages_to_propagation_node.get.return_value = True
|
|
app.config.show_suggested_community_interfaces.get.return_value = True
|
|
app.config.lxmf_local_propagation_node_enabled.get.return_value = False
|
|
app.config.lxmf_preferred_propagation_node_destination_hash.get.return_value = (
|
|
None
|
|
)
|
|
app.config.lxmf_preferred_propagation_node_auto_sync_interval_seconds.get.return_value = 3600
|
|
app.config.lxmf_preferred_propagation_node_last_synced_at.get.return_value = 0
|
|
app.config.lxmf_user_icon_name.get.return_value = "user"
|
|
app.config.lxmf_user_icon_foreground_colour.get.return_value = "#ffffff"
|
|
app.config.lxmf_user_icon_background_colour.get.return_value = "#000000"
|
|
app.config.lxmf_auto_sync_propagation_nodes_enabled.get.return_value = True
|
|
app.config.lxmf_auto_sync_propagation_nodes_interval_seconds.get.return_value = 3600
|
|
app.config.lxmf_auto_sync_propagation_nodes_last_synced_at.get.return_value = 0
|
|
app.config.lxmf_auto_sync_propagation_nodes_min_hops.get.return_value = 1
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_hops.get.return_value = 5
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_count.get.return_value = 10
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_age_seconds.get.return_value = (
|
|
86400
|
|
)
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_size_bytes.get.return_value = (
|
|
1000000
|
|
)
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_total_size_bytes.get.return_value = 10000000
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_total_count.get.return_value = (
|
|
100
|
|
)
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_total_age_seconds.get.return_value = 864000
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_total_size_bytes_per_node.get.return_value = 1000000
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_total_count_per_node.get.return_value = 100
|
|
app.config.lxmf_auto_sync_propagation_nodes_max_total_age_seconds_per_node.get.return_value = 864000
|
|
|
|
app.websocket_broadcast = MagicMock(side_effect=lambda data: None)
|
|
app.is_destination_blocked = MagicMock(return_value=False)
|
|
app.check_spam_keywords = MagicMock(return_value=False)
|
|
app.db_upsert_lxmf_message = MagicMock()
|
|
app.handle_forwarding = MagicMock()
|
|
app.convert_db_announce_to_dict = MagicMock(return_value={})
|
|
app.get_config_dict = MagicMock(return_value={"test_config": "test_value"})
|
|
app.resend_failed_messages_for_destination = MagicMock(
|
|
side_effect=lambda dest: None,
|
|
)
|
|
|
|
yield app
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(
|
|
num_announces=st.integers(min_value=10, max_value=100),
|
|
)
|
|
def test_announce_overload(mock_app, num_announces):
|
|
"""Test handling of multiple announces in rapid succession."""
|
|
mock_app.announce_manager.upsert_announce.reset_mock()
|
|
mock_app.websocket_broadcast.reset_mock()
|
|
|
|
aspect = "lxmf.delivery"
|
|
app_data = b"test_app_data"
|
|
|
|
# Mock database to return a valid announce dict
|
|
mock_app.database.announces.get_announce_by_hash.return_value = {
|
|
"aspect": "lxmf.delivery",
|
|
"destination_hash": "some_hash",
|
|
"display_name": "Test Peer",
|
|
}
|
|
|
|
for i in range(num_announces):
|
|
destination_hash = os.urandom(16)
|
|
announced_identity = MagicMock()
|
|
announced_identity.hash = os.urandom(32)
|
|
announce_packet_hash = os.urandom(16)
|
|
|
|
mock_app.on_lxmf_announce_received(
|
|
aspect, destination_hash, announced_identity, app_data, announce_packet_hash,
|
|
)
|
|
|
|
# Verify that the database was called for each announce
|
|
assert mock_app.announce_manager.upsert_announce.call_count == num_announces
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(
|
|
num_messages=st.integers(min_value=10, max_value=100),
|
|
)
|
|
def test_message_spamming(mock_app, num_messages):
|
|
"""Test handling of many LXMF messages in rapid succession."""
|
|
mock_app.db_upsert_lxmf_message.reset_mock()
|
|
|
|
for i in range(num_messages):
|
|
mock_message = MagicMock()
|
|
mock_message.source_hash = os.urandom(16)
|
|
mock_message.hash = os.urandom(16)
|
|
mock_message.get_fields.return_value = {} # No telemetry field
|
|
mock_message.title = f"Spam Title {i}"
|
|
mock_message.content = f"Spam Content {i}"
|
|
|
|
mock_app.on_lxmf_delivery(mock_message)
|
|
|
|
assert mock_app.db_upsert_lxmf_message.call_count == num_messages
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(
|
|
num_messages=st.integers(min_value=10, max_value=50),
|
|
payload_size=st.integers(min_value=1000, max_value=50000),
|
|
)
|
|
def test_message_spamming_large_payloads(mock_app, num_messages, payload_size):
|
|
"""Test handling of many LXMF messages with large payloads."""
|
|
mock_app.db_upsert_lxmf_message.reset_mock()
|
|
|
|
for i in range(num_messages):
|
|
mock_message = MagicMock()
|
|
mock_message.source_hash = os.urandom(16)
|
|
mock_message.hash = os.urandom(16)
|
|
mock_message.get_fields.return_value = {}
|
|
mock_message.title = f"Spam Title {i}"
|
|
mock_message.content = "A" * payload_size
|
|
|
|
mock_app.on_lxmf_delivery(mock_message)
|
|
|
|
assert mock_app.db_upsert_lxmf_message.call_count == num_messages
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(
|
|
msg=st.dictionaries(
|
|
keys=st.text(),
|
|
values=st.one_of(
|
|
st.text(),
|
|
st.integers(),
|
|
st.booleans(),
|
|
st.dictionaries(keys=st.text(), values=st.text()),
|
|
),
|
|
min_size=1,
|
|
max_size=10,
|
|
).flatmap(
|
|
lambda d: st.sampled_from(
|
|
[
|
|
"ping",
|
|
"config.set",
|
|
"nomadnet.download.cancel",
|
|
"nomadnet.page.archives.get",
|
|
"lxmf.forwarding.rule.add",
|
|
"lxmf.forwarding.rule.delete",
|
|
"lxm.ingest_uri",
|
|
"lxm.generate_paper_uri",
|
|
"keyboard_shortcuts.get",
|
|
],
|
|
).map(lambda t: {**d, "type": t}),
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_websocket_api_hypothesis(mock_app, msg):
|
|
"""Fuzz the websocket API using Hypothesis to generate varied messages."""
|
|
# Use MagicMock instead of AsyncMock to avoid coroutine warnings
|
|
mock_client = MagicMock()
|
|
mock_client.send_str = MagicMock(side_effect=lambda data: None)
|
|
try:
|
|
await mock_app.on_websocket_data_received(mock_client, msg)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_websocket_api_fuzzing(mock_app):
|
|
"""Fuzz the websocket API with various message types and payloads."""
|
|
# Use MagicMock instead of AsyncMock to avoid coroutine warnings
|
|
mock_client = MagicMock()
|
|
mock_client.send_str = MagicMock(side_effect=lambda data: None)
|
|
|
|
# Test cases with different message types and malformed/unexpected data
|
|
fuzz_messages = [
|
|
{"type": "ping"},
|
|
{"type": "config.set", "config": {"invalid_key": "invalid_value"}},
|
|
{"type": "config.set", "config": "not_a_dict"},
|
|
{"type": "nomadnet.download.cancel", "download_id": "non_existent_id"},
|
|
{
|
|
"type": "nomadnet.page.archives.get",
|
|
"destination_hash": "invalid_hash",
|
|
"page_path": "/invalid",
|
|
},
|
|
{"type": "lxmf.forwarding.rule.add", "rule": {}},
|
|
{"type": "lxmf.forwarding.rule.delete", "id": -1},
|
|
{"type": "lxm.ingest_uri", "uri": "invalid_uri"},
|
|
{
|
|
"type": "lxm.generate_paper_uri",
|
|
"destination_hash": "00" * 16,
|
|
"content": "test",
|
|
},
|
|
{"type": "non_existent_type", "data": "random_data"},
|
|
]
|
|
|
|
for msg in fuzz_messages:
|
|
try:
|
|
await mock_app.on_websocket_data_received(mock_client, msg)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_config_fuzzing(mock_app):
|
|
"""Fuzz the config update logic with various values."""
|
|
fuzz_configs = [
|
|
{"display_name": "A" * 1000},
|
|
{"display_name": None},
|
|
{"auto_resend_failed_messages_when_announce_received": "not_a_bool"},
|
|
{"unknown_config_option": 123},
|
|
{},
|
|
]
|
|
|
|
for config in fuzz_configs:
|
|
try:
|
|
if hasattr(mock_app, "update_config"):
|
|
await mock_app.update_config(config)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def test_malformed_announce_data(mock_app):
|
|
"""Test handling of malformed or unexpected data in announces."""
|
|
aspect = "lxmf.delivery"
|
|
destination_hash = b"too_short" # Malformed hash
|
|
|
|
# Test with None identity
|
|
mock_app.on_lxmf_announce_received(aspect, destination_hash, None, None, b"")
|
|
|
|
# Test with identity having None hash
|
|
announced_identity = MagicMock()
|
|
announced_identity.hash = None
|
|
mock_app.on_lxmf_announce_received(
|
|
aspect, destination_hash, announced_identity, None, b"",
|
|
)
|
|
|
|
|
|
def test_malformed_message_data(mock_app):
|
|
"""Test handling of malformed LXMF messages."""
|
|
mock_message = MagicMock()
|
|
# Simulate missing attributes or methods
|
|
del mock_message.source_hash
|
|
|
|
# This should be caught by the try-except in on_lxmf_delivery
|
|
mock_app.on_lxmf_delivery(mock_message)
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(
|
|
weird_string=st.text(min_size=0, max_size=1000),
|
|
large_binary=st.binary(min_size=0, max_size=10000),
|
|
)
|
|
def test_database_dao_fuzzing(mock_app, weird_string, large_binary):
|
|
"""Fuzz the database DAOs with weird strings and large binary data."""
|
|
# Test AnnounceDAO
|
|
announce_data = {
|
|
"destination_hash": os.urandom(16).hex(),
|
|
"aspect": weird_string,
|
|
"identity_hash": weird_string,
|
|
"identity_public_key": large_binary.hex(),
|
|
"app_data": large_binary,
|
|
"rssi": random.randint(-120, 0),
|
|
"snr": random.uniform(-20, 20),
|
|
"quality": random.uniform(0, 100),
|
|
}
|
|
try:
|
|
mock_app.database.announces.upsert_announce(announce_data)
|
|
except Exception:
|
|
pass
|
|
|
|
# Test MessageDAO
|
|
message_data = {
|
|
"hash": os.urandom(16).hex(),
|
|
"source_hash": os.urandom(16).hex(),
|
|
"destination_hash": os.urandom(16).hex(),
|
|
"state": weird_string,
|
|
"title": weird_string,
|
|
"content": weird_string,
|
|
"fields": {"weird": weird_string},
|
|
"timestamp": time.time(),
|
|
"is_incoming": random.choice([0, 1]),
|
|
"is_spam": random.choice([0, 1]),
|
|
}
|
|
try:
|
|
mock_app.database.messages.upsert_lxmf_message(message_data)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(
|
|
audio_bytes=st.binary(min_size=0, max_size=5000),
|
|
image_bytes=st.binary(min_size=0, max_size=10000),
|
|
)
|
|
def test_lxmf_field_fuzzing(audio_bytes, image_bytes):
|
|
"""Fuzz the LXMF field helper classes."""
|
|
try:
|
|
LxmfAudioField(audio_mode=random.randint(0, 10), audio_bytes=audio_bytes)
|
|
LxmfImageField(
|
|
image_type=random.choice(["png", "jpg", "webp", "invalid"]),
|
|
image_bytes=image_bytes,
|
|
)
|
|
LxmfFileAttachment(file_name="test.txt", file_bytes=audio_bytes)
|
|
except Exception as e:
|
|
pytest.fail(f"LXMF field classes crashed: {e}")
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(command_bytes=st.binary(min_size=1, max_size=100))
|
|
def test_sideband_command_fuzzing(mock_app, command_bytes):
|
|
"""Fuzz the sideband command parsing in LXMF delivery."""
|
|
mock_message = MagicMock()
|
|
mock_message.source_hash = os.urandom(16)
|
|
mock_message.hash = os.urandom(16)
|
|
# 0x01 is SidebandCommands.TELEMETRY_REQUEST
|
|
mock_message.get_fields.return_value = {LXMF.FIELD_COMMANDS: [command_bytes]}
|
|
|
|
try:
|
|
mock_app.on_lxmf_delivery(mock_message)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(
|
|
destination_hash=st.text(min_size=0, max_size=100),
|
|
page_path=st.text(min_size=0, max_size=500),
|
|
content=st.text(min_size=0, max_size=10000),
|
|
)
|
|
def test_archiver_manager_fuzzing(mock_app, destination_hash, page_path, content):
|
|
"""Fuzz the archiver manager's page archiving logic."""
|
|
try:
|
|
mock_app.archive_page(destination_hash, page_path, content, is_manual=True)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(state=st.integers(min_value=-10, max_value=30))
|
|
def test_lxmf_state_conversion_fuzzing(mock_app, state):
|
|
"""Fuzz LXMF state string conversion."""
|
|
mock_message = MagicMock()
|
|
mock_message.state = state
|
|
try:
|
|
ReticulumMeshChat.convert_lxmf_state_to_string(mock_message)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
|
@given(method=st.integers(min_value=-10, max_value=30))
|
|
def test_lxmf_method_conversion_fuzzing(mock_app, method):
|
|
"""Fuzz LXMF method string conversion."""
|
|
mock_message = MagicMock()
|
|
mock_message.method = method
|
|
try:
|
|
ReticulumMeshChat.convert_lxmf_method_to_string(mock_message)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def test_telephone_announce_fuzzing(mock_app):
|
|
"""Fuzz telephone announce reception."""
|
|
aspect = "telephone.call"
|
|
destination_hash = os.urandom(16)
|
|
announced_identity = MagicMock()
|
|
announced_identity.hash = os.urandom(32)
|
|
app_data = b"test_app_data"
|
|
announce_packet_hash = os.urandom(16)
|
|
|
|
try:
|
|
mock_app.on_telephone_announce_received(
|
|
aspect, destination_hash, announced_identity, app_data, announce_packet_hash,
|
|
)
|
|
except Exception:
|
|
pass
|