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.
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/MANIFEST.in +1 -1
- {bisocket-0.0.2a2/bisocket.egg-info → bisocket-0.0.2a4}/PKG-INFO +1 -1
- bisocket-0.0.2a2/bisocket/main.py → bisocket-0.0.2a4/bisocket/cython/c_main.pyx +4 -4
- bisocket-0.0.2a4/bisocket/main.py +745 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4/bisocket.egg-info}/PKG-INFO +1 -1
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/bisocket.egg-info/SOURCES.txt +2 -1
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/setup.py +2 -2
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/LICENSE +0 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/README.md +0 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/bisocket/__init__.py +0 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/bisocket/cython/c_main.c +0 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/bisocket.egg-info/dependency_links.txt +0 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/bisocket.egg-info/requires.txt +0 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/bisocket.egg-info/top_level.txt +0 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/pyproject.toml +0 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/requirements.txt +0 -0
- {bisocket-0.0.2a2 → bisocket-0.0.2a4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bisocket
|
|
3
|
-
Version: 0.0.
|
|
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
|
-
#
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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.
|
|
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
|
|
@@ -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-
|
|
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
|
-
'
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|