diff --git a/README.md b/README.md index a99cccc..fff1643 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # filebridging -Share files via a bridge server using TCP over SSL and aes-256-cbc encryption. +Share files via a bridge server using TCP over SSL and end-to-end encryption. ## Requirements Python3.8+ is needed for this package. @@ -21,15 +21,33 @@ python -m filebridging.client --help ``` ## Examples -Client-server example -```bash -# 3 distinct tabs -python -m filebridging.server --host localhost --port 5000 --certificate ~/.ssh/server.crt --key ~/.ssh/server.key -python -m filebridging.client s --host localhost --port 5000 --certificate ~/.ssh/server.crt --token 12345678 --password supersecretpasswordhere --path ~/file_to_send -python -m filebridging.client r --host localhost --port 5000 --certificate ~/.ssh/server.crt --token 12345678 --password supersecretpasswordhere --path ~/Downloads -``` +* Client-server example + ```bash + # 3 distinct tabs + python -m filebridging.server --host localhost --port 5000 --certificate ~/.ssh/server.crt --key ~/.ssh/server.key + python -m filebridging.client s --host localhost --port 5000 --certificate ~/.ssh/server.crt --token 12345678 --password supersecretpasswordhere --path ~/file_to_send + python -m filebridging.client r --host localhost --port 5000 --certificate ~/.ssh/server.crt --token 12345678 --password supersecretpasswordhere --path ~/Downloads + ``` -Client-client example -```bash +* Client-client example + ```bash + # 2 distinct tabs + python -m filebridging.client s --host localhost --port 5000 --certificate ~/.ssh/server.crt --key ~/.ssh/private.key --token 12345678 --password supersecretpasswordhere --path ~/file_to_send --standalone + python -m filebridging.client r --host localhost --port 5000 --certificate ~/.ssh/server.crt --token 12345678 --password supersecretpasswordhere --path ~/Downloads + ``` + The receiver client may be standalone as well: just add the `--key` parameter (for SSL-secured sessions) and the `--standalone` flag. -``` +* Configuration file example + ```python + #!/bin/python + + host = "www.example.com" + port = 5000 + certificate = "/path/to/public.crt" + key = "/path/to/private.key" + + action = 'r' + password = 'verysecretpassword' + token = 'sessiontok' + file_path = '.' + ``` diff --git a/filebridging/client.py b/filebridging/client.py index 385b8c3..1cff6ad 100644 --- a/filebridging/client.py +++ b/filebridging/client.py @@ -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 " diff --git a/filebridging/server.py b/filebridging/server.py index e01e2bb..7110632 100644 --- a/filebridging/server.py +++ b/filebridging/server.py @@ -1,6 +1,12 @@ """Server class. May be a local server or a publicly reachable server. + +Arguments + - host: localhost, IPv4 address or domain (e.g. www.example.com) + - port: port to reach (must be enabled) + - certificate [optional]: server certificate for SSL + - key [optional]: needed only for standalone clients """ import argparse diff --git a/setup.py b/setup.py index ac743d2..e460863 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setuptools.setup( author=find_information("author", "filebridging", "__init__.py"), author_email=find_information("email", "filebridging", "__init__.py"), description=( - "Share files via a bridge server using TCP over SSL and aes-256-cbc " + "Share files via a bridge server using TCP over SSL and end-to-end " "encryption." ), license=find_information("license", "filebridging", "__init__.py"),