backend: Tor integration:
- Add the `txtorcon` optional dependency, used with `tor` extra. - Add settings for Tor. - Use Tor endpoint when an `.onion` XMPP domain is used, of when Tor is explicitely enabled in conf. - Disable WebRTC, for now it's the safer option as WebRTC can leak IP. It should be possible to use it over Tor by forcing the use of a .onion relay, but this is to be explored later (A/V should be to slow to be usable, but that stay relevant for file transfer or other WebRTC powered things). - A new `treq` instance using Tor WebAgent is used when Tor is enabled. Properties has been added to SatXMPPEntity to conveniently get the right instance. - Another `tor_mode` property has been added with corresponding bridge methods, to see if Tor is activated and in which mode for a given profile. For now there are only two modes (disabled, and enabled with WebRTC disabled), but in the future it may be possible to activate WebRTC over Tor, or to use Tor only to connect to XMPP server, while using clearnet for other connections. - Tor dedicated models have been added, and bridge methods have been added to build and get them. - Fix tests. rel 473
This commit is contained in:
parent
4db02adf1d
commit
c5396f0acf
18 changed files with 484 additions and 43 deletions
|
|
@ -1113,3 +1113,24 @@ sig_out=
|
|||
doc=Cleans expired notifications and older profile-specific notifications
|
||||
doc_param_0=limit_timestamp: Timestamp limit for older notifications. If -1.0, only truly expired notifications are removed.
|
||||
doc_param_1=profile_key: Profile key (use "@NONE@" to indicate no specific profile, otherwise only notification for given profile will be expired, in addition to truly expired notifications).
|
||||
|
||||
[tor_infos_get]
|
||||
async=
|
||||
type=method
|
||||
category=core
|
||||
sig_in=ss
|
||||
sig_out=s
|
||||
doc=Get informations on running Tor daemon and its state with current profile
|
||||
doc_param_0=extra: extra data
|
||||
doc_param_1=%(doc_profile_key)s
|
||||
doc_return=serialised ``libervia.backend.models.tor.TorData``.
|
||||
|
||||
[tor_mode_get]
|
||||
async=
|
||||
type=method
|
||||
category=core
|
||||
sig_in=s
|
||||
sig_out=s
|
||||
doc=Tell is Tor is used for this profile.
|
||||
doc_param_0=%(doc_profile_key)s
|
||||
doc_return=State of the use of Tor for this profile.
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ class DBusObject(objects.DBusObject):
|
|||
Method("session_infos_get", arguments="s", returns="a{ss}"),
|
||||
Method("sub_waiting_get", arguments="s", returns="a{ss}"),
|
||||
Method("subscription", arguments="sss", returns=""),
|
||||
Method("tor_infos_get", arguments="ss", returns="s"),
|
||||
Method("version_get", arguments="", returns="s"),
|
||||
Signal("_debug", "sa{ss}s"),
|
||||
Signal("action_new", "ssis"),
|
||||
|
|
@ -530,6 +531,9 @@ class DBusObject(objects.DBusObject):
|
|||
def dbus_subscription(self, sub_type, entity, profile_key="@DEFAULT@"):
|
||||
return self._callback("subscription", sub_type, entity, profile_key)
|
||||
|
||||
def dbus_tor_infos_get(self, extra, profile_key):
|
||||
return self._callback("tor_infos_get", extra, profile_key)
|
||||
|
||||
def dbus_version_get(
|
||||
self,
|
||||
):
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ from libervia.backend.models.core import MessageData
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from libervia.backend.core.xmpp import LiberviaRosterProtocol
|
||||
from libervia.backend.models.tor import TorMode
|
||||
from treq.client import HTTPClient
|
||||
from txtorcon import Tor
|
||||
|
||||
|
||||
EncryptionPlugin = namedtuple(
|
||||
|
|
@ -79,8 +82,33 @@ class SatXMPPEntity(ABC):
|
|||
jid: t_jid.JID
|
||||
is_component: bool
|
||||
server_jid: t_jid.JID
|
||||
port: int
|
||||
identities: list[disco.DiscoIdentity]
|
||||
xmlstream: xmlstream.XmlStream
|
||||
tor: "Tor | None" = None
|
||||
|
||||
@property
|
||||
def tor_mode(self) -> "TorMode":
|
||||
"""Return Tor mode for this profile."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def treq(self) -> "HTTPClient":
|
||||
"""Get the HTTP client.
|
||||
|
||||
@return (HTTPClient): the HTTP client
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def treq_ignore_tls(self) -> "HTTPClient":
|
||||
"""Get the HTTP client ignoring TLS errors.
|
||||
|
||||
@return (HTTPClient): the HTTP client ignoring TLS errors
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -98,11 +98,17 @@ class InternalError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class InitError(Exception):
|
||||
"""Something went wrong during initialisation."""
|
||||
|
||||
|
||||
class FeatureNotFound(
|
||||
Exception
|
||||
): # a disco feature/identity which is needed is not present
|
||||
pass
|
||||
|
||||
class FeatureBlocked(Exception):
|
||||
"""Feature is implemented but blocked."""
|
||||
|
||||
class BridgeInitError(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -23,14 +23,12 @@ import hashlib
|
|||
import copy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import Optional, List, Tuple, Dict
|
||||
|
||||
from pydantic import ValidationError
|
||||
from typing import Optional, List, Tuple, Dict, cast
|
||||
|
||||
from libervia import backend
|
||||
from libervia.backend.core.i18n import _, D_, language_switch
|
||||
from libervia.backend.core import patches
|
||||
from libervia.backend.tools.config import DBusBridgeSettings
|
||||
from libervia.backend.models.tor import TorData, TorMode
|
||||
|
||||
patches.apply()
|
||||
from twisted.application import app, service
|
||||
|
|
@ -41,7 +39,7 @@ from wokkel.xmppim import RosterItem
|
|||
from libervia.backend import G
|
||||
from libervia.backend.core import xmpp
|
||||
from libervia.backend.core import exceptions
|
||||
from libervia.backend.core.core_types import SatXMPPEntity
|
||||
from libervia.backend.core.core_types import SatXMPPClient, SatXMPPEntity
|
||||
from libervia.backend.core.log import getLogger
|
||||
|
||||
from libervia.backend.core.constants import Const as C
|
||||
|
|
@ -271,6 +269,8 @@ class LiberviaBackend(service.Service):
|
|||
self.bridge.register_method(
|
||||
"notifications_expired_clean", self.memory._notifications_expired_clean
|
||||
)
|
||||
self.bridge.register_method("tor_infos_get", self._get_tor_infos)
|
||||
self.bridge.register_method("tor_mode_get", self._tor_mode_get)
|
||||
|
||||
await self.memory.initialise()
|
||||
self.common_cache = cache.Cache(self, None)
|
||||
|
|
@ -805,6 +805,8 @@ class LiberviaBackend(service.Service):
|
|||
data = {"jid": client.jid.full(), "started": str(int(client.started))}
|
||||
return defer.succeed(data)
|
||||
|
||||
# infos
|
||||
|
||||
def _get_devices_infos(self, bare_jid, profile_key):
|
||||
client = self.get_client(profile_key)
|
||||
if not bare_jid:
|
||||
|
|
@ -866,6 +868,28 @@ class LiberviaBackend(service.Service):
|
|||
|
||||
return ret_data
|
||||
|
||||
def _get_tor_infos(self, extra_s: str, profile_key: str) -> defer.Deferred[str]:
|
||||
client = self.get_client(profile_key)
|
||||
d = defer.ensureDeferred(
|
||||
self.get_tor_infos(client, data_format.deserialise(extra_s))
|
||||
)
|
||||
d.addCallback(lambda tor_data: tor_data.model_dump_json())
|
||||
return cast(defer.Deferred[str], d)
|
||||
|
||||
async def get_tor_infos(self, client: SatXMPPClient, extra: dict) -> TorData:
|
||||
"""Return various informations on Tor daemon and its state with current profile.
|
||||
|
||||
@param client: Client instance.
|
||||
@param extra: Extra data (reserver for future use).
|
||||
@return: Tor data
|
||||
@raise exceptions.FeatureNotFound: Tor is not available for this profile.
|
||||
"""
|
||||
return await TorData.from_client(client)
|
||||
|
||||
def _tor_mode_get(self, profile_key: str) -> defer.Deferred[str]:
|
||||
client = self.get_client(profile_key)
|
||||
return defer.succeed(client.tor_mode)
|
||||
|
||||
# images
|
||||
|
||||
def _image_check(self, path):
|
||||
|
|
|
|||
|
|
@ -24,14 +24,18 @@ import mimetypes
|
|||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Callable, Dict, Optional, Tuple, cast
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, cast
|
||||
from urllib.parse import unquote, urlparse
|
||||
import uuid
|
||||
|
||||
import shortuuid
|
||||
from treq.api import default_pool
|
||||
from treq.client import HTTPClient
|
||||
from twisted.internet import defer, error as internet_error, reactor
|
||||
from twisted.internet import ssl
|
||||
from twisted.internet.endpoints import TCP4ClientEndpoint, UNIXClientEndpoint
|
||||
from twisted.python import failure
|
||||
from twisted.web.client import Agent
|
||||
from twisted.words.protocols.jabber import xmlstream
|
||||
from twisted.words.protocols.jabber import error
|
||||
from twisted.words.protocols.jabber import jid
|
||||
|
|
@ -52,12 +56,26 @@ from libervia.backend.memory import cache
|
|||
from libervia.backend.memory import encryption
|
||||
from libervia.backend.memory import persistent
|
||||
from libervia.backend.models.core import MessageData
|
||||
from libervia.backend.models.tor import TorData, TorMode
|
||||
from libervia.backend.tools import xml_tools
|
||||
from libervia.backend.tools import utils
|
||||
from libervia.backend.tools.common import data_format
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import txtorcon
|
||||
|
||||
from txtorcon import Tor
|
||||
from txtorcon.util import default_control_port
|
||||
else:
|
||||
try:
|
||||
import txtorcon
|
||||
from txtorcon import Tor
|
||||
from txtorcon.util import default_control_port
|
||||
except ImportError:
|
||||
txtorcon = None
|
||||
|
||||
|
||||
NS_DISCO_INFO = "http://jabber.org/protocol/disco#info"
|
||||
NS_ROSTER_VER = "urn:xmpp:features:rosterver"
|
||||
|
|
@ -115,6 +133,8 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
|
|||
self._connected_d = None
|
||||
self.profile = profile
|
||||
self.host_app = host_app
|
||||
self._treq: HTTPClient | None = None
|
||||
self._treq_ignore_tls: HTTPClient | None = None
|
||||
self.cache = cache.Cache(host_app, profile)
|
||||
self.mess_id2uid = {} # map from message id to uid used in history.
|
||||
# Key: (full_jid, message_id) Value: uid
|
||||
|
|
@ -131,6 +151,44 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
|
|||
def __repr__(self):
|
||||
return f"{super().__repr__()} - profile: {self.profile!r}"
|
||||
|
||||
@property
|
||||
def tor_mode(self) -> TorMode:
|
||||
"""Return Tor mode for this profile."""
|
||||
if self.tor is None:
|
||||
return TorMode.DISABLED
|
||||
else:
|
||||
return TorMode.ENABLED_NO_WEBRTC
|
||||
|
||||
@property
|
||||
def treq(self) -> HTTPClient:
|
||||
"""Get treq instance, using Tor if activated."""
|
||||
if self._treq is None:
|
||||
if self.tor is not None:
|
||||
treq = HTTPClient(agent=self.tor.web_agent()) # type: ignore
|
||||
self._treq = treq
|
||||
else:
|
||||
pool = default_pool(reactor, None, None)
|
||||
agent = Agent(reactor, pool=pool)
|
||||
self._treq = HTTPClient(agent=agent) # type: ignore
|
||||
return self._treq
|
||||
|
||||
@property
|
||||
def treq_ignore_tls(self) -> HTTPClient:
|
||||
"""Get treq instance WHICH DOESN'T CHECK TLS. Tor is used if activated.
|
||||
|
||||
This is obviously highly insecure and should not be used without explicit warning.
|
||||
In the case where Tor is use, it can be legitimate to ignore TLS for .onion
|
||||
domains.
|
||||
"""
|
||||
if self._treq_ignore_tls is None:
|
||||
if self.tor is not None:
|
||||
self._treq_ignore_tls = HTTPClient(agent=self.tor.web_agent(tls_context_factory=NoCheckContextFactory())) # type: ignore
|
||||
else:
|
||||
pool = default_pool(reactor, None, None)
|
||||
agent = Agent(reactor, contextFactory=NoCheckContextFactory(), pool=pool) # type: ignore
|
||||
self._treq_ignore_tls = HTTPClient(agent=agent) # type: ignore
|
||||
return self._treq_ignore_tls
|
||||
|
||||
## initialisation ##
|
||||
|
||||
async def _call_connection_triggers(self, connection_timer):
|
||||
|
|
@ -198,6 +256,28 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
|
|||
timer["connected"] = time.time() - connected_start
|
||||
timer["total"] += timer["connected"]
|
||||
|
||||
@staticmethod
|
||||
async def _prepare_tor() -> Tor:
|
||||
log.debug("We have an .onion domain, we use Tor to connect.")
|
||||
if txtorcon is None:
|
||||
raise exceptions.MissingModule(
|
||||
'"txtorcon" module is not available, we can\'t use Tor. Please '
|
||||
"install it and make sur that Tor daemon is available."
|
||||
)
|
||||
try:
|
||||
config = G.config.tor
|
||||
if config.control_path is not None:
|
||||
endpoint = UNIXClientEndpoint(reactor, config.control_path)
|
||||
else:
|
||||
endpoint = TCP4ClientEndpoint(
|
||||
reactor, config.control_address, config.control_port
|
||||
)
|
||||
return await txtorcon.connect(reactor, endpoint)
|
||||
except Exception as e:
|
||||
msg = f"Can't connect to Tor: {e}"
|
||||
log.error(msg)
|
||||
raise exceptions.InitError(msg)
|
||||
|
||||
@classmethod
|
||||
async def start_connection(cls, host, profile, max_retries):
|
||||
"""instantiate the entity and start the connection"""
|
||||
|
|
@ -261,6 +341,14 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
|
|||
)
|
||||
log.debug(f"removing unconnected profile {profile!r}")
|
||||
del host.profiles[profile]
|
||||
|
||||
kwargs = {}
|
||||
|
||||
if not cls.is_component and (
|
||||
entity_jid.host.endswith(".onion") or G.config.tor.enabled
|
||||
):
|
||||
kwargs["tor"] = await cls._prepare_tor()
|
||||
|
||||
entity = host.profiles[profile] = cls(
|
||||
host,
|
||||
profile,
|
||||
|
|
@ -272,6 +360,7 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
|
|||
or None,
|
||||
port,
|
||||
max_retries,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
await entity.encryption.load_sessions()
|
||||
|
|
@ -950,6 +1039,7 @@ class SatXMPPClient(SatXMPPEntity, core_types.SatXMPPClient, wokkel_client.XMPPC
|
|||
host=None,
|
||||
port=C.XMPP_C2S_PORT,
|
||||
max_retries=C.XMPP_MAX_RETRIES,
|
||||
tor: Tor | None = None,
|
||||
):
|
||||
# XXX: DNS SRV records are checked when the host is not specified.
|
||||
# If no SRV record is found, the host is directly extracted from the JID.
|
||||
|
|
@ -991,7 +1081,7 @@ class SatXMPPClient(SatXMPPEntity, core_types.SatXMPPClient, wokkel_client.XMPPC
|
|||
"check_certificate", "Connection", profile_key=profile
|
||||
)
|
||||
|
||||
if self.check_certificate:
|
||||
if self.check_certificate and tor is None:
|
||||
tls_required, configurationForTLS = True, None
|
||||
else:
|
||||
tls_required = False
|
||||
|
|
@ -1007,10 +1097,11 @@ class SatXMPPClient(SatXMPPEntity, core_types.SatXMPPClient, wokkel_client.XMPPC
|
|||
configurationForTLS=configurationForTLS,
|
||||
)
|
||||
SatXMPPEntity.__init__(self, host_app, profile, max_retries)
|
||||
self.tor = tor
|
||||
|
||||
if not self.check_certificate:
|
||||
msg = _(
|
||||
"Certificate validation is deactivated, this is unsecure and "
|
||||
"Certificate validation is deactivated, this is insecure and "
|
||||
"somebody may be spying on you. If you have no good reason to disable "
|
||||
'certificate validation, please activate "Check certificate" in your '
|
||||
'settings in "Connection" tab.'
|
||||
|
|
@ -1038,6 +1129,19 @@ class SatXMPPClient(SatXMPPEntity, core_types.SatXMPPClient, wokkel_client.XMPPC
|
|||
self.presence = SatPresenceProtocol(self.host_app)
|
||||
self.presence.setHandlerParent(self)
|
||||
|
||||
def _getConnection(self):
|
||||
if self.tor is not None:
|
||||
assert self.tor is not None
|
||||
log.info(f"Connecting {self.profile} via Tor.")
|
||||
try:
|
||||
endpoint = self.tor.stream_via(self.jid.host, self.port)
|
||||
return endpoint.connect(self.factory)
|
||||
except Exception as e:
|
||||
log.exception("Can't connect to Tor domain.")
|
||||
raise exceptions.NetworkError(str(e))
|
||||
else:
|
||||
return super()._getConnection()
|
||||
|
||||
@classmethod
|
||||
async def start_connection(cls, host, profile, max_retries):
|
||||
try:
|
||||
|
|
|
|||
153
libervia/backend/models/tor.py
Normal file
153
libervia/backend/models/tor.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Libervia Tor models
|
||||
# Copyright (C) 2009-2025 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from enum import StrEnum, auto
|
||||
from typing import TYPE_CHECKING, Self
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from libervia.backend.core import exceptions
|
||||
from libervia.backend.core.log import getLogger
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from libervia.backend.core.core_types import SatXMPPClient, SatXMPPEntity
|
||||
|
||||
|
||||
class TorMode(StrEnum):
|
||||
#: Tor is completely disabled.
|
||||
DISABLED = auto()
|
||||
#: Tor is enable for all connections, but WebRTC is disabled (to avoid leaking IP).
|
||||
ENABLED_NO_WEBRTC = auto()
|
||||
|
||||
|
||||
class Relay(BaseModel):
|
||||
id: str = Field(description="Hex-encoded router identity fingerprint")
|
||||
name: str = Field(description="Router nickname or name")
|
||||
ip: str = Field(description="IP address")
|
||||
flags: list = Field(description='Router flags (e.g., "Guard", "Fast")')
|
||||
bandwidth: int | None = Field(
|
||||
default=None, description="Reported bandwidth in bytes/sec"
|
||||
)
|
||||
country: str = Field(description="Country code (ISO 3166-1 alpha-2)")
|
||||
details_url: str = Field(
|
||||
default_factory=lambda data: f"https://metrics.torproject.org/rs.html#details/{data['id']}",
|
||||
description="URL of metrics website with more details on the relay.",
|
||||
)
|
||||
|
||||
|
||||
class Circuit(BaseModel):
|
||||
id: int = Field(description="Unique identifier for the circuit")
|
||||
state: str = Field(
|
||||
description="Current state of the circuit (e.g., 'BUILT', 'FAILED')"
|
||||
)
|
||||
purpose: str = Field(
|
||||
description="Purpose of the circuit (e.g., 'GENERAL', 'HS_CLIENT_INTRO')"
|
||||
)
|
||||
path: list[Relay] = Field(description="List of relays in the circuit path")
|
||||
|
||||
|
||||
class Traffic(BaseModel):
|
||||
read: int = Field(description="Bytes read (downloaded) since Tor started")
|
||||
written: int = Field(description="Bytes written (uploaded) since Tor started")
|
||||
|
||||
|
||||
class TorData(BaseModel):
|
||||
mode: TorMode = Field(description="In which mode Tor active")
|
||||
version: str = Field(description="Tor version")
|
||||
uptime: int = Field(description="Uptime of the Tor daemon in seconds")
|
||||
traffic: Traffic = Field(description="Global traffic statistics")
|
||||
circuit: Circuit | None = Field(
|
||||
default=None, description="Circuit used for XMPP connection"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def from_client(cls, client: "SatXMPPClient") -> Self:
|
||||
"""Built an instance from ``client.tor``.
|
||||
|
||||
@return: Instance of TorData
|
||||
"""
|
||||
if client.tor is None:
|
||||
raise exceptions.FeatureNotFound("Tor is not used for this profile.")
|
||||
host = client.jid.host
|
||||
|
||||
# Initialize with basic version info
|
||||
infos = {
|
||||
"mode": client.tor_mode,
|
||||
"version": client.tor.version,
|
||||
"uptime": None,
|
||||
"traffic": None,
|
||||
"circuit": None,
|
||||
}
|
||||
|
||||
state = await client.tor.create_state()
|
||||
|
||||
for stream in state.streams.values():
|
||||
if (
|
||||
stream.target_host == host
|
||||
and stream.target_port == client.port
|
||||
and stream.circuit is not None
|
||||
and stream.state == "SUCCEEDED"
|
||||
):
|
||||
circuit = stream.circuit
|
||||
# We have the active stream.
|
||||
router_infos = []
|
||||
for router in circuit.path:
|
||||
try:
|
||||
location = await router.get_location()
|
||||
country = location.countrycode
|
||||
except Exception:
|
||||
log.exception("Can't get location.")
|
||||
country = "UNKNOWN"
|
||||
|
||||
router_info = Relay(
|
||||
id=router.id_hex.lstrip("$"),
|
||||
name=router.name,
|
||||
ip=router.ip,
|
||||
flags=router.flags,
|
||||
bandwidth=getattr(router, "bandwidth", None),
|
||||
country=country,
|
||||
)
|
||||
router_infos.append(router_info)
|
||||
|
||||
circuit_info = Circuit(
|
||||
id=circuit.id,
|
||||
state=circuit.state,
|
||||
purpose=circuit.purpose,
|
||||
path=router_infos,
|
||||
)
|
||||
infos["circuit"] = circuit_info
|
||||
|
||||
try:
|
||||
infos_dict = await client.tor.protocol.get_info(
|
||||
"uptime", "traffic/read", "traffic/written"
|
||||
)
|
||||
except Exception:
|
||||
log.exception("Can't get Tor infos.")
|
||||
raise exceptions.DataError("Tor infos not available.")
|
||||
else:
|
||||
if "uptime" in infos_dict:
|
||||
infos["uptime"] = infos_dict["uptime"]
|
||||
if "traffic/read" in infos_dict and "traffic/written" in infos_dict:
|
||||
infos["traffic"] = Traffic(
|
||||
read=infos_dict["traffic/read"].strip(),
|
||||
written=infos_dict["traffic/written"].strip(),
|
||||
)
|
||||
|
||||
return cls(**infos)
|
||||
|
|
@ -21,7 +21,7 @@ from pathlib import Path
|
|||
from typing import Any, Dict, Optional, Union, Tuple, Callable
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
import treq
|
||||
from treq import collect
|
||||
from twisted.internet import defer
|
||||
from twisted.words.protocols.jabber import error as jabber_error
|
||||
|
||||
|
|
@ -33,7 +33,6 @@ from libervia.backend.core.log import getLogger
|
|||
from libervia.backend.tools import xml_tools
|
||||
from libervia.backend.tools import stream
|
||||
from libervia.backend.tools.common import data_format
|
||||
from libervia.backend.tools.web import treq_client_no_ssl
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
|
@ -344,9 +343,9 @@ class DownloadPlugin(object):
|
|||
|
||||
if options.get("ignore_tls_errors", False):
|
||||
log.warning("TLS certificate check disabled, this is highly insecure")
|
||||
treq_client = treq_client_no_ssl
|
||||
treq_client = client.treq_ignore_tls
|
||||
else:
|
||||
treq_client = treq
|
||||
treq_client = client.treq
|
||||
|
||||
head_data = await treq_client.head(url)
|
||||
try:
|
||||
|
|
@ -366,7 +365,7 @@ class DownloadPlugin(object):
|
|||
|
||||
resp = await treq_client.get(url, unbuffered=True)
|
||||
if resp.code == 200:
|
||||
d = treq.collect(resp, file_obj.write)
|
||||
d = collect(resp, file_obj.write)
|
||||
d.addBoth(lambda _: file_obj.close())
|
||||
else:
|
||||
d = defer.Deferred()
|
||||
|
|
|
|||
|
|
@ -26,14 +26,13 @@ from cryptography.hazmat.primitives import ciphers
|
|||
from cryptography.hazmat.primitives.ciphers import modes
|
||||
from cryptography.hazmat import backends
|
||||
from cryptography.exceptions import AlreadyFinalized
|
||||
import treq
|
||||
from treq import collect
|
||||
from twisted.internet import defer
|
||||
from libervia.backend.core.i18n import _
|
||||
from libervia.backend.core.constants import Const as C
|
||||
from libervia.backend.core import exceptions
|
||||
from libervia.backend.tools import stream
|
||||
from libervia.backend.core.log import getLogger
|
||||
from libervia.backend.tools.web import treq_client_no_ssl
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
|
@ -103,9 +102,9 @@ class AESGCM(object):
|
|||
|
||||
if options.get("ignore_tls_errors", False):
|
||||
log.warning("TLS certificate check disabled, this is highly insecure")
|
||||
treq_client = treq_client_no_ssl
|
||||
treq_client = client.treq_ignore_tls
|
||||
else:
|
||||
treq_client = treq
|
||||
treq_client = client.treq
|
||||
|
||||
head_data = await treq_client.head(download_url)
|
||||
content_length = int(head_data.headers.getRawHeaders("content-length")[0])
|
||||
|
|
@ -124,7 +123,7 @@ class AESGCM(object):
|
|||
|
||||
resp = await treq_client.get(download_url, unbuffered=True)
|
||||
if resp.code == 200:
|
||||
d = treq.collect(
|
||||
d = collect(
|
||||
resp,
|
||||
partial(
|
||||
self.on_data_download,
|
||||
|
|
|
|||
|
|
@ -31,17 +31,18 @@ from libervia.backend.core.constants import Const as C
|
|||
from libervia.backend.core.core_types import SatXMPPEntity
|
||||
from libervia.backend.core.i18n import D_, _
|
||||
from libervia.backend.core.log import getLogger
|
||||
from libervia.backend.models.tor import TorMode
|
||||
from libervia.backend.tools import xml_tools
|
||||
from libervia.backend.tools.common import data_format
|
||||
|
||||
from . import mapping
|
||||
from ..plugin_xep_0166 import BaseApplicationHandler
|
||||
from .constants import (
|
||||
NS_AV_CONFERENCES,
|
||||
NS_JINGLE_RTP,
|
||||
NS_JINGLE_RTP_AUDIO,
|
||||
NS_JINGLE_RTP_INFO,
|
||||
NS_JINGLE_RTP_VIDEO,
|
||||
NS_AV_CONFERENCES,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -278,7 +279,13 @@ class XEP_0167(BaseApplicationHandler):
|
|||
|
||||
@raises exceptions.DataError: If media data is invalid or duplicate content name
|
||||
(mid) is found.
|
||||
@raise exceptions.FeatureBlocked: WebRTC is blocked for pricavy reasons.
|
||||
"""
|
||||
if client.tor_mode != TorMode.DISABLED:
|
||||
raise exceptions.FeatureBlocked(
|
||||
"WebRTC is disabled when using Tor to avoid leaking IP address."
|
||||
)
|
||||
|
||||
metadata = self.parse_call_data(call_data)
|
||||
contents = self.get_contents(call_data, metadata)
|
||||
if not contents:
|
||||
|
|
@ -415,7 +422,13 @@ class XEP_0167(BaseApplicationHandler):
|
|||
@param call_type: Type of media (audio or video).
|
||||
|
||||
@return: True if the call has been accepted
|
||||
@raise exceptions.FeatureBlocked: WebRTC is blocked for pricavy reasons.
|
||||
"""
|
||||
if client.tor_mode != TorMode.DISABLED:
|
||||
raise exceptions.FeatureBlocked(
|
||||
"WebRTC is disabled when using Tor to avoid leaking IP address."
|
||||
)
|
||||
|
||||
peer_jid = session["peer_jid"]
|
||||
|
||||
session["call_type"] = call_type
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from libervia.backend.core.constants import Const as C
|
|||
from libervia.backend.core.core_types import SatXMPPEntity
|
||||
from libervia.backend.core.i18n import D_, _
|
||||
from libervia.backend.core.log import getLogger
|
||||
from libervia.backend.models.tor import TorMode
|
||||
from libervia.backend.tools import xml_tools
|
||||
from libervia.backend.tools import utils
|
||||
from libervia.backend.tools import stream
|
||||
|
|
@ -407,6 +408,12 @@ class XEP_0234(BaseApplicationHandler):
|
|||
@param file_desc: description of the file
|
||||
@return: progress id
|
||||
"""
|
||||
if extra and extra.get("webrtc") and client.tor_mode != TorMode.DISABLED:
|
||||
raise exceptions.FeatureBlocked(
|
||||
"WebRTC file transfer is disabled when using Tor to avoid leaking IP "
|
||||
"address."
|
||||
)
|
||||
|
||||
progress_id_d = defer.Deferred()
|
||||
if extra is None:
|
||||
extra = {}
|
||||
|
|
@ -778,6 +785,12 @@ class XEP_0234(BaseApplicationHandler):
|
|||
"""parse file_elt, and handle user permission/file opening"""
|
||||
transport_data = content_data["transport_data"]
|
||||
webrtc = transport_data.get("webrtc", False)
|
||||
if webrtc and client.tor_mode != TorMode.DISABLED:
|
||||
raise exceptions.FeatureBlocked(
|
||||
"WebRTC file transfer is disabled when using Tor to avoid leaking IP "
|
||||
"address."
|
||||
)
|
||||
|
||||
# file may have been already accepted in preflight
|
||||
pre_accepted = session.get("pre_accepted", False)
|
||||
file_data = await self.parse_file_element(client, file_elt, file_data, given=True)
|
||||
|
|
|
|||
|
|
@ -16,19 +16,14 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import namedtuple
|
||||
from functools import partial
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
Final,
|
||||
List,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Self,
|
||||
Tuple,
|
||||
|
|
@ -37,7 +32,7 @@ from typing import (
|
|||
)
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
import treq
|
||||
from treq import collect
|
||||
from twisted.internet import defer
|
||||
from twisted.words.xish import domish
|
||||
|
||||
|
|
@ -50,7 +45,6 @@ from libervia.backend.plugins.plugin_xep_0103 import URLData, XEP_0103
|
|||
from libervia.backend.plugins.plugin_xep_0358 import JinglePub, XEP_0358
|
||||
from libervia.backend.plugins.plugin_xep_0446 import FileMetadata, XEP_0446
|
||||
from libervia.backend.tools import stream
|
||||
from libervia.backend.tools.web import treq_client_no_ssl
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
|
@ -271,9 +265,9 @@ class XEP_0447:
|
|||
|
||||
if extra.get("ignore_tls_errors", False):
|
||||
log.warning("TLS certificate check disabled, this is highly insecure")
|
||||
treq_client = treq_client_no_ssl
|
||||
treq_client = client.treq_ignore_tls
|
||||
else:
|
||||
treq_client = treq
|
||||
treq_client = client.treq
|
||||
|
||||
try:
|
||||
file_size = int(attachment["size"])
|
||||
|
|
@ -293,7 +287,7 @@ class XEP_0447:
|
|||
|
||||
resp = await treq_client.get(download_url, unbuffered=True)
|
||||
if resp.code == 200:
|
||||
d = treq.collect(resp, file_obj.write)
|
||||
d = collect(resp, file_obj.write)
|
||||
d.addCallback(lambda __: file_obj.close())
|
||||
else:
|
||||
d = defer.Deferred()
|
||||
|
|
|
|||
|
|
@ -21,15 +21,14 @@ from functools import partial
|
|||
from pathlib import Path
|
||||
import secrets
|
||||
from textwrap import dedent
|
||||
from typing import Any, Dict, Optional, Self, Tuple, Union, cast
|
||||
from typing import Any, Self, Union, cast
|
||||
|
||||
from cryptography.exceptions import AlreadyFinalized
|
||||
from cryptography.hazmat import backends
|
||||
from cryptography.hazmat.primitives import ciphers
|
||||
from cryptography.hazmat.primitives.ciphers import CipherContext, modes
|
||||
from cryptography.hazmat.primitives.padding import PKCS7, PaddingContext
|
||||
from pydantic import BaseModel, ValidationError
|
||||
import treq
|
||||
from treq import collect
|
||||
from twisted.internet import defer
|
||||
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
|
||||
from twisted.words.xish import domish
|
||||
|
|
@ -43,10 +42,9 @@ from libervia.backend.core.i18n import _
|
|||
from libervia.backend.core.log import getLogger
|
||||
from libervia.backend.plugins.plugin_misc_download import DownloadPlugin
|
||||
from libervia.backend.plugins.plugin_xep_0103 import XEP_0103
|
||||
from libervia.backend.plugins.plugin_xep_0300 import NS_HASHES, XEP_0300, Hash
|
||||
from libervia.backend.plugins.plugin_xep_0300 import XEP_0300, Hash
|
||||
from libervia.backend.plugins.plugin_xep_0447 import XEP_0447, Source
|
||||
from libervia.backend.tools import stream
|
||||
from libervia.backend.tools.web import treq_client_no_ssl
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
|
@ -202,9 +200,9 @@ class XEP_0448:
|
|||
|
||||
if extra.get("ignore_tls_errors", False):
|
||||
log.warning("TLS certificate check disabled, this is highly insecure")
|
||||
treq_client = treq_client_no_ssl
|
||||
treq_client = client.treq_ignore_tls
|
||||
else:
|
||||
treq_client = treq
|
||||
treq_client = client.treq
|
||||
|
||||
try:
|
||||
file_size = int(attachment["size"])
|
||||
|
|
@ -266,7 +264,7 @@ class XEP_0448:
|
|||
|
||||
resp = await treq_client.get(download_url, unbuffered=True)
|
||||
if resp.code == 200:
|
||||
d = treq.collect(resp, partial(decrypt_cb))
|
||||
d = collect(resp, partial(decrypt_cb))
|
||||
if finalize_cb is not None:
|
||||
d.addCallback(lambda __: finalize_cb())
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -234,6 +234,18 @@ class TLSSettings(ConfigOptions):
|
|||
)
|
||||
|
||||
|
||||
class TorSettings(ConfigOptions):
|
||||
"""Tor configuration options."""
|
||||
|
||||
enabled: bool = Field(default=False, description="Use Tor for connections")
|
||||
|
||||
control_path: Path | None = Field(
|
||||
default=None, description="Tor control Unix Socket Path"
|
||||
)
|
||||
control_address: str = Field(default="127.0.0.1", description="Tor control address")
|
||||
control_port: int = Field(default=9051, description="Tor control port")
|
||||
|
||||
|
||||
class CommonSettings(ConfigOptions):
|
||||
# "media_dir": "/usr/share/" + Const.APP_NAME_FILE + "/media",
|
||||
media_dir: DirectoryPath | None = Field(
|
||||
|
|
@ -386,6 +398,7 @@ class Settings(BaseSettings):
|
|||
common: CommonSettings = Field(default_factory=CommonSettings)
|
||||
email: EmailSettings = Field(default_factory=EmailSettings)
|
||||
tls: TLSSettings = Field(default_factory=TLSSettings)
|
||||
tor: TorSettings = Field(default_factory=TorSettings)
|
||||
web: WebSettings = Field(default_factory=WebSettings)
|
||||
plugins: dict = Field(default_factory=dict, description="Plugin specific settings.")
|
||||
components: dict = Field(
|
||||
|
|
|
|||
|
|
@ -1439,6 +1439,23 @@ class bridge:
|
|||
kwargs["error_handler"] = error_handler
|
||||
return self.db_core_iface.subscription(sub_type, entity, profile_key, **kwargs)
|
||||
|
||||
def tor_infos_get(self, extra, profile_key, callback=None, errback=None):
|
||||
if callback is None:
|
||||
error_handler = None
|
||||
else:
|
||||
if errback is None:
|
||||
errback = log.error
|
||||
error_handler = lambda err: errback(dbus_to_bridge_exception(err))
|
||||
return str(
|
||||
self.db_core_iface.tor_infos_get(
|
||||
extra,
|
||||
profile_key,
|
||||
timeout=const_TIMEOUT,
|
||||
reply_handler=callback,
|
||||
error_handler=error_handler,
|
||||
)
|
||||
)
|
||||
|
||||
def version_get(self, callback=None, errback=None):
|
||||
if callback is None:
|
||||
error_handler = None
|
||||
|
|
@ -2718,6 +2735,22 @@ class AIOBridge(bridge):
|
|||
)
|
||||
return fut
|
||||
|
||||
def tor_infos_get(self, extra, profile_key):
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = loop.create_future()
|
||||
reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret)
|
||||
error_handler = lambda err: loop.call_soon_threadsafe(
|
||||
fut.set_exception, dbus_to_bridge_exception(err)
|
||||
)
|
||||
self.db_core_iface.tor_infos_get(
|
||||
extra,
|
||||
profile_key,
|
||||
timeout=const_TIMEOUT,
|
||||
reply_handler=reply_handler,
|
||||
error_handler=error_handler,
|
||||
)
|
||||
return fut
|
||||
|
||||
def version_get(self):
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = loop.create_future()
|
||||
|
|
|
|||
|
|
@ -1010,6 +1010,15 @@ class bridge(object):
|
|||
else:
|
||||
d.addErrback(self._errback, ori_errback=errback)
|
||||
|
||||
def tor_infos_get(self, extra, profile_key, callback=None, errback=None):
|
||||
d = self.root.callRemote("tor_infos_get", extra, profile_key)
|
||||
if callback is not None:
|
||||
d.addCallback(callback)
|
||||
if errback is None:
|
||||
d.addErrback(self._generic_errback)
|
||||
else:
|
||||
d.addErrback(self._errback, ori_errback=errback)
|
||||
|
||||
def version_get(self, callback=None, errback=None):
|
||||
d = self.root.callRemote("version_get")
|
||||
if callback is not None:
|
||||
|
|
@ -1489,6 +1498,11 @@ class AIOBridge(bridge):
|
|||
d.addErrback(self._errback)
|
||||
return d.asFuture(asyncio.get_event_loop())
|
||||
|
||||
def tor_infos_get(self, extra, profile_key):
|
||||
d = self.root.callRemote("tor_infos_get", extra, profile_key)
|
||||
d.addErrback(self._errback)
|
||||
return d.asFuture(asyncio.get_event_loop())
|
||||
|
||||
def version_get(self):
|
||||
d = self.root.callRemote("version_get")
|
||||
d.addErrback(self._errback)
|
||||
|
|
|
|||
|
|
@ -96,13 +96,39 @@ remote-control = [
|
|||
conferences = [
|
||||
"autobahn ~= 23.6"
|
||||
]
|
||||
tor = [
|
||||
"txtorcon ~= 24.8.0"
|
||||
]
|
||||
|
||||
debugging = [
|
||||
"ipdb", "pudb"
|
||||
]
|
||||
|
||||
tests = [
|
||||
"pytest ~= 9.0",
|
||||
"pytest-twisted",
|
||||
"aiosmtpd"
|
||||
]
|
||||
|
||||
docs = [
|
||||
"sphinx ~= 9.0",
|
||||
]
|
||||
|
||||
dev = [
|
||||
"libervia-backend[debugging]",
|
||||
"libervia-backend[tests]",
|
||||
"libervia-backend[docs]",
|
||||
]
|
||||
|
||||
|
||||
# All features but dev ones
|
||||
all = [
|
||||
"libervia-backend[SVG]",
|
||||
"libervia-backend[GUI]",
|
||||
"libervia-backend[TUI]",
|
||||
"libervia-backend[remote-control]",
|
||||
"libervia-backend[conferences]"
|
||||
"libervia-backend[conferences]",
|
||||
"libervia-backend[tor]"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
|
@ -130,11 +156,7 @@ features = ["all"]
|
|||
|
||||
[tool.hatch.envs.dev]
|
||||
dependencies = [
|
||||
"ipdb",
|
||||
"pudb",
|
||||
"pytest",
|
||||
"pytest-twisted",
|
||||
"aiosmtpd"
|
||||
"libervia-backend[dev]",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from twisted.words.protocols.jabber import jid
|
|||
from libervia.backend import G
|
||||
from wokkel.data_form import NS_X_DATA
|
||||
from libervia.backend.core.main import LiberviaBackend
|
||||
from libervia.backend.models.tor import TorMode
|
||||
from libervia.backend.tools import async_trigger as trigger
|
||||
from libervia.backend.core import xmpp
|
||||
|
||||
|
|
@ -101,4 +102,6 @@ def client():
|
|||
client.server_jid = jid.JID("test.example")
|
||||
client.pubsub_service = jid.JID("pubsub.test.example")
|
||||
client.pubsub_client = AsyncMock()
|
||||
client.tor = None
|
||||
client.tor_mode = TorMode.DISABLED
|
||||
return client
|
||||
|
|
|
|||
Loading…
Reference in a new issue