forked from p15670423/monkey
Island, Agent: Move local IP and interface retrieval to common
Both, island and agent needs to get local IP's/interfaces. We should also consider merging them, since interfaces also contain IP's
This commit is contained in:
parent
ae073de766
commit
11318dad14
|
@ -1,4 +1,30 @@
|
||||||
from typing import Optional, Tuple
|
import ipaddress
|
||||||
|
from ipaddress import IPv4Interface
|
||||||
|
from typing import List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
from netifaces import AF_INET, ifaddresses, interfaces
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_ip_addresses() -> Sequence[str]:
|
||||||
|
ip_list = []
|
||||||
|
for interface in interfaces():
|
||||||
|
addresses = ifaddresses(interface).get(AF_INET, [])
|
||||||
|
ip_list.extend([link["addr"] for link in addresses if link["addr"] != "127.0.0.1"])
|
||||||
|
return ip_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_interfaces() -> List[IPv4Interface]:
|
||||||
|
local_interfaces = []
|
||||||
|
for interface in interfaces():
|
||||||
|
addresses = ifaddresses(interface).get(AF_INET, [])
|
||||||
|
local_interfaces.extend(
|
||||||
|
[
|
||||||
|
ipaddress.IPv4Interface(link["addr"] + "/" + link["netmask"])
|
||||||
|
for link in addresses
|
||||||
|
if link["addr"] != "127.0.0.1"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return local_interfaces
|
||||||
|
|
||||||
|
|
||||||
# TODO: `address_to_port()` should return the port as an integer.
|
# TODO: `address_to_port()` should return the port as an integer.
|
||||||
|
|
|
@ -7,8 +7,9 @@ import requests
|
||||||
from urllib3 import disable_warnings
|
from urllib3 import disable_warnings
|
||||||
|
|
||||||
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
|
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
|
||||||
|
from common.network.network_utils import get_local_ip_addresses
|
||||||
from infection_monkey.config import GUID
|
from infection_monkey.config import GUID
|
||||||
from infection_monkey.network.info import get_host_subnets, local_ips
|
from infection_monkey.network.info import get_host_subnets
|
||||||
from infection_monkey.utils import agent_process
|
from infection_monkey.utils import agent_process
|
||||||
|
|
||||||
disable_warnings() # noqa DUO131
|
disable_warnings() # noqa DUO131
|
||||||
|
@ -38,7 +39,7 @@ class ControlClient:
|
||||||
monkey = {
|
monkey = {
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"hostname": hostname,
|
"hostname": hostname,
|
||||||
"ip_addresses": local_ips(),
|
"ip_addresses": get_local_ip_addresses(),
|
||||||
"networks": get_host_subnets(),
|
"networks": get_host_subnets(),
|
||||||
"description": " ".join(platform.uname()),
|
"description": " ".join(platform.uname()),
|
||||||
"parent": parent,
|
"parent": parent,
|
||||||
|
|
|
@ -11,8 +11,8 @@ from common import AgentRegistrationData
|
||||||
from common.agent_configuration import AgentConfiguration
|
from common.agent_configuration import AgentConfiguration
|
||||||
from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT
|
from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT
|
||||||
from common.credentials import Credentials
|
from common.credentials import Credentials
|
||||||
|
from common.network.network_utils import get_local_interfaces
|
||||||
from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError
|
from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError
|
||||||
from infection_monkey.network.info import get_network_interfaces
|
|
||||||
from infection_monkey.utils import agent_process
|
from infection_monkey.utils import agent_process
|
||||||
from infection_monkey.utils.ids import get_agent_id, get_machine_id
|
from infection_monkey.utils.ids import get_agent_id, get_machine_id
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class ControlChannel(IControlChannel):
|
||||||
# parent_id=parent,
|
# parent_id=parent,
|
||||||
parent_id=None, # None for now, until we change GUID to UUID
|
parent_id=None, # None for now, until we change GUID to UUID
|
||||||
cc_server=self._control_channel_server,
|
cc_server=self._control_channel_server,
|
||||||
network_interfaces=get_network_interfaces(),
|
network_interfaces=get_local_interfaces(),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -15,7 +15,11 @@ from common.event_serializers import (
|
||||||
register_common_agent_event_serializers,
|
register_common_agent_event_serializers,
|
||||||
)
|
)
|
||||||
from common.events import CredentialsStolenEvent
|
from common.events import CredentialsStolenEvent
|
||||||
from common.network.network_utils import address_to_ip_port
|
from common.network.network_utils import (
|
||||||
|
address_to_ip_port,
|
||||||
|
get_local_interfaces,
|
||||||
|
get_local_ip_addresses,
|
||||||
|
)
|
||||||
from common.utils.argparse_types import positive_int
|
from common.utils.argparse_types import positive_int
|
||||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||||
from common.version import get_version
|
from common.version import get_version
|
||||||
|
@ -45,7 +49,7 @@ from infection_monkey.master import AutomatedMaster
|
||||||
from infection_monkey.master.control_channel import ControlChannel
|
from infection_monkey.master.control_channel import ControlChannel
|
||||||
from infection_monkey.model import VictimHostFactory
|
from infection_monkey.model import VictimHostFactory
|
||||||
from infection_monkey.network.firewall import app as firewall
|
from infection_monkey.network.firewall import app as firewall
|
||||||
from infection_monkey.network.info import get_free_tcp_port, get_network_interfaces, local_ips
|
from infection_monkey.network.info import get_free_tcp_port
|
||||||
from infection_monkey.network.relay import TCPRelay
|
from infection_monkey.network.relay import TCPRelay
|
||||||
from infection_monkey.network.relay.utils import (
|
from infection_monkey.network.relay.utils import (
|
||||||
find_server,
|
find_server,
|
||||||
|
@ -201,7 +205,7 @@ class InfectionMonkey:
|
||||||
self._cmd_island_port,
|
self._cmd_island_port,
|
||||||
client_disconnect_timeout=config.keep_tunnel_open_time,
|
client_disconnect_timeout=config.keep_tunnel_open_time,
|
||||||
)
|
)
|
||||||
relay_servers = [f"{ip}:{relay_port}" for ip in local_ips()]
|
relay_servers = [f"{ip}:{relay_port}" for ip in get_local_ip_addresses()]
|
||||||
|
|
||||||
if not maximum_depth_reached(config.propagation.maximum_depth, self._current_depth):
|
if not maximum_depth_reached(config.propagation.maximum_depth, self._current_depth):
|
||||||
self._relay.start()
|
self._relay.start()
|
||||||
|
@ -220,7 +224,7 @@ class InfectionMonkey:
|
||||||
return agent_event_serializer_registry
|
return agent_event_serializer_registry
|
||||||
|
|
||||||
def _build_master(self, relay_servers: List[str]):
|
def _build_master(self, relay_servers: List[str]):
|
||||||
local_network_interfaces = InfectionMonkey._get_local_network_interfaces()
|
local_network_interfaces = get_local_interfaces()
|
||||||
|
|
||||||
# TODO control_channel and control_client have same responsibilities, merge them
|
# TODO control_channel and control_client have same responsibilities, merge them
|
||||||
propagation_credentials_repository = AggregatingPropagationCredentialsRepository(
|
propagation_credentials_repository = AggregatingPropagationCredentialsRepository(
|
||||||
|
@ -271,14 +275,6 @@ class InfectionMonkey:
|
||||||
AgentEventForwarder(server_address, agent_event_serializer_registry).send_event
|
AgentEventForwarder(server_address, agent_event_serializer_registry).send_event
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_local_network_interfaces() -> List[IPv4Interface]:
|
|
||||||
local_network_interfaces = get_network_interfaces()
|
|
||||||
for interface in local_network_interfaces:
|
|
||||||
logger.debug(f"Found local interface {str(interface)}")
|
|
||||||
|
|
||||||
return local_network_interfaces
|
|
||||||
|
|
||||||
def _build_puppet(
|
def _build_puppet(
|
||||||
self,
|
self,
|
||||||
propagation_credentials_repository: IPropagationCredentialsRepository,
|
propagation_credentials_repository: IPropagationCredentialsRepository,
|
||||||
|
|
|
@ -2,10 +2,9 @@ import itertools
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from ipaddress import IPv4Interface
|
|
||||||
from random import shuffle # noqa: DUO102
|
from random import shuffle # noqa: DUO102
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Dict, List, Set
|
from typing import Dict, Set
|
||||||
|
|
||||||
import netifaces
|
import netifaces
|
||||||
import psutil
|
import psutil
|
||||||
|
@ -29,10 +28,6 @@ class NetworkAddress:
|
||||||
domain: str
|
domain: str
|
||||||
|
|
||||||
|
|
||||||
def get_network_interfaces() -> List[IPv4Interface]:
|
|
||||||
return [IPv4Interface(f"{i['addr']}/{i['netmask']}") for i in get_host_subnets()]
|
|
||||||
|
|
||||||
|
|
||||||
def get_host_subnets():
|
def get_host_subnets():
|
||||||
"""
|
"""
|
||||||
Returns a list of subnets visible to host (omitting loopback and auto conf networks)
|
Returns a list of subnets visible to host (omitting loopback and auto conf networks)
|
||||||
|
@ -60,20 +55,12 @@ def get_host_subnets():
|
||||||
|
|
||||||
if is_windows_os():
|
if is_windows_os():
|
||||||
|
|
||||||
def local_ips():
|
|
||||||
local_hostname = socket.gethostname()
|
|
||||||
return socket.gethostbyname_ex(local_hostname)[2]
|
|
||||||
|
|
||||||
def get_routes():
|
def get_routes():
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
from fcntl import ioctl
|
from fcntl import ioctl
|
||||||
|
|
||||||
def local_ips():
|
|
||||||
valid_ips = [network["addr"] for network in get_host_subnets()]
|
|
||||||
return valid_ips
|
|
||||||
|
|
||||||
def get_routes(): # based on scapy implementation for route parsing
|
def get_routes(): # based on scapy implementation for route parsing
|
||||||
try:
|
try:
|
||||||
f = open("/proc/net/route", "r")
|
f = open("/proc/net/route", "r")
|
||||||
|
|
|
@ -19,7 +19,7 @@ from mongoengine import (
|
||||||
from monkey_island.cc.models.command_control_channel import CommandControlChannel
|
from monkey_island.cc.models.command_control_channel import CommandControlChannel
|
||||||
from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document
|
from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document
|
||||||
from monkey_island.cc.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
|
from monkey_island.cc.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
|
||||||
from monkey_island.cc.server_utils.network_utils import get_ip_addresses
|
from monkey_island.cc.server_utils.network_utils import get_cached_local_ip_addresses
|
||||||
|
|
||||||
|
|
||||||
class ParentNotFoundError(Exception):
|
class ParentNotFoundError(Exception):
|
||||||
|
@ -123,7 +123,8 @@ class Monkey(Document):
|
||||||
def get_label_by_id(object_id):
|
def get_label_by_id(object_id):
|
||||||
current_monkey = Monkey.get_single_monkey_by_id(object_id)
|
current_monkey = Monkey.get_single_monkey_by_id(object_id)
|
||||||
label = Monkey.get_hostname_by_id(object_id) + " : " + current_monkey.ip_addresses[0]
|
label = Monkey.get_hostname_by_id(object_id) + " : " + current_monkey.ip_addresses[0]
|
||||||
if len(set(current_monkey.ip_addresses).intersection(get_ip_addresses())) > 0:
|
local_ips = map(str, get_cached_local_ip_addresses())
|
||||||
|
if len(set(current_monkey.ip_addresses).intersection(local_ips)) > 0:
|
||||||
label = "MonkeyIsland - " + label
|
label = "MonkeyIsland - " + label
|
||||||
return label
|
return label
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ from monkey_island.cc.server_utils.consts import ( # noqa: E402
|
||||||
MONKEY_ISLAND_ABS_PATH,
|
MONKEY_ISLAND_ABS_PATH,
|
||||||
)
|
)
|
||||||
from monkey_island.cc.server_utils.island_logger import reset_logger, setup_logging # noqa: E402
|
from monkey_island.cc.server_utils.island_logger import reset_logger, setup_logging # noqa: E402
|
||||||
from monkey_island.cc.server_utils.network_utils import get_ip_addresses # noqa: E402
|
from monkey_island.cc.server_utils.network_utils import get_cached_local_ip_addresses # noqa: E402
|
||||||
from monkey_island.cc.services.initialize import initialize_services # noqa: E402
|
from monkey_island.cc.services.initialize import initialize_services # noqa: E402
|
||||||
from monkey_island.cc.setup import island_config_options_validator # noqa: E402
|
from monkey_island.cc.setup import island_config_options_validator # noqa: E402
|
||||||
from monkey_island.cc.setup import ( # noqa: E402
|
from monkey_island.cc.setup import ( # noqa: E402
|
||||||
|
@ -106,7 +106,7 @@ def _configure_logging(config_options):
|
||||||
def _collect_system_info() -> Tuple[Sequence[str], Deployment, Version]:
|
def _collect_system_info() -> Tuple[Sequence[str], Deployment, Version]:
|
||||||
deployment = _get_deployment()
|
deployment = _get_deployment()
|
||||||
version = Version(get_version(), deployment)
|
version = Version(get_version(), deployment)
|
||||||
return (get_ip_addresses(), deployment, version)
|
return (get_cached_local_ip_addresses(), deployment, version)
|
||||||
|
|
||||||
|
|
||||||
def _get_deployment() -> Deployment:
|
def _get_deployment() -> Deployment:
|
||||||
|
|
|
@ -1,23 +1,14 @@
|
||||||
import ipaddress
|
from ipaddress import IPv4Address, IPv4Interface
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from netifaces import AF_INET, ifaddresses, interfaces
|
|
||||||
from ring import lru
|
from ring import lru
|
||||||
|
|
||||||
# TODO: This functionality is duplicated in the agent. Unify them after 2216-tcp-relay is merged
|
from common.network.network_utils import get_local_interfaces, get_local_ip_addresses
|
||||||
|
|
||||||
|
|
||||||
# The local IP addresses list should not change often. Therefore, we can cache the result and never
|
|
||||||
# call this function more than once. This stopgap measure is here since this function is called a
|
|
||||||
# lot of times during the report generation. This means that if the interfaces of the Island machine
|
|
||||||
# change, the Island process needs to be restarted.
|
|
||||||
@lru(maxsize=1)
|
@lru(maxsize=1)
|
||||||
def get_ip_addresses() -> Sequence[str]:
|
def get_cached_local_ip_addresses() -> Sequence[IPv4Address]:
|
||||||
ip_list = []
|
return get_local_ip_addresses()
|
||||||
for interface in interfaces():
|
|
||||||
addresses = ifaddresses(interface).get(AF_INET, [])
|
|
||||||
ip_list.extend([link["addr"] for link in addresses if link["addr"] != "127.0.0.1"])
|
|
||||||
return ip_list
|
|
||||||
|
|
||||||
|
|
||||||
# The subnets list should not change often. Therefore, we can cache the result and never call this
|
# The subnets list should not change often. Therefore, we can cache the result and never call this
|
||||||
|
@ -25,15 +16,5 @@ def get_ip_addresses() -> Sequence[str]:
|
||||||
# during the report generation. This means that if the interfaces or subnets of the Island machine
|
# during the report generation. This means that if the interfaces or subnets of the Island machine
|
||||||
# change, the Island process needs to be restarted.
|
# change, the Island process needs to be restarted.
|
||||||
@lru(maxsize=1)
|
@lru(maxsize=1)
|
||||||
def get_subnets():
|
def get_cached_local_interfaces() -> Sequence[IPv4Interface]:
|
||||||
subnets = []
|
return get_local_interfaces()
|
||||||
for interface in interfaces():
|
|
||||||
addresses = ifaddresses(interface).get(AF_INET, [])
|
|
||||||
subnets.extend(
|
|
||||||
[
|
|
||||||
ipaddress.ip_interface(link["addr"] + "/" + link["netmask"]).network
|
|
||||||
for link in addresses
|
|
||||||
if link["addr"] != "127.0.0.1"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return subnets
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import monkey_island.cc.services.log
|
||||||
from monkey_island.cc import models
|
from monkey_island.cc import models
|
||||||
from monkey_island.cc.database import mongo
|
from monkey_island.cc.database import mongo
|
||||||
from monkey_island.cc.models import Monkey
|
from monkey_island.cc.models import Monkey
|
||||||
from monkey_island.cc.server_utils.network_utils import get_ip_addresses
|
from monkey_island.cc.server_utils.network_utils import get_cached_local_ip_addresses
|
||||||
from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService
|
from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService
|
||||||
from monkey_island.cc.services.edge.edge import EdgeService
|
from monkey_island.cc.services.edge.edge import EdgeService
|
||||||
from monkey_island.cc.services.utils.node_states import NodeStates
|
from monkey_island.cc.services.utils.node_states import NodeStates
|
||||||
|
@ -110,7 +110,7 @@ class NodeService:
|
||||||
def get_monkey_label(monkey):
|
def get_monkey_label(monkey):
|
||||||
# todo
|
# todo
|
||||||
label = monkey["hostname"] + " : " + monkey["ip_addresses"][0]
|
label = monkey["hostname"] + " : " + monkey["ip_addresses"][0]
|
||||||
ip_addresses = get_ip_addresses()
|
ip_addresses = get_cached_local_ip_addresses()
|
||||||
if len(set(monkey["ip_addresses"]).intersection(ip_addresses)) > 0:
|
if len(set(monkey["ip_addresses"]).intersection(ip_addresses)) > 0:
|
||||||
label = "MonkeyIsland - " + label
|
label = "MonkeyIsland - " + label
|
||||||
return label
|
return label
|
||||||
|
@ -118,7 +118,7 @@ class NodeService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_monkey_group(monkey):
|
def get_monkey_group(monkey):
|
||||||
keywords = []
|
keywords = []
|
||||||
if len(set(monkey["ip_addresses"]).intersection(get_ip_addresses())) != 0:
|
if len(set(monkey["ip_addresses"]).intersection(get_cached_local_ip_addresses())) != 0:
|
||||||
keywords.extend(["island", "monkey"])
|
keywords.extend(["island", "monkey"])
|
||||||
else:
|
else:
|
||||||
monkey_type = "manual" if NodeService.get_monkey_manual_run(monkey) else "monkey"
|
monkey_type = "manual" if NodeService.get_monkey_manual_run(monkey) else "monkey"
|
||||||
|
@ -275,7 +275,7 @@ class NodeService:
|
||||||
# It's better to just initialize the island machine on reset I think
|
# It's better to just initialize the island machine on reset I think
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_monkey_island_monkey():
|
def get_monkey_island_monkey():
|
||||||
ip_addresses = get_ip_addresses()
|
ip_addresses = get_cached_local_ip_addresses()
|
||||||
for ip_address in ip_addresses:
|
for ip_address in ip_addresses:
|
||||||
monkey = NodeService.get_monkey_by_ip(ip_address)
|
monkey = NodeService.get_monkey_by_ip(ip_address)
|
||||||
if monkey is not None:
|
if monkey is not None:
|
||||||
|
@ -297,7 +297,7 @@ class NodeService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_monkey_island_node():
|
def get_monkey_island_node():
|
||||||
island_node = NodeService.get_monkey_island_pseudo_net_node()
|
island_node = NodeService.get_monkey_island_pseudo_net_node()
|
||||||
island_node["ip_addresses"] = get_ip_addresses()
|
island_node["ip_addresses"] = get_cached_local_ip_addresses()
|
||||||
island_node["domain_name"] = socket.gethostname()
|
island_node["domain_name"] = socket.gethostname()
|
||||||
return island_node
|
return island_node
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,10 @@ from monkey_island.cc.database import mongo
|
||||||
from monkey_island.cc.models import Monkey
|
from monkey_island.cc.models import Monkey
|
||||||
from monkey_island.cc.models.report import get_report, save_report
|
from monkey_island.cc.models.report import get_report, save_report
|
||||||
from monkey_island.cc.repository import IAgentConfigurationRepository, ICredentialsRepository
|
from monkey_island.cc.repository import IAgentConfigurationRepository, ICredentialsRepository
|
||||||
from monkey_island.cc.server_utils.network_utils import get_ip_addresses, get_subnets
|
from monkey_island.cc.server_utils.network_utils import (
|
||||||
|
get_cached_local_interfaces,
|
||||||
|
get_cached_local_ip_addresses,
|
||||||
|
)
|
||||||
from monkey_island.cc.services.node import NodeService
|
from monkey_island.cc.services.node import NodeService
|
||||||
from monkey_island.cc.services.reporting.exploitations.manual_exploitation import get_manual_monkeys
|
from monkey_island.cc.services.reporting.exploitations.manual_exploitation import get_manual_monkeys
|
||||||
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
|
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
|
||||||
|
@ -175,7 +178,7 @@ class ReportService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_island_cross_segment_issues():
|
def get_island_cross_segment_issues():
|
||||||
issues = []
|
issues = []
|
||||||
island_ips = get_ip_addresses()
|
island_ips = get_cached_local_ip_addresses()
|
||||||
for monkey in mongo.db.monkey.find(
|
for monkey in mongo.db.monkey.find(
|
||||||
{"tunnel": {"$exists": False}}, {"tunnel": 1, "guid": 1, "hostname": 1}
|
{"tunnel": {"$exists": False}}, {"tunnel": 1, "guid": 1, "hostname": 1}
|
||||||
):
|
):
|
||||||
|
@ -194,7 +197,9 @@ class ReportService:
|
||||||
"type": "island_cross_segment",
|
"type": "island_cross_segment",
|
||||||
"machine": monkey["hostname"],
|
"machine": monkey["hostname"],
|
||||||
"networks": [str(subnet) for subnet in monkey_subnets],
|
"networks": [str(subnet) for subnet in monkey_subnets],
|
||||||
"server_networks": [str(subnet) for subnet in get_subnets()],
|
"server_networks": [
|
||||||
|
str(interface.network) for interface in get_cached_local_interfaces()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue