Autoformat

This commit is contained in:
hhh
2024-05-27 17:12:21 +03:00
parent 0afca0dd67
commit 918d5af851
25 changed files with 358 additions and 378 deletions

View File

@@ -1,5 +1,4 @@
from dragonion_server import main from dragonion_server import main
if __name__ == "__main__":
if __name__ == '__main__':
main() main()

View File

@@ -1,6 +1,3 @@
from .common import cli from .common import cli
__all__ = ["cli"]
__all__ = [
'cli'
]

View File

@@ -2,34 +2,34 @@ import os
import click import click
from dragonion_server.utils.config import db
from dragonion_server.common import console from dragonion_server.common import console
from dragonion_server.utils.config import db
class ServiceRemoveCommand(click.Command): class ServiceRemoveCommand(click.Command):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
name='remove', name="remove",
callback=self.callback, callback=self.callback,
params=[ params=[
click.Option( click.Option(
('--name', '-n'), ("--name", "-n"),
required=True, required=True,
prompt=True, prompt=True,
type=str, type=str,
help='Name of service to write to' help="Name of service to write to",
) )
] ],
) )
@staticmethod @staticmethod
def callback(name: str): def callback(name: str):
try: try:
del db.services[name] del db.services[name]
if os.path.isfile(f'{name}.auth'): if os.path.isfile(f"{name}.auth"):
os.remove(f'{name}.auth') os.remove(f"{name}.auth")
print(f'Removed service {name}') print(f"Removed service {name}")
except KeyError: except KeyError:
print(f'Service "{name}" does not exist in this storage') print(f'Service "{name}" does not exist in this storage')
except Exception as e: except Exception as e:

View File

@@ -1,60 +1,63 @@
import sys import sys
import click import click
from dragonion_server.modules.server import run, run_without_onion, integrate_onion
from dragonion_server.common import console from dragonion_server.common import console
from dragonion_server.modules.server import integrate_onion, run, run_without_onion
class ServiceRunCommand(click.Command): class ServiceRunCommand(click.Command):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
name='run', name="run",
callback=self.callback, callback=self.callback,
params=[ params=[
click.Option( click.Option(
('--name', '-n'), ("--name", "-n"),
required=True, required=True,
prompt=True, prompt=True,
type=str, type=str,
help='Name of service to write to' help="Name of service to write to",
), ),
click.Option( click.Option(
('--port', '-p'), ("--port", "-p"),
required=False, required=False,
prompt=True, prompt=True,
prompt_required=False, prompt_required=False,
type=int, type=int,
help='Port to start service on' help="Port to start service on",
), ),
click.Option( click.Option(
('--without-tor', '-wt'), ("--without-tor", "-wt"),
is_flag=True, is_flag=True,
help='Run service without tor' help="Run service without tor",
), ),
click.Option( click.Option(
('--only-tor', '-ot'), ("--only-tor", "-ot"),
is_flag=True, is_flag=True,
help='Run only tor proxy to service' help="Run only tor proxy to service",
) ),
] ],
) )
@staticmethod @staticmethod
def callback(name: str, port: int | None, without_tor: bool, only_tor: bool): def callback(name: str, port: int | None, without_tor: bool, only_tor: bool):
try: try:
if without_tor and only_tor: if without_tor and only_tor:
print('Cannot run only tor without tor, exiting') print("Cannot run only tor without tor, exiting")
sys.exit(1) sys.exit(1)
elif without_tor: elif without_tor:
run_without_onion(name, port) run_without_onion(name, port)
elif only_tor: elif only_tor:
if port is None: if port is None:
print('For this mode, you need to specify port, ' print(
'to which requests will be redirected. Cannot start ' "For this mode, you need to specify port, "
'tor service, exiting') "to which requests will be redirected. Cannot start "
"tor service, exiting"
)
sys.exit(1) sys.exit(1)
onion = integrate_onion(port, name) onion = integrate_onion(port, name)
input('Press Enter to stop onion and service...') input("Press Enter to stop onion and service...")
onion.cleanup() onion.cleanup()
else: else:
run(name, port) run(name, port)

View File

@@ -1,14 +1,13 @@
from ...utils import ModuleGroup from ...utils import ModuleGroup
from .write import ServiceWriteCommand
from .run import ServiceRunCommand
from .remove import ServiceRemoveCommand from .remove import ServiceRemoveCommand
from .run import ServiceRunCommand
from .write import ServiceWriteCommand
service_group = ModuleGroup( service_group = ModuleGroup(
name='service', name="service",
commands={ commands={
'write': ServiceWriteCommand(), "write": ServiceWriteCommand(),
'run': ServiceRunCommand(), "run": ServiceRunCommand(),
'remove': ServiceRemoveCommand() "remove": ServiceRemoveCommand(),
} },
) )

View File

@@ -1,30 +1,30 @@
import click import click
from dragonion_server.utils.onion import Onion
from dragonion_server.common import console from dragonion_server.common import console
from dragonion_server.utils.onion import Onion
class ServiceWriteCommand(click.Command): class ServiceWriteCommand(click.Command):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
name='write', name="write",
callback=self.callback, callback=self.callback,
params=[ params=[
click.Option( click.Option(
('--name', '-n'), ("--name", "-n"),
required=True, required=True,
prompt=True, prompt=True,
type=str, type=str,
help='Name of service to write to' help="Name of service to write to",
), ),
click.Option( click.Option(
('--port', '-p'), ("--port", "-p"),
required=True, required=True,
prompt=True, prompt=True,
type=int, type=int,
help='Port to start service on' help="Port to start service on",
) ),
] ],
) )
@staticmethod @staticmethod

View File

@@ -1,10 +1,5 @@
import click import click
from .cmd.service.service import service_group from .cmd.service.service import service_group
cli = click.CommandCollection(name="dragonion-server", sources=[service_group()])
cli = click.CommandCollection(
name='dragonion-server',
sources=[
service_group()
]
)

View File

@@ -1,6 +1,3 @@
from .groups import ModuleGroup from .groups import ModuleGroup
__all__ = ["ModuleGroup"]
__all__ = [
'ModuleGroup'
]

View File

@@ -1,6 +1,7 @@
import click
import typing as t import typing as t
import click
class ModuleGroup(click.Group): class ModuleGroup(click.Group):
def __init__( def __init__(
@@ -13,6 +14,6 @@ class ModuleGroup(click.Group):
) -> None: ) -> None:
new_commands = dict() new_commands = dict()
for command_key in commands.keys(): for command_key in commands.keys():
new_commands[f'{name}-{command_key}'] = commands[command_key] new_commands[f"{name}-{command_key}"] = commands[command_key]
super().__init__(name, new_commands, **attrs) super().__init__(name, new_commands, **attrs)

View File

@@ -1,9 +1,4 @@
from .server import run, run_without_onion
from .integration import integrate_onion from .integration import integrate_onion
from .server import run, run_without_onion
__all__ = ["run", "run_without_onion", "integrate_onion"]
__all__ = [
'run',
'run_without_onion',
'integrate_onion'
]

View File

@@ -1,11 +1,11 @@
from attrs import define from attrs import define
from fastapi import WebSocket
from dragonion_core.proto.web.webmessage import ( from dragonion_core.proto.web.webmessage import (
WebErrorMessage,
set_time, set_time,
webmessages_union,
webmessage_error_message_literal, webmessage_error_message_literal,
WebErrorMessage webmessages_union,
) )
from fastapi import WebSocket
@define @define
@@ -23,17 +23,10 @@ class Connection(object):
""" """
await self.ws.send_text(set_time(obj).to_json()) await self.ws.send_text(set_time(obj).to_json())
async def send_error( async def send_error(self, error_message: webmessage_error_message_literal):
self,
error_message: webmessage_error_message_literal
):
""" """
Sends error with specified messages Sends error with specified messages
:param error_message: See webmessage_error_message_literal for available :param error_message: See webmessage_error_message_literal for available
:return: :return:
""" """
await self.send_webmessage( await self.send_webmessage(WebErrorMessage(error_message=error_message))
WebErrorMessage(
error_message=error_message
)
)

View File

@@ -1,25 +1,24 @@
from attrs import define from datetime import datetime
from .connection import Connection from json.decoder import JSONDecodeError
from .exceptions import GotInvalidWebmessage
from typing import Dict from typing import Dict
from attrs import define
from dragonion_core.proto.web.webmessage import (
WebBroadcastableMessage,
WebConnectionMessage,
WebConnectionResultMessage,
WebDisconnectMessage,
WebErrorMessage,
WebMessageMessage,
WebNotificationMessage,
set_time,
webmessage_error_message_literal,
webmessages_union,
)
from fastapi import WebSocket from fastapi import WebSocket
from json.decoder import JSONDecodeError from .connection import Connection
from .exceptions import GotInvalidWebmessage
from dragonion_core.proto.web.webmessage import (
webmessages_union,
set_time,
WebMessageMessage,
WebBroadcastableMessage,
WebNotificationMessage,
webmessage_error_message_literal,
WebErrorMessage,
WebConnectionMessage,
WebDisconnectMessage,
WebConnectionResultMessage
)
from datetime import datetime
@define @define
@@ -33,50 +32,53 @@ class Room(object):
:param ws: Websocket of connection :param ws: Websocket of connection
:return: :return:
""" """
print('Incoming connection') print("Incoming connection")
await ws.accept() await ws.accept()
try: try:
connection_message = WebConnectionMessage.from_json( connection_message = WebConnectionMessage.from_json(await ws.receive_text())
await ws.receive_text()
)
except JSONDecodeError: except JSONDecodeError:
await ws.send_text(set_time(WebErrorMessage( await ws.send_text(
'invalid_webmessage' set_time(WebErrorMessage("invalid_webmessage")).to_json()
)).to_json()) )
await ws.close(reason='invalid_webmessage') await ws.close(reason="invalid_webmessage")
return return
connection = Connection( connection = Connection(
username=connection_message.username, username=connection_message.username,
ws=ws, ws=ws,
public_key=connection_message.public_key, public_key=connection_message.public_key,
password=connection_message.password password=connection_message.password,
) )
if connection_message.username in self.connections.keys(): if connection_message.username in self.connections.keys():
await connection.send_error( await connection.send_error("username_exists")
'username_exists' await ws.close(reason="username_exists")
)
await ws.close(reason='username_exists')
return return
self.connections[connection_message.username] = connection self.connections[connection_message.username] = connection
await connection.send_webmessage(WebConnectionResultMessage( await connection.send_webmessage(
WebConnectionResultMessage(
connected_users=dict( connected_users=dict(
map( map(
lambda i, j: (i, j), lambda i, j: (i, j),
[_username for _username in list(self.connections.keys()) [
if self.connections[_username].password == _username
connection_message.password], for _username in list(self.connections.keys())
[_connection.public_key for _connection if self.connections[_username].password
in self.connections.values() if _connection.password == == connection_message.password
connection_message.password] ],
[
_connection.public_key
for _connection in self.connections.values()
if _connection.password == connection_message.password
],
)
)
) )
) )
))
await self.broadcast_webmessage(connection_message) await self.broadcast_webmessage(connection_message)
print(f'[{datetime.now().time()}] Accepted {connection_message.username}') print(f"[{datetime.now().time()}] Accepted {connection_message.username}")
return connection return connection
async def broadcast_webmessage(self, obj: webmessages_union): async def broadcast_webmessage(self, obj: webmessages_union):
@@ -112,26 +114,15 @@ class Room(object):
:param message: Content :param message: Content
:return: :return:
""" """
await self.broadcast_webmessage( await self.broadcast_webmessage(WebNotificationMessage(message=message))
WebNotificationMessage(
message=message
)
)
async def broadcast_error( async def broadcast_error(self, error_message: webmessage_error_message_literal):
self,
error_message: webmessage_error_message_literal
):
""" """
Broadcasts server error Broadcasts server error
:param error_message: See webmessage_error_message_literal :param error_message: See webmessage_error_message_literal
:return: :return:
""" """
await self.broadcast_webmessage( await self.broadcast_webmessage(WebErrorMessage(error_message=error_message))
WebErrorMessage(
error_message=error_message
)
)
async def broadcast_user_disconnected(self, username: str): async def broadcast_user_disconnected(self, username: str):
""" """
@@ -139,11 +130,7 @@ class Room(object):
:param username: Username of user that disconnected :param username: Username of user that disconnected
:return: :return:
""" """
await self.broadcast_webmessage( await self.broadcast_webmessage(WebDisconnectMessage(username=username))
WebDisconnectMessage(
username=username
)
)
async def get_connection_by(self, attribute: str, value: str) -> Connection | None: async def get_connection_by(self, attribute: str, value: str) -> Connection | None:
""" """
@@ -170,9 +157,7 @@ class Room(object):
del self.connections[connection.username] del self.connections[connection.username]
try: try:
await connection.ws.close( await connection.ws.close(reason=close_reason)
reason=close_reason
)
except Exception as e: except Exception as e:
assert e assert e

View File

@@ -1,10 +1,9 @@
from .connection import Connection
from .room import Room
from typing import Dict from typing import Dict
from dragonion_core.proto.web.webmessage import ( from dragonion_core.proto.web.webmessage import webmessage_error_message_literal
webmessage_error_message_literal
) from .connection import Connection
from .room import Room
class Service(object): class Service(object):
@@ -21,10 +20,7 @@ class Service(object):
for room in self.rooms.values(): for room in self.rooms.values():
await room.broadcast_notification(message) await room.broadcast_notification(message)
async def broadcast_error( async def broadcast_error(self, error_message: webmessage_error_message_literal):
self,
error_message: webmessage_error_message_literal
):
for room in self.rooms.values(): for room in self.rooms.values():
await room.broadcast_error(error_message) await room.broadcast_error(error_message)
@@ -51,7 +47,7 @@ class Service(object):
if connection := await room.get_connection_by(attribute, value): if connection := await room.get_connection_by(attribute, value):
return connection return connection
async def close_room(self, room_name: str, reason: str = 'Unknown reason'): async def close_room(self, room_name: str, reason: str = "Unknown reason"):
""" """
Closes all connections in room Closes all connections in room
:param room_name: Close name :param room_name: Close name
@@ -64,6 +60,5 @@ class Service(object):
for connection in room.connections.values(): for connection in room.connections.values():
await room.disconnect( await room.disconnect(
connection=connection, connection=connection, close_reason=f"Room is closed: {reason}"
close_reason=f'Room is closed: {reason}'
) )

View File

@@ -1,13 +1,11 @@
from fastapi import WebSocket, WebSocketDisconnect
from .managers.service import Service
from dragonion_core.proto.web.webmessage import (
webmessages_union,
WebMessage
)
from .managers.exceptions import GotInvalidWebmessage
from datetime import datetime from datetime import datetime
from dragonion_core.proto.web.webmessage import WebMessage, webmessages_union
from fastapi import WebSocket, WebSocketDisconnect
from .managers.exceptions import GotInvalidWebmessage
from .managers.service import Service
service = Service() service = Service()

View File

@@ -1,11 +1,11 @@
import sys import sys
from dragonion_server.utils.onion import Onion
from dragonion_core.proto.file import AuthFile from dragonion_core.proto.file import AuthFile
from dragonion_server.utils.config.db import services
from rich import print from rich import print
from dragonion_server.utils.config.db import services
from dragonion_server.utils.onion import Onion
def integrate_onion(port: int, name: str) -> Onion: def integrate_onion(port: int, name: str) -> Onion:
""" """
@@ -24,15 +24,19 @@ def integrate_onion(port: int, name: str) -> Onion:
onion.cleanup() onion.cleanup()
sys.exit(1) sys.exit(1)
print(f'[green]Available on[/] ' print(
f'{(onion_host := onion.start_onion_service(name))}.onion') f"[green]Available on[/] "
f"{(onion_host := onion.start_onion_service(name))}.onion"
)
auth = AuthFile(name) auth = AuthFile(name)
auth['host'] = f'{onion_host}.onion' auth["host"] = f"{onion_host}.onion"
auth['auth'] = onion.auth_string auth["auth"] = onion.auth_string
print(f'To connect to server just share [green]{auth.filename}[/] file') print(f"To connect to server just share [green]{auth.filename}[/] file")
print(f'Or use [#ff901b]service id[/] and [#564ec3]auth string[/]: \n' print(
f'[#ff901b]{onion_host}[/] \n' f"Or use [#ff901b]service id[/] and [#564ec3]auth string[/]: \n"
f'[#564ec3]{services[name].client_auth_priv_key}[/]') f"[#ff901b]{onion_host}[/] \n"
f"[#564ec3]{services[name].client_auth_priv_key}[/]"
)
return onion return onion

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, WebSocket from fastapi import APIRouter, WebSocket
from .handlers.websocket_server import serve_websocket
from .handlers.websocket_server import serve_websocket
router = APIRouter() router = APIRouter()

View File

@@ -1,6 +1,8 @@
from fastapi import FastAPI
import uvicorn import uvicorn
from fastapi import FastAPI
from dragonion_server.utils.onion import get_available_port from dragonion_server.utils.onion import get_available_port
from .integration import integrate_onion from .integration import integrate_onion
from .routes import router from .routes import router
@@ -14,9 +16,9 @@ def get_app(port: int, name: str) -> FastAPI:
""" """
onion = integrate_onion(port, name) onion = integrate_onion(port, name)
return FastAPI( return FastAPI(
title=f'dragonion-server: {name}', title=f"dragonion-server: {name}",
description=f'Secure dragonion chat endpoint server - service {name}', description=f"Secure dragonion chat endpoint server - service {name}",
on_shutdown=[onion.cleanup] on_shutdown=[onion.cleanup],
) )
@@ -31,7 +33,7 @@ def run(name: str, port: int | None = get_available_port()):
port = get_available_port() port = get_available_port()
app = get_app(port, name) app = get_app(port, name)
app.include_router(router) app.include_router(router)
uvicorn.run(app, host='0.0.0.0', port=port) uvicorn.run(app, host="0.0.0.0", port=port)
def run_without_onion(name: str, port: int | None = get_available_port()): def run_without_onion(name: str, port: int | None = get_available_port()):
@@ -39,8 +41,8 @@ def run_without_onion(name: str, port: int | None = get_available_port()):
port = get_available_port() port = get_available_port()
app = FastAPI( app = FastAPI(
title=f'dragonion-server: {name}', title=f"dragonion-server: {name}",
description=f'Secure dragonion chat endpoint server - service {name}' description=f"Secure dragonion chat endpoint server - service {name}",
) )
app.include_router(router) app.include_router(router)
uvicorn.run(app, host='0.0.0.0', port=port) uvicorn.run(app, host="0.0.0.0", port=port)

View File

@@ -1,6 +1,3 @@
from . import db, models from . import db, models
__all__ = [ __all__ = ["db", "models"]
'db',
'models'
]

View File

@@ -3,12 +3,8 @@ import sqlitedict
class ConfigDatabase(sqlitedict.SqliteDict): class ConfigDatabase(sqlitedict.SqliteDict):
def __init__(self, tablename): def __init__(self, tablename):
super().__init__( super().__init__(filename="data.storage", tablename=tablename, autocommit=True)
filename='data.storage',
tablename=tablename,
autocommit=True
)
config = ConfigDatabase('config') config = ConfigDatabase("config")
services = ConfigDatabase('services') services = ConfigDatabase("services")

View File

@@ -13,5 +13,5 @@ class ServiceModel:
client_auth_pub_key: str client_auth_pub_key: str
service_id: str = None service_id: str = None
key_content: str = 'ED25519-V3' key_content: str = "ED25519-V3"
key_type: str = 'NEW' key_type: str = "NEW"

View File

@@ -5,28 +5,33 @@ import sys
def get_resource_path(filename): def get_resource_path(filename):
application_path = 'resources' application_path = "resources"
return os.path.join(application_path, filename) return os.path.join(application_path, filename)
def get_tor_paths(): def get_tor_paths():
if (platform.system() != "Darwin" and if platform.system() != "Darwin" and platform.machine().lower() in [
platform.machine().lower() in ['aarch64', 'arm64']): "aarch64",
if shutil.which('tor'): "arm64",
return 'tor' ]:
if shutil.which("tor"):
return "tor"
else: else:
print('Detected ARM system and tor is not installed or added to PATH. ' print(
'Please, consider reading documentation and installing application ' "Detected ARM system and tor is not installed or added to PATH. "
'properly') "Please, consider reading documentation and installing application "
"properly"
)
sys.exit(1) sys.exit(1)
else: else:
from ..onion.tor_downloader import download_tor from ..onion.tor_downloader import download_tor
if platform.system() in ["Linux", "Darwin"]: if platform.system() in ["Linux", "Darwin"]:
tor_path = os.path.join(build_data_dir(), 'tor/tor') tor_path = os.path.join(build_data_dir(), "tor/tor")
elif platform.system() == "Windows": elif platform.system() == "Windows":
tor_path = os.path.join(build_data_dir(), 'tor/tor.exe') tor_path = os.path.join(build_data_dir(), "tor/tor.exe")
else: else:
raise Exception("Platform not supported") raise Exception("Platform not supported")
@@ -37,7 +42,7 @@ def get_tor_paths():
def build_data_dir(): def build_data_dir():
dragonion_data_dir = 'data' dragonion_data_dir = "data"
os.makedirs(dragonion_data_dir, exist_ok=True) os.makedirs(dragonion_data_dir, exist_ok=True)
return dragonion_data_dir return dragonion_data_dir

View File

@@ -1,7 +1,3 @@
from .onion import Onion from .onion import Onion, get_available_port
from .onion import get_available_port
__all__ = [ __all__ = ["Onion", "get_available_port"]
'Onion',
'get_available_port'
]

View File

@@ -1,23 +1,23 @@
from stem.control import Controller import base64
from .stem_process import launch_tor_with_config
from stem import ProtocolError
import socket
import random
import os import os
import psutil import platform
import random
import socket
import subprocess import subprocess
import tempfile import tempfile
import platform
import time import time
import base64
import nacl.public import nacl.public
import psutil
from rich import print from rich import print
from stem import ProtocolError
from stem.control import Controller
from dragonion_server.utils.core import dirs
from dragonion_server.utils import config from dragonion_server.utils import config
from dragonion_server.utils.config.db import services from dragonion_server.utils.config.db import services
from dragonion_server.utils.core import dirs
from .stem_process import launch_tor_with_config
def get_available_port(min_port: int = 1000, max_port: int = 65535): def get_available_port(min_port: int = 1000, max_port: int = 65535):
@@ -82,18 +82,18 @@ class Onion(object):
self.kill_same_tor() self.kill_same_tor()
tor_config = { tor_config = {
'DataDirectory': tor_data_directory_name, "DataDirectory": tor_data_directory_name,
'SocksPort': str(self.tor_socks_port), "SocksPort": str(self.tor_socks_port),
'CookieAuthentication': '1', "CookieAuthentication": "1",
'CookieAuthFile': self.tor_cookie_auth_file, "CookieAuthFile": self.tor_cookie_auth_file,
'AvoidDiskWrites': '1', "AvoidDiskWrites": "1",
'Log': [ "Log": ["NOTICE stdout"],
'NOTICE stdout'
]
} }
if platform.system() in ["Windows", "Darwin"] or \ if (
len(tor_data_directory_name) > 90: platform.system() in ["Windows", "Darwin"]
or len(tor_data_directory_name) > 90
):
try: try:
self.tor_control_port = get_available_port(1000, 65535) self.tor_control_port = get_available_port(1000, 65535)
tor_config = tor_config | {"ControlPort": str(self.tor_control_port)} tor_config = tor_config | {"ControlPort": str(self.tor_control_port)}
@@ -102,24 +102,22 @@ class Onion(object):
self.tor_control_socket = None self.tor_control_socket = None
else: else:
self.tor_control_port = None self.tor_control_port = None
self.tor_control_socket = os.path.abspath(os.path.join( self.tor_control_socket = os.path.abspath(
tor_data_directory_name, "control_socket" os.path.join(tor_data_directory_name, "control_socket")
)) )
tor_config = tor_config | {"ControlSocket": str(self.tor_control_socket)} tor_config = tor_config | {"ControlSocket": str(self.tor_control_socket)}
return tor_config return tor_config
def connect(self): def connect(self):
self.tor_data_directory = tempfile.TemporaryDirectory( self.tor_data_directory = tempfile.TemporaryDirectory(dir=dirs.build_tmp_dir())
dir=dirs.build_tmp_dir()
)
self.tor_data_directory_name = self.tor_data_directory.name self.tor_data_directory_name = self.tor_data_directory.name
self.tor_proc = launch_tor_with_config( self.tor_proc = launch_tor_with_config(
config=self.get_config(self.tor_data_directory_name), config=self.get_config(self.tor_data_directory_name),
tor_cmd=self.tor_path, tor_cmd=self.tor_path,
take_ownership=True, take_ownership=True,
init_msg_handler=print init_msg_handler=print,
) )
time.sleep(2) time.sleep(2)
@@ -149,14 +147,12 @@ class Onion(object):
client_auth_priv_key_raw = nacl.public.PrivateKey.generate() client_auth_priv_key_raw = nacl.public.PrivateKey.generate()
client_auth_priv_key = key_str(client_auth_priv_key_raw) client_auth_priv_key = key_str(client_auth_priv_key_raw)
client_auth_pub_key = key_str( client_auth_pub_key = key_str(client_auth_priv_key_raw.public_key)
client_auth_priv_key_raw.public_key
)
services[name] = config.models.ServiceModel( services[name] = config.models.ServiceModel(
port=port, port=port,
client_auth_priv_key=client_auth_priv_key, client_auth_priv_key=client_auth_priv_key,
client_auth_pub_key=client_auth_pub_key client_auth_pub_key=client_auth_pub_key,
) )
return services[name] return services[name]
@@ -167,7 +163,7 @@ class Onion(object):
:return: .onion url :return: .onion url
""" """
if name not in services.keys(): if name not in services.keys():
raise 'Service not created' raise "Service not created"
service: config.models.ServiceModel = services[name] service: config.models.ServiceModel = services[name]
@@ -193,8 +189,9 @@ class Onion(object):
service.key_type = "ED25519-V3" service.key_type = "ED25519-V3"
service.key_content = res.private_key service.key_content = res.private_key
self.auth_string = f'{res.service_id}:descriptor:' \ self.auth_string = (
f'x25519:{service.client_auth_priv_key}' f"{res.service_id}:descriptor:" f"x25519:{service.client_auth_priv_key}"
)
services[name] = service services[name] = service
@@ -204,9 +201,7 @@ class Onion(object):
service: config.models.ServiceModel = services[name] service: config.models.ServiceModel = services[name]
if service.service_id: if service.service_id:
try: try:
self.c.remove_ephemeral_hidden_service( self.c.remove_ephemeral_hidden_service(service.service_id)
service.service_id
)
except Exception as e: except Exception as e:
print(e) print(e)
@@ -237,9 +232,7 @@ class Onion(object):
num_rend_circuits += 1 num_rend_circuits += 1
if num_rend_circuits == 0: if num_rend_circuits == 0:
print( print("\rTor rendezvous circuits have closed" + " " * 20)
"\rTor rendezvous circuits have closed" + " " * 20
)
break break
if num_rend_circuits == 1: if num_rend_circuits == 1:
@@ -271,7 +264,7 @@ class Onion(object):
try: try:
self.tor_data_directory.cleanup() self.tor_data_directory.cleanup()
except Exception as e: except Exception as e:
print(f'Cannot cleanup temporary directory: {e}') print(f"Cannot cleanup temporary directory: {e}")
@property @property
def get_tor_socks_port(self): def get_tor_socks_port(self):

View File

@@ -15,13 +15,21 @@ import stem.util.str_tools
import stem.util.system import stem.util.system
import stem.version import stem.version
NO_TORRC = '<no torrc>' NO_TORRC = "<no torrc>"
DEFAULT_INIT_TIMEOUT = 90 DEFAULT_INIT_TIMEOUT = 90
def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100, def launch_tor(
init_msg_handler=None, timeout=DEFAULT_INIT_TIMEOUT, tor_cmd="tor",
take_ownership=False, close_output=True, stdin=None): args=None,
torrc_path=None,
completion_percent=100,
init_msg_handler=None,
timeout=DEFAULT_INIT_TIMEOUT,
take_ownership=False,
close_output=True,
stdin=None,
):
""" """
Initializes a tor process. This blocks until initialization completes or we Initializes a tor process. This blocks until initialization completes or we
error out. error out.
@@ -68,13 +76,14 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
if stem.util.system.is_windows(): if stem.util.system.is_windows():
if timeout is not None and timeout != DEFAULT_INIT_TIMEOUT: if timeout is not None and timeout != DEFAULT_INIT_TIMEOUT:
raise OSError('You cannot launch tor with a timeout on Windows') raise OSError("You cannot launch tor with a timeout on Windows")
timeout = None timeout = None
elif threading.current_thread().__class__.__name__ != '_MainThread': elif threading.current_thread().__class__.__name__ != "_MainThread":
if timeout is not None and timeout != DEFAULT_INIT_TIMEOUT: if timeout is not None and timeout != DEFAULT_INIT_TIMEOUT:
raise OSError( raise OSError(
'Launching tor with a timeout can only be done in the main thread') "Launching tor with a timeout can only be done in the main thread"
)
timeout = None timeout = None
@@ -88,8 +97,10 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
elif not os.path.isfile(tor_cmd): elif not os.path.isfile(tor_cmd):
raise OSError("'%s' doesn't exist" % tor_cmd) raise OSError("'%s' doesn't exist" % tor_cmd)
elif not stem.util.system.is_available(tor_cmd): elif not stem.util.system.is_available(tor_cmd):
raise OSError(f"{tor_cmd} isn't available on your system. " raise OSError(
f"Maybe it's not in your PATH?") f"{tor_cmd} isn't available on your system. "
f"Maybe it's not in your PATH?"
)
# double check that we have a torrc to work with # double check that we have a torrc to work with
if torrc_path not in (None, NO_TORRC) and not os.path.exists(torrc_path): if torrc_path not in (None, NO_TORRC) and not os.path.exists(torrc_path):
@@ -103,13 +114,13 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
if torrc_path: if torrc_path:
if torrc_path == NO_TORRC: if torrc_path == NO_TORRC:
temp_file = tempfile.mkstemp(prefix='empty-torrc-', text=True)[1] temp_file = tempfile.mkstemp(prefix="empty-torrc-", text=True)[1]
runtime_args += ['-f', temp_file] runtime_args += ["-f", temp_file]
else: else:
runtime_args += ['-f', torrc_path] runtime_args += ["-f", torrc_path]
if take_ownership: if take_ownership:
runtime_args += ['__OwningControllerProcess', str(os.getpid())] runtime_args += ["__OwningControllerProcess", str(os.getpid())]
tor_process = None tor_process = None
@@ -119,8 +130,9 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
env=None if platform.system() == 'Windows' else env=None
{"LD_LIBRARY_PATH": os.path.dirname(tor_cmd)} if platform.system() == "Windows"
else {"LD_LIBRARY_PATH": os.path.dirname(tor_cmd)},
) )
if stdin: if stdin:
@@ -129,15 +141,16 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
tor_process.stdin.close() tor_process.stdin.close()
if timeout: if timeout:
def timeout_handler(*_): def timeout_handler(*_):
raise OSError('reached a %i second timeout without success' % timeout) raise OSError("reached a %i second timeout without success" % timeout)
signal.signal(signal.SIGALRM, timeout_handler) signal.signal(signal.SIGALRM, timeout_handler)
signal.setitimer(signal.ITIMER_REAL, timeout) signal.setitimer(signal.ITIMER_REAL, timeout)
bootstrap_line = re.compile('Bootstrapped ([0-9]+)%') bootstrap_line = re.compile("Bootstrapped ([0-9]+)%")
problem_line = re.compile('\\[(warn|err)] (.*)$') problem_line = re.compile("\\[(warn|err)] (.*)$")
last_problem = 'Timed out' last_problem = "Timed out"
while True: while True:
# Tor's stdout will be read as ASCII bytes. This is fine for python 2, but # Tor's stdout will be read as ASCII bytes. This is fine for python 2, but
@@ -147,12 +160,12 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
# It seems like python 2.x is perfectly happy for this to be unicode, so # It seems like python 2.x is perfectly happy for this to be unicode, so
# normalizing to that. # normalizing to that.
init_line = tor_process.stdout.readline().decode('utf-8', 'replace').strip() init_line = tor_process.stdout.readline().decode("utf-8", "replace").strip()
# this will provide empty results if the process is terminated # this will provide empty results if the process is terminated
if not init_line: if not init_line:
raise OSError('Process terminated: %s' % last_problem) raise OSError("Process terminated: %s" % last_problem)
# provide the caller with the initialization message if they want it # provide the caller with the initialization message if they want it
@@ -169,9 +182,9 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
elif problem_match: elif problem_match:
runlevel, msg = problem_match.groups() runlevel, msg = problem_match.groups()
if 'see warnings above' not in msg: if "see warnings above" not in msg:
if ': ' in msg: if ": " in msg:
msg = msg.split(': ')[-1].strip() msg = msg.split(": ")[-1].strip()
last_problem = msg last_problem = msg
except Exception as e: except Exception as e:
@@ -199,9 +212,15 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
assert e assert e
def launch_tor_with_config(config, tor_cmd='tor', completion_percent=100, def launch_tor_with_config(
init_msg_handler=None, timeout=DEFAULT_INIT_TIMEOUT, config,
take_ownership=False, close_output=True): tor_cmd="tor",
completion_percent=100,
init_msg_handler=None,
timeout=DEFAULT_INIT_TIMEOUT,
take_ownership=False,
close_output=True,
):
""" """
Initializes a tor process, like :func:`~stem.process.launch_tor`, but with a Initializes a tor process, like :func:`~stem.process.launch_tor`, but with a
customized configuration. This writes a temporary torrc to disk, launches customized configuration. This writes a temporary torrc to disk, launches
@@ -246,62 +265,71 @@ def launch_tor_with_config(config, tor_cmd='tor', completion_percent=100,
""" """
try: try:
use_stdin = stem.version.get_system_tor_version( use_stdin = (
tor_cmd) >= stem.version.Requirement.TORRC_VIA_STDIN stem.version.get_system_tor_version(tor_cmd)
>= stem.version.Requirement.TORRC_VIA_STDIN
)
except IOError: except IOError:
use_stdin = False use_stdin = False
# we need to be sure that we're logging to stdout to figure out when we're # we need to be sure that we're logging to stdout to figure out when we're
# done bootstrapping # done bootstrapping
if 'Log' in config: if "Log" in config:
stdout_options = ['DEBUG stdout', 'INFO stdout', 'NOTICE stdout'] stdout_options = ["DEBUG stdout", "INFO stdout", "NOTICE stdout"]
if isinstance(config['Log'], str): if isinstance(config["Log"], str):
config['Log'] = [config['Log']] config["Log"] = [config["Log"]]
has_stdout = False has_stdout = False
for log_config in config['Log']: for log_config in config["Log"]:
if log_config in stdout_options: if log_config in stdout_options:
has_stdout = True has_stdout = True
break break
if not has_stdout: if not has_stdout:
config['Log'].append('NOTICE stdout') config["Log"].append("NOTICE stdout")
config_str = '' config_str = ""
for key, values in list(config.items()): for key, values in list(config.items()):
if isinstance(values, str): if isinstance(values, str):
config_str += '%s %s\n' % (key, values) config_str += "%s %s\n" % (key, values)
else: else:
for value in values: for value in values:
config_str += '%s %s\n' % (key, value) config_str += "%s %s\n" % (key, value)
if use_stdin: if use_stdin:
return launch_tor( return launch_tor(
tor_cmd=tor_cmd, tor_cmd=tor_cmd,
args=['-f', '-'], args=["-f", "-"],
completion_percent=completion_percent, completion_percent=completion_percent,
init_msg_handler=init_msg_handler, init_msg_handler=init_msg_handler,
timeout=timeout, timeout=timeout,
take_ownership=take_ownership, take_ownership=take_ownership,
close_output=close_output, close_output=close_output,
stdin=config_str stdin=config_str,
) )
else: else:
torrc_descriptor, torrc_path = tempfile.mkstemp(prefix='torrc-', text=True) torrc_descriptor, torrc_path = tempfile.mkstemp(prefix="torrc-", text=True)
try: try:
with open(torrc_path, 'w') as torrc_file: with open(torrc_path, "w") as torrc_file:
torrc_file.write(config_str) torrc_file.write(config_str)
# prevents tor from error-ing out due to a missing torrc if it gets a sighup # prevents tor from error-ing out due to a missing torrc if it gets a sighup
args = ['__ReloadTorrcOnSIGHUP', '0'] args = ["__ReloadTorrcOnSIGHUP", "0"]
return launch_tor(tor_cmd, args, torrc_path, completion_percent, return launch_tor(
init_msg_handler, timeout, take_ownership) tor_cmd,
args,
torrc_path,
completion_percent,
init_msg_handler,
timeout,
take_ownership,
)
finally: finally:
try: try:
os.close(torrc_descriptor) os.close(torrc_descriptor)

View File

@@ -1,51 +1,51 @@
import os
import io import io
import tarfile import os
import requests
import re import re
import sys import sys
import tarfile
from typing import Literal from typing import Literal
import requests
def get_latest_version() -> str: def get_latest_version() -> str:
""" """
Gets latest non-alfa version name from dist.torproject.org Gets latest non-alfa version name from dist.torproject.org
:return: :return:
""" """
r = requests.get('https://dist.torproject.org/torbrowser/').text r = requests.get("https://dist.torproject.org/torbrowser/").text
results = re.findall(r'<a href=".+/">(.+)/</a>', r) results = re.findall(r'<a href=".+/">(.+)/</a>', r)
for res in results: for res in results:
if 'a' not in res: if "a" not in res:
return res return res
def get_build() -> Literal[ def get_build() -> Literal[
'windows-x86_64', "windows-x86_64", "linux-x86_64", "macos-x86_64", "macos-aarch64"
'linux-x86_64',
'macos-x86_64',
'macos-aarch64'
]: ]:
""" """
Gets proper build name for your system Gets proper build name for your system
:return: :return:
""" """
if sys.platform == 'win32': if sys.platform == "win32":
return 'windows-x86_64' return "windows-x86_64"
elif sys.platform == 'linux': elif sys.platform == "linux":
return 'linux-x86_64' return "linux-x86_64"
elif sys.platform == 'darwin': elif sys.platform == "darwin":
import platform import platform
if platform.uname().machine == 'arm64':
return 'macos-aarch64' if platform.uname().machine == "arm64":
return "macos-aarch64"
else: else:
return 'macos-x86_64' return "macos-x86_64"
else: else:
raise 'System not supported' raise "System not supported"
def get_tor_expert_bundles(version: str = get_latest_version(), def get_tor_expert_bundles(
platform: str = get_build()): version: str = get_latest_version(), platform: str = get_build()
):
""" """
Returns a link for downloading tor expert bundle by version and platform Returns a link for downloading tor expert bundle by version and platform
:param version: Tor expert bundle version that exists in dist.torproject.org :param version: Tor expert bundle version that exists in dist.torproject.org
@@ -53,11 +53,13 @@ def get_tor_expert_bundles(version: str = get_latest_version(),
get_build() get_build()
:return: :return:
""" """
return f'https://dist.torproject.org/torbrowser/{version}/tor-expert-bundle-' \ return (
f'{version}-{platform}.tar.gz' f"https://dist.torproject.org/torbrowser/{version}/tor-expert-bundle-"
f"{version}-{platform}.tar.gz"
)
def download_tor(url: str = get_tor_expert_bundles(), dist: str = 'tor'): def download_tor(url: str = get_tor_expert_bundles(), dist: str = "tor"):
""" """
Downloads tor from url and unpacks it to specified directory. Note, that Downloads tor from url and unpacks it to specified directory. Note, that
it doesn't unpack only tor executable to dist folder, but creates there it doesn't unpack only tor executable to dist folder, but creates there
@@ -69,15 +71,15 @@ def download_tor(url: str = get_tor_expert_bundles(), dist: str = 'tor'):
if not os.path.exists(dist): if not os.path.exists(dist):
os.makedirs(dist) os.makedirs(dist)
(tar := tarfile.open(fileobj=io.BytesIO(requests.get(url).content), (
mode='r:gz')).extractall( tar := tarfile.open(fileobj=io.BytesIO(requests.get(url).content), mode="r:gz")
).extractall(
members=[ members=[
tarinfo tarinfo for tarinfo in tar.getmembers() if tarinfo.name.startswith("tor/")
for tarinfo ],
in tar.getmembers() path=dist,
if tarinfo.name.startswith("tor/") )
], path=dist)
if __name__ == '__main__': if __name__ == "__main__":
download_tor() download_tor()