Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F4483
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
24 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rLANK Encrypted Communications
Attached
Detach File
Event Timeline
Log In to Comment