Compare commits

..

72 Commits

Author SHA1 Message Date
ca49fb3037 Main file added 2020-11-15 20:46:34 +01:00
60c87ecb19 Merge master branch 2020-04-19 20:43:35 +02:00
25cf7271f4 Test 10 2020-04-19 20:00:29 +02:00
8b07741c59 Test 9 2020-04-19 19:58:37 +02:00
14992dd19b Test 8 2020-04-19 19:56:43 +02:00
f52429fc4d Test 7 2020-04-19 19:54:52 +02:00
bd8eb7da4c Test 6 2020-04-19 19:45:48 +02:00
f56fc6d60c Test 5 2020-04-19 19:42:55 +02:00
f6e65d5495 Test 4 2020-04-19 19:42:18 +02:00
05cc3cb8e6 Test 3 2020-04-19 19:41:06 +02:00
a381805c3c Test 2 2020-04-19 19:37:19 +02:00
1f200b30ab Test 2020-04-19 19:33:03 +02:00
60761ebc5e Typo 2020-04-19 16:28:35 +02:00
b4e6be4bfa create_certificate script provided 2020-04-19 16:28:24 +02:00
557363d3de Working on SSL certificate generation 2020-04-19 00:04:48 +02:00
7311ef3e72 Working on SSL certificate generation 2020-04-18 19:17:25 +02:00
22a20b98fc Handle SSL exceptions 2020-04-17 21:39:59 +02:00
f21a7fdfb9 Test 2020-04-17 21:09:46 +02:00
2ff847b44b Simplify timed_input definition 2020-04-17 20:13:57 +02:00
90df61d3a6 Serious bug silently bypassing SSL context fixed. Previous versions do not really support SSL! 2020-04-17 16:20:55 +02:00
e40989e304 Implemented non-unix timed_input function 2020-04-15 16:27:45 +02:00
4ba5065271 New version 2020-04-13 23:11:43 +02:00
e77d4b4146 Prevent bad behaviour when terminal windows is too small 2020-04-13 23:10:41 +02:00
d703054c58 Documentation 2020-04-13 20:57:39 +02:00
3f5384f9e9 Refactoring 2020-04-13 19:45:05 +02:00
3b7aa265ab Refactoring 2020-04-13 12:58:37 +02:00
932760bcb6 Log server disconnection as error, not info 2020-04-12 19:26:46 +02:00
5f0ed1295f Organize as package 2020-04-12 19:15:06 +02:00
4c56ff7723 Whitespace again 2020-04-12 11:54:30 +02:00
be6f8bbafc Whitespace again 2020-04-12 11:53:50 +02:00
0877a08713 Whitespace 2020-04-12 11:50:38 +02:00
31ab51ec52 Whitespace 2020-04-12 11:49:54 +02:00
e203e35f8e Newline after each variable 2020-04-12 11:43:18 +02:00
bf57baad45 Typos 2020-04-12 11:39:30 +02:00
4fc9ebc38a Offer to store new settings in config file 2020-04-12 11:35:44 +02:00
a95d0aaa91 Do not check file_path if it is None (again) 2020-04-12 10:51:50 +02:00
ae41102d4c False requirement 2020-04-12 01:24:49 +02:00
2aead918bf Do not check file_path if it is None 2020-04-12 00:29:39 +02:00
4a05b05ace Show progress bar in client 2020-04-12 00:25:04 +02:00
8bd0ac76f2 Use original file name 2020-04-11 21:48:37 +02:00
b77de07d6e Use main function, avoid unnecessary trailing underscores 2020-04-11 20:55:26 +02:00
4f01831169 Pass file information to receiver client 2020-04-11 20:35:18 +02:00
e68ab4282c Allow multiple client connections 2020-04-11 19:59:09 +02:00
c0dd046670 Misplacement of writer.drain() 2020-04-10 16:09:39 +02:00
f1d54861ee Whitespace 2020-04-10 16:08:31 +02:00
4352f2908b Quotes prevent variable splitting if file name has spaces 2020-04-10 16:06:45 +02:00
5f1c8fdf47 Send file while encrypting it 2020-04-10 15:15:45 +02:00
0069f9e5ea Allow end-to-end encryption 2020-04-10 13:41:36 +02:00
685b4e6756 Fixed ssl transmission 2020-04-10 10:18:24 +02:00
f7a9f76aad TODO: adjust to ssl 2020-04-09 23:44:01 +02:00
db0da8b24b Implemented SSL 2020-04-09 23:34:04 +02:00
1ec3a4b5e2 Set self.working 2020-04-09 23:06:55 +02:00
ca3aa5857b Get client role as first line 2020-04-09 23:05:03 +02:00
1853d85a79 Typo 2020-04-09 22:33:01 +02:00
40f48e3b95 Cancel pending tasks although they are done 2020-04-09 22:16:46 +02:00
458cc0f5e7 Prevent shadowing of variables 2020-04-09 22:01:50 +02:00
25750736f4 Allow multiple sessions 2020-04-09 19:51:10 +02:00
d2522b3e08 Command Line Interface 2020-04-09 19:46:52 +02:00
d4abc1f2a5 TODO: ask peer its role 2020-04-09 14:51:02 +02:00
9dba9673a3 Use DNS name 2020-04-09 14:45:38 +02:00
018bea5606 Use the same port 2020-04-08 14:53:59 +02:00
7549977fc9 Use aruba as host 2020-03-30 18:01:49 +02:00
248d4ccb88 IT WORKS! 2020-03-28 20:54:41 +01:00
69c06dc4dc almos exhausted 2020-03-28 19:53:06 +01:00
c8409e12ef Works with 2 ports 2020-03-28 19:37:20 +01:00
70f2cd64d7 A lot of work for nothing? 2020-03-28 19:02:03 +01:00
bc287833cc May have found a similar idea, but too tired to give a look
See you next time
2020-03-26 15:02:12 +01:00
cab7527c63 Working on server but getting errors
ConnectionRefusedError: [Errno 111] Connect call failed ('127.0.0.1', 5000)
2020-03-26 14:49:17 +01:00
40dba845fd Working on server 2020-03-26 12:32:01 +01:00
971927a798 Working on server 2020-03-26 10:54:01 +01:00
add5b42a3b asyncore is deprecated 2020-03-25 22:34:39 +01:00
a932184757 test 2020-03-25 22:34:05 +01:00
7 changed files with 250 additions and 25 deletions

View File

@@ -4,6 +4,10 @@ Share files via a bridge server using TCP over SSL and end-to-end encryption.
## Requirements
Python3.8+ is needed for this package.
You may find it [here](https://www.python.org/downloads/).
OpenSSL 1.1.1+ is required as well to handle SSL connection and end-to-end cryptography.
On Windows, installing [git for Windows](https://gitforwindows.org/) will install OpenSSL as well.
## Usage
If you need a virtual environment, create it.
@@ -29,6 +33,7 @@ python -m filebridging.client --help
python -m filebridging.client r --host localhost --port 5000 --certificate ~/.ssh/server.crt --token 12345678 --password supersecretpasswordhere --path ~/Downloads
```
* Client-client example
```bash
# 2 distinct tabs
@@ -37,6 +42,7 @@ python -m filebridging.client --help
```
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
@@ -51,3 +57,33 @@ python -m filebridging.client --help
token = 'sessiontok'
file_path = '.'
```
## Generating SSL certificates
You may use `filebridging.create_certificate.py` script or use openssl from the command line.
### Via script
```bash
python -m filebridging.create_certificate --name example --domain example.com --force
```
### Via command line
Store configuration in file `mycert.csr.cnf` and run the following command to generate a self-signed SSL certificate.
```bash
openssl req -newkey rsa:4096 -nodes -keyout ./mycert.key \
-x509 -days 365 -out ./mycert.crt \
-config mycert.csr.cnf
```
**mycert.csr.cnf**
```text
[ req ]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
[ dn ]
CN = yourdomain.com
```

View File

@@ -13,6 +13,6 @@ __author__ = "Davide Testa"
__email__ = "davide@davte.it"
__credits__ = []
__license__ = "GNU General Public License v3.0"
__version__ = "0.0.1"
__version__ = "0.0.10"
__maintainer__ = "Davide Testa"
__contact__ = "t.me/davte"

6
filebridging/__main__.py Normal file
View File

@@ -0,0 +1,6 @@
mode = input("Do you want to run a filebridging (S)erver or (C)lient?\t\t")
if mode.lower().startswith('s'):
from .server import main
else:
from .client import main
main()

View File

@@ -40,26 +40,27 @@ class Client:
buffer_length_limit=10 ** 4,
file_path=None,
password=None,
token=None):
token=None,
ssl_handshake_timeout=None):
self._host = host
self._port = port
self._ssl_context = ssl_context
self._action = action
self._standalone = standalone
self._stopping = False
self._reader = None
self._writer = None
# Shared queue of bytes
self.buffer = collections.deque()
self._file_path = file_path
self._password = password
self._token = token
self._ssl_handshake_timeout = ssl_handshake_timeout
# How many bytes per chunk
self._buffer_chunk_size = buffer_chunk_size
# How many chunks in buffer
self._buffer_length_limit = buffer_length_limit
self._file_path = file_path
# Shared queue of bytes
self.buffer = collections.deque()
self._working = False
self._token = token
self._password = password
self._ssl_context = None
self._stopping = False
self._reader = None
self._writer = None
self._encryption_complete = False
self._file_name = None
self._file_size = None
@@ -139,6 +140,16 @@ class Client:
def set_ssl_context(self, ssl_context: ssl.SSLContext):
self._ssl_context = ssl_context
@property
def ssl_handshake_timeout(self) -> Union[int, None]:
"""Return SSL handshake timeout.
If SSL context is not set, return None.
Otherwise, return seconds to wait before considering handshake failed.
"""
if self.ssl_context:
return self._ssl_handshake_timeout
@property
def token(self):
"""Session token.
@@ -199,11 +210,16 @@ class Client:
reader, writer = await asyncio.open_connection(
host=self.host,
port=self.port,
ssl=self.ssl_context
ssl=self.ssl_context,
ssl_handshake_timeout=self.ssl_handshake_timeout
)
except (ConnectionRefusedError, ConnectionResetError) as exception:
except (ConnectionRefusedError, ConnectionResetError,
ConnectionAbortedError) as exception:
logging.error(f"Connection error: {exception}")
return
except ssl.SSLError as exception:
logging.error(f"SSL error: {exception}")
return
await self.connect(reader=reader, writer=writer)
async def _connect(self, reader: asyncio.StreamReader,
@@ -272,7 +288,7 @@ class Client:
while 1:
server_hello = await self.reader.readline()
if not server_hello:
logging.error("Server disconnected.")
logging.error("Server refused connection.")
return
server_hello = server_hello.decode('utf-8').strip('\n').split('|')
if self.action == 'receive' and server_hello[0] == 's':
@@ -383,6 +399,7 @@ class Client:
self.print_progress_bar(
progress=new_progress,
bytes_=bytes_sent,
force=(new_progress == 100)
)
print() # New line after progress_bar
writer.close()
@@ -415,7 +432,8 @@ class Client:
)
self.print_progress_bar(
progress=new_progress,
bytes_=bytes_received
bytes_=bytes_received,
force=(new_progress == 100)
)
if not input_data:
break

View File

@@ -0,0 +1,126 @@
"""Create a SSL certificate.
Requirements: OpenSSL.
"""
import argparse
import logging
import os
import subprocess
def get_paths(path):
""""""
return [
os.path.abspath(path) + string
for string in (".crt", ".key", "csr.cnf")
]
def main():
# noinspection SpellCheckingInspection
log_formatter = logging.Formatter(
"%(asctime)s [%(module)-15s %(levelname)-8s] %(message)s",
style='%'
)
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.DEBUG)
root_logger.addHandler(console_handler)
cli_parser = argparse.ArgumentParser(description='Create SSL certificate',
allow_abbrev=False)
cli_parser.add_argument('-n', '--name',
type=str,
default=None,
required=False,
help='Certificate, key and configuration file name')
cli_parser.add_argument('-d', '--domain',
type=str,
default=None,
required=False,
help='Server domain (e.g. example.com)')
cli_parser.add_argument('-f', '--force', '--overwrite',
action='store_true',
help='Overwrite certificate and key if they exist')
arguments = vars(cli_parser.parse_args())
name = arguments['name']
if name is None:
try:
from config import name
except ImportError:
name = None
while not name or not os.access(os.path.dirname(os.path.abspath(name)),
os.W_OK):
try:
name = input(
"Enter a valid file name for certificate, key and "
"configuration file. Directory must be writeable.\n"
"\t\t"
)
except KeyboardInterrupt:
print()
logging.error("Aborting...")
return
certificate_path, key_path, configuration_path = get_paths(
name
)
if not os.access(os.path.dirname(certificate_path), os.W_OK):
logging.error(f"Invalid path `{certificate_path}`!")
return
if any(
os.path.isfile(path)
for path in (certificate_path, key_path, configuration_path)
) and not arguments['force'] and not input(
"Do you want to overwrite existing certificate, key and "
"configuration file?"
"\n[Y]es or [N]o\t\t\t\t"
).lower().startswith('y'):
logging.error("Interrupted. Provide a different --name.")
return
domain = arguments['domain']
if domain is None:
try:
from config import domain
except ImportError:
domain = None
while not domain:
domain = input("Enter server domain (e.g. example.com)\n\t\t")
with open(configuration_path, 'w') as configuration_file:
logging.info("Writing configuration file...")
configuration_file.write(
"[req]\n"
"default_bits = 4096\n"
"prompt = no\n"
"default_md = sha256\n"
"distinguished_name = dn\n"
"\n"
"[dn]\n"
f"CN = {domain}\n"
)
logging.info("Generating certificate and key...")
subprocess.run(
[
f"openssl req -newkey rsa:4096 -nodes "
f"-keyout \"{key_path}\" -x509 -days 365 "
f"-out \"{certificate_path}\" "
f"-config \"{configuration_path}\""
],
capture_output=True,
text=True,
shell=True
)
with open(certificate_path, 'r') as certificate_file:
logging.info(
"Certificate:\n\n{certificate}".format(
certificate=''.join(certificate_file.readlines())
),
)
logging.info("Done!")
if __name__ == '__main__':
main()

View File

@@ -33,7 +33,6 @@ class Server:
self._buffer_length_limit = buffer_length_limit
self._working = False
self._server = None
self._ssl_context = None
@property
def host(self) -> str:
@@ -74,7 +73,7 @@ class Server:
def set_ssl_context(self, ssl_context: ssl.SSLContext):
self._ssl_context = ssl_context
async def run_reader(self, reader, connection_token):
async def run_reader(self, reader: asyncio.StreamReader, connection_token):
while 1:
try:
# Wait one second if buffer is full
@@ -91,7 +90,7 @@ class Server:
except Exception as e:
logging.error(f"Unexpected exception:\n{e}", exc_info=True)
async def run_writer(self, writer, connection_token):
async def run_writer(self, writer: asyncio.StreamWriter, connection_token):
consecutive_interruptions = 0
errors = 0
while connection_token in self.buffers:
@@ -101,6 +100,7 @@ class Server:
# Slow down if buffer is empty; after 1.5 s of silence, break
consecutive_interruptions += 1
if consecutive_interruptions > 3:
logging.error("Too many interruptions...")
break
await asyncio.sleep(.5)
continue
@@ -130,7 +130,11 @@ class Server:
Decide whether client is sender or receiver and start transmission.
"""
client_hello = await reader.readline()
client_hello = client_hello.decode('utf-8').strip('\n').split('|')
try:
client_hello = client_hello.decode('utf-8').strip('\n').split('|')
except UnicodeDecodeError:
logging.error("Invalid client hello.")
return
if len(client_hello) != 4:
await self.refuse_connection(writer=writer,
message="Invalid client_hello!")
@@ -163,6 +167,7 @@ class Server:
else:
return 0 # On success, return 0
# On exception, disconnect and return 1
logging.error("Disconnecting...")
self.disconnect(connection_token=connection_token)
return 1
@@ -234,8 +239,10 @@ class Server:
return
def disconnect(self, connection_token: str) -> None:
del self.buffers[connection_token]
del self.connections[connection_token]
if connection_token in self.buffers:
del self.buffers[connection_token]
if connection_token in self.connections:
del self.connections[connection_token]
def run(self):
loop = asyncio.get_event_loop()

View File

@@ -4,6 +4,8 @@ import logging
import shutil
import signal
import sys
import time
from typing import Union
units_of_measurements = {
@@ -75,9 +77,9 @@ def timed_action(interval: Union[int, float, datetime.timedelta] = None):
timedelta = interval
def timer(function_to_time):
def timed_function(*args, **kwargs):
def timed_function(*args, force: bool = False, **kwargs):
nonlocal last_call
if now() > last_call + timedelta:
if force or now() > last_call + timedelta:
last_call = now()
return function_to_time(*args, **kwargs)
return
@@ -86,11 +88,17 @@ def timed_action(interval: Union[int, float, datetime.timedelta] = None):
return timer
def timed_input(message: str = None,
timeout: int = 5):
def unix_timed_input(message: str = None,
timeout: int = 5):
"""Print `message` and return input within `timeout` seconds.
If nothing was entered in time, return None.
This works only on unix systems, since `signal.alarm` is needed.
"""
class TimeoutExpired(Exception):
pass
# noinspection PyUnusedLocal
def interrupted(signal_number, stack_frame):
"""Called when read times out."""
raise TimeoutExpired
@@ -108,3 +116,27 @@ def timed_input(message: str = None,
logging.info("Timeout!")
signal.alarm(0)
return given_input
def non_unix_timed_input(message: str = None,
timeout: int = 5):
"""Print message and wait `timeout` seconds before reading standard input.
This works on all systems, but cannot last less then `timeout` even if
user presses enter.
"""
print(message, end='')
time.sleep(timeout)
input_ = sys.stdin.readline()
if not input_.endswith("\n"):
print() # Print end of line
if input_:
return input_
return
timed_input = (
unix_timed_input
if sys.platform.startswith('linux')
else non_unix_timed_input()
)