bisocket 0.0.2a2__tar.gz → 0.0.2a4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,4 +3,4 @@ include LICENSE
3
3
  include requirements.txt
4
4
  include pyproject.toml
5
5
 
6
- graft buelon/cython
6
+ graft bisocket/cython
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bisocket
3
- Version: 0.0.2a2
3
+ Version: 0.0.2a4
4
4
  Summary: bisocket is a high-level Python library for simple, secure, and truly bidirectional socket communication, using a dual-socket architecture to enable non-blocking, full-duplex I/O. It provides automatic AES-GCM encryption and supports both synchronous (threading) and asynchronous (asyncio) client-server applications
5
5
  Home-page:
6
6
  Author: Daniel Olson
@@ -734,8 +734,8 @@ def server_handler_example(request: ServerRequest) -> None:
734
734
  if request.method == 'echo':
735
735
  request.send_data(request.data)
736
736
 
737
- # # async not supported at this time
738
- # async def aserver_handler_example(request: ServerRequest) -> None:
739
- # if request.method == 'echo':
740
- # request.send_data(request.data)
737
+ # async not supported at this time
738
+ async def aserver_handler_example(request: ServerRequest) -> None:
739
+ if request.method == 'echo':
740
+ request.send_data(request.data)
741
741
 
@@ -0,0 +1,745 @@
1
+ import os
2
+ import bz2
3
+ import uuid
4
+ import time
5
+ import json
6
+ import socket
7
+ import inspect
8
+ import asyncio
9
+ import traceback
10
+ import threading
11
+ import queue
12
+ from typing import Callable, Awaitable
13
+ from dataclasses import dataclass
14
+
15
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
16
+ from cryptography.hazmat.primitives import hashes
17
+ from cryptography.hazmat.backends import default_backend
18
+
19
+
20
+ VERSION = '0.0.0'
21
+ END_TOKEN = b'|[-_-]|'
22
+ SPLIT_TOKEN = b'|(---)|'
23
+ SPLIT_TOKEN2 = b'|{***}|'
24
+ LENGTH_OF_END_TOKEN = len(END_TOKEN)
25
+
26
+
27
+ def receive(conn: socket.socket) -> bytes:
28
+ data = b''
29
+ while not data.endswith(END_TOKEN):
30
+ v = conn.recv(1024)
31
+ if not v:
32
+ # If the connection is closed, we'll break out of the loop
33
+ break
34
+ data += v
35
+
36
+ if not data.endswith(END_TOKEN):
37
+ # If we broke out of the loop and don't have the end token,
38
+ # it means the connection was closed prematurely.
39
+ try:
40
+ decoded_data = data.decode()
41
+ except UnicodeDecodeError:
42
+ decoded_data = repr(data)
43
+
44
+ return decoded_data
45
+ raise ValueError(f'Invalid value received: `{decoded_data}`')
46
+
47
+ return data[:-LENGTH_OF_END_TOKEN]
48
+
49
+
50
+ def send(conn: socket.socket, data: bytes) -> None:
51
+ conn.sendall(data+END_TOKEN)
52
+
53
+
54
+ async def async_receive(sock: socket.socket) -> bytes | str:
55
+ loop = asyncio.get_running_loop()
56
+ data = b''
57
+
58
+ while not data.endswith(END_TOKEN):
59
+ chunk = await loop.sock_recv(sock, 1024)
60
+ if not chunk: # connection closed
61
+ try:
62
+ return data.decode()
63
+ except UnicodeDecodeError:
64
+ return repr(data)
65
+ data += chunk
66
+
67
+ return data[:-LENGTH_OF_END_TOKEN]
68
+
69
+
70
+ async def async_send(sock: socket.socket, data: bytes) -> None:
71
+ loop = asyncio.get_running_loop()
72
+ await loop.sock_sendall(sock, data + END_TOKEN)
73
+
74
+
75
+ def compress_bytes(data):
76
+ return bz2.compress(data, compresslevel=9)
77
+
78
+
79
+ def decompress_bytes(data):
80
+ return bz2.decompress(data)
81
+
82
+
83
+ @dataclass
84
+ class CompressedEncryptedData:
85
+ data: bytes
86
+ iv: bytes
87
+
88
+ def to_dict(self) -> dict:
89
+ return {
90
+ 'data': list(self.data), # .hex(),
91
+ 'iv': list(self.iv), # .hex(),
92
+ }
93
+
94
+ def to_json(self) -> str:
95
+ return json.dumps(self.to_dict())
96
+
97
+ def to_bytes(self) -> bytes:
98
+ return compress_bytes(self.to_json().encode())
99
+
100
+ @classmethod
101
+ def from_dict(cls, data: dict) -> 'CompressedEncryptedData':
102
+ return cls(
103
+ data=bytes(data['data']),
104
+ iv=bytes(data['iv']),
105
+ )
106
+
107
+ @classmethod
108
+ def from_json(cls, data: str) -> 'CompressedEncryptedData':
109
+ return cls.from_dict(json.loads(data))
110
+
111
+ def encrypted_data(self) -> 'EncryptedData':
112
+ return EncryptedData(
113
+ data=decompress_bytes(self.data),
114
+ iv=decompress_bytes(self.iv),
115
+ )
116
+
117
+
118
+ @dataclass
119
+ class EncryptedData:
120
+ data: bytes
121
+ iv: bytes
122
+
123
+ def to_dict(self) -> dict:
124
+ return {
125
+ 'data': list(self.data), # .hex(),
126
+ 'iv': list(self.iv), # .hex(),
127
+ }
128
+
129
+ def to_json(self) -> str:
130
+ return json.dumps(self.to_dict())
131
+
132
+ def to_bytes(self) -> bytes:
133
+ return compress_bytes(SPLIT_TOKEN2.join([self.data, self.iv]))
134
+
135
+ @classmethod
136
+ def from_dict(cls, data: dict) -> 'EncryptedData':
137
+ return cls(
138
+ data=bytes(data['data']),
139
+ iv=bytes(data['iv']),
140
+ )
141
+
142
+ @classmethod
143
+ def from_json(cls, data: str) -> 'EncryptedData':
144
+ return cls.from_dict(json.loads(data))
145
+
146
+ @classmethod
147
+ def from_bytes(cls, data: bytes) -> 'EncryptedData':
148
+ data, iv = decompress_bytes(data).split(SPLIT_TOKEN2)
149
+ return cls(
150
+ data=data,
151
+ iv=iv,
152
+ )
153
+
154
+ def compress_data(self) -> 'CompressedEncryptedData':
155
+ return CompressedEncryptedData(
156
+ data=compress_bytes(self.data),
157
+ iv=compress_bytes(self.iv),
158
+ )
159
+
160
+ @classmethod
161
+ def decompress_data(cls, data: 'CompressedEncryptedData') -> 'EncryptedData':
162
+ return EncryptedData(
163
+ data=decompress_bytes(data.data),
164
+ iv=decompress_bytes(data.iv),
165
+ )
166
+
167
+
168
+ class EncryptionService:
169
+ def __init__(self, key_string: str):
170
+ self.key = self.derive_key(key_string)
171
+ self.aesgcm = AESGCM(self.key)
172
+
173
+ @staticmethod
174
+ def derive_key(key_string: str) -> bytes:
175
+ """Derive a 32-byte key from the input string using SHA-256"""
176
+ digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
177
+ digest.update(key_string.encode())
178
+ return digest.finalize()
179
+
180
+ def encrypt_obj(self, obj: dict) -> EncryptedData:
181
+ """Encrypt data using AES-GCM"""
182
+ # Convert data to JSON string and encode
183
+ data_bytes = json.dumps(obj).encode()
184
+
185
+ return self.encrypt_data(data_bytes)
186
+
187
+ def decrypt_obj(self, encrypted: bytes, iv: bytes) -> dict:
188
+ """Decrypt data using AES-GCM"""
189
+ return json.loads(self.decrypt_data(encrypted, iv).decode())
190
+
191
+ def encrypt_data(self, data: bytes) -> EncryptedData:
192
+ """Encrypt data using AES-GCM"""
193
+ # Generate 12-byte IV
194
+ iv = os.urandom(12)
195
+
196
+ # Encrypt data (includes auth tag automatically)
197
+ encrypted = self.aesgcm.encrypt(iv, data, None)
198
+
199
+ return EncryptedData(encrypted, iv)
200
+
201
+ def decrypt_data(self, encrypted_data: EncryptedData) -> bytes:
202
+ """Decrypt data using AES-GCM"""
203
+ try:
204
+ # Decrypt data
205
+ decrypted_bytes = self.aesgcm.decrypt(encrypted_data.iv, encrypted_data.data, None)
206
+
207
+ # Parse JSON data
208
+ return decrypted_bytes # json.loads(decrypted_bytes.decode())
209
+ except Exception as e:
210
+ print(f"Decryption error: {str(e)}")
211
+ raise ValueError("Failed to decrypt data")
212
+
213
+
214
+ @dataclass
215
+ class Message:
216
+ request_id: str
217
+ data: bytes
218
+
219
+
220
+ class Client:
221
+ def __init__(
222
+ self,
223
+ host: str,
224
+ port: int,
225
+ on_receive: callable,
226
+ ) -> None:
227
+ # Initialize encryption service
228
+ self.client_id = str(uuid.uuid4())
229
+
230
+ crypto_key = os.environ.get('CRYPTO_KEY', 'secret-lol')
231
+ self.encryption_service = EncryptionService(crypto_key)
232
+
233
+ self.host = host
234
+ self.port = port
235
+
236
+ self.on_receive = on_receive
237
+ self.receive_queue = queue.Queue()
238
+ self.areceive_queue = asyncio.Queue()
239
+
240
+ self.send_conn: socket.socket | None = None
241
+ self.receive_conn: socket.socket | None = None
242
+
243
+ self.receiving = False
244
+ self.receiving_thread: threading.Thread | None = None
245
+ self._receiving_thread: threading.Thread | None = None
246
+
247
+ self.receiving_task: threading.Thread | None = None
248
+ self._receiving_task: asyncio.Task | None = None
249
+
250
+ def __enter__(self):
251
+ self.open()
252
+ return self
253
+
254
+ def __exit__(self, exc_type, exc_val, exc_tb):
255
+ self.close()
256
+
257
+ def encrypt(self, data: bytes) -> bytes:
258
+ return self.encryption_service.encrypt_data(data).to_bytes()
259
+
260
+ def decrypt(self, data: bytes) -> bytes:
261
+ encrypted_data: EncryptedData = EncryptedData.from_bytes(data)
262
+ return self.encryption_service.decrypt_data(encrypted_data)
263
+
264
+ def send(self, method: str, data: bytes) -> str:
265
+ request_id = str(uuid.uuid4())
266
+ send(self.send_conn, self.encrypt(SPLIT_TOKEN.join([method.encode(), request_id.encode(), data])))
267
+ receive(self.send_conn)
268
+ return request_id
269
+
270
+ def __receive_thread(self) -> bytes:
271
+ while self.receiving:
272
+ try:
273
+ while (data := self.receive_queue.get()) is not None:
274
+ if data:
275
+ request_id, data = self.decrypt(data).split(SPLIT_TOKEN)
276
+
277
+ if data == b'__close__':
278
+ # print('close!', request_id, data)
279
+ break
280
+
281
+ msg = Message(request_id.decode(), data)
282
+ self.on_receive(msg)
283
+ except Exception as e:
284
+ print(f'Error receiving data: {e}')
285
+ traceback.print_exc()
286
+ break
287
+
288
+ def _receive_thread(self) -> bytes:
289
+ while self.receiving:
290
+ try:
291
+ data = receive(self.receive_conn)
292
+ if data:
293
+ self.receive_queue.put(data)
294
+ # request_id, data = self.decrypt(data).split(SPLIT_TOKEN)
295
+
296
+ # if data == b'__close__':
297
+ # print('close!', request_id, data)
298
+ # break
299
+
300
+ # msg = Message(request_id.decode(), data)
301
+ # self.on_receive(msg)
302
+ except Exception as e:
303
+ print(f'Error receiving data: {e}')
304
+ traceback.print_exc()
305
+ break
306
+ self.receive_queue.put(None)
307
+
308
+ def open(self) -> None:
309
+ # Send the client ID to the server
310
+ # self.send(self.client_id.encode())
311
+
312
+ self.receive_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
313
+ self.receive_conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
314
+ self.receive_conn.connect((self.host, self.port))
315
+
316
+ send(self.receive_conn, self.encrypt(SPLIT_TOKEN.join([b'receive', self.client_id.encode()])))
317
+ assert b'ok' == self.decrypt(receive(self.receive_conn))
318
+
319
+ # Start the receive thread
320
+ self.receiving = True
321
+ self.receiving_thread = threading.Thread(target=self._receive_thread, daemon=True)
322
+ self.receiving_thread.start()
323
+ self._receiving_thread = threading.Thread(target=self.__receive_thread, daemon=True)
324
+ self._receiving_thread.start()
325
+
326
+ self.send_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
327
+ self.send_conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
328
+ self.send_conn.connect((self.host, self.port))
329
+
330
+ send(self.send_conn, self.encrypt(SPLIT_TOKEN.join([b'send', self.client_id.encode()])))
331
+ assert b'ok' == self.decrypt(receive(self.send_conn))
332
+
333
+ def close(self) -> None:
334
+ self.send('close', b'closing')
335
+ assert self.decrypt(self.server_receive(self.send_conn)) == b'__close__'
336
+ self.send_conn.close()
337
+
338
+ self.receiving = False
339
+ if self.receiving_thread:
340
+ self.receiving_thread.join()
341
+ self.receiving_thread = None
342
+
343
+ if self._receive_thread:
344
+ self._receiving_thread.join()
345
+ self._receiving_thread = None
346
+
347
+ self.receive_conn.close()
348
+
349
+ def server_receive(self, s):
350
+ while not (data := receive(s)):
351
+ pass
352
+ return data
353
+
354
+ async def __aenter__(self):
355
+ await self.aopen()
356
+ return self
357
+
358
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
359
+ await self.aclose()
360
+
361
+ async def aencrypt(self, data: bytes) -> bytes:
362
+ # return self.encryption_service.encrypt_data(data).to_bytes()
363
+ return await asyncio.to_thread(self.encrypt, data)
364
+
365
+ async def adecrypt(self, data: bytes) -> bytes:
366
+ # data = EncryptedData.from_bytes(data)
367
+ # return self.encryption_service.decrypt_data(data)
368
+ return await asyncio.to_thread(self.decrypt, data)
369
+
370
+ async def asend(self, method: str, data: bytes) -> str:
371
+ request_id = str(uuid.uuid4())
372
+ await async_send(self.send_conn, await self.aencrypt(SPLIT_TOKEN.join([method.encode(), request_id.encode(), data])))
373
+ await async_receive(self.send_conn)
374
+ return request_id
375
+
376
+ async def __areceive_thread(self) -> bytes:
377
+ while self.receiving:
378
+ try:
379
+ while (data := await self.areceive_queue.get()) is not None:
380
+ if data:
381
+ request_id, data = (await self.adecrypt(data)).split(SPLIT_TOKEN)
382
+
383
+ if data == b'__close__':
384
+ # print('close!', request_id, data)
385
+ break
386
+
387
+ msg = Message(request_id.decode(), data)
388
+
389
+ if inspect.iscoroutinefunction(self.on_receive):
390
+ await self.on_receive(msg)
391
+ else:
392
+ # self.on_receive(msg)
393
+ await asyncio.to_thread(self.on_receive, msg)
394
+ except Exception as e:
395
+ print(f'Error receiving data: {e}')
396
+ traceback.print_exc()
397
+ break
398
+
399
+ def _areceive_thread(self) -> bytes:
400
+ while self.receiving:
401
+ try:
402
+ data = receive(self.receive_conn)
403
+ if data:
404
+ self.areceive_queue.put_nowait(data)
405
+ # request_id, data = self.decrypt(data).split(SPLIT_TOKEN)
406
+
407
+ # if data == b'__close__':
408
+ # print('close!', request_id, data)
409
+ # break
410
+
411
+ # msg = Message(request_id.decode(), data)
412
+ # self.on_receive(msg)
413
+ except Exception as e:
414
+ print(f'Error receiving data: {e}')
415
+ traceback.print_exc()
416
+ break
417
+ self.areceive_queue.put_nowait(None)
418
+
419
+ async def aopen(self) -> None:
420
+ loop = asyncio.get_running_loop()
421
+
422
+ def receive_socket_setup():
423
+ self.receive_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
424
+ # self.receive_conn.setblocking(False)
425
+ self.receive_conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
426
+ # await loop.sock_connect(self.receive_conn, (self.host, self.port))
427
+ self.receive_conn.connect((self.host, self.port))
428
+
429
+ # await async_send(self.receive_conn, await self.aencrypt(SPLIT_TOKEN.join([b'receive', self.client_id.encode()])))
430
+ # assert b'ok' == await self.adecrypt(await async_receive(self.receive_conn))
431
+ send(self.receive_conn, self.encrypt(SPLIT_TOKEN.join([b'receive', self.client_id.encode()])))
432
+ assert b'ok' == self.decrypt(receive(self.receive_conn))
433
+
434
+ await asyncio.to_thread(receive_socket_setup)
435
+
436
+ # Start the receive thread
437
+ self.receiving = True
438
+ self.receiving_task = threading.Thread(target=self._areceive_thread, daemon=True) # asyncio.create_task(self._areceive_thread())
439
+ self.receiving_task.start()
440
+ self._receiving_task = asyncio.create_task(self.__areceive_thread())
441
+ # self._receiving_task.start()
442
+
443
+ self.send_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
444
+ self.send_conn.setblocking(False)
445
+ self.send_conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
446
+ await loop.sock_connect(self.send_conn, (self.host, self.port))
447
+ # self.send_conn.connect((self.host, self.port))
448
+
449
+ await async_send(self.send_conn, await self.aencrypt(SPLIT_TOKEN.join([b'send', self.client_id.encode()])))
450
+ assert b'ok' == await self.adecrypt(await async_receive(self.send_conn))
451
+
452
+ async def aclose(self) -> None:
453
+ await self.asend('close', b'closing')
454
+ assert (await self.adecrypt(await self.aserver_receive(self.send_conn))) == b'__close__'
455
+ self.send_conn.close()
456
+
457
+ self.receiving = False
458
+ if self.receiving_task:
459
+ self.receiving_task.join()
460
+ self.receiving_task = None
461
+
462
+ if self._receiving_task:
463
+ await self._receiving_task
464
+ self._receiving_task = None
465
+
466
+ self.receive_conn.close()
467
+
468
+ async def aserver_receive(self, s):
469
+ while not (data := await async_receive(s)):
470
+ pass
471
+ return data
472
+
473
+
474
+ @dataclass
475
+ class ServerRequest:
476
+ client_id: str
477
+ request_id: str
478
+ method: str
479
+ data: bytes
480
+ send_data: Callable[[bytes], None]
481
+
482
+
483
+ class Server:
484
+ def __init__(self, host: str, port: int, handler: Callable[[ServerRequest], None | Awaitable[None]]):
485
+ self.host = host
486
+ self.port = port
487
+ self.handler = handler
488
+
489
+ self.client_queue: list[str, queue.Queue | asyncio.Queue] = {}
490
+ self.client_send_socket: list[str, queue.Queue] = {}
491
+
492
+ crypto_key = os.environ.get('CRYPTO_KEY', 'secret-lol')
493
+ self.encryption_service = EncryptionService(crypto_key)
494
+
495
+ def encrypt(self, data: bytes) -> bytes:
496
+ return self.encryption_service.encrypt_data(data).to_bytes()
497
+
498
+ def decrypt(self, data: bytes) -> bytes:
499
+ encrypted_data: EncryptedData = EncryptedData.from_bytes(data)
500
+ return self.encryption_service.decrypt_data(encrypted_data)
501
+
502
+ def start(self):
503
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
504
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
505
+ try:
506
+ attempts = 5
507
+ for attempt in range(1, attempts + 1):
508
+ try:
509
+ server.bind((self.host, self.port))
510
+ break
511
+ except OSError:
512
+ print(f"Port {self.port} is already in use. Retrying...")
513
+ time.sleep(5 * attempt)
514
+ if attempt == attempts:
515
+ raise
516
+ server.listen()
517
+ print(f"Server listening on {self.host}:{self.port}")
518
+
519
+ while True:
520
+ client_socket, addr = server.accept()
521
+ print(f"Connection from {addr}")
522
+ client_thread = threading.Thread(target=self.handle_client, args=(client_socket,), daemon=True)
523
+ client_thread.start()
524
+ finally:
525
+ server.shutdown(socket.SHUT_RDWR)
526
+ server.close()
527
+ exit()
528
+
529
+ def server_receive(self, s):
530
+ while not (data := receive(s)):
531
+ pass
532
+ return data
533
+
534
+ def handle_requests(self, client_id: str, s, q: queue.Queue, request_q: queue.Queue, loop=None):
535
+ method, request_id, data = self.decrypt(self.server_receive(s)).split(SPLIT_TOKEN)
536
+ send(s, b'ok')
537
+
538
+ while method != b'close':
539
+ def get_send_data_func(req_id: bytes):
540
+ def send_data(data: bytes) -> None:
541
+ q.put_nowait(SPLIT_TOKEN.join([req_id, data]))
542
+ return send_data
543
+
544
+ request_q.put_nowait(
545
+ ServerRequest(
546
+ client_id,
547
+ request_id.decode(),
548
+ method.decode(),
549
+ data,
550
+ get_send_data_func(request_id)
551
+ )
552
+ )
553
+ # self.handler(ServerRequest(method.decode(), data, send_data))
554
+ method, request_id, data = self.decrypt(self.server_receive(s)).split(SPLIT_TOKEN)
555
+ send(s, b'ok')
556
+
557
+ if loop:
558
+ loop.call_soon_threadsafe(request_q.put_nowait, None)
559
+ else:
560
+ request_q.put_nowait(None)
561
+
562
+
563
+ def handle_client(self, s):
564
+ with s:
565
+ client_type, client_id = self.decrypt(receive(s)).split(SPLIT_TOKEN)
566
+ client_id = client_id.decode()
567
+
568
+ if client_type == b'send':
569
+ request_q: queue.Queue = queue.Queue()
570
+
571
+ q: queue.Queue = queue.Queue()
572
+ self.client_queue[client_id] = q
573
+ self.client_send_socket[client_id] = s
574
+
575
+ send(s, self.encrypt(b'ok'))
576
+
577
+ # method, request_id, data = self.decrypt(self.server_receive(s)).split(SPLIT_TOKEN)
578
+ # while method != b'close':
579
+ # def send_data(data: bytes) -> None:
580
+ # q.put(SPLIT_TOKEN.join([request_id, data]))
581
+ # self.handler(ServerRequest(method.decode(), data, send_data))
582
+ # method, request_id, data = self.decrypt(self.server_receive(s)).split(SPLIT_TOKEN)
583
+
584
+ t = threading.Thread(target=self.handle_requests, args=(client_id, s, q, request_q), daemon=True)
585
+ t.start()
586
+
587
+ while (request := request_q.get()) is not None:
588
+ request: ServerRequest
589
+ # print('handling', request)
590
+ self.handler(request)
591
+
592
+ q.put(b'close')
593
+ while client_id in self.client_send_socket:
594
+ time.sleep(1.0)
595
+ elif client_type == b'receive':
596
+ send(s, self.encrypt(b'ok'))
597
+
598
+ t = time.time()
599
+ while client_id not in self.client_queue:
600
+ time.sleep(0.1)
601
+ if time.time() - t > 60:
602
+ raise ValueError('Timeout waiting for client queue')
603
+
604
+ q: queue.Queue = self.client_queue[client_id]
605
+
606
+ while (data := q.get()) != b'close':
607
+ if data:
608
+ send(s, self.encrypt(data))
609
+
610
+ send(s, self.encrypt(SPLIT_TOKEN.join([b'empty-id', b'__close__'])))
611
+
612
+ del self.client_queue[client_id]
613
+ send(self.client_send_socket[client_id], self.encrypt(b'__close__'))
614
+ del self.client_send_socket[client_id]
615
+ time.sleep(1.0)
616
+ # method, data = receive(s).split(SPLIT_TOKEN)
617
+
618
+ async def aencrypt(self, data: bytes) -> bytes:
619
+ return await asyncio.to_thread(self.encrypt, data)
620
+
621
+ async def adecrypt(self, data: bytes) -> bytes:
622
+ return await asyncio.to_thread(self.decrypt, data)
623
+
624
+ async def astart(self):
625
+ loop = asyncio.get_running_loop()
626
+
627
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
628
+ server.setblocking(False)
629
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
630
+ # await loop.sock_accept(self.send_conn, (self.host, self.port))
631
+
632
+ try:
633
+ attempts = 5
634
+ for attempt in range(1, attempts + 1):
635
+ try:
636
+ server.bind((self.host, self.port))
637
+ break
638
+ except OSError:
639
+ print(f"Port {self.port} is already in use. Retrying...")
640
+ await asyncio.sleep(5 * attempt)
641
+ if attempt == attempts:
642
+ raise
643
+ server.listen()
644
+ print(f"Server listening on {self.host}:{self.port}")
645
+
646
+ while True:
647
+ client_socket, addr = await loop.sock_accept(server) # server.accept()
648
+ print(f"Connection from {addr}")
649
+ # client_thread = threading.Thread(target=self.ahandle_client, args=(client_socket,), daemon=True)
650
+ # client_thread.start()
651
+ client_socket.setblocking(False)
652
+ asyncio.create_task(self.ahandle_client(client_socket))
653
+ finally:
654
+ server.shutdown(socket.SHUT_RDWR)
655
+ server.close()
656
+ exit()
657
+
658
+ async def aserver_receive(self, s):
659
+ while not (data := await async_receive(s)):
660
+ pass
661
+ return data
662
+
663
+ async def ahandle_requests(self, client_id: str, s, q: asyncio.Queue, request_q: asyncio.Queue):
664
+ method, request_id, data = (await self.adecrypt(await self.aserver_receive(s))).split(SPLIT_TOKEN)
665
+
666
+ while method != b'close':
667
+ # async def send_data(data: bytes) -> None:
668
+ # await q.put(SPLIT_TOKEN.join([request_id, data]))
669
+ def get_send_data_func(req_id: bytes):
670
+ def send_data(data: bytes) -> None:
671
+ q.put_nowait(SPLIT_TOKEN.join([req_id, data]))
672
+ return send_data
673
+
674
+ await request_q.put(ServerRequest(client_id, request_id.decode(), method.decode(), data, get_send_data_func(request_id)))
675
+ # self.handler(ServerRequest(method.decode(), data, send_data))
676
+ method, request_id, data = (await self.adecrypt(await self.aserver_receive(s))).split(SPLIT_TOKEN)
677
+
678
+ await request_q.put(None)
679
+
680
+
681
+ async def ahandle_client(self, s: socket.socket):
682
+ with s:
683
+ client_type, client_id = (await self.adecrypt(await async_receive(s))).split(SPLIT_TOKEN)
684
+ client_id = client_id.decode()
685
+
686
+ if client_type == b'send':
687
+ request_q: asyncio.Queue = asyncio.Queue()
688
+
689
+ q: asyncio.Queue = asyncio.Queue()
690
+ self.client_queue[client_id] = q
691
+ self.client_send_socket[client_id] = s
692
+
693
+ await async_send(s, self.encrypt(b'ok'))
694
+
695
+ s.setblocking(True)
696
+ t = threading.Thread(target=self.handle_requests, args=(client_id, s, q, request_q, asyncio.get_running_loop()), daemon=True)
697
+ t.start()
698
+ # asyncio.create_task(self.ahandle_requests(s, q, request_q))
699
+
700
+ while (request := await request_q.get()) is not None:
701
+ request: ServerRequest
702
+ print('handling', request)
703
+ # self.handler(request)
704
+ if inspect.iscoroutinefunction(self.handler):
705
+ await self.handler(request)
706
+ else:
707
+ await asyncio.to_thread(self.handler, request)
708
+
709
+ await q.put(b'close')
710
+ while client_id in self.client_send_socket:
711
+ await asyncio.sleep(1.0)
712
+ elif client_type == b'receive':
713
+ await async_send(s, await self.aencrypt(b'ok'))
714
+
715
+ t = time.time()
716
+ while client_id not in self.client_queue:
717
+ await asyncio.sleep(0.1)
718
+ if time.time() - t > 60:
719
+ raise ValueError('Timeout waiting for client queue')
720
+
721
+ q: asyncio.Queue = self.client_queue[client_id]
722
+
723
+ while (data := await q.get()) != b'close':
724
+ if data:
725
+ await async_send(s, await self.aencrypt(data))
726
+
727
+ await async_send(s, await self.aencrypt(SPLIT_TOKEN.join([b'empty-id', b'__close__'])))
728
+
729
+ del self.client_queue[client_id]
730
+ self.client_send_socket[client_id].setblocking(False)
731
+ await async_send(self.client_send_socket[client_id], await self.aencrypt(b'__close__'))
732
+ del self.client_send_socket[client_id]
733
+ await asyncio.sleep(1.0)
734
+ # method, data = receive(s).split(SPLIT_TOKEN)
735
+
736
+
737
+ def server_handler_example(request: ServerRequest) -> None:
738
+ if request.method == 'echo':
739
+ request.send_data(request.data)
740
+
741
+ # # async not supported at this time
742
+ # async def aserver_handler_example(request: ServerRequest) -> None:
743
+ # if request.method == 'echo':
744
+ # request.send_data(request.data)
745
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bisocket
3
- Version: 0.0.2a2
3
+ Version: 0.0.2a4
4
4
  Summary: bisocket is a high-level Python library for simple, secure, and truly bidirectional socket communication, using a dual-socket architecture to enable non-blocking, full-duplex I/O. It provides automatic AES-GCM encryption and supports both synchronous (threading) and asynchronous (asyncio) client-server applications
5
5
  Home-page:
6
6
  Author: Daniel Olson
@@ -11,4 +11,5 @@ bisocket.egg-info/SOURCES.txt
11
11
  bisocket.egg-info/dependency_links.txt
12
12
  bisocket.egg-info/requires.txt
13
13
  bisocket.egg-info/top_level.txt
14
- bisocket/cython/c_main.c
14
+ bisocket/cython/c_main.c
15
+ bisocket/cython/c_main.pyx
@@ -20,7 +20,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
20
20
 
21
21
  setup(
22
22
  name="bisocket",
23
- version="0.0.2-alpha2",
23
+ version="0.0.2-alpha4",
24
24
  author="Daniel Olson",
25
25
  author_email="daniel@orphos.cloud",
26
26
  description="bisocket is a high-level Python library for simple, secure, and truly bidirectional socket communication, using a dual-socket architecture to enable non-blocking, full-duplex I/O. It provides automatic AES-GCM encryption and supports both synchronous (threading) and asynchronous (asyncio) client-server applications",
@@ -48,7 +48,7 @@ setup(
48
48
  keywords="socket bidirectional",
49
49
  ext_modules=cythonize(extensions),
50
50
  package_data={
51
- 'buelon/cython': [
51
+ 'bisocket/cython': [
52
52
  "*.pyx", # This line ensures the .pyx files are installed with the final package
53
53
  "*.c", # Include the generated C file as well
54
54
  ]
File without changes
File without changes
File without changes
File without changes
File without changes