initial commit for refactoring magneticod (far, far away from complete!)
This commit is contained in:
parent
90538b10af
commit
1df6204a5f
0
magneticod/magneticod/bittorrent/codec.py
Normal file
0
magneticod/magneticod/bittorrent/codec.py
Normal file
0
magneticod/magneticod/bittorrent/protocol.py
Normal file
0
magneticod/magneticod/bittorrent/protocol.py
Normal file
0
magneticod/magneticod/bittorrent/service.py
Normal file
0
magneticod/magneticod/bittorrent/service.py
Normal file
0
magneticod/magneticod/bittorrent/transport.py
Normal file
0
magneticod/magneticod/bittorrent/transport.py
Normal file
0
magneticod/magneticod/dht/mainline/__init__.py
Normal file
0
magneticod/magneticod/dht/mainline/__init__.py
Normal file
14
magneticod/magneticod/dht/mainline/codec.py
Normal file
14
magneticod/magneticod/dht/mainline/codec.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
def encode():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def decode(data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EncodeError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DecodeError(Exception):
|
||||||
|
pass
|
146
magneticod/magneticod/dht/mainline/protocol.py
Normal file
146
magneticod/magneticod/dht/mainline/protocol.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from . import transport
|
||||||
|
|
||||||
|
|
||||||
|
class Protocol:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Offered Functionality
|
||||||
|
# =====================
|
||||||
|
def on_ping_query(self, query: PingQuery) -> typing.Optional[typing.Union[PingResponse, Error]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_find_node_query(self, query: FindNodeQuery) -> typing.Optional[typing.Union[FindNodeResponse, Error]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_get_peers_query(self, query: GetPeersQuery) -> typing.Optional[typing.Union[GetPeersQuery, Error]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_announce_peer_query(self, query: AnnouncePeerQuery) -> typing.Optional[typing.Union[AnnouncePeerResponse, Error]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_ping_OR_announce_peer_response(self, response: PingResponse) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_find_node_response(self, response: FindNodeResponse) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_get_peers_response(self, response: GetPeersResponse) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_error(self, response: Error) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Private Functionality
|
||||||
|
# =====================
|
||||||
|
def when_message_received(self, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
NodeID = typing.NewType("NodeID", bytes)
|
||||||
|
InfoHash = typing.NewType("InfoHash", bytes)
|
||||||
|
NodeInfo = typing.NamedTuple("NodeInfo", [
|
||||||
|
("id", NodeID),
|
||||||
|
("address", transport.Address),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class BaseQuery:
|
||||||
|
method_name = b""
|
||||||
|
|
||||||
|
def __init__(self, id_: NodeID):
|
||||||
|
self.id = id_
|
||||||
|
|
||||||
|
def to_message(self, *, transaction_id: bytes, client_version: bytes=b"") -> typing.Dict[bytes, typing.Any]:
|
||||||
|
return {
|
||||||
|
b"t": transaction_id,
|
||||||
|
b"y": b"q",
|
||||||
|
b"v": client_version,
|
||||||
|
b"q": self.method_name,
|
||||||
|
b"a": self.__dict__
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PingQuery(BaseQuery):
|
||||||
|
method_name = b"ping"
|
||||||
|
|
||||||
|
def __init__(self, id_: NodeID):
|
||||||
|
super().__init__(id_)
|
||||||
|
|
||||||
|
|
||||||
|
class FindNodeQuery(BaseQuery):
|
||||||
|
method_name = b"find_node"
|
||||||
|
|
||||||
|
def __init__(self, id_: NodeID, target: NodeID):
|
||||||
|
super().__init__(id_)
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
|
||||||
|
class GetPeersQuery(BaseQuery):
|
||||||
|
method_name = b"get_peers"
|
||||||
|
|
||||||
|
def __init__(self, id_: NodeID, info_hash: InfoHash):
|
||||||
|
super().__init__(id_)
|
||||||
|
self.info_hash = info_hash
|
||||||
|
|
||||||
|
|
||||||
|
class AnnouncePeerQuery(BaseQuery):
|
||||||
|
method_name = b"announce_peer"
|
||||||
|
|
||||||
|
def __init__(self, id_: NodeID, info_hash: InfoHash, port: int, token: bytes, implied_port: int=0):
|
||||||
|
super().__init__(id_)
|
||||||
|
self.info_hash = info_hash
|
||||||
|
self.port = port
|
||||||
|
self.token = token
|
||||||
|
self.implied_port = implied_port
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResponse:
|
||||||
|
def __init__(self, id_: NodeID):
|
||||||
|
self.id = id_
|
||||||
|
|
||||||
|
def to_message(self, *, transaction_id: bytes, client_version: bytes = b"") -> typing.Dict[bytes, typing.Any]:
|
||||||
|
return {
|
||||||
|
b"t": transaction_id,
|
||||||
|
b"y": b"r",
|
||||||
|
b"v": client_version,
|
||||||
|
b"r": self._return_values()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _return_values(self) -> typing.Dict[bytes, typing.Any]:
|
||||||
|
return {b"id": self.id}
|
||||||
|
|
||||||
|
|
||||||
|
class PingResponse(BaseResponse):
|
||||||
|
def __init__(self, id_: NodeID):
|
||||||
|
super().__init__(id_)
|
||||||
|
|
||||||
|
|
||||||
|
class FindNodeResponse(BaseResponse):
|
||||||
|
def __init__(self, id_: NodeID, nodes: typing.List[NodeInfo]):
|
||||||
|
super().__init__(id_)
|
||||||
|
self.nodes = nodes
|
||||||
|
|
||||||
|
def _return_values(self) -> typing.Dict[bytes, typing.Any]:
|
||||||
|
d = super()._return_values()
|
||||||
|
d.update({
|
||||||
|
b"nodes": self.nodes # TODO: this is not right obviously, encode & decode!
|
||||||
|
})
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
class GetPeersResponse(BaseResponse):
|
||||||
|
def __init__(self, id_: NodeID, token: bytes, *, values, nodes: typing.Optional[typing.List[NodeInfo]]=None):
|
||||||
|
assert bool(values) ^ bool(nodes)
|
||||||
|
|
||||||
|
super().__init__(id_)
|
||||||
|
self.token = token
|
||||||
|
self.values = values,
|
||||||
|
self.nodes = nodes
|
||||||
|
|
||||||
|
|
||||||
|
class AnnouncePeerResponse(BaseResponse):
|
||||||
|
def __init__(self, id_: NodeID):
|
||||||
|
super().__init__(id_)
|
0
magneticod/magneticod/dht/mainline/service.py
Normal file
0
magneticod/magneticod/dht/mainline/service.py
Normal file
95
magneticod/magneticod/dht/mainline/transport.py
Normal file
95
magneticod/magneticod/dht/mainline/transport.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import asyncio
|
||||||
|
import collections
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from . import codec
|
||||||
|
|
||||||
|
Address = typing.Tuple[str, int]
|
||||||
|
|
||||||
|
MessageQueueEntry = typing.NamedTuple("MessageQueueEntry", [
|
||||||
|
("queued_on", float),
|
||||||
|
("message", bytes),
|
||||||
|
("address", Address)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class Transport(asyncio.DatagramProtocol):
|
||||||
|
"""
|
||||||
|
Mainline DHT Transport
|
||||||
|
|
||||||
|
The signature `class Transport(asyncio.DatagramProtocol)` seems almost oxymoron, but it's indeed more sensible than
|
||||||
|
it first seems. `Transport` handles ALL that is related to transporting messages, which includes receiving them
|
||||||
|
(`asyncio.DatagramProtocol.datagram_received`), sending them (`asyncio.DatagramTransport.send_to`), pausing and
|
||||||
|
resuming writing as requested by the asyncio, and also handling operational errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.__datagram_transport = asyncio.DatagramTransport()
|
||||||
|
self.__write_allowed = asyncio.Event()
|
||||||
|
self.__queue_nonempty = asyncio.Event()
|
||||||
|
self.__message_queue = collections.deque() # type: typing.Deque[MessageQueueEntry]
|
||||||
|
self.__messenger_task = asyncio.Task(self.__send_messages())
|
||||||
|
|
||||||
|
# Offered Functionality
|
||||||
|
# =====================
|
||||||
|
def send_message(self, message, address: Address) -> None:
|
||||||
|
self.__message_queue.append(MessageQueueEntry(time.monotonic(), message, address))
|
||||||
|
if not self.__queue_nonempty.is_set():
|
||||||
|
self.__queue_nonempty.set()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def on_message(message: dict, address: Address):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Private Functionality
|
||||||
|
# =====================
|
||||||
|
def connection_made(self, transport: asyncio.DatagramTransport) -> None:
|
||||||
|
self.__datagram_transport = transport
|
||||||
|
self.__write_allowed.set()
|
||||||
|
|
||||||
|
def datagram_received(self, data: bytes, address: Address) -> None:
|
||||||
|
try:
|
||||||
|
message = codec.decode(data)
|
||||||
|
except codec.EncodeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(message, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on_message(message, address)
|
||||||
|
|
||||||
|
def error_received(self, exc: OSError):
|
||||||
|
logging.debug("Mainline DHT received error!", exc_info=exc)
|
||||||
|
|
||||||
|
def pause_writing(self):
|
||||||
|
self.__write_allowed.clear()
|
||||||
|
|
||||||
|
def resume_writing(self):
|
||||||
|
self.__write_allowed.set()
|
||||||
|
|
||||||
|
def connection_lost(self, exc: Exception):
|
||||||
|
if exc:
|
||||||
|
logging.fatal("Mainline DHT lost connection! (See the following log entry for the exception.)",
|
||||||
|
exc_info=exc
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.fatal("Mainline DHT lost connection!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
async def __send_messages(self) -> None:
|
||||||
|
while True:
|
||||||
|
await asyncio.wait([self.__write_allowed.wait(), self.__queue_nonempty.wait()])
|
||||||
|
try:
|
||||||
|
queued_on, message, address = self.__message_queue.pop()
|
||||||
|
except IndexError:
|
||||||
|
self.__queue_nonempty.clear()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if time.monotonic() - queued_on > 60:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.__datagram_transport.sendto(message, address)
|
0
magneticod/magneticod/dht/sink.py
Normal file
0
magneticod/magneticod/dht/sink.py
Normal file
Loading…
Reference in New Issue
Block a user