Skip to content

Commit

Permalink
[py] improved serving Firebird and data files
Browse files Browse the repository at this point in the history
  • Loading branch information
DraTeots committed Jul 26, 2024
1 parent c07f9d6 commit bc9578f
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 32 deletions.
2 changes: 1 addition & 1 deletion pyrobird/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ classifiers = [
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
"click", "rich", "pyyaml"
"click", "rich", "pyyaml", "flask_cors"
]

#[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion pyrobird/src/pyrobird/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# This file is part of Firebird Event Display and is licensed under the LGPLv3.
# See the LICENSE file in the project root for full license information.

__version__ = "0.0.6"
__version__ = "0.0.7"
52 changes: 47 additions & 5 deletions pyrobird/src/pyrobird/cli/serve/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,60 @@
import pyrobird.server

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

unsecure_files_help = (
"Allow unrestricted access to download files in a system. "
"When enabled, the server allows downloads of all files which running user has access to. "
"When disabled only files in `work-path` and subdirectories are allowed. "
"This option could be safe on personal machines with one user, who runs the server in interactive terminal."
"(!) It is considered dangerous in all other cases: farms, interactive nodes, production environments, servers, etc. "
)

allow_cors_help = (
"Enable CORS for downloaded files. This option should be used if you need to support web "
"applications from different domains accessing the files. Such your server from central firebird server"
)


@click.command()
@click.option("--unsecure-files", "unsecure_files", is_flag=True, show_default=True, default=False, help="Allow unrestricted files download in a system")
@click.option("--allow-any-file", "unsecure_files", is_flag=True, show_default=True, default=False, help=unsecure_files_help)
@click.option("--allow-cors", "allow_cors", is_flag=True, show_default=True, default=False, help=allow_cors_help)
@click.option("--disable-files", "disable_download", is_flag=True, show_default=True, default=False, help="Disable all file downloads from the server")
@click.option("--work-path", "work_path", show_default=True, default="", help="Set the base directory path for file downloads. Defaults to the current working directory.")
@click.pass_context
def serve(ctx, unsecure_files):
def serve(ctx, unsecure_files, allow_cors, disable_download, work_path):
"""
Operations with database (create tables, erase everything, etc)
Start the server that serves Firebird frontend and can communicate with it.
This server allows firebird to work with local files and local file system as
well as to complement frontend features such as open xrootd files, etc.
This command initializes the Flask server with specific settings for file handling
and cross-origin resource sharing, tailored to operational and security requirements.
Examples:
- Start server with default settings, Firebird works with files in current directory:
fbd serve
- Enable unrestricted file downloads (absolute paths allowed) and CORS:
fbd serve --allow-any-file --allow-cors
- Set, where firebird will take files from
fbd serve --work-path=/home/username/datafiles
Now if you set file local://filename.root in Firebird UI,
the file /home/username/datafiles/filename.root will be opened
"""
pyrobird.server.run(debug=True, config={"ALLOW_UNRESTRICTED_DOWNLOAD": unsecure_files})

# Log the state of each flag
logging.info(f"Unsecure Files Allowed: {unsecure_files}")
logging.info(f"CORS Allowed: {allow_cors}")
logging.info(f"File Download Disabled: {disable_download}")
logging.info(f"Work Path Set To: {work_path if work_path else 'Current Working Directory'}")

pyrobird.server.run(debug=True, config={
"DOWNLOAD_ALLOW_UNRESTRICTED": unsecure_files,
"DOWNLOAD_DISABLE": disable_download,
"DOWNLOAD_PATH": work_path,
"DOWNLOAD_ALLOW_CORS": allow_cors})


if __name__ == '__main__':
Expand Down
116 changes: 91 additions & 25 deletions pyrobird/src/pyrobird/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from csv import excel

import werkzeug.exceptions
from flask import render_template, send_from_directory, Flask, send_file, abort, Config
from pandas.io.common import file_exists
from flask import render_template, send_from_directory, Flask, send_file, abort, Config, current_app
import flask

# Configure logging
logging.basicConfig(level=logging.INFO)
Expand All @@ -17,39 +17,98 @@

server_dir = os.path.abspath(os.path.dirname(__file__))
static_dir = os.path.join(server_dir, "static")
app = Flask(__name__, static_folder='static')
app.config.update()

flask_app = Flask(__name__, static_folder='static')
flask_app.config.update()

@app.route('/download/<path:filename>')

def _can_user_download_file(filename):
"""
Determine if the user is allowed to download the specified file based on application configuration.
Parameters:
- filename (str): The path to the file that the user wants to download. Can be absolute or relative.
Returns:
- bool: True if the file can be downloaded, False otherwise.
Process:
- If downloading is globally disabled (DOWNLOAD_DISABLE=True), returns False.
- If unrestricted downloads are allowed (DOWNLOAD_ALLOW_UNRESTRICTED=True), returns True.
- For relative paths, assumes that the download is allowable.
- For absolute paths, checks that the file resides within the configured DOWNLOAD_PATH.
- Logs a warning and returns False if the file is outside the allowed download path or if downloading is disabled.
"""

app = flask.current_app

# If any downloads are disabled
if app.config.get("DOWNLOAD_DISABLE") is True:
logger.warning("Can't download file. DOWNLOAD_DISABLE=True")
return False

# If we allow any download
unrestricted_download = app.config.get("DOWNLOAD_ALLOW_UNRESTRICTED") is True
if unrestricted_download:
return True

# if allowed/disable checks are done and we are here,
# if relative path is given, it will be joined with DOWNLOAD_PATH
if not os.path.isabs(filename):
return True

# HERE we have and absolute path! Check if it is safe to download

allowed_path = app.config.get("DOWNLOAD_PATH")
if not allowed_path:
allowed_path = os.getcwd()

# Check file will be downloaded from safe folder
can_download = os.path.realpath(filename).startswith(os.path.realpath(allowed_path))
if not can_download:
logger.warning("Can't download file. File is not in DOWNLOAD_PATH")
return False

# All is fine!
return True


@flask_app.route('/download/<path:filename>')
def download_file(filename):
# Ensure the path is safe and only files within a certain directory can be accessed
# For example, let's say you store your files in a 'files' directory within the server root
safe_path = filename
base_path = ''
if base_path:
safe_path = os.path.join(base_path, filename)
safe_path = os.path.abspath(safe_path) # Resolve any path traversal attempts

if not safe_path.startswith(base_path):
# Security check failed

# All checks and flags that user can download the file
if not _can_user_download_file(filename):
abort(404)

if os.path.exists(safe_path) and os.path.isfile(safe_path):
return send_file(safe_path, as_attachment=True)
# If it is relative, combine it with DOWNLOAD_PATH
if not os.path.isabs(filename):
download_path = flask.current_app.config.get("DOWNLOAD_PATH")
if not download_path:
download_path = os.getcwd()

# normalize the path
download_path = os.path.abspath(download_path)

# combine the file path
filename = os.path.join(download_path, filename)

# Check if the file exists and is a file
if os.path.exists(filename) and os.path.isfile(filename):
return send_file(filename, as_attachment=True, conditional=True)
else:
abort(404)
logger.warning(f"Can't download file. File does not exist")
abort(404) # Return 404 if the file does not exist


@app.route('/')
@flask_app.route('/')
def index():
return static_file("index.html")


@app.route('/<path:path>')
@flask_app.route('/<path:path>')
def static_file(path):

if app.debug:
if flask_app.debug:
print("Serve path:")
print(" Server dir :", server_dir)
print(" Static dir :", static_dir)
Expand All @@ -58,16 +117,23 @@ def static_file(path):
try:
return send_from_directory(static_dir, path)
except werkzeug.exceptions.NotFound as ex:
if app.debug:
if flask_app.debug:
print("File is not found, assuming it is SPA and serving index.html")
return send_from_directory(static_dir, "index.html")


def run(config=None, host=None, port=None, debug=True, load_dotenv=True):
if config:
if isinstance(config, Config) or isinstance(config, map):
app.config.from_mapping(config)
flask_app.config.from_mapping(config)
else:
app.config.from_object(config)
flask_app.config.from_object(config)

if flask_app.config and flask_app.config.get("DOWNLOAD_ALLOW_CORS") is True:
from flask_cors import CORS

# Enable CORS for all routes and specify the domains and settings
CORS(flask_app, resources={r"/download/*": {"origins": "*"}})


app.run(host=host, port=port, debug=debug, load_dotenv=load_dotenv)
flask_app.run(host=host, port=port, debug=debug, load_dotenv=load_dotenv)

0 comments on commit bc9578f

Please sign in to comment.