Lee Miller
1286ecdc6d
make a separate a bit controversial class structure.OnionPeer(), having .from_object() and .to_object() instead of .from_bytes() etc.
264 lines
8.6 KiB
Python
264 lines
8.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Protocol structures"""
|
|
import base64
|
|
import binascii
|
|
import hashlib
|
|
import logging
|
|
import re
|
|
import socket
|
|
import struct
|
|
import time
|
|
|
|
from . import shared
|
|
|
|
|
|
class VarInt():
|
|
"""varint object"""
|
|
def __init__(self, n):
|
|
self.n = n
|
|
|
|
def to_bytes(self):
|
|
if self.n < 0xfd:
|
|
return struct.pack('>B', self.n)
|
|
|
|
if self.n <= 0xffff:
|
|
return b'\xfd' + struct.pack('>H', self.n)
|
|
|
|
if self.n <= 0xffffffff:
|
|
return b'\xfe' + struct.pack('>I', self.n)
|
|
|
|
return b'\xff' + struct.pack('>Q', self.n)
|
|
|
|
@staticmethod
|
|
def length(b):
|
|
if b == 0xfd:
|
|
return 3
|
|
if b == 0xfe:
|
|
return 5
|
|
if b == 0xff:
|
|
return 9
|
|
return 1
|
|
|
|
@classmethod
|
|
def from_bytes(cls, b):
|
|
if cls.length(b[0]) > 1:
|
|
b = b[1:]
|
|
n = int.from_bytes(b, 'big')
|
|
return cls(n)
|
|
|
|
|
|
class Object():
|
|
"""The 'object' message payload"""
|
|
def __init__(
|
|
self, nonce, expires_time, object_type, version,
|
|
stream_number, object_payload
|
|
):
|
|
self.nonce = nonce
|
|
self.expires_time = expires_time
|
|
self.object_type = object_type
|
|
self.version = version
|
|
self.stream_number = stream_number
|
|
self.object_payload = object_payload
|
|
self.vector = hashlib.sha512(hashlib.sha512(
|
|
self.to_bytes()).digest()).digest()[:32]
|
|
|
|
self.tag = (
|
|
# broadcast from version 5 and pubkey/getpukey from version 4
|
|
self.object_payload[:32] if object_type == 3 and version == 5
|
|
or (object_type in (0, 1) and version == 4)
|
|
else None)
|
|
|
|
def __repr__(self):
|
|
return 'object, vector: {}'.format(
|
|
base64.b16encode(self.vector).decode())
|
|
|
|
@classmethod
|
|
def from_message(cls, m):
|
|
"""Decode message payload"""
|
|
payload = m.payload
|
|
nonce, expires_time, object_type = struct.unpack('>8sQL', payload[:20])
|
|
payload = payload[20:]
|
|
version_varint_length = VarInt.length(payload[0])
|
|
version = VarInt.from_bytes(payload[:version_varint_length]).n
|
|
payload = payload[version_varint_length:]
|
|
stream_number_varint_length = VarInt.length(payload[0])
|
|
stream_number = VarInt.from_bytes(
|
|
payload[:stream_number_varint_length]).n
|
|
payload = payload[stream_number_varint_length:]
|
|
return cls(
|
|
nonce, expires_time, object_type, version, stream_number, payload)
|
|
|
|
def to_bytes(self):
|
|
"""Serialize to bytes"""
|
|
payload = b''
|
|
payload += self.nonce
|
|
payload += struct.pack('>QL', self.expires_time, self.object_type)
|
|
payload += (
|
|
VarInt(self.version).to_bytes()
|
|
+ VarInt(self.stream_number).to_bytes())
|
|
payload += self.object_payload
|
|
return payload
|
|
|
|
def is_expired(self):
|
|
"""Check if object's TTL is expired"""
|
|
return self.expires_time + 3 * 3600 < time.time()
|
|
|
|
def is_valid(self):
|
|
"""Checks the object validity"""
|
|
if self.is_expired():
|
|
logging.debug(
|
|
'Invalid object %s, reason: expired',
|
|
base64.b16encode(self.vector).decode())
|
|
return False
|
|
if self.expires_time > time.time() + 28 * 24 * 3600 + 3 * 3600:
|
|
logging.warning(
|
|
'Invalid object %s, reason: end of life too far in the future',
|
|
base64.b16encode(self.vector).decode())
|
|
return False
|
|
if len(self.object_payload) > 2**18:
|
|
logging.warning(
|
|
'Invalid object %s, reason: payload is too long',
|
|
base64.b16encode(self.vector).decode())
|
|
return False
|
|
if self.stream_number != shared.stream:
|
|
logging.warning(
|
|
'Invalid object %s, reason: not in stream %i',
|
|
base64.b16encode(self.vector).decode(), shared.stream)
|
|
return False
|
|
|
|
pow_value = int.from_bytes(
|
|
hashlib.sha512(hashlib.sha512(
|
|
self.nonce + self.pow_initial_hash()
|
|
).digest()).digest()[:8], 'big')
|
|
target = self.pow_target()
|
|
if target < pow_value:
|
|
logging.warning(
|
|
'Invalid object %s, reason: insufficient pow',
|
|
base64.b16encode(self.vector).decode())
|
|
return False
|
|
return True
|
|
|
|
def pow_target(self):
|
|
"""Compute PoW target"""
|
|
data = self.to_bytes()[8:]
|
|
length = len(data) + 8 + shared.payload_length_extra_bytes
|
|
dt = max(self.expires_time - time.time(), 0)
|
|
return int(
|
|
2 ** 64 / (
|
|
shared.nonce_trials_per_byte * (
|
|
length + (dt * length) / (2 ** 16))))
|
|
|
|
def pow_initial_hash(self):
|
|
"""Compute the initial hash for PoW"""
|
|
return hashlib.sha512(self.to_bytes()[8:]).digest()
|
|
|
|
|
|
class NetAddrNoPrefix():
|
|
"""Network address"""
|
|
def __init__(self, services, host, port):
|
|
self.services = services
|
|
self.host = host
|
|
self.port = port
|
|
|
|
def __repr__(self):
|
|
return 'net_addr_no_prefix, services: {}, host: {}, port {}'.format(
|
|
self.services, self.host, self.port)
|
|
|
|
def to_bytes(self):
|
|
b = b''
|
|
b += struct.pack('>Q', self.services)
|
|
try:
|
|
host = socket.inet_pton(socket.AF_INET, self.host)
|
|
b += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + host
|
|
except socket.error:
|
|
b += socket.inet_pton(socket.AF_INET6, self.host)
|
|
b += struct.pack('>H', int(self.port))
|
|
return b
|
|
|
|
@staticmethod
|
|
def network_group(host):
|
|
"""A simplified network group identifier from pybitmessage protocol"""
|
|
try:
|
|
host = socket.inet_pton(socket.AF_INET, host)
|
|
return host[:2]
|
|
except socket.error:
|
|
try:
|
|
host = socket.inet_pton(socket.AF_INET6, host)
|
|
return host[:12]
|
|
except OSError:
|
|
return host
|
|
except TypeError:
|
|
return host
|
|
|
|
@classmethod
|
|
def from_bytes(cls, b):
|
|
services, host, port = struct.unpack('>Q16sH', b)
|
|
if host.startswith(
|
|
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF'):
|
|
host = socket.inet_ntop(socket.AF_INET, host[-4:])
|
|
else:
|
|
host = socket.inet_ntop(socket.AF_INET6, host)
|
|
return cls(services, host, port)
|
|
|
|
|
|
class NetAddr():
|
|
"""Network address with time and stream"""
|
|
def __init__(self, services, host, port, stream=shared.stream):
|
|
self.stream = stream
|
|
self.services = services
|
|
self.host = host
|
|
self.port = port
|
|
|
|
def __repr__(self):
|
|
return 'net_addr, stream: {}, services: {}, host: {}, port {}'.format(
|
|
self.stream, self.services, self.host, self.port)
|
|
|
|
def to_bytes(self):
|
|
b = b''
|
|
b += struct.pack('>Q', int(time.time()))
|
|
b += struct.pack('>I', self.stream)
|
|
b += NetAddrNoPrefix(self.services, self.host, self.port).to_bytes()
|
|
return b
|
|
|
|
@classmethod
|
|
def from_bytes(cls, b):
|
|
stream, net_addr = struct.unpack('>QI26s', b)[1:]
|
|
n = NetAddrNoPrefix.from_bytes(net_addr)
|
|
return cls(n.services, n.host, n.port, stream)
|
|
|
|
|
|
class OnionPeer():
|
|
def __init__(self, host, port=8444, stream=None, dest_pub=None):
|
|
self.stream = stream or shared.stream
|
|
self.host = host
|
|
self.port = port
|
|
try:
|
|
self.dest_pub = dest_pub or base64.b32decode(
|
|
re.search(r'(.*)\.onion', host).groups()[0], True)
|
|
except (AttributeError, binascii.Error) as e:
|
|
raise ValueError('Malformed hostname') from e
|
|
|
|
def __repr__(self):
|
|
return 'onion_peer, stream: {}, host: {}, port {}'.format(
|
|
self.stream, self.host, self.port)
|
|
|
|
def to_object(self):
|
|
payload = b''
|
|
payload += VarInt(self.port).to_bytes()
|
|
payload += b'\xfd\x87\xd8\x7e\xeb\x43'
|
|
payload += self.dest_pub
|
|
return Object(
|
|
b'\x00' * 8, int(time.time() + 8 * 3600), shared.onion_obj_type,
|
|
shared.onion_obj_version, self.stream, payload)
|
|
|
|
@classmethod
|
|
def from_object(cls, obj):
|
|
payload = obj.object_payload
|
|
port_length = VarInt.length(payload[0])
|
|
port = VarInt.from_bytes(payload[:port_length]).n
|
|
if payload[port_length:port_length + 6] != b'\xfd\x87\xd8\x7e\xeb\x43':
|
|
raise ValueError('Malformed onion peer object')
|
|
dest_pub = payload[port_length + 6:]
|
|
host = base64.b32encode(dest_pub).lower().decode() + '.onion'
|
|
return cls(host, port, obj.stream_number, dest_pub)
|