Coverage for src/lib/server.py: 19%
1131 statements
« prev ^ index » next coverage.py v7.2.7, created at 2025-03-09 17:37 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2025-03-09 17:37 +0000
2import datetime as dt
3from socket import socket as Socket, timeout as SocketTimeout, gaierror as SocketGetaddrinfo, gethostname, gethostbyname, create_server, AF_INET, AF_INET6, SOCK_STREAM, SOCK_DGRAM, SOL_SOCKET, SO_REUSEADDR, SO_BROADCAST
4from selectors import DefaultSelector, EVENT_READ
5from ssl import TLSVersion, SSLContext, PROTOCOL_TLS_SERVER, PROTOCOL_TLS_CLIENT, CERT_NONE
6from struct import unpack, error as StructError
7from base64 import b64encode, b64decode
8from uuid import uuid4
9from os import path, getenv, getpid, mkdir, remove
10from logging import getLogger
12from cryptography.hazmat.primitives import serialization, hashes
13from cryptography.hazmat.primitives.asymmetric import padding
14from cryptography.fernet import Fernet
15from cryptography.exceptions import InvalidSignature
17from lib.client import Client, Action
18from lib.address_book import AddressBook
19from lib.helper import is_valid_uuid, binary_encode, binary_decode, password_key_derivation
20from lib.mail import Mail, Queue as MailQueue, Database as MailDatabase
21from lib.network import Network, RawCommandsType, SslHandshakeError
22from lib.cash import Cash
23from lib.contact import Contact
24from lib.overlay import Node, Distance
27VERSION = 1
28SSL_HANDSHAKE_WAIT = 0.3
29SSL_HANDSHAKE_TIMEOUT = 5
30SSL_MINIMUM_VERSION = TLSVersion.TLSv1_2
32class Server(Network):
33 _config: dict
34 _selectors: DefaultSelector
35 _main_server_socket: Socket
36 _discovery_socket: Socket
37 _ipc_server_socket: Socket
38 _address_book: AddressBook
39 _mail_queue: MailQueue
40 _mail_db: MailDatabase
41 _hostname: str
42 _lan_ip: str
43 _clients: list[Client]
44 _local_node: Node
45 _public_key_b64: str
46 _pid_file_path: str
47 _wrote_pid_file: bool
48 _client_auth_timeout: dt.timedelta
49 _client_action_retention_time: dt.timedelta
50 _contact: Contact
52 def __init__(self, config: dict = {}):
53 self._host_name = gethostname()
54 self._lan_ip = gethostbyname(self._host_name)
55 self._clients = []
56 self._selectors = DefaultSelector()
57 self._public_key = None
58 self._public_key_b64 = None
59 self._private_key = None
60 self._address_book = None
61 self._mail_queue = None
62 self._mail_db = None
63 self._wrote_pid_file = False
64 self._client_auth_timeout = None
65 self._client_action_retention_time = None
66 self._ssl_handshake_timeout = dt.timedelta(seconds=SSL_HANDSHAKE_TIMEOUT)
68 self._logger = getLogger('app.server')
69 self._logger.info('init()')
71 self._config = config
73 # TODO: use below
74 if 'contact' in self._config:
75 self._contact = Contact.resolve(self._config['contact'])
76 else:
77 self._contact = Contact()
79 if 'address_book' not in self._config:
80 self._config['address_book'] = {
81 'max_clients': 20,
82 'client_retention_time': 24,
83 }
85 if 'client' not in self._config:
86 self._config['client'] = {
87 'auth_timeout': 2,
88 'action_retention_time': 5,
89 }
90 self._client_auth_timeout = dt.timedelta(seconds=self._config['client']['auth_timeout'])
91 self._client_action_retention_time = dt.timedelta(minutes=self._config['client']['action_retention_time'])
93 if 'data_dir' in self._config:
94 self._pid_file_path = path.join(self._config['data_dir'], 'server.pid')
95 self._write_pid_file()
97 if 'public_key_file' not in self._config:
98 self._config['public_key_file'] = path.join(self._config['data_dir'], 'public_key.pem')
99 if 'private_key_file' not in self._config:
100 self._config['private_key_file'] = path.join(self._config['data_dir'], 'private_key.pem')
102 self._certificate_file = path.join(self._config['data_dir'], 'certificate.pem')
104 if 'keys_dir' not in self._config:
105 self._config['keys_dir'] = path.join(self._config['data_dir'], 'keys')
106 if not path.isdir(self._config['keys_dir']):
107 mkdir(self._config['keys_dir'])
109 address_book_path = path.join(self._config['data_dir'], 'address_book.json')
110 self._address_book = AddressBook(address_book_path, self._config)
111 self._address_book.load()
113 bootstrap_path = path.join(self._config['data_dir'], 'bootstrap.json')
114 if path.isfile(bootstrap_path):
115 self._address_book.add_bootstrap(bootstrap_path)
117 mail_queue_path = path.join(self._config['data_dir'], 'mail_queue.json')
118 self._mail_queue = MailQueue(mail_queue_path, self._config)
119 self._mail_queue.load()
121 mail_db_path = path.join(self._config['data_dir'], 'mail_db.json')
122 self._mail_db = MailDatabase(mail_db_path)
123 self._mail_db.load()
125 if 'challenge' not in self._config:
126 self._config['challenge'] = {'min': 15, 'max': 20}
128 if 'id' in self._config:
129 self._local_node = Node.parse(self._config['id'])
131 if isinstance(self._config['discovery'], bool):
132 self._config['discovery'] = {
133 'enabled': self._config['discovery'],
134 'port': 26000,
135 }
137 if 'bootstrap' not in self._config:
138 self._config['bootstrap'] = 'default'
140 def __del__(self):
141 self._logger.info('__del__()')
142 self._selectors.close()
144 if self._address_book:
145 self._address_book.save()
147 if self._mail_queue:
148 self._mail_queue.save()
150 if self._mail_db:
151 self._mail_db.save()
153 self._remove_pid_file()
155 self._logger.info('__del__() end')
157 def _write_pid_file(self):
158 if path.isfile(self._pid_file_path):
159 self._logger.error('Another instance of FluxChat is already running.')
160 self._logger.error('If this is not the case, delete the file: %s', self._pid_file_path)
161 exit(1)
163 with open(self._pid_file_path, 'w') as fh:
164 fh.write(str(getpid()))
165 self._wrote_pid_file = True
167 def _remove_pid_file(self):
168 self._logger.info('_remove_pid_file()')
169 if not self._wrote_pid_file:
170 return
171 if path.isfile(self._pid_file_path):
172 remove(self._pid_file_path)
174 def start(self):
175 self._logger.info('start')
177 self._logger.info('password_key_derivation')
178 self._pkd = password_key_derivation(getenv('FLUXCHAT_KEY_PASSWORD', 'password').encode()).encode()
180 self._load_public_key_from_pem_file()
181 self._load_private_key_from_pem_file()
183 self._main_server_ssl = SSLContext(PROTOCOL_TLS_SERVER)
184 self._main_server_ssl.minimum_version = SSL_MINIMUM_VERSION
185 self._main_server_ssl.load_cert_chain(certfile=self._certificate_file, keyfile=self._config['private_key_file'], password=self._pkd)
187 try:
188 self._logger.debug('create server %s:%s', self._config['address'], self._config['port'])
189 # IPv4
190 self._main_server_socket = create_server((self._config['address'], self._config['port']), family=AF_INET, reuse_port=True)
191 except OSError as e:
192 self._logger.error('OSError: %s', e)
193 raise e
194 except Exception as e:
195 self._logger.error('Exception: %s', e)
196 raise e
198 self._logger.debug('listen')
199 self._main_server_socket.listen()
200 self._main_server_socket.setblocking(False)
201 self._selectors.register(self._main_server_socket, EVENT_READ, data={'type': 'main_server'})
203 if 'discovery' in self._config and self._config['discovery']['enabled']:
204 self._logger.debug('discovery')
206 # IPv4
207 self._discovery_socket = Socket(AF_INET, SOCK_DGRAM) # UDP
208 self._discovery_socket.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
209 self._discovery_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
211 try:
212 self._discovery_socket.bind(('0.0.0.0', self._config['discovery']['port']))
213 except OSError as e:
214 self._logger.error('OSError: %s', e)
215 raise e
217 self._discovery_socket.setblocking(False)
219 if self.has_contact():
220 self._logger.debug('send broadcast')
221 # TODO for production: set port to self._config['discovery']['port'] instead of hard-coded 26000
222 res = self._discovery_socket.sendto(self.get_contact().encode(), ('<broadcast>', 26000))
223 self._logger.debug('res %s', res)
225 self._selectors.register(self._discovery_socket, EVENT_READ, data={'type': 'discovery'})
227 if 'ipc' in self._config and self._config['ipc']['enabled']:
228 ipc_addr = (self._config['ipc']['address'], self._config['ipc']['port'])
229 self._logger.debug('ipc %s', ipc_addr)
231 # IPv4
232 self._ipc_server_socket = Socket(AF_INET, SOCK_STREAM)
233 self._ipc_server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
234 self._ipc_server_socket.bind(ipc_addr)
235 self._ipc_server_socket.listen()
236 self._ipc_server_socket.setblocking(False)
238 self._selectors.register(self._ipc_server_socket, EVENT_READ, data={'type': 'ipc_server'})
240 def _load_private_key_from_pem_file(self) -> None:
241 self._logger.debug('load private key from pem file')
243 if not path.isfile(self._config['private_key_file']):
244 raise Exception('private key file not found: {}'.format(self._config['private_key_file']))
246 _pkd = password_key_derivation(getenv('FLUXCHAT_KEY_PASSWORD', 'password').encode()).encode()
248 with open(self._config['private_key_file'], 'rb') as f:
249 self._private_key = serialization.load_pem_private_key(f.read(), password=_pkd)
251 def _load_public_key_from_pem_file(self) -> None:
252 self._logger.debug('load public key from pem file')
254 if not path.isfile(self._config['public_key_file']):
255 raise Exception('public key file not found: {}'.format(self._config['public_key_file']))
257 with open(self._config['public_key_file'], 'rb') as f:
258 self._public_key = serialization.load_pem_public_key(f.read())
260 # DER is binary representation of public key.
261 public_bin = self._public_key.public_bytes(
262 encoding=serialization.Encoding.DER,
263 format=serialization.PublicFormat.SubjectPublicKeyInfo
264 )
266 self._public_key_b64 = b64encode(public_bin).decode()
268 # TODO: remove, replace with Contact class
269 def has_contact(self) -> bool:
270 if 'contact' in self._config:
271 if self._config['contact'] == 'disabled' or self._config['contact'] == 'private':
272 return False
273 elif bool(self._config['contact']) == False:
274 return False
276 return True
278 return False
280 # TODO: remove, replace with Contact class
281 def get_contact(self) -> str:
282 if self.has_contact():
283 items = self._config['contact'].split(':')
284 item_len = len(items)
286 if item_len == 1:
287 return '{}:{}'.format(items[0], self._config['port'])
289 return self._config['contact']
291 return 'N/A'
293 def _client_is_connected(self, client: Client) -> bool:
294 # self._logger.debug('_client_is_connected()')
296 def ffunc(_client):
297 return _client.uuid == client.uuid or _client.id == client.id or _client.address == client.address and _client.port == client.port
298 clients = list(filter(ffunc, self._clients))
300 return len(clients) > 0
302 def _accept_main_server(self, server_sock: Socket):
303 self._logger.debug('_accept_main_server()')
305 client_sock, addr = server_sock.accept()
306 client_sock.setblocking(False)
308 client_ssl = self._main_server_ssl.wrap_socket(client_sock, server_side=True, do_handshake_on_connect=False)
310 try:
311 self._ssl_handshake(client_ssl)
312 except SslHandshakeError as e:
313 self._logger.error('ssl handshake error: %s', e)
314 return
316 self._logger.debug('client_sock: %s', client_sock)
317 self._logger.debug('client_ssl: %s', client_ssl)
318 self._logger.debug('accepted: %s', addr)
320 client = Client()
321 client.sock = client_ssl
322 client.conn_mode = 1
323 client.dir_mode = 'i'
324 client.debug_add = 'accept'
326 self._selectors.register(client_ssl, EVENT_READ, data={
327 'type': 'main_client',
328 'client': client,
329 })
331 self._clients.append(client)
333 self._logger.debug('_accept_main_server() client: %s', client)
335 def _read_discovery(self, server_sock: Socket):
336 self._logger.debug('_read_discovery()')
338 data, addr = server_sock.recvfrom(1024)
339 c_contact_raw = data.decode()
341 self._logger.debug('data: %s', data)
342 self._logger.debug('addr: %s', addr)
344 if addr[0] == self._lan_ip and addr[1] == self._config['discovery']['port']:
345 self._logger.debug('skip self')
346 return
348 c_contact = Contact.resolve(c_contact_raw, addr[0])
350 if not c_contact.is_valid:
351 return
353 client = self._address_book.get_client_by_addr_port(c_contact.addr, c_contact.port)
354 if client is None:
355 client = self._address_book.add_client(addr=c_contact.addr, port=c_contact.port)
356 client.debug_add = 'discovery, contact: {}'.format(c_contact_raw)
357 else:
358 self._logger.debug('client: %s', client)
360 self._logger.debug('read_discovery client: %s', client)
362 self._client_connect(client)
364 def _accept_ipc_server(self, server_sock: Socket):
365 self._logger.debug('_accept_ipc_server()')
367 client_sock, addr = server_sock.accept()
368 client_sock.setblocking(False)
370 self._selectors.register(client_sock, EVENT_READ, data={
371 'type': 'ipc_client',
372 })
374 def _client_connect(self, client: Client) -> bool:
375 self._logger.debug('_client_connect(%s)', client)
377 # TODO: activate for production
378 # if client.address == self._lan_ip and os.environ.get('ALLOW_SELF_CONNECT') != '1':
379 # self._logger.debug('skip, client.address == self._lan_ip')
380 # return False
381 if client.node == self._local_node:
382 self._logger.debug('skip, client.node == self._local_node')
383 return False
384 if client.address is None or client.port is None:
385 self._logger.debug('skip, client.address is None or client.port is None')
386 return False
388 client.conn_mode = 1
389 client.dir_mode = 'o'
390 client.refresh_used_at()
392 client_ssl = SSLContext(PROTOCOL_TLS_CLIENT)
393 client_ssl.minimum_version = SSL_MINIMUM_VERSION
394 client_ssl.check_hostname = False
395 client_ssl.verify_mode = CERT_NONE
397 # IPv4
398 client_sock = Socket(AF_INET, SOCK_STREAM)
399 client_sock.settimeout(2)
400 try:
401 self._logger.debug('client sock connect to %s:%s', client.address, client.port)
402 client_sock.connect((client.address, client.port))
403 self._logger.debug('client sock connect done')
404 except ConnectionRefusedError as e:
405 self._logger.error('ConnectionRefusedError: %s', e)
406 return False
407 except TimeoutError as e:
408 self._logger.error('TimeoutError: %s', e)
409 return False
410 except SocketTimeout as e:
411 self._logger.error('SocketTimeout: %s', e)
412 return False
413 except SocketGetaddrinfo as e:
414 self._logger.error('SocketGetaddrinfo: %s', e)
415 return False
416 except OSError as e:
417 self._logger.error('OSError: %s', e)
418 return False
420 client_sock.settimeout(None)
421 client_sock.setblocking(False)
423 client_ssl = client_ssl.wrap_socket(client_sock, do_handshake_on_connect=False)
424 # client.sock = client_sock
425 client.sock = client_ssl
427 self._selectors.register(client_ssl, EVENT_READ, data={
428 'type': 'main_client',
429 'client': client,
430 })
432 self._clients.append(client)
434 try:
435 self._ssl_handshake(client_ssl)
436 except SslHandshakeError as e:
437 self._logger.error('ssl handshake error: %s', e)
438 return False
440 self._logger.debug('_client_connect done')
441 return True
443 def _client_commands(self, sock: Socket, client: Client, commands: RawCommandsType):
444 self._logger.debug('_client_commands(%s)', client)
446 for group_i, command_i, payload in commands:
447 #group_i, command_i, payload = raw_command
448 payload_len = len(payload)
450 self._logger.debug('group: %d, command %d', group_i, command_i)
451 self._logger.debug('payload: %d %s', payload_len, payload)
453 if group_i >= 2 and client.auth != 15:
454 self._logger.debug('not authenticated: %s', client.auth)
455 self._logger.debug('conn mode 0')
456 client.conn_mode = 0
457 client.conn_msg = 'not authenticated'
458 continue
460 if group_i == 0: # Basic
461 if command_i == 0:
462 self._logger.info('OK command')
464 elif group_i == 1: # Connection, Authentication, etc
465 if command_i == 1:
466 self._logger.info('CHALLENGE command')
468 if client.auth & 2 != 0:
469 self._logger.debug('skip, already got CHALLENGE')
470 continue
472 client.auth |= 2
474 client.challenge.min = int.from_bytes(payload[0], 'little')
475 client.challenge.max = int.from_bytes(payload[1], 'little')
476 client.challenge.data = str(payload[2].decode())
478 self._logger.debug('challenge: %s', client.challenge)
480 c_data_len = len(client.challenge.data)
481 if c_data_len > 36:
482 self._logger.warning('skip, challenge data too long: %d > 36', c_data_len)
483 self._logger.debug('conn mode 0')
484 client.conn_mode = 0
485 client.conn_msg = 'challenge data too long'
486 continue
488 if client.challenge.min > self._config['challenge']['max']:
489 self._logger.warning('skip, challenge min is too big: %d > %d', client.challenge.min, self._config['challenge']['max'])
490 self._logger.debug('conn mode 0')
491 client.conn_mode = 0
492 client.conn_msg = 'challenge min is too big: %d > %d'.format(client.challenge.min, self._config['challenge']['max'])
493 continue
495 cash = Cash(client.challenge.data, client.challenge.min)
496 self._logger.debug('mine')
497 cash.mine()
498 self._logger.debug('mine done')
500 client.challenge.proof = cash.proof
501 client.challenge.nonce = cash.nonce
503 self._logger.debug('challenge: %s', client.challenge)
504 self._logger.debug('cash.min: %s', client.challenge.min)
505 self._logger.debug('cash.max: %s', client.challenge.max)
506 self._logger.debug('cash.data: %s', client.challenge.data)
507 self._logger.debug('cash.proof: %s', cash.proof)
508 self._logger.debug('cash.nonce: %s', cash.nonce)
510 elif command_i == 2:
511 self._logger.info('ID command')
513 if client.auth & 2 == 0:
514 self._logger.warning('skip, client has first to send CHALLENGE')
515 continue
517 if client.auth & 8 != 0:
518 self._logger.debug('skip, already authenticated')
519 continue
521 c_version = int.from_bytes(payload[0], 'little')
522 c_id = payload[1].decode()
523 c_contact_s = payload[2].decode()
524 c_cc_proof = payload[3].decode()
525 c_cc_nonce = int.from_bytes(payload[4], 'little')
527 self._logger.debug('c_version: %s', c_version)
528 self._logger.debug('c_id: %s', c_id)
529 self._logger.debug('c_contact_s: %s', c_contact_s)
530 self._logger.debug('c_cc_proof: %s', c_cc_proof)
531 self._logger.debug('c_cc_nonce: %s', c_cc_nonce)
533 # Local
534 if self._local_node == c_id:
535 self._logger.debug('skip, ID is local node')
536 self._logger.debug('conn mode 0')
537 client.conn_mode = 0
538 client.conn_msg = 'ID is local node'
539 continue
541 # Version
542 if c_version != VERSION:
543 self._logger.warning('skip, version mismatch: %d != %d', c_version, VERSION)
544 self._logger.debug('conn mode 0')
545 client.conn_mode = 0
546 client.conn_msg = 'version mismatch'
547 continue
549 # Challenge
550 if not client.cash.verify(c_cc_proof, c_cc_nonce):
551 self._logger.warning('skip, challenge not verified')
552 self._logger.debug('conn mode 0')
553 client.conn_mode = 0
554 client.conn_msg = 'challenge not verified'
555 continue
556 self._logger.debug('cash verified')
558 # Contact info
559 addr = sock.getpeername()
560 c_contact = Contact.resolve(c_contact_s, addr[0])
561 c_contact_addr = c_contact.addr
562 c_contact_port = c_contact.port
563 c_has_contact_info = c_contact.is_valid
565 c_switch = False
566 if client.dir_mode == 'i':
567 # Client is incoming
568 self._logger.debug('client is incoming')
570 if c_has_contact_info:
571 # Client sent contact info
572 _client = self._address_book.get_client_by_id(c_id)
573 if _client is None:
574 self._logger.debug('client not found by ID (A)')
576 _client = self._address_book.get_client_by_addr_port(c_contact_addr, c_contact_port)
577 if _client is None:
578 self._logger.debug('client not found by Addr:Port (B)')
580 _client = self._address_book.add_client(c_id, c_contact_addr, c_contact_port)
581 _client.dir_mode = client.dir_mode
582 _client.debug_add = 'id command, incoming, contact infos, not found by id, not found by addr:port, original: ' + client.debug_add
583 else:
584 self._logger.debug('client found B: %s', _client)
585 else:
586 self._logger.debug('client found A: %s', _client)
588 _client.address = c_contact_addr
589 _client.port = c_contact_port
590 else:
591 # Client sent no contact info
592 _client = self._address_book.get_client_by_id(c_id)
593 if _client is None:
594 self._logger.debug('client not found C')
596 _client = self._address_book.add_client(c_id)
597 _client.dir_mode = client.dir_mode
598 _client.debug_add = 'id command, incoming, no contact infos, not found by id, original: ' + client.debug_add
599 else:
600 self._logger.debug('client found C: {}'.format(_client))
602 c_switch = True
604 elif client.dir_mode == 'o':
605 # Client is outgoing
606 self._logger.debug('client is outgoing')
608 _client = client
610 if c_has_contact_info:
611 self._logger.debug('client has contact infos')
612 _client.address = c_contact_addr
613 _client.port = c_contact_port
614 else:
615 self._logger.debug('client has NO contact infos')
617 if _client.id is None:
618 _client.id = c_id
620 self._logger.debug('Client A: %s', client)
621 self._logger.debug('Client B: %s', _client)
623 _client.refresh_seen_at()
624 _client.refresh_used_at()
625 _client.inc_meetings()
627 _client.sock = sock
628 _client.conn_mode = client.conn_mode
629 _client.auth = client.auth | 8
630 _client.actions = client.actions
631 _client.challenge = client.challenge
633 # Update Address Book because also an existing client can be updated
634 self._address_book.changed()
636 if c_switch and _client != client:
637 self._logger.debug('switch client')
638 self._clients.remove(client)
639 self._clients.append(_client)
641 self._selectors.unregister(sock)
642 self._selectors.register(_client.sock, EVENT_READ, data={
643 'type': 'main_client',
644 'client': _client,
645 })
647 self._client_send_ok(_client.sock)
649 self._logger.debug('Client Z: %s', _client)
651 elif command_i == 3:
652 self._logger.info('PING command')
653 self._client_send_pong(sock)
655 elif command_i == 4:
656 self._logger.info('PONG command')
658 elif group_i == 2: # Overlay, Address Book, Routing, etc
659 if command_i == 1:
660 self._logger.info('GET_NEAREST_TO command')
662 try:
663 node = Node(payload[0].decode())
664 except:
665 self._logger.warning('skip, invalid node')
666 continue
668 client_ids = []
669 clients = self._address_book.get_nearest_to(node, with_contact_infos=True)
670 for _client in clients:
671 self._logger.debug('client: %s %s', _client, _client.distance(node))
672 if _client.id != self._local_node.id and _client.id != node.id:
673 contact_infos = [_client.id, _client.address, str(_client.port)]
674 self._logger.debug('contact infos: %s', contact_infos)
675 client_ids.append(':'.join(contact_infos))
677 self._client_send_get_nearest_response(sock, client_ids)
679 elif command_i == 2:
680 self._logger.info('GET_NEAREST_TO RESPONSE command')
682 action = client.resolve_action('nearest_response')
683 if action is None:
684 self._logger.warning('skip, not requested')
685 continue
687 self._logger.debug('action: %s', action)
689 nearest_client = None
690 distance = Distance()
691 for c_contact in payload:
692 self._logger.debug('client contact A: %s', c_contact)
694 c_id, c_contact_raw = c_contact.decode().split(':', 1)
695 self._logger.debug('client contact B: %s %s', c_id, c_contact_raw)
697 c_contact = Contact.resolve(c_contact_raw)
698 self._logger.debug('client contact C: %s %s %s', c_contact.addr, c_contact.port, c_contact.is_valid)
700 if c_id == self._local_node.id:
701 continue
703 _client = self._address_book.get_client_by_id(c_id)
704 if _client is None:
705 self._logger.debug('client not found')
706 _client = self._address_book.add_client(c_id, c_contact.addr, c_contact.port)
707 _client.debug_add = 'nearest response, not found by id'
709 _c_distance = _client.distance(self._local_node)
710 if _c_distance < distance:
711 # distance = _client.distance(self._local_node)
712 distance = _c_distance
713 self._logger.debug('new distance: %s', distance)
715 nearest_client = _client
716 else:
717 self._logger.debug('client found: %s', _client)
719 if nearest_client is not None:
720 self._logger.debug('nearest client: %s', nearest_client)
722 bootstrap_count = action.data - 1
723 self._logger.debug('bootstrap count: %d', bootstrap_count)
725 if bootstrap_count > 0 and not self._client_is_connected(nearest_client):
726 self._client_connect(nearest_client)
728 nearest_client.add_action(Action('bootstrap', data=bootstrap_count))
730 elif command_i == 3:
731 self._logger.info('REQUEST PUBLIC KEY FOR NODE command')
733 is_relay = False
734 fwd_clients = []
735 node_id = payload[0].decode()
736 self._logger.debug('node id: %s', node_id)
738 try:
739 target = Node.parse(node_id)
740 except:
741 self._logger.debug('skip, invalid node')
742 continue
744 if target == self._local_node:
745 self._logger.debug('local node')
746 self._client_response_public_key_for_node(sock, target.id, self._public_key_b64)
747 else:
748 self._logger.debug('not local node')
750 _client = self._address_book.get_client_by_id(target.id)
751 if _client is None:
752 self._logger.debug('client not found')
754 is_relay = True
755 fwd_clients = self._address_book.get_nearest_to(target, with_contact_infos=True)
756 else:
757 self._logger.debug('client found: %s', _client)
759 if _client.has_public_key():
760 self._logger.debug('client has public key')
762 self._client_response_public_key_for_node(sock, target.id, _client.get_base64_public_key())
763 else:
764 self._logger.debug('client does not have public key')
766 self._logger.debug('relay')
767 is_relay = True
768 fwd_clients = [_client]
770 if is_relay:
771 for _client in fwd_clients:
772 if client == _client:
773 self._logger.debug('client is self')
774 continue
776 self._logger.debug('client: %s', _client)
778 if _client.has_action('request_public_key_for_node', target.id):
779 self._logger.debug('client already has action request_public_key_for_node/%s', target.id)
780 else:
781 self._logger.debug('create action request_public_key_for_node/%s', target.id)
783 action = self._create_action_request_public_key_for_node(target, 'r')
785 action.func = lambda _arg_client: self._client_response_public_key_for_node(sock, target.id, _arg_client.get_base64_public_key())
787 _client.add_action(action)
789 elif command_i == 4:
790 self._logger.info('RESPONSE PUBLIC KEY FOR NODE command')
792 node_id = payload[0].decode()
793 public_key_raw = payload[1].decode()
794 self._logger.debug('node id: %s', node_id)
795 self._logger.debug('public key raw: %s', public_key_raw)
797 try:
798 node = Node.parse(node_id)
799 self._logger.debug('node: %s', node)
800 except:
801 self._logger.debug('skip, invalid node')
802 continue
804 action = client.resolve_action('request_public_key_for_node', node.id, force_remove=True)
805 if action is None:
806 self._logger.warning('skip, not requested')
807 continue
809 if node == self._local_node:
810 self._logger.warning('skip, local node')
811 continue
813 self._logger.debug('action: %s', action)
815 _client = self._address_book.get_client_by_id(node.id)
816 if _client is None:
817 self._logger.debug('client not found')
819 _client = Client()
820 _client.debug_add = 'public key response'
821 _client.set_id(node.id)
822 _client.load_public_key_from_pem(public_key_raw)
824 if _client.verify_public_key():
825 self._logger.debug('public key verified')
827 self._address_book.append_client(_client)
828 self._logger.debug('client added: %s', _client)
829 else:
830 self._logger.debug('public key not verified')
831 _client = None
832 else:
833 self._logger.debug('client found: %s', _client)
835 if _client.has_public_key():
836 self._logger.debug('client has public key')
837 else:
838 _client.load_public_key_from_pem(public_key_raw)
839 if _client.verify_public_key():
840 self._logger.debug('public key verified')
841 self._address_book.changed()
842 else:
843 self._logger.debug('public key not verified')
844 _client.reset_public_key()
846 if _client is not None and _client.has_public_key():
847 self._logger.debug('client is set and has public key')
848 self._logger.debug('client: %s', _client)
850 if action.func is not None:
851 self._logger.debug('action has func')
852 self._logger.debug('call func')
853 action.func(_client)
855 elif group_i == 3: # Mail
856 if command_i == 1:
857 self._logger.debug('SEND MAIL command')
859 mail_uuid, mail_target, mail_data = payload
861 self._logger.debug('mail uuid: %s', mail_uuid)
862 if not is_valid_uuid(mail_uuid):
863 self._logger.debug('invalid mail uuid')
864 continue
866 if self._mail_db.has_mail(mail_uuid):
867 self._logger.debug('DB, mail already exists')
868 continue
870 if self._mail_queue.has_mail(mail_uuid):
871 self._logger.debug('QUEUE, mail already exists')
872 continue
874 try:
875 mail_target = Node.parse(mail_target)
876 self._logger.debug('mail target: %s', mail_target)
877 except:
878 self._logger.debug('invalid mail target')
879 continue
881 self._logger.debug('mail data: %s', mail_data)
883 mail = Mail(mail_uuid)
884 mail.receiver = mail_target.id
885 mail.target = mail_target
886 mail.body = mail_data
887 mail.is_encrypted = True
888 mail.received_now()
890 if mail_target == self._local_node:
891 self._logger.debug('mail target is local node')
892 self._decrypt_mail(mail)
893 self._mail_db.add_mail(mail)
894 else:
895 self._logger.debug('mail target is not local node')
896 mail.forwarded_to.append(client.id)
897 self._mail_queue.add_mail(mail)
899 else:
900 self._logger.debug('unknown group %d, command %d', group_i, command_i)
901 self._logger.debug('conn mode 0')
902 client.conn_mode = 0
903 client.conn_msg = 'unknown group %d, command %d' % (group_i, command_i)
905 def _client_send_ok(self, sock: Socket):
906 self._logger.debug('_client_send_ok()')
907 self._client_write(sock, 0, 0)
909 def _client_send_challenge(self, sock: Socket, challenge: str):
910 self._logger.debug('_client_send_challenge(%s)', challenge)
912 self._client_write(sock, 1, 1, [
913 self._config['challenge']['min'],
914 self._config['challenge']['max'],
915 challenge,
916 ])
918 def _client_send_id(self, sock: Socket, proof: str, nonce: int):
919 self._logger.debug('_client_send_id(%s, %d)', proof, nonce)
920 data = [
921 VERSION,
922 self._config['id'],
923 self._config['contact'],
924 proof,
925 nonce,
926 ]
928 # self._logger.debug('data: %s', data)
929 self._client_write(sock, 1, 2, data)
931 def _client_send_ping(self, sock: Socket):
932 self._logger.debug('_client_send_ping()')
933 self._client_write(sock, 1, 3)
935 def _client_send_pong(self, sock: Socket):
936 self._logger.debug('_client_send_pong()')
937 self._client_write(sock, 1, 4)
939 def _client_send_get_nearest_to(self, sock: Socket, id: str):
940 self._logger.debug('_client_send_get_nearest_to()')
941 self._client_write(sock, 2, 1, [id])
943 def _client_send_get_nearest_response(self, sock: Socket, client_ids: list):
944 self._logger.debug('_client_send_get_nearest_response()')
945 self._client_write(sock, 2, 2, client_ids)
947 def _client_request_public_key_for_node(self, sock: Socket, id: str):
948 self._logger.debug('_client_request_public_key_for_node(%s)', id)
949 self._client_write(sock, 2, 3, [id])
951 def _client_response_public_key_for_node(self, sock: Socket, id: str, public_key: str):
952 self._logger.debug('_client_response_public_key_for_node()')
953 # self._logger.debug('type: %s', type(id))
954 # self._logger.debug('type: %s', type(public_key))
955 self._logger.debug('public key: %s', public_key)
957 self._client_write(sock, 2, 4, [id, public_key])
959 def _client_send_mail(self, sock: Socket, mail: Mail):
960 self._logger.debug('_client_send_mail()')
961 if not mail.is_encrypted:
962 self._logger.debug('mail not encrypted')
963 return
965 self._logger.debug('mail: %s', type(mail.body))
967 self._client_write(sock, 3, 1, [
968 mail.uuid,
969 mail.target.id,
970 mail.body,
971 ])
973 def _ipc_client_read(self, sock: Socket):
974 self._logger.debug('_ipc_client_read()')
976 try:
977 raw = sock.recv(2048)
978 except TimeoutError as e:
979 self._logger.error('IPC TimeoutError: %s', e)
980 return
981 except ConnectionResetError as e:
982 self._logger.error('IPC ConnectionResetError: %s', e)
983 raw = False
985 if raw:
986 raw_len = len(raw)
988 raw_pos = 0
989 commands = []
990 while raw_pos < raw_len:
991 try:
992 flags_i = raw[raw_pos]
993 raw_pos += 1
995 group = raw[raw_pos]
996 raw_pos += 1
998 command = raw[raw_pos]
999 raw_pos += 1
1000 except IndexError as e:
1001 self._logger.error('IPC IndexError: %s', e)
1002 self._logger.error('IPC unregister socket')
1003 self._selectors.unregister(sock)
1004 return
1006 lengths_are_4_bytes = flags_i & 1 != 0
1008 try:
1009 # length = unpack('<I', raw[raw_pos:raw_pos + 4])[0]
1010 length = int.from_bytes(raw[raw_pos:raw_pos + 4], 'little')
1011 raw_pos += 4
1012 except StructError as e:
1013 self._logger.error('IPC struct.error: %s', e)
1014 self._logger.error('IPC unregister socket')
1015 self._selectors.unregister(sock)
1016 return
1018 payload_raw = raw[raw_pos:]
1019 payload_items = []
1021 self._logger.debug('IPC group: %d', group)
1022 self._logger.debug('IPC command: %d', command)
1023 self._logger.debug('IPC length: %d %s', length, type(length))
1025 pos = 0
1026 while pos < length:
1027 self._logger.debug('IPC pos: %d', pos)
1028 if lengths_are_4_bytes:
1029 # item_len = unpack('<I', payload_raw[pos:pos + 4])[0]
1030 item_len = int.from_bytes(payload_raw[pos:pos + 4], 'little')
1031 pos += 3
1032 else:
1033 item_len = payload_raw[pos]
1034 pos += 1
1036 self._logger.debug('IPC item len: %d', item_len)
1038 item = payload_raw[pos:pos + item_len]
1039 self._logger.debug('IPC item: %s', item)
1041 payload_items.append(item.decode())
1042 pos += item_len
1044 commands.append([group, command, payload_items])
1045 raw_pos += length + 1
1047 self._ipc_client_commands(sock, commands)
1048 else:
1049 self._logger.debug('no data')
1051 self._logger.debug('IPC unregister socket')
1052 self._selectors.unregister(sock)
1054 def _ipc_client_commands(self, sock: Socket, commands: RawCommandsType):
1055 self._logger.debug('_ipc_client_commands()')
1056 self._logger.debug('commands: %s', commands)
1058 for group_i, command_i, payload in commands:
1059 payload_len = len(payload)
1061 self._logger.debug('group %d, command %d', group_i, command_i)
1062 self._logger.debug('payload_len: %d', payload_len)
1063 self._logger.debug('payload: %s', payload)
1065 if group_i == 0: # Basic
1066 if command_i == 0:
1067 self._logger.info('OK command')
1069 elif group_i == 1:
1070 if command_i == 0:
1071 self._logger.info('SEND MAIL command')
1073 print(f'-> payload: {payload}')
1075 target = payload[0]
1076 body = payload[1]
1077 self._logger.debug('target: %s', target)
1078 self._logger.debug('body: %s', body)
1080 mail = Mail()
1081 mail.set_receiver(target)
1082 mail.body = body
1083 self._mail_queue.add_mail(mail)
1085 self._logger.debug('uuid: %s', mail.uuid)
1087 self._client_send_ok(sock)
1089 elif command_i == 1:
1090 self._logger.info('LIST MAILS command')
1092 flags_i = int.from_bytes(payload[0], 'little')
1093 only_new = flags_i & 1 != 0
1094 self._logger.debug('flags_i: %d', flags_i)
1095 self._logger.debug('only_new: %s', only_new)
1097 mails = list(self._mail_db.get_mails())
1099 if only_new:
1100 mails = list(filter(lambda _mail: _mail[1].is_new, mails))
1101 # mails = dict(filter(lambda _mail: _mail[1].is_new, mails))
1103 # print('mails: %s' % mails)
1105 chunks = []
1106 for n in range(0, len(mails), 5):
1107 encoded_mails = list(map(lambda _mail: _mail[1].ipc_encode(), mails[n:n + 5]))
1108 chunks.append(encoded_mails)
1110 chunks_len = len(chunks)
1111 self._logger.debug('chunks_len: %d', chunks_len)
1113 for n in range(chunks_len):
1114 self._logger.debug('chunk n: %d', n)
1115 self._ipc_client_send_list_mail(sock, chunks_len, n, chunks[n])
1117 elif command_i == 2:
1118 self._logger.info('READ MAIL command')
1120 m_uuid = payload[0].decode()
1121 self._logger.debug('m_uuid: %s', m_uuid)
1123 mail = self._mail_db.get_mail(m_uuid)
1124 if mail is None:
1125 self._logger.error('mail not found')
1126 mail_encoded = None
1127 else:
1128 self._logger.debug('mail: %s', mail)
1130 mail_encoded = mail.ipc_encode()
1131 self._logger.debug('mail_encoded: %s', mail_encoded)
1133 self._ipc_client_send_read_mail(sock, mail_encoded)
1135 elif group_i == 2:
1136 if command_i == 0:
1137 self._logger.debug('SAVE command')
1138 self.save()
1140 if command_i == 1:
1141 self._logger.debug('STOP command')
1142 self._scheduler.shutdown('STOP command')
1144 def _ipc_client_send_list_mail(self, sock: Socket, chunks_len: int, chunk_num: int, mails: list):
1145 self._logger.debug('_ipc_client_send_list_mail()')
1146 self._logger.debug('mails: %s', mails)
1148 self._client_write(sock, 1, 1, [chunks_len, chunk_num] + mails)
1150 def _ipc_client_send_read_mail(self, sock: Socket, mail: str):
1151 self._logger.debug('_ipc_client_send_read_mail()')
1152 self._logger.debug('mail: %s', mail)
1154 if mail is not None:
1155 data = [1, mail]
1156 else:
1157 data = [0]
1159 self._client_write(sock, 1, 2, data)
1161 def handle_sockets(self) -> bool:
1162 # self._logger.debug('handle_sockets()')
1164 data_processed = False
1166 events = self._selectors.select(timeout=0)
1167 for key, mask in events:
1168 self._logger.debug('handle_sockets mask: %d', mask)
1170 if key.data is not None:
1171 if key.data['type'] == 'main_server':
1172 self._accept_main_server(key.fileobj)
1174 elif key.data['type'] == 'main_client':
1175 status = self._client_read(key.fileobj)
1177 if status.disconnect:
1178 self._logger.debug('client disconnect: %s', status.msg)
1179 key.data['client'].conn_mode = 0
1180 key.data['client'].conn_msg = status.msg
1182 self._client_commands(key.fileobj, key.data['client'], status.commands)
1184 elif key.data['type'] == 'discovery':
1185 self._logger.debug('discovery')
1186 self._read_discovery(key.fileobj)
1188 elif key.data['type'] == 'ipc_server':
1189 self._accept_ipc_server(key.fileobj)
1191 elif key.data['type'] == 'ipc_client':
1192 self._ipc_client_read(key.fileobj)
1194 data_processed = True
1196 return data_processed # will be returned to the Scheduler
1198 def contact_address_book(self) -> bool:
1199 self._logger.debug('contact_address_book()')
1201 _clients = list(self._address_book.get_clients().values())
1202 _clients.sort(key=lambda _client: _client.meetings, reverse=True)
1204 # self._logger.debug('clients: %d', len(_clients))
1206 connect_to_clients: list[Client] = []
1207 zero_meetings_clients = []
1208 for client in _clients:
1209 self._logger.debug('contact: %s', client)
1211 if client.meetings > 0:
1212 if not self._client_is_connected(client):
1213 self._logger.debug('client is not connected A')
1214 connect_to_clients.append(client)
1215 else:
1216 zero_meetings_clients.append(client)
1218 zero_meetings_clients.sort(key=lambda _client: _client.distance(self._local_node))
1219 for client in zero_meetings_clients:
1220 self._logger.debug('zero_meetings_client: %s', client)
1221 if not self._client_is_connected(client):
1222 self._logger.debug('client is not connected B')
1223 connect_to_clients.append(client)
1225 is_bootstrapping = self.is_bootstrap_phase()
1227 for client in connect_to_clients:
1228 if is_bootstrapping:
1229 client.add_action(Action('bootstrap', data=2)) # TODO for production: set to 7
1230 self._client_connect(client)
1232 return True
1234 def get_clients(self) -> list[Client]:
1235 return self._clients
1237 def add_client(self, client: Client):
1238 self._clients.append(client)
1240 def handle_clients(self) -> bool:
1241 for client in self._clients:
1243 # Remove clients that are not connected
1244 if client.conn_mode == 0:
1245 self._logger.debug('remove client: %s', client)
1246 self._logger.debug('reason: %s', client.conn_msg)
1247 self._selectors.unregister(client.sock)
1248 client.sock.close()
1249 self._clients.remove(client)
1251 client.reset()
1253 if client.conn_mode == 1:
1254 if client.auth & 1 == 0:
1255 data_org = str(uuid4())
1256 client.cash = Cash(data_org, self._config['challenge']['min'])
1258 self._logger.debug('send CHALLENGE')
1259 self._client_send_challenge(client.sock, data_org)
1260 client.auth |= 1
1262 elif client.auth & 2 != 0 and client.auth & 4 == 0:
1263 self._logger.debug('send ID')
1264 self._client_send_id(client.sock, client.challenge.proof, client.challenge.nonce)
1265 client.auth |= 4
1267 if client.auth == 15:
1268 client.conn_mode = 2
1270 # Auth Timeout
1271 if dt.datetime.now(dt.UTC) - client.used_at >= self._client_auth_timeout:
1272 self._logger.debug('client used_at: %s', client.used_at)
1273 self._logger.debug('client timeout (%s)', self._client_auth_timeout)
1274 client.conn_mode = 0
1275 client.conn_msg = 'timeout'
1277 return True
1279 def ping_clients(self) -> bool:
1280 for client in self._clients:
1281 if client.conn_mode == 2:
1282 self._logger.debug('send PING')
1283 self._client_send_ping(client.sock)
1285 return True
1287 def save(self) -> bool:
1288 self._logger.debug('save()')
1290 self._address_book.save()
1291 self._mail_queue.save()
1292 self._mail_db.save()
1294 return True
1296 def clean_up(self) -> bool:
1297 self._logger.debug('clean_up')
1299 # self._address_book.hard_clean_up(self._local_node.id)
1300 self._address_book.soft_clean_up(self._local_node.id)
1302 self._mail_queue.clean_up()
1304 return True
1306 def debug_clients(self) -> bool:
1307 self._logger.debug('debug_clients() -> %d', len(self._clients))
1309 for client in self._clients:
1310 self._logger.debug('debug %s', client)
1312 return True
1314 def client_actions(self) -> bool:
1315 self._logger.debug('client_actions() -> %d', len(self._clients))
1317 had_actions = False
1319 for client in self._clients:
1320 self._logger.debug('client %s', client)
1322 for action in client.get_actions(soft_reset=True):
1323 self._logger.debug('action %s', action)
1325 if action.id == 'bootstrap':
1326 self._client_send_get_nearest_to(client.sock, self._local_node.id)
1327 client.add_action(Action('nearest_response', data=action.data))
1329 elif action.id == 'request_public_key_for_node':
1330 self._logger.debug('request_public_key_for_node (try: %d)', action.data['try'])
1332 self._client_request_public_key_for_node(client.sock, action.data['target'].id)
1333 action.data['try'] += 1
1335 elif action.id == 'mail':
1336 mail = action.data
1337 self._logger.debug('mail %s', mail)
1339 self._client_send_mail(client.sock, mail)
1341 mail.forwarded_to.append(client.id)
1342 mail.is_delivered = client.id == mail.target
1344 self._mail_queue.changed()
1346 elif action.id == 'test':
1347 had_actions = True
1349 if action.valid_until is not None and dt.datetime.now(dt.UTC) >= action.valid_until:
1350 self._logger.debug('action is invalid: %s', action)
1351 client.remove_action(action)
1353 return had_actions
1355 def _create_action_request_public_key_for_node(self, target: Node, mode: str) -> Action:
1356 self._logger.debug('_create_action_request_public_key_for_node(%s, %s)', target, mode)
1358 action_data = {
1359 'target': target,
1360 'mode': mode, # (o)riginal sender, (r)elay
1361 # 'step': 0, # 0 = request created, 1 = send request to client
1362 'try': 0, # 0 = first try, 1 = second try, etc
1363 }
1364 action = Action('request_public_key_for_node', target.id, data=action_data)
1365 action.valid_until = dt.datetime.now(dt.UTC) + self._client_action_retention_time
1366 action.is_strong = True
1368 return action
1370 def is_bootstrap_phase(self) -> bool:
1371 if self._config['bootstrap'] == 'default':
1372 clients_len = self._address_book.get_clients_len()
1373 bootstrap_clients_len = self._address_book.get_bootstrap_clients_len()
1374 return clients_len <= bootstrap_clients_len
1376 return bool(self._config['bootstrap'])
1378 def handle_mail_queue(self) -> bool:
1379 self._logger.debug('handle_mail_queue()')
1381 for mail_uuid, mail in self._mail_queue.get_mails().items():
1382 self._logger.debug('mail %s', mail)
1384 if mail.is_delivered:
1385 self._logger.debug('mail is delivered')
1386 continue
1388 if mail.target is None:
1389 self._logger.debug('mail has no target')
1390 continue
1392 if mail.target == self._local_node.id:
1393 self._logger.debug('mail is for me')
1394 continue
1396 clients = self._address_book.get_nearest_to(mail.target, with_contact_infos=True)
1397 self._logger.debug('clients %s', clients)
1399 for client in clients:
1400 self._logger.debug('client for mail: %s', client)
1401 if self._client_is_connected(client):
1402 # self._logger.debug('client is connected')
1403 pass
1404 else:
1405 self._logger.debug('client is not connected C')
1406 self._client_connect(client)
1408 if mail.is_encrypted:
1409 self._logger.debug('mail is encrypted')
1411 for client in clients:
1412 self._logger.debug('client %s', client)
1413 self._logger.debug('forwarded_to %s', mail.forwarded_to)
1415 if not self._client_is_connected(client):
1416 self._logger.debug('client is not connected D')
1417 continue
1419 if client.id in mail.forwarded_to:
1420 self._logger.debug('client already received mail')
1421 continue
1423 if client.has_action('mail', mail.uuid):
1424 self._logger.debug('client already has action')
1425 continue
1427 self._logger.debug('add action for mail')
1428 action = Action('mail', mail.uuid, data=mail)
1429 action.valid_until = dt.datetime.now(dt.UTC) + self._client_action_retention_time
1430 client.add_action(action)
1431 else:
1432 self._logger.debug('mail is not encrypted yet')
1434 client = self._address_book.get_client_by_id(mail.target.id)
1435 if client is None or not client.has_public_key():
1436 self._logger.debug('client is set and has no public key')
1437 for client in clients:
1439 if client.has_action('request_public_key_for_node', mail.target.id):
1440 self._logger.debug('client already has action request_public_key_for_node/%s', mail.target.id)
1441 else:
1442 self._logger.debug('create action request_public_key_for_node from client: %s', client)
1444 action = self._create_action_request_public_key_for_node(mail.target, 'o')
1446 action.func = lambda _client: self._encrypt_mail(mail, _client)
1448 client.add_action(action)
1449 else:
1450 self._encrypt_mail(mail, client)
1452 return True
1454 def handle_mail_db(self) -> bool:
1455 # self._logger.debug('handle_mail_db()')
1457 for mail_uuid, mail in self._mail_db.get_mails():
1458 # self._logger.debug('mail %s', mail)
1460 clients = self._address_book.get_nearest_to(mail.origin, with_contact_infos=True)
1461 # self._logger.debug('clients %s', clients)
1463 for client in clients:
1464 # self._logger.debug('client for mail: %s', client)
1465 if self._client_is_connected(client):
1466 pass
1467 # self._logger.debug('client is connected')
1468 else:
1469 # self._logger.debug('client is not connected C')
1470 self._client_connect(client)
1472 if mail.verified == 'n':
1473 self._logger.debug('mail is not verified')
1475 _client = self._address_book.get_client_by_id(mail.origin.id)
1477 request_public_key_for_node_action = False
1478 if _client is None:
1479 self._logger.debug('client not found by id: %s', mail.origin.id)
1480 request_public_key_for_node_action = True
1481 else:
1482 self._logger.debug('client found by id: %s', mail.origin.id)
1483 if _client.has_public_key():
1484 self._logger.debug('client has public key')
1485 self._verify_mail(mail, _client)
1486 else:
1487 self._logger.debug('client has no public key')
1488 request_public_key_for_node_action = True
1490 if request_public_key_for_node_action:
1491 for client in clients:
1492 if client.has_action('request_public_key_for_node', mail.origin.id):
1493 self._logger.debug('client already has action request_public_key_for_node/%s', mail.origin.id)
1494 else:
1495 self._logger.debug('create action request_public_key_for_node from client: %s', client)
1496 action = self._create_action_request_public_key_for_node(mail.origin, 'o')
1497 action.func = lambda client: self._verify_mail(mail, client)
1498 client.add_action(action)
1500 # Use public key to encrypt symmetric key.
1501 # Use symmetric key to encrypt mail body.
1502 def _encrypt_mail(self, mail: Mail, client: Client):
1503 self._logger.debug('_encrypt_mail() -> {}'.format(mail.is_encrypted))
1504 self._logger.debug('mail %s', mail)
1505 self._logger.debug('client %s', client)
1507 if mail.is_encrypted:
1508 self._logger.debug('mail is already encrypted')
1509 return
1511 # Raw Body
1512 raw_body = b64decode(mail.body)
1513 raw_body_len = len(raw_body).to_bytes(2, 'little')
1514 self._logger.debug('raw body "%s"', raw_body)
1515 self._logger.debug('raw body len %s', raw_body_len)
1517 # Symmetric Key
1518 sym_key = Fernet.generate_key()
1519 self._logger.debug('sym_key: %s', sym_key)
1520 self._logger.debug('sym_key hex: %s', sym_key.hex())
1522 # Signature Data
1523 hasher = hashes.Hash(hashes.SHA256())
1524 hasher.update(sym_key)
1525 hasher.update(raw_body)
1526 sign_hash = hasher.finalize()
1528 sign_hash_b64 = b64encode(sign_hash).decode()
1529 self._logger.debug('sign_hash: %s', sign_hash_b64)
1531 # Signature
1532 signature = self._private_key.sign(
1533 sign_hash,
1534 padding.PSS(
1535 mgf=padding.MGF1(hashes.SHA256()),
1536 salt_length=padding.PSS.MAX_LENGTH
1537 ),
1538 hashes.SHA256()
1539 )
1540 sig_len = len(signature).to_bytes(2, 'little')
1541 self._logger.debug('sign len: %d %s', len(signature), sig_len)
1542 self._logger.debug('signature: %s', signature)
1544 # Symmetric Data
1545 sym_items = {
1546 0x00: signature,
1547 0x01: raw_body,
1548 }
1549 sym_data = binary_encode(sym_items, 2)
1551 # Symmetric Key encrypted
1552 f = Fernet(sym_key)
1553 token = f.encrypt(sym_data)
1554 token_len = len(token).to_bytes(4, 'little')
1555 self._logger.debug('token_len "%s"', token_len)
1556 self._logger.debug('token "%s"', token)
1558 # Encrypted Symmetric Key using Public Key
1559 enc_sym_key = client.encrypt(sym_key)
1560 enc_sym_key_len = len(enc_sym_key).to_bytes(2, 'little')
1561 self._logger.debug('enc_sym_key_len "%s"', enc_sym_key_len)
1562 self._logger.debug('enc_sym_key "%s"', enc_sym_key)
1564 # Public Data
1565 pub_items = {
1566 0x00: enc_sym_key,
1567 0x01: token,
1568 }
1569 # 4 bytes for length = 4 * 8 bits = 32 bits = 2^32 = 4.294.967.296 bytes
1570 pub_data = binary_encode(pub_items, 4)
1572 encoded = b64encode(pub_data).decode()
1573 self._logger.debug('pub data b64 "%s"', encoded)
1575 mail.body = encoded
1576 mail.is_encrypted = True
1578 self._mail_queue.changed()
1580 client.refresh_used_at()
1581 self._address_book.changed()
1583 def _decrypt_mail(self, mail: Mail):
1584 self._logger.debug('_decrypt_mail()')
1586 if not mail.is_encrypted:
1587 self._logger.debug('mail already decrypted')
1588 return
1590 self._logger.debug('body %s', mail.body)
1592 # base64 decode body
1593 pub_data = b64decode(mail.body)
1594 self._logger.debug('pub_data: %s', pub_data)
1596 pub_items = binary_decode(pub_data, 4)
1597 self._logger.debug('pub_items: %s', pub_items)
1598 enc_sym_key = pub_items[0x00]
1599 token = pub_items[0x01]
1601 # Private key decryption
1602 sym_key = self._private_key.decrypt(
1603 enc_sym_key,
1604 padding.OAEP(
1605 mgf=padding.MGF1(algorithm=hashes.SHA256()),
1606 algorithm=hashes.SHA256(),
1607 label=None
1608 )
1609 )
1610 self._logger.debug('sym_key: %s', sym_key)
1612 # Decrypt token
1613 f = Fernet(sym_key)
1614 sym_data = f.decrypt(token)
1615 # self._logger.debug('sym_data:', sym_data)
1617 sym_items = binary_decode(sym_data, 2)
1618 # self._logger.debug('sym_items:', sym_items)
1620 signature = sym_items[0x00]
1621 raw_body = sym_items[0x01]
1622 self._logger.debug('signature: %s', signature)
1623 self._logger.debug('raw_body: %s', raw_body)
1625 # Signature Data
1626 hasher = hashes.Hash(hashes.SHA256())
1627 hasher.update(sym_key)
1628 hasher.update(raw_body)
1629 sign_token = hasher.finalize()
1630 self._logger.debug('sign_token: %s', sign_token.hex())
1632 mail.is_encrypted = False
1633 mail.is_new = True
1634 mail.verified = 'n'
1635 mail.sign_hash = b64encode(sign_token).decode()
1636 mail.sign = b64encode(signature).decode()
1637 mail.decode(raw_body)
1639 self._mail_db.changed()
1641 def _verify_mail(self, mail: Mail, client: Client):
1642 self._logger.debug('_verify_mail()')
1643 self._logger.debug('mail: %s', mail)
1644 self._logger.debug('client: %s', client)
1646 self._logger.debug('sign_hash A: %s', mail.sign_hash)
1647 sign_hash = b64decode(mail.sign_hash)
1648 self._logger.debug('sign_hash B: %s', sign_hash)
1650 self._logger.debug('sign A: %s', mail.sign)
1651 sign = b64decode(mail.sign)
1652 self._logger.debug('sign B: %s', sign)
1654 try:
1655 client.public_key.verify(
1656 sign,
1657 sign_hash,
1658 padding.PSS(
1659 mgf=padding.MGF1(hashes.SHA256()),
1660 salt_length=padding.PSS.MAX_LENGTH
1661 ),
1662 hashes.SHA256()
1663 )
1664 except InvalidSignature:
1665 self._logger.error('InvalidSignature')
1666 mail.verified = 'e'
1667 else:
1668 self._logger.debug('mail signature OK')
1669 mail.verified = 'y'
1670 mail.sign_hash = None
1671 mail.sign = None
1673 self._mail_db.changed()
1675 def get_addressbook(self) -> AddressBook:
1676 return self._address_book
1678 def get_mail_db(self) -> MailDatabase:
1679 return self._mail_db
1681 def get_mail_queue(self) -> MailQueue:
1682 return self._mail_queue