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:
Goffi 2025-12-15 11:35:19 +01:00
commit c5396f0acf
18 changed files with 484 additions and 43 deletions

View file

@ -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.

View file

@ -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,
):

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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:

View 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)

View file

@ -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()

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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:

View file

@ -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(

View file

@ -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()

View file

@ -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)

View file

@ -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]

View file

@ -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