Page MenuHomePhorge

No OneTemporary

diff --git a/CHANGELOG b/CHANGELOG
index 8bd0d20..3a87245 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,44 +1,46 @@
-Copyright (C) 2022 LANNOCC (Shawn A. Wilson [lannocc@yahoo.com])
+Copyright (C) 2022-2023 LANNOCC (Shawn A. Wilson [lannocc@yahoo.com])
@%@~LICENSE:MIT~@%@
+saw_031622_1 - Get initial nodes by querying DNS for node.lank.im.
+
saw_072422_1 - Add call to master.on_peered() in peer handler.
saw_071422_1 - Begin to support peer text messaging.
saw_071022_1 - Remove command-line peer commands. Add GetHistory/History.
saw_070922_4 - Fix signature verification for previous commit.
saw_070922_3 - Add alias option for peer sign-on.
saw_070922_2 - Remove Python-3.8 syntax magic so we can run on 3.7 (raspbian).
saw_070922_1 - Move database "name" constants into their own file.
saw_070822_2 - Make sure we don't create db unless we start a node master.
saw_070822_1 - Implement ListLabels, LabelsList, LabelInterest, and LabelIgnore.
saw_070722_1 - Peers are now able to sign on to the nodes.
saw_070622_3 - Implement re-registration. Fix a security concern for
registration... this will invalidate existing signed entries.
Implement proper signature verification on nodes.
saw_070622_2 - Fix error with register command.
saw_070622_1 - Much work implementiong node protocol v2 and proper node-based
registration.
saw_062622_1 - Implementation of peer protocol v1 (basic) and v2 (encrypted).
saw_061722_1 - Beginning crypto handler v1 (label registration).
saw_061622_1 - Beginning node protocol v1 (ping-pong).
saw_061422_3 - Use os.makedirs() for the user_data_dir (creates parents).
saw_061422_2 - Now have a simple `lank` command.
saw_061422_1 - Initial commit (barebones pip project skeleton).
diff --git a/lank/__version__.py b/lank/__version__.py
index 131251f..c3a8971 100644
--- a/lank/__version__.py
+++ b/lank/__version__.py
@@ -1,2 +1,2 @@
-__version__ = '0.4.7'
+__version__ = '0.5.0'
diff --git a/lank/node/__init__.py b/lank/node/__init__.py
index 3e09031..e3e4c94 100644
--- a/lank/node/__init__.py
+++ b/lank/node/__init__.py
@@ -1,275 +1,275 @@
from .protocol import get_handler, HELLO, KEEPALIVE, VERSION
from gevent import socket, wait #, spawn
from gevent.pool import Pool
from gevent.server import StreamServer
from bidict import bidict
from ntplib import NTPClient
from uuid import UUID
from datetime import datetime, timedelta, timezone
+import socket
DEFAULT_PORT = 42024
HELLO_SIZE = len(HELLO)
HELLO_TIMEOUT = 9 # seconds
GENERAL_TIMEOUT = KEEPALIVE * 2 # seconds
-NODES = [ # FIXME -- this is temporary (put in database?)
- ('localhost', 42024),
- #('localhost', 42124),
- ('72.202.195.53', 42024),
- ('ruckusist.com', 42024),
-]
-
+NODES = 'node.lank.im' # initial nodes will be gathered from DNS
NODES_MIN = 3
NODES_MAX = 9
NODES_WAIT = 3 * 60 # seconds
NTP = 'pool.ntp.org'
class Master:
def __init__(self, port=DEFAULT_PORT):
self.port = port
import lank.node.db as ldb
self.ldb = ldb
uuid = ldb.get_meta(ldb.META_NODE_UUID)
assert uuid
self.uuid = UUID(uuid)
print(f' our uuid is {self.uuid}')
print(f' getting time from {NTP}...')
ntp = NTPClient().request(NTP, version=3)
self.offset = ntp.offset
print(f' our clock is {abs(self.offset)} seconds ', end='')
if self.offset < 0: print('fast')
else: print('slow')
self.pool = Pool()
self.stream_server = StreamServer(('0.0.0.0', self.port), self.server,
spawn=self.pool)
self.buffer = bytearray(HELLO_SIZE)
self.labels_by_id = bidict({ })
self.nodes_by_uuid = { }
self.nodes_client = { }
self.reservations = { }
self.registrations = { }
self.signed_recently = { }
self.peers_by_label = { }
self.label_interests_by_label = { }
self.label_interests_by_handler = { }
def run(self):
for label in self.ldb.list_labels():
self.labels_by_id[label['id']] = label['name']
print(f'S listening on port {self.port}')
self.stream_server.start()
while True:
if len(self.nodes_by_uuid) < NODES_MIN:
# FIXME: limit spawning to NODES_MAX - len(self.nodes)
# and then wait
- for addr in NODES:
+ nodes = socket.getaddrinfo(NODES, 0, type=socket.SOCK_STREAM)
+
+ for node in nodes:
+ addr = node[4][0]
+
if len(self.nodes_by_uuid) >= NODES_MAX:
break
if addr in self.nodes_client:
continue
self.pool.spawn(self.client, addr)
wait(timeout=NODES_WAIT)
self.status()
def now(self):
return datetime.now(timezone.utc) + timedelta(seconds=self.offset)
def status(self):
nodes = len(self.nodes_by_uuid)
peers = len(self.peers_by_label)
time = self.now().isoformat()
- print(f'** STATUS ** nodes={nodes} ** peers={peers} ** time={time}')
+ print(f'>>>[STATUS]>>> ** NODES={nodes} ** PEERS={peers} ** TIME={time}'\
+ + ' ** <<<[STATUS]<<<')
def broadcast_nodes(self, msg, skip=None):
print(f'B (NODES) <- {msg}')
self.pool.spawn(self._broadcast_nodes_, msg, skip)
def _broadcast_nodes_(self, msg, skip=None):
try:
handlers = self.label_interests_by_label[msg.label]
except KeyError:
handlers = [ ]
for handler in self.nodes_by_uuid.values():
if handler is skip:
continue
if handler not in handlers:
handler.send(msg)
for handler in handlers:
handler.send(msg)
def add_label_interest(self, label, handler):
try:
handlers = self.label_interests_by_label[label]
except KeyError:
handlers = [ ]
self.label_interests_by_label[label] = handlers
if handler not in handlers:
handlers.append(handler)
try:
labels = self.label_interests_by_handler[handler]
except KeyError:
labels = [ ]
self.label_interests_by_handler[handler] = labels
if label not in labels:
labels.append(label)
def remove_label_interest(self, label, handler):
try:
handlers = self.label_interests_by_label[label]
except KeyError:
return
if handler not in handlers:
return
del handlers[handlers.index(handler)]
labels = self.label_interests_by_handler[handler]
del labels[labels.index(label)]
def remove_label_interests(self, handler):
if handler not in self.label_interests_by_handler:
return
labels = self.label_interests_by_handler[handler]
for label in labels:
handlers = self.label_interests_by_label[label]
del handlers[handlers.index(handler)]
del self.label_interests_by_handler[handler]
def client(self, addr):
print(f'C+ connecting to {addr}')
self.nodes_client[addr] = True
try:
sock = socket.create_connection(addr, timeout=HELLO_TIMEOUT)
sock.settimeout(HELLO_TIMEOUT)
try:
handler = get_handler(sock, addr, VERSION)
handler.hello()
sock.settimeout(GENERAL_TIMEOUT)
try:
handler.client(self)
print(f'C- finished {addr}')
except KeyError as e:
print(f'C- terminated {addr} [DENIED: {e}]')
if 'NodeIsSelf' in str(e):
self.nodes_client[addr] = False
except ValueError as e:
print(f'C- terminated {addr} [BAD MESSAGE: {e}]')
except socket.timeout:
print(f'C- terminated {addr} [GENERAL TIMEOUT]')
finally:
self.remove_label_interests(handler)
except BrokenPipeError:
print(f'C- closed {addr} [BROKEN PIPE]')
except ConnectionResetError:
print(f'C- closed {addr} [CONNECTION RESET]')
except OSError:
print(f'C- closed {addr} [GENERAL NETWORK ERROR]')
except socket.timeout:
print(f'C- terminated {addr} [HELLO TIMEOUT]')
except ConnectionRefusedError:
print(f'C- closed {addr} [CONNECTION REFUSED]')
except socket.timeout:
print(f'C- terminated {addr} [CONNECT TIMEOUT]')
finally:
if self.nodes_client[addr]:
del self.nodes_client[addr]
def server(self, sock, addr):
print(f'S+ connection from {addr}')
sock.settimeout(HELLO_TIMEOUT)
try:
read = sock.recv_into(self.buffer)
if read == HELLO_SIZE:
if self.buffer == HELLO:
sock.settimeout(GENERAL_TIMEOUT)
try:
handler = get_handler(sock, addr)
if handler:
try:
handler.server(self)
print(f'S- finished {addr}')
except KeyError as e:
print(f'S- terminated {addr} [DENIED: {e}]')
except ValueError as e:
print(f'S- terminated {addr}' \
+ f' [BAD MESSAGE: {e}]')
finally:
self.remove_label_interests(handler)
else:
print(f'S- closed {addr} [CLIENT ABORT]')
except ValueError as e:
print(f'S- terminated {addr} [PROTOCOL VERSION]')
except socket.timeout:
print(f'S- terminated {addr} [GENERAL TIMEOUT]')
else:
print(f'S- terminated {addr} [BAD HELLO]')
elif read:
print(f'S- terminated {addr} [BAD HELLO]')
else:
print(f'S- closed {addr} [CLIENT ABORT]')
except BrokenPipeError:
print(f'S- closed {addr} [BROKEN PIPE]')
except ConnectionResetError:
print(f'S- closed {addr} [CONNECTION RESET]')
except OSError:
print(f'S- closed {addr} [GENERAL NETWORK ERROR]')
except socket.timeout:
print(f'S- terminated {addr} [HELLO TIMEOUT]')
diff --git a/lank/registration.py b/lank/registration.py
index d06952f..a6d2919 100644
--- a/lank/registration.py
+++ b/lank/registration.py
@@ -1,344 +1,356 @@
from .crypto import get_handler as crypto
from .node import NODES, HELLO_TIMEOUT, GENERAL_TIMEOUT, KEEPALIVE
from .node.protocol.v2 import *
from threading import Thread, Event
import socket
import sys
from getpass import getpass
from uuid import uuid4
class Interactive:
- def __init__(self):
+ def __init__(self, client=None):
+ if client is None:
+ client = Client()
+ self.client = client
+
self.crypto = crypto()
- print(f' - crypto handler v{self.crypto.VERSION}')
+ self.print(f' - crypto handler v{self.crypto.VERSION}')
def run(self):
- client = Client()
+ client = self.client
client.start()
try:
client.ready.wait()
if not client.go:
- print('ABORTED: unable to connect to node')
+ self.print('ABORTED: unable to connect to node')
return
- print()
- print('Ready to create/update a label with a new key pair.')
- print()
+ self.print()
+ self.print('Ready to create/update a label with a new key pair.')
+ self.print()
- label = input('Label: ')
+ label = self.input('Label: ')
if label: label = label.strip()
if not label or not client.go:
- print('ABORTED')
+ self.print('ABORTED')
return
uuid = uuid4()
exists = client.check_label(uuid, label)
exists_priv_key = None
if exists is None:
- print('ABORTED')
+ self.print('ABORTED')
return
elif not exists:
- password = getpass('Password: ')
+ password = self.getpass('Password: ')
else:
- print(' A label with that name already exists.')
+ self.print(' A label with that name already exists.')
uuid = exists.uuid
try:
priv_key = self.crypto.load_private_key(exists.key_pair_pem)
- print(' ' \
+ self.print(' ' \
+ 'ERROR: The existing key is OPEN and must remain so.')
- print('ABORTED')
+ self.print('ABORTED')
return
except TypeError: # (needs a password)
pass # this is expected
- exists_password = getpass('Existing Password: ')
+ exists_password = self.getpass('Existing Password: ')
if not exists_password:
- print('ABORTED')
+ self.print('ABORTED')
return
try:
priv_key = self.crypto.load_private_key(exists.key_pair_pem,
password=exists_password)
except ValueError as e:
if e.args: e = ' | '.join(e.args)
- print(f' ERROR: {e}')
- print('ABORTED')
+ self.print(f' ERROR: {e}')
+ self.print('ABORTED')
return
exists_priv_key = priv_key
- password = getpass('New Password: ')
+ password = self.getpass('New Password: ')
if password == exists_password:
- print(' ' \
+ self.print(' ' \
+ 'WARNING: New password is same as the old password.')
if password:
results = self.crypto.PASS_POLICY.test(password)
if label.lower() in password.lower():
results.append('Contains Label Name')
if results:
- print(' WARNING: You have entered a WEAK PASSWORD.')
- print(' ' \
+ self.print(' WARNING: You have entered a WEAK PASSWORD.')
+ self.print(' ' \
+ 'This makes it VERY LIKELY somebody will STEAL it.')
- print(' The following tests FAILED:')
+ self.print(' The following tests FAILED:')
for result in results:
- print(f' - {result}')
- print(' Proceed with CAUTION!')
+ self.print(f' - {result}')
+ self.print(' Proceed with CAUTION!')
- confirm = getpass('Confirm Password: ')
+ confirm = self.getpass('Confirm Password: ')
if confirm != password:
- print('ABORTED (passwords do not match)')
+ self.print('ABORTED (passwords do not match)')
return
else:
- print(' WARNING: Empty password creates an OPEN key pair.')
- print(' ' \
+ self.print(' ' \
+ + 'WARNING: Empty password creates an OPEN key pair.')
+ self.print(' ' \
+ 'This means EVERYBODY is allowed to control the label')
- print(' ' \
+ self.print(' ' \
+ 'FOREVER and CANNOT BE UNDONE. Proceed with CAUTION!')
- agree = input('Type AGREE to continue: ')
+ agree = self.input('Type AGREE to continue: ')
if agree != 'AGREE':
- print('ABORTED')
+ self.print('ABORTED')
return
- print()
- print('Generating key pair...', end='')
- sys.stdout.flush()
+ self.print()
+ self.print('Generating key pair...', end='')
keys = self.crypto.make_keys(password)
- print(' [done]')
+ self.print(' [done]')
priv_key = keys[0]
priv_key_pem = keys[1]
pub_key_pem = keys[2]
- print('Creating signature...', end='')
- sys.stdout.flush()
+ self.print('Creating signature...', end='')
if not exists:
time_nonce = self.crypto.make_time_nonce()
msg = self.crypto.get_register_message(label, time_nonce)
signature = self.crypto.sign(priv_key, msg)
else:
time_nonce = None
msg = self.crypto.get_reregister_message(
exists.time_nonce,
exists.uuid,
priv_key_pem + pub_key_pem)
signature = self.crypto.sign(exists_priv_key, msg)
- print(' [done]')
+ self.print(' [done]')
- print('Sanity check...', end='')
- sys.stdout.flush()
+ self.print('Sanity check...', end='')
if not exists:
assert self.crypto.verify(priv_key.public_key(), msg, signature)
else:
assert self.crypto.verify(exists_priv_key.public_key(), msg,
signature)
- print(' [done]')
+ self.print(' [done]')
- print('Transmitting...', end='')
- sys.stdout.flush()
+ self.print('Transmitting...', end='')
if client.register_label(uuid, label, priv_key_pem, pub_key_pem,
signature, self.crypto.VERSION, time_nonce):
- print(' [SUCCESS]')
+ self.print(' [SUCCESS]')
else:
- print(' [FAIL]')
+ self.print(' [FAIL]')
finally:
client.stop()
client.join()
+ def print(self, txt='', end='\n'):
+ print(txt, end)
+ if end == '':
+ sys.stdout.flush()
+
+ def input(self, label):
+ return input(label)
+
+ def getpass(self, label):
+ return getpass(label)
+
class Client(Thread):
def __init__(self):
super().__init__(name='registration client')
self.go = False
self.ready = Event()
self.input = None
self.output = None
def stop(self):
#print('STOP')
self.go = False
def check_label(self, uuid, label):
self.ready.clear()
self.input = Reservation(label, uuid)
self.ready.wait()
if isinstance(self.output, Reservation):
return False
elif isinstance(self.output, ReservationCancel):
#return False if self.output.exists else None
# FIXME: if exists, transmit request to get the key
if not self.output.exists:
self._error_('LABEL RESERVATION CONFLICT')
return None
self.ready.clear()
self.input = GetRegistration(label)
self.ready.wait()
assert isinstance(self.output, Registration)
return self.output
else:
return None
def register_label(self, uuid, label, priv_key_pem, pub_key_pem,
signature, version, time_nonce=None):
self.ready.clear()
if time_nonce:
self.input = Registration(uuid, label, version, time_nonce,
priv_key_pem + pub_key_pem,
signature)
else:
self.input = ReRegistration(uuid4(), label, version, str(uuid),
priv_key_pem + pub_key_pem,
signature)
self.ready.wait()
if isinstance(self.output, RegistrationSuccess):
return True
else:
return False
def run(self):
self.go = True
print(' - connecting to node:')
node = None
for addr in NODES:
print(f' * trying {addr}... ', end='')
sys.stdout.flush()
try:
sock = socket.create_connection(addr, timeout=HELLO_TIMEOUT)
sock.settimeout(HELLO_TIMEOUT)
node = Handler(sock, addr)
node.hello()
print('[READY]')
break
except ConnectionRefusedError:
print('[REFUSED]')
node = None
except socket.timeout:
print('[TIMEOUT]')
node = None
self.ready.set()
if not node:
self.go = False
while self.go:
node.sock.settimeout(1)
seconds = 0
while self.go and not self.input and seconds < KEEPALIVE:
try:
msg = node.recv()
if not msg:
self._error_('LOST CONNECTION')
return
else:
self._error_(f'UNEXPECTED RESPONSE: {msg}')
return
except socket.timeout:
pass # this is expected
seconds += 1
if self.go and not self.input:
self._handle_(node, Ping())
if self.go and self.input:
self._handle_(node, self.input)
self.input = None
def _handle_(self, node, req):
node.sock.settimeout(GENERAL_TIMEOUT)
try:
node.send(req)
resp = node.recv()
if not resp:
self._error_('LOST CONNECTION')
return
elif isinstance(resp, NodeIsIsolated):
self._error_('NODE IS ISOLATED')
return
if isinstance(req, Ping):
if not isinstance(resp, Pong):
self._error_(f'UNEXPECTED RESPONSE: {resp}')
elif resp.nonce != req.nonce:
self._error_(f'BAD NONCE: {resp}')
elif isinstance(req, Reservation):
if isinstance(resp, Reservation) \
or isinstance(resp, ReservationCancel):
self.output = resp
self.ready.set()
else:
self._error_(f'UNEXPECTED RESPONSE: {resp}')
elif isinstance(req, Registration):
if isinstance(resp, RegistrationSuccess):
self.output = resp
self.ready.set()
else:
self._error_(f'UNEXPECTED RESPONSE: {resp}')
elif isinstance(req, GetRegistration):
if isinstance(resp, Registration):
self.output = resp
self.ready.set()
else:
self._error_(f'UNEXPECTED RESPONSE: {resp}')
else:
self._error_(f'UNHANDLED REQUEST: {req}')
except socket.timeout:
self._error_('TIMEOUT')
except BrokenPipeError:
self._error_('BROKEN PIPE')
except ConnectionResetError:
self._error_('CONNECTION RESET')
def _error_(self, txt):
print()
print(f'** ERROR ** [{txt}]')
self.go = False
self.ready.set()

File Metadata

Mime Type
text/x-diff
Expires
Tue, Sep 15, 8:36 PM (5 d, 20 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3679
Default Alt Text
(24 KB)

Event Timeline