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
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,5 @@
import click
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
__all__ = [
'ModuleGroup'
]
__all__ = ["ModuleGroup"]

View File

@@ -1,6 +1,7 @@
import click
import typing as t
import click
class ModuleGroup(click.Group):
def __init__(
@@ -13,6 +14,6 @@ class ModuleGroup(click.Group):
) -> None:
new_commands = dict()
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)

View File

@@ -1,9 +1,4 @@
from .server import run, run_without_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 fastapi import WebSocket
from dragonion_core.proto.web.webmessage import (
WebErrorMessage,
set_time,
webmessages_union,
webmessage_error_message_literal,
WebErrorMessage
webmessages_union,
)
from fastapi import WebSocket
@define
@@ -19,21 +19,14 @@ class Connection(object):
"""
Sends WebMessage object to this connection
:param obj: Should be some type of WebMessage
:return:
:return:
"""
await self.ws.send_text(set_time(obj).to_json())
async def send_error(
self,
error_message: webmessage_error_message_literal
):
async def send_error(self, error_message: webmessage_error_message_literal):
"""
Sends error with specified messages
:param error_message: See webmessage_error_message_literal for available
:return:
:return:
"""
await self.send_webmessage(
WebErrorMessage(
error_message=error_message
)
)
await self.send_webmessage(WebErrorMessage(error_message=error_message))

View File

@@ -1,25 +1,24 @@
from attrs import define
from .connection import Connection
from .exceptions import GotInvalidWebmessage
from datetime import datetime
from json.decoder import JSONDecodeError
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 json.decoder import JSONDecodeError
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
from .connection import Connection
from .exceptions import GotInvalidWebmessage
@define
@@ -28,62 +27,65 @@ class Room(object):
async def accept_connection(self, ws: WebSocket) -> Connection | None:
"""
Accepts connection, checks username availability and adds it to dict of
connections
:param ws: Websocket of connection
:return:
Accepts connection, checks username availability and adds it to dict of
connections
:param ws: Websocket of connection
:return:
"""
print('Incoming connection')
print("Incoming connection")
await ws.accept()
try:
connection_message = WebConnectionMessage.from_json(
await ws.receive_text()
)
connection_message = WebConnectionMessage.from_json(await ws.receive_text())
except JSONDecodeError:
await ws.send_text(set_time(WebErrorMessage(
'invalid_webmessage'
)).to_json())
await ws.close(reason='invalid_webmessage')
return
await ws.send_text(
set_time(WebErrorMessage("invalid_webmessage")).to_json()
)
await ws.close(reason="invalid_webmessage")
return
connection = Connection(
username=connection_message.username,
ws=ws,
public_key=connection_message.public_key,
password=connection_message.password
password=connection_message.password,
)
if connection_message.username in self.connections.keys():
await connection.send_error(
'username_exists'
)
await ws.close(reason='username_exists')
return
await connection.send_error("username_exists")
await ws.close(reason="username_exists")
return
self.connections[connection_message.username] = connection
await connection.send_webmessage(WebConnectionResultMessage(
connected_users=dict(
map(
lambda i, j: (i, j),
[_username for _username in list(self.connections.keys())
if self.connections[_username].password ==
connection_message.password],
[_connection.public_key for _connection
in self.connections.values() if _connection.password ==
connection_message.password]
await connection.send_webmessage(
WebConnectionResultMessage(
connected_users=dict(
map(
lambda i, j: (i, j),
[
_username
for _username in list(self.connections.keys())
if self.connections[_username].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)
print(f'[{datetime.now().time()}] Accepted {connection_message.username}')
print(f"[{datetime.now().time()}] Accepted {connection_message.username}")
return connection
async def broadcast_webmessage(self, obj: webmessages_union):
"""
Broadcasts WebMessages to all connections in room
:param obj:
:return:
:param obj:
:return:
"""
for connection in self.connections.values():
await connection.send_webmessage(obj)
@@ -91,11 +93,11 @@ class Room(object):
async def broadcast_message(self, broadcastable: WebBroadcastableMessage):
"""
Broadcasts message to every user in room
:param broadcastable: String object with json representation of
:param broadcastable: String object with json representation of
WebBroadcastableMessage
:return:
:return:
"""
try:
try:
for to_username in broadcastable.messages.keys():
try:
await self.connections[to_username].send_webmessage(
@@ -110,28 +112,17 @@ class Room(object):
"""
Broadcasts notification from server
:param message: Content
:return:
:return:
"""
await self.broadcast_webmessage(
WebNotificationMessage(
message=message
)
)
await self.broadcast_webmessage(WebNotificationMessage(message=message))
async def broadcast_error(
self,
error_message: webmessage_error_message_literal
):
async def broadcast_error(self, error_message: webmessage_error_message_literal):
"""
Broadcasts server error
:param error_message: See webmessage_error_message_literal
:return:
:return:
"""
await self.broadcast_webmessage(
WebErrorMessage(
error_message=error_message
)
)
await self.broadcast_webmessage(WebErrorMessage(error_message=error_message))
async def broadcast_user_disconnected(self, username: str):
"""
@@ -139,18 +130,14 @@ class Room(object):
:param username: Username of user that disconnected
:return:
"""
await self.broadcast_webmessage(
WebDisconnectMessage(
username=username
)
)
await self.broadcast_webmessage(WebDisconnectMessage(username=username))
async def get_connection_by(self, attribute: str, value: str) -> Connection | None:
"""
Search for connection by attribute and value in it
:param attribute:
:param value:
:return:
:param attribute:
:param value:
:return:
"""
for connection in self.connections.values():
if getattr(connection, attribute) == value:
@@ -158,11 +145,11 @@ class Room(object):
async def disconnect(self, connection: Connection, close_reason: str | None = None):
"""
Disconnects by connection object.
:param connection: Object of connection.
Disconnects by connection object.
:param connection: Object of connection.
It can be obtained using get_connection_by
:param close_reason: Reason if exists
:return:
:return:
"""
if connection not in self.connections.values():
return
@@ -170,9 +157,7 @@ class Room(object):
del self.connections[connection.username]
try:
await connection.ws.close(
reason=close_reason
)
await connection.ws.close(reason=close_reason)
except Exception as e:
assert e

View File

@@ -1,10 +1,9 @@
from .connection import Connection
from .room import Room
from typing import Dict
from dragonion_core.proto.web.webmessage import (
webmessage_error_message_literal
)
from dragonion_core.proto.web.webmessage import webmessage_error_message_literal
from .connection import Connection
from .room import Room
class Service(object):
@@ -21,10 +20,7 @@ class Service(object):
for room in self.rooms.values():
await room.broadcast_notification(message)
async def broadcast_error(
self,
error_message: webmessage_error_message_literal
):
async def broadcast_error(self, error_message: webmessage_error_message_literal):
for room in self.rooms.values():
await room.broadcast_error(error_message)
@@ -32,31 +28,31 @@ class Service(object):
"""
Searches for room by valid connection object in it
:param connection: Connection in unknown room to search
:return:
:return:
"""
for room in self.rooms.values():
if connection in room.connections.values():
return room
async def get_connection_by_attribute(
self, attribute: str, value: str
self, attribute: str, value: str
) -> Connection:
"""
Gets connection in some room by attribute and value in it
:param attribute:
:param value:
:return:
:param attribute:
:param value:
:return:
"""
for room in self.rooms.values():
if connection := await room.get_connection_by(attribute, value):
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
:param room_name: Close name
:param reason: Reason to close room, default is Unknown reason
:return:
:return:
"""
room = self.rooms.get(room_name)
if room is None:
@@ -64,6 +60,5 @@ class Service(object):
for connection in room.connections.values():
await room.disconnect(
connection=connection,
close_reason=f'Room is closed: {reason}'
connection=connection, 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 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()

View File

@@ -1,15 +1,15 @@
import sys
from dragonion_server.utils.onion import Onion
from dragonion_core.proto.file import AuthFile
from dragonion_server.utils.config.db import services
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:
"""
Starts onion service, writes it to config
Starts onion service, writes it to config
:param port: Port, where local service is started
:param name: Name of service to get or write to config
:return: Onion object, that is connected and service is started
@@ -23,16 +23,20 @@ def integrate_onion(port: int, name: str) -> Onion:
if not onion.connected_to_tor:
onion.cleanup()
sys.exit(1)
print(f'[green]Available on[/] '
f'{(onion_host := onion.start_onion_service(name))}.onion')
print(
f"[green]Available on[/] "
f"{(onion_host := onion.start_onion_service(name))}.onion"
)
auth = AuthFile(name)
auth['host'] = f'{onion_host}.onion'
auth['auth'] = onion.auth_string
print(f'To connect to server just share [green]{auth.filename}[/] file')
print(f'Or use [#ff901b]service id[/] and [#564ec3]auth string[/]: \n'
f'[#ff901b]{onion_host}[/] \n'
f'[#564ec3]{services[name].client_auth_priv_key}[/]')
auth["host"] = f"{onion_host}.onion"
auth["auth"] = onion.auth_string
print(f"To connect to server just share [green]{auth.filename}[/] file")
print(
f"Or use [#ff901b]service id[/] and [#564ec3]auth string[/]: \n"
f"[#ff901b]{onion_host}[/] \n"
f"[#564ec3]{services[name].client_auth_priv_key}[/]"
)
return onion

View File

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

View File

@@ -1,6 +1,8 @@
from fastapi import FastAPI
import uvicorn
from fastapi import FastAPI
from dragonion_server.utils.onion import get_available_port
from .integration import integrate_onion
from .routes import router
@@ -8,15 +10,15 @@ from .routes import router
def get_app(port: int, name: str) -> FastAPI:
"""
Creates FastAPI object and runs integrate_onion
:param port: Must be same with port on which uvicorn is running
:param port: Must be same with port on which uvicorn is running
:param name: Name of service
:return: FastAPI object with onion.cleanup function on shutdown
"""
onion = integrate_onion(port, name)
return FastAPI(
title=f'dragonion-server: {name}',
description=f'Secure dragonion chat endpoint server - service {name}',
on_shutdown=[onion.cleanup]
title=f"dragonion-server: {name}",
description=f"Secure dragonion chat endpoint server - service {name}",
on_shutdown=[onion.cleanup],
)
@@ -25,22 +27,22 @@ def run(name: str, port: int | None = get_available_port()):
Runs service with specified name and starts onion
:param name: Name of service
:param port: Port where to start service, if not specified - gets random available
:return:
:return:
"""
if port is None:
port = get_available_port()
app = get_app(port, name)
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()):
if port is None:
port = get_available_port()
app = FastAPI(
title=f'dragonion-server: {name}',
description=f'Secure dragonion chat endpoint server - service {name}'
title=f"dragonion-server: {name}",
description=f"Secure dragonion chat endpoint server - service {name}",
)
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
__all__ = [
'db',
'models'
]
__all__ = ["db", "models"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,13 +15,21 @@ import stem.util.str_tools
import stem.util.system
import stem.version
NO_TORRC = '<no torrc>'
NO_TORRC = "<no torrc>"
DEFAULT_INIT_TIMEOUT = 90
def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100,
init_msg_handler=None, timeout=DEFAULT_INIT_TIMEOUT,
take_ownership=False, close_output=True, stdin=None):
def launch_tor(
tor_cmd="tor",
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
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 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
elif threading.current_thread().__class__.__name__ != '_MainThread':
elif threading.current_thread().__class__.__name__ != "_MainThread":
if timeout is not None and timeout != DEFAULT_INIT_TIMEOUT:
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
@@ -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):
raise OSError("'%s' doesn't exist" % tor_cmd)
elif not stem.util.system.is_available(tor_cmd):
raise OSError(f"{tor_cmd} isn't available on your system. "
f"Maybe it's not in your PATH?")
raise OSError(
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
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 == NO_TORRC:
temp_file = tempfile.mkstemp(prefix='empty-torrc-', text=True)[1]
runtime_args += ['-f', temp_file]
temp_file = tempfile.mkstemp(prefix="empty-torrc-", text=True)[1]
runtime_args += ["-f", temp_file]
else:
runtime_args += ['-f', torrc_path]
runtime_args += ["-f", torrc_path]
if take_ownership:
runtime_args += ['__OwningControllerProcess', str(os.getpid())]
runtime_args += ["__OwningControllerProcess", str(os.getpid())]
tor_process = None
@@ -119,8 +130,9 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
env=None if platform.system() == 'Windows' else
{"LD_LIBRARY_PATH": os.path.dirname(tor_cmd)}
env=None
if platform.system() == "Windows"
else {"LD_LIBRARY_PATH": os.path.dirname(tor_cmd)},
)
if stdin:
@@ -129,15 +141,16 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
tor_process.stdin.close()
if timeout:
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.setitimer(signal.ITIMER_REAL, timeout)
bootstrap_line = re.compile('Bootstrapped ([0-9]+)%')
problem_line = re.compile('\\[(warn|err)] (.*)$')
last_problem = 'Timed out'
bootstrap_line = re.compile("Bootstrapped ([0-9]+)%")
problem_line = re.compile("\\[(warn|err)] (.*)$")
last_problem = "Timed out"
while True:
# 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
# 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
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
@@ -169,9 +182,9 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
elif problem_match:
runlevel, msg = problem_match.groups()
if 'see warnings above' not in msg:
if ': ' in msg:
msg = msg.split(': ')[-1].strip()
if "see warnings above" not in msg:
if ": " in msg:
msg = msg.split(": ")[-1].strip()
last_problem = msg
except Exception as e:
@@ -199,9 +212,15 @@ def launch_tor(tor_cmd='tor', args=None, torrc_path=None, completion_percent=100
assert e
def launch_tor_with_config(config, tor_cmd='tor', completion_percent=100,
init_msg_handler=None, timeout=DEFAULT_INIT_TIMEOUT,
take_ownership=False, close_output=True):
def launch_tor_with_config(
config,
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
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:
use_stdin = stem.version.get_system_tor_version(
tor_cmd) >= stem.version.Requirement.TORRC_VIA_STDIN
use_stdin = (
stem.version.get_system_tor_version(tor_cmd)
>= stem.version.Requirement.TORRC_VIA_STDIN
)
except IOError:
use_stdin = False
# we need to be sure that we're logging to stdout to figure out when we're
# done bootstrapping
if 'Log' in config:
stdout_options = ['DEBUG stdout', 'INFO stdout', 'NOTICE stdout']
if "Log" in config:
stdout_options = ["DEBUG stdout", "INFO stdout", "NOTICE stdout"]
if isinstance(config['Log'], str):
config['Log'] = [config['Log']]
if isinstance(config["Log"], str):
config["Log"] = [config["Log"]]
has_stdout = False
for log_config in config['Log']:
for log_config in config["Log"]:
if log_config in stdout_options:
has_stdout = True
break
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()):
if isinstance(values, str):
config_str += '%s %s\n' % (key, values)
config_str += "%s %s\n" % (key, values)
else:
for value in values:
config_str += '%s %s\n' % (key, value)
config_str += "%s %s\n" % (key, value)
if use_stdin:
return launch_tor(
tor_cmd=tor_cmd,
args=['-f', '-'],
args=["-f", "-"],
completion_percent=completion_percent,
init_msg_handler=init_msg_handler,
timeout=timeout,
take_ownership=take_ownership,
close_output=close_output,
stdin=config_str
stdin=config_str,
)
else:
torrc_descriptor, torrc_path = tempfile.mkstemp(prefix='torrc-', text=True)
torrc_descriptor, torrc_path = tempfile.mkstemp(prefix="torrc-", text=True)
try:
with open(torrc_path, 'w') as torrc_file:
with open(torrc_path, "w") as torrc_file:
torrc_file.write(config_str)
# 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,
init_msg_handler, timeout, take_ownership)
return launch_tor(
tor_cmd,
args,
torrc_path,
completion_percent,
init_msg_handler,
timeout,
take_ownership,
)
finally:
try:
os.close(torrc_descriptor)

View File

@@ -1,51 +1,51 @@
import os
import io
import tarfile
import requests
import os
import re
import sys
import tarfile
from typing import Literal
import requests
def get_latest_version() -> str:
"""
Gets latest non-alfa version name from dist.torproject.org
: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)
for res in results:
if 'a' not in res:
if "a" not in res:
return res
def get_build() -> Literal[
'windows-x86_64',
'linux-x86_64',
'macos-x86_64',
'macos-aarch64'
"windows-x86_64", "linux-x86_64", "macos-x86_64", "macos-aarch64"
]:
"""
Gets proper build name for your system
:return:
"""
if sys.platform == 'win32':
return 'windows-x86_64'
elif sys.platform == 'linux':
return 'linux-x86_64'
elif sys.platform == 'darwin':
if sys.platform == "win32":
return "windows-x86_64"
elif sys.platform == "linux":
return "linux-x86_64"
elif sys.platform == "darwin":
import platform
if platform.uname().machine == 'arm64':
return 'macos-aarch64'
if platform.uname().machine == "arm64":
return "macos-aarch64"
else:
return 'macos-x86_64'
return "macos-x86_64"
else:
raise 'System not supported'
raise "System not supported"
def get_tor_expert_bundles(version: str = get_latest_version(),
platform: str = get_build()):
def get_tor_expert_bundles(
version: str = get_latest_version(), platform: str = get_build()
):
"""
Returns a link for downloading tor expert bundle by version and platform
: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()
:return:
"""
return f'https://dist.torproject.org/torbrowser/{version}/tor-expert-bundle-' \
f'{version}-{platform}.tar.gz'
return (
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
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):
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=[
tarinfo
for tarinfo
in tar.getmembers()
if tarinfo.name.startswith("tor/")
], path=dist)
tarinfo for tarinfo in tar.getmembers() if tarinfo.name.startswith("tor/")
],
path=dist,
)
if __name__ == '__main__':
if __name__ == "__main__":
download_tor()