Skip to content
This repository has been archived by the owner on Dec 2, 2024. It is now read-only.

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
proofrock committed May 13, 2024
0 parents commit c62e8cc
Show file tree
Hide file tree
Showing 7 changed files with 379 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pem
test/
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Copyright (c) 2024- Germano Rizzo <oss AT germanorizzo DOT it>

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
119 changes: 119 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# fileserver v0.1.0

This project aids in setting up a tunnel that serves a single file on an encrypted connection, allowing to source it from your system and downloading it on another system you don't have "direct" access to, because under a firewall or such reasons.

It employs a "jump server" to which it will reverse tunnel a local port via SSH.

It has been tested under Linux, both for the source and destination system; it will be adapted to Windows and MacOS.

## Prerequisites

- A "jump server" that you can access via SSH from the source system;
- A port on it, accessible by "the world";
- SSH on the jump server must be configured to allow remote tunnels (see below);
- `python` v3 on the source system;
- `curl` on the destination system.

## Usage

- You configure and run the script `filetunnel.sh` with the file to transfer:
```bash
./fileserver.sh myFile.binary
```
- It will output a `curl` command to use on the destination system to download the file.

Behind the scenes, the script opens a web server using python, on a random local port; then reverse tunnels it on the jump server, making it available remotely.

The `curl` script, when executed on the destination system, will connect to the port and download the file, assigning the correct filename to it.

## Setup

### The jump server

This is a "normal" server such as a VPS, that you can access via SSH from the source system.

A port (to configure inside the script) must be accessible from outside, at least from the destination system.

On ssh, (reverse) tunneling must be enabled. Ensure that you have this setting in `/etc/ssh/sshd_config`:

```
AllowTcpForwarding yes
```

We'll also need to access the remote-forwarded port from outside. So, set:

```python
GatewayPorts clientspecified # or 'yes'
```

**WARNING!** This setting allows the forwarded port (*any* forwarded port, even for other uses) to be globally accessible. Carefully consider the security implications of this.

### The source system

Download `filetunnel.sh` from the release page.

Open it, and configure the variables in the first section. You'll need:

- `SSH_SERVER`: address to contact the jump server from the source system, using ssh, in form `user@host`.
- `FILE_SERVER`: IP or DNS name to contact the jump server from the destination system.
- `PORT`: port on the jump server for the tunnel, accessible from the destination server.

If you want HTTPS, see the next section.

### Setup https

First, generate the certificates using:
```bash
openssl req -newkey rsa:4096 -nodes -keyout key.pem -x509 -days 365 -out cert.pem
```
This will generate a `cert.pem` and a (secret) `key.pem` files.

Then configure `fileserver.sh` to use HTTPS, by setting the relevant variables:

- `DO_HTTPS`: set to `1`.
- `CERT_FILE`, `KEY_FILE`: paths to the `cert.pem` and `key.pem` files generated by `openssl`.

## Security

- There is an inherent risk in doing reverse tunneling. It's a good idea to reserve the jump server to this use;
- The connection between the source system and the jump server is protected by `ssh`;
- The connection between the jump server and the destination system is protected by (optional) HTTPS, using a user-provided certificate;
- Also, the generated URL is random;
- Once transferred the file, it's good measure to terminate the script to avoid continued exposure.

## Troubleshooting

### When stopping the script w/ `ctrl-c`, a python error is shown
```
Traceback (most recent call last):
File "<string>", line 18, in <module>
File "/usr/lib/python3.12/socketserver.py", line 235, in serve_forever
ready = selector.select(poll_interval)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/selectors.py", line 415, in select
fd_event_list = self._selector.poll(timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyboardInterrupt
```
This is ok, for now just ignore it. It's on the todo not to show it.

## To do

- Adapt and fully test under MacOS, Windows
- Optional "one shot" mode: when the file is downloaded, the server exists;
- Optional compression;
- When stopping the script, the python part emits an ugly-looking error, even if it's totally intended.

## Build and contribute

In the `src` folder there are three script (one bash, two python) to manually "compile" into the target script. Make a copy of the bash script, and then follow the comments that contain `[BUILD]`.

If you have any good idea, please feel free to hack on it! The code should be fairly simple to understand and change, and doesn't have many dependencies.

## License

```
filetunnel v0.1.0 https://github.com/proofrock/filetunnel
Copyright (c) 2024- Germano Rizzo <oss AT germanorizzo DOT it>
See LICENSE file (MIT License)
```
115 changes: 115 additions & 0 deletions filetunnel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/bin/bash

# filetunnel v0.1.0 https://github.com/proofrock/filetunnel
# Copyright (c) 2024- Germano Rizzo <oss AT germanorizzo DOT it>
# See LICENSE file (MIT License)

#####################
# Edit this section #
#####################

# How to contact the jump server from the "file server side". user@host
SSH_SERVER=user@123.123.123.123
# How to contact the jump server from the "client side".
FILE_SERVER=123.123.123.123
# Port on the jump server to tunnel the HTTP server on
PORT=7017

# Setup HTTPS
DO_HTTPS=0
CERT_FILE=./cert.pem
KEY_FILE=./key.pem

###################
# Not from now on #
###################

RND=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 24)

FILESERVERSCRIPT=`cat <<PYDOC
import os, sys, ssl
from http.server import HTTPServer, SimpleHTTPRequestHandler
# CLI parameters (internal)
file_path = sys.argv[1] # File to serve
url_path = sys.argv[2] # Random string for URL path
http_port = int(sys.argv[3]) # Port for the server
do_ssl = sys.argv[4] == "1" # Do ssl?
if do_ssl:
cert_file = sys.argv[5]
key_file = sys.argv[6]
# Size
file_size = os.stat(file_path).st_size
# Create a custom request handler
class SingleFileRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == f'/{url_path}':
self.send_response(200)
self.send_header('Content-Type', 'binary/octet-stream')
self.send_header('Content-Length', str(file_size))
self.send_header('Content-Disposition', f'attachment; filename="{os.path.basename(file_path)}"')
self.end_headers()
with open(file_path, 'rb') as file:
self.wfile.write(file.read())
# elif self.path == '/exit':
# self.wfile.write(b'bye')
# self.send_response(200)
# exit(0)
else:
self.send_error(404)
# Set up the HTTPS server
server_address = ('', http_port)
httpd = HTTPServer(server_address, SingleFileRequestHandler)
if do_ssl:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_file, key_file)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
httpd.serve_forever()
PYDOC`
FREEPORTSCRIPT=`cat <<PYDOC
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
addr, port = s.getsockname()
print(port)
PYDOC`
LOCAL_PORT=$(python -c "$FREEPORTSCRIPT")
function handle_interrupt {
kill -TERM "$PID1" "$PID2" 2>/dev/null
wait "$PID1" "$PID2" 2>/dev/null
# [TODO] Python dies with ugly-looking "logs"
echo "Bye bye!"
exit 0
}
trap handle_interrupt SIGINT
ssh $SSH_SERVER -N -R:$PORT:localhost:$LOCAL_PORT &
PID1=$!
{ python -c "$FILESERVERSCRIPT" "$1" "$RND" "$LOCAL_PORT" "$DO_HTTPS" "$CERT_FILE" "$KEY_FILE"; kill $PID1; } & # At exit, kills the ssh session
PID2=$!
PROTO="http"
if [ "$DO_HTTPS" -eq "1" ]; then
PROTO="https"
ADD_FLAG="k"
fi
echo "All set up, using local port $LOCAL_PORT. Now use the following command:"
echo
echo "curl -OJf$ADD_FLAG $PROTO://$FILE_SERVER:$PORT/$RND"
echo
echo "Then you can close this process with ctrl-c."
wait "$PID1" "$PID2"
44 changes: 44 additions & 0 deletions src/fileserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os, sys, ssl
from http.server import HTTPServer, SimpleHTTPRequestHandler

# CLI parameters (internal)

file_path = sys.argv[1] # File to serve
url_path = sys.argv[2] # Random string for URL path
http_port = int(sys.argv[3]) # Port for the server
do_ssl = sys.argv[4] == "1" # Do ssl?
if do_ssl:
cert_file = sys.argv[5]
key_file = sys.argv[6]

# Size
file_size = os.stat(file_path).st_size

# Create a custom request handler
class SingleFileRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == f'/{url_path}':
self.send_response(200)
self.send_header('Content-Type', 'binary/octet-stream')
self.send_header('Content-Length', str(file_size))
self.send_header('Content-Disposition', f'attachment; filename="{os.path.basename(file_path)}"')
self.end_headers()
with open(file_path, 'rb') as file:
self.wfile.write(file.read())
# elif self.path == '/exit':
# self.wfile.write(b'bye')
# self.send_response(200)
# exit(0)
else:
self.send_error(404)

# Set up the HTTPS server
server_address = ('', http_port)
httpd = HTTPServer(server_address, SingleFileRequestHandler)

if do_ssl:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_file, key_file)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)

httpd.serve_forever()
69 changes: 69 additions & 0 deletions src/filetunnel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/bin/bash

# filetunnel v0.1.0 https://github.com/proofrock/filetunnel
# Copyright (c) 2024- Germano Rizzo <oss AT germanorizzo DOT it>
# See LICENSE file (MIT License)

#####################
# Edit this section #
#####################

# How to contact the jump server from the "file server side". user@host
SSH_SERVER=user@123.123.123.123
# How to contact the jump server from the "client side".
FILE_SERVER=123.123.123.123
# Port on the jump server to tunnel the HTTP server on
PORT=7017

# Setup HTTPS
DO_HTTPS=0
CERT_FILE=./cert.pem
KEY_FILE=./key.pem

###################
# Not from now on #
###################

RND=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 24)

# [BUILD] Put the contents of 'fileserver.py' in this section
FREEPORTSCRIPT=`cat <<PYDOC
# Put 'fileserver.py' here
PYDOC`
# [BUILD] Put the contents of 'freeport.py' in this section
FILESERVERSCRIPT=`cat <<PYDOC
# Put 'freeport.py' here
PYDOC`
LOCAL_PORT=$(python -c "$FREEPORTSCRIPT")
function handle_interrupt {
kill -TERM "$PID1" "$PID2" 2>/dev/null
wait "$PID1" "$PID2" 2>/dev/null
# [TODO] Python dies with ugly-looking "logs"
echo "Bye bye!"
exit 0
}
trap handle_interrupt SIGINT
ssh $SSH_SERVER -N -R:$PORT:localhost:$LOCAL_PORT &
PID1=$!
{ python -c "$FILESERVERSCRIPT" "$1" "$RND" "$LOCAL_PORT" "$DO_HTTPS" "$CERT_FILE" "$KEY_FILE"; kill $PID1; } & # At exit, kills the ssh session
PID2=$!
PROTO="http"
if [ "$DO_HTTPS" -eq "1" ]; then
PROTO="https"
ADD_FLAG="k"
fi
echo "All set up, using local port $LOCAL_PORT. Now use the following command:"
echo
echo "curl -OJf$ADD_FLAG $PROTO://$FILE_SERVER:$PORT/$RND"
echo
echo "Then you can close this process with ctrl-c."
wait "$PID1" "$PID2"
8 changes: 8 additions & 0 deletions src/freeport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Returns a free port

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
addr, port = s.getsockname()
print(port)

0 comments on commit c62e8cc

Please # to comment.