|
@@ -1,4 +1,17 @@
|
|
|
-"""Receiver and sender client class."""
|
|
|
+"""Receiver and sender client class.
|
|
|
+
|
|
|
+Arguments
|
|
|
+ - host: localhost, IPv4 address or domain (e.g. www.example.com)
|
|
|
+ - port: port to reach (must be enabled)
|
|
|
+ - action: either [S]end or [R]eceive
|
|
|
+ - file_path: file to send / destination folder
|
|
|
+ - token: session token (6-10 alphanumerical characters)
|
|
|
+ - certificate [optional]: server certificate for SSL
|
|
|
+ - key [optional]: needed only for standalone clients
|
|
|
+ - password [optional]: necessary to end-to-end encryption
|
|
|
+ - standalone [optional]: allow client-to-client communication (the host
|
|
|
+ must be reachable by both clients)
|
|
|
+"""
|
|
|
|
|
|
import argparse
|
|
|
import asyncio
|
|
@@ -15,6 +28,11 @@ from . import utilities
|
|
|
|
|
|
|
|
|
class Client:
|
|
|
+ """Sender or receiver client.
|
|
|
+
|
|
|
+ Create a Client object providing host, port and other optional parameters.
|
|
|
+ Then, run it with `Client().run()` method
|
|
|
+ """
|
|
|
def __init__(self, host='localhost', port=5000, ssl_context=None,
|
|
|
action=None,
|
|
|
standalone=False,
|
|
@@ -49,10 +67,15 @@ class Client:
|
|
|
|
|
|
@property
|
|
|
def host(self) -> str:
|
|
|
+ """Host to reach.
|
|
|
+
|
|
|
+ For standalone clients, you must be able to listen this host.
|
|
|
+ """
|
|
|
return self._host
|
|
|
|
|
|
@property
|
|
|
def port(self) -> int:
|
|
|
+ """Port number."""
|
|
|
return self._port
|
|
|
|
|
|
@property
|
|
@@ -84,14 +107,25 @@ class Client:
|
|
|
|
|
|
@property
|
|
|
def buffer_length_limit(self) -> int:
|
|
|
+ """Max number of buffer chunks in memory.
|
|
|
+
|
|
|
+ You may want to reduce this limit to allocate less memory, or increase
|
|
|
+ it to boost performance.
|
|
|
+ """
|
|
|
return self._buffer_length_limit
|
|
|
|
|
|
@property
|
|
|
def buffer_chunk_size(self) -> int:
|
|
|
+ """Length (bytes) of buffer chunks in memory.
|
|
|
+
|
|
|
+ You may want to reduce this limit to allocate less memory, or increase
|
|
|
+ it to boost performance.
|
|
|
+ """
|
|
|
return self._buffer_chunk_size
|
|
|
|
|
|
@property
|
|
|
def file_path(self) -> str:
|
|
|
+ """Path of file to send or destination folder."""
|
|
|
return self._file_path
|
|
|
|
|
|
@property
|
|
@@ -107,6 +141,11 @@ class Client:
|
|
|
|
|
|
@property
|
|
|
def token(self):
|
|
|
+ """Session token.
|
|
|
+
|
|
|
+ 6-10 alphanumerical characters to provide to server to link sender and
|
|
|
+ receiver.
|
|
|
+ """
|
|
|
return self._token
|
|
|
|
|
|
@property
|
|
@@ -128,6 +167,7 @@ class Client:
|
|
|
|
|
|
@property
|
|
|
def file_size_string(self):
|
|
|
+ """Formatted file size (e.g. 64.22 MB)."""
|
|
|
return self._file_size_string
|
|
|
|
|
|
async def run_client(self) -> None:
|
|
@@ -168,6 +208,13 @@ class Client:
|
|
|
|
|
|
async def _connect(self, reader: asyncio.StreamReader,
|
|
|
writer: asyncio.StreamWriter):
|
|
|
+ """Wrap connect method to catch exceptions.
|
|
|
+
|
|
|
+ This is required since callbacks are never awaited and potential
|
|
|
+ exception would be logged at loop.close().
|
|
|
+ Only standalone clients need this wrapper, regular clients might use
|
|
|
+ connect method directly.
|
|
|
+ """
|
|
|
try:
|
|
|
return await self.connect(reader, writer)
|
|
|
except KeyboardInterrupt:
|
|
@@ -178,6 +225,12 @@ class Client:
|
|
|
async def connect(self,
|
|
|
reader: asyncio.StreamReader,
|
|
|
writer: asyncio.StreamWriter):
|
|
|
+ """Communicate with the server or the other client.
|
|
|
+
|
|
|
+ Send information about the client (connection token, role, file name
|
|
|
+ and size), get information from the server (file name and size), wait
|
|
|
+ for start signal and then send or receive the file.
|
|
|
+ """
|
|
|
self._reader = reader
|
|
|
self._writer = writer
|
|
|
|
|
@@ -250,6 +303,10 @@ class Client:
|
|
|
await self.receive(reader=self.reader)
|
|
|
|
|
|
async def encrypt_file(self, input_file, output_file):
|
|
|
+ """Use openssl to encrypt the input_file.
|
|
|
+
|
|
|
+ The encrypted file will overwrite `output_file` if it exists.
|
|
|
+ """
|
|
|
self._encryption_complete = False
|
|
|
logging.info("Encrypting file...")
|
|
|
stdout, stderr = ''.encode(), ''.encode()
|
|
@@ -273,6 +330,11 @@ class Client:
|
|
|
self._encryption_complete = True
|
|
|
|
|
|
async def send(self, writer: asyncio.StreamWriter):
|
|
|
+ """Encrypt and send the file.
|
|
|
+
|
|
|
+ Caution: if no password is provided, the file will be sent as clear
|
|
|
+ text.
|
|
|
+ """
|
|
|
self._working = True
|
|
|
file_path = self.file_path
|
|
|
if self.password:
|
|
@@ -327,6 +389,10 @@ class Client:
|
|
|
return
|
|
|
|
|
|
async def receive(self, reader: asyncio.StreamReader):
|
|
|
+ """Download the file and decrypt it.
|
|
|
+
|
|
|
+ If no password is provided, the file cannot be decrypted.
|
|
|
+ """
|
|
|
self._working = True
|
|
|
file_path = os.path.join(
|
|
|
os.path.abspath(
|
|
@@ -355,6 +421,11 @@ class Client:
|
|
|
break
|
|
|
file_to_receive.write(input_data)
|
|
|
print() # New line after sys.stdout.write
|
|
|
+ if bytes_received < self.file_size:
|
|
|
+ logging.warning("Transmission terminated too soon!")
|
|
|
+ if self.password:
|
|
|
+ logging.error("Partial files can not be decrypted!")
|
|
|
+ return
|
|
|
logging.info("File received.")
|
|
|
if self.password:
|
|
|
logging.info("Decrypting file...")
|
|
@@ -688,17 +759,16 @@ def main():
|
|
|
else:
|
|
|
logging.info("Proceeding without storing values...")
|
|
|
ssl_context = None
|
|
|
- if certificate is not None:
|
|
|
- if key is None: # Server-dependent client
|
|
|
- ssl_context = ssl.create_default_context(
|
|
|
- purpose=ssl.Purpose.SERVER_AUTH
|
|
|
- )
|
|
|
- ssl_context.load_verify_locations(certificate)
|
|
|
- else: # Standalone client
|
|
|
- ssl_context = ssl.create_default_context(
|
|
|
- purpose=ssl.Purpose.CLIENT_AUTH
|
|
|
- )
|
|
|
- ssl_context.load_cert_chain(certificate, key)
|
|
|
+ if certificate and key and standalone: # Standalone client
|
|
|
+ ssl_context = ssl.create_default_context(
|
|
|
+ purpose=ssl.Purpose.CLIENT_AUTH
|
|
|
+ )
|
|
|
+ ssl_context.load_cert_chain(certificate, key)
|
|
|
+ elif certificate: # Server-dependent client
|
|
|
+ ssl_context = ssl.create_default_context(
|
|
|
+ purpose=ssl.Purpose.SERVER_AUTH
|
|
|
+ )
|
|
|
+ ssl_context.load_verify_locations(certificate)
|
|
|
else:
|
|
|
logging.warning(
|
|
|
"Please consider using SSL. To do so, add in `config.py` or "
|