diff --git a/pyproject.toml b/pyproject.toml index 326f1c8..4576481 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,8 @@ dependencies = [ "lxml>=5.2.0", "pillow>=10.3.0", "numpy>=1.26.0", + "grpcio>=1.68.0", + "grpcio-tools>=1.68.0", ] [tool.uv] diff --git a/python-examples/GRPC_README.md b/python-examples/GRPC_README.md new file mode 100644 index 0000000..d7c0911 --- /dev/null +++ b/python-examples/GRPC_README.md @@ -0,0 +1,108 @@ +# gRPC Python Example + +This example demonstrates a simple gRPC client-server application based on the [official Python quickstart guide](https://grpc.io/docs/languages/python/quickstart/). + +## Overview + +The example implements a `Greeter` service with two RPC methods: +- `SayHello`: Returns a greeting message +- `SayHelloAgain`: Returns another greeting message + +## Files + +- `grpc_example.proto` - Protocol Buffer definition file +- `grpcio-example.py` - Complete client/server implementation + +## Requirements + +```bash +pip install grpcio grpcio-tools +``` + +## Setup + +First, generate the gRPC Python code from the proto file: + +```bash +python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. python-examples/grpc_example.proto +``` + +Or use the built-in setup command: + +```bash +cd python-examples +python grpcio-example.py setup +``` + +This generates: +- `grpc_example_pb2.py` - Protocol buffer message classes +- `grpc_example_pb2_grpc.py` - gRPC service classes + +## Running the Example + +### Terminal 1 - Start the server + +```bash +cd python-examples +python grpcio-example.py server +``` + +Output: +``` +✓ gRPC Server started on port 50051 + Waiting for client connections... + Press Ctrl+C to stop +``` + +### Terminal 2 - Run the client + +```bash +cd python-examples +python grpcio-example.py client +``` + +Output: +``` +Connecting to gRPC server at localhost:50051... + +1. Calling SayHello RPC... + ✓ Client received: Hello, World! + +2. Calling SayHelloAgain RPC... + ✓ Client received: Hello again, gRPC User! + +✓ All RPC calls completed successfully! +``` + +## Custom Port + +Start server on a different port: +```bash +python grpcio-example.py server 50052 +``` + +Connect client to custom port: +```bash +python grpcio-example.py client localhost 50052 +``` + +## Usage + +``` +python grpcio-example.py setup - Generate proto files +python grpcio-example.py server [port] - Start server (default: 50051) +python grpcio-example.py client [host] [port] - Run client (default: localhost:50051) +``` + +## Notes + +- This example uses **insecure channels** for simplicity +- In production, use secure channels with TLS/SSL certificates +- The server runs indefinitely until Ctrl+C is pressed +- Each client invocation makes two RPC calls and exits + +## Learn More + +- [gRPC Python Documentation](https://grpc.io/docs/languages/python/) +- [Protocol Buffers](https://protobuf.dev/) +- [gRPC Core Concepts](https://grpc.io/docs/what-is-grpc/core-concepts/) diff --git a/python-examples/djvu-pdf-example.py b/python-examples/djvu-pdf-example.py index 1155c18..8c116d8 100644 --- a/python-examples/djvu-pdf-example.py +++ b/python-examples/djvu-pdf-example.py @@ -5,6 +5,8 @@ import fnmatch import os import subprocess +import shutil +from pathlib import Path # global variables (change to suit your needs) inputfolderpath = '~' # set to import folder path outputpath = '~' # set to output folder (must exist) @@ -26,32 +28,67 @@ def find_files(directory, pattern): for filename in find_files(inputfolderpath, '*.djvu'): print(f"[*] Processing DJVU to PDF for {filename}...") i = i + 1 - inputfull = inputfolderpath+filename - outputfilename = filename[:-4]+i+'pdf' # make filename unique - outputfilepath = outputpath - p = subprocess.Popen(["djvu2pdf", inputfull], stdout=subprocess.PIPE) + inputfull = os.path.join(inputfolderpath, filename) + # Validate that the file exists and is a regular file + if not os.path.isfile(inputfull): + print(f"[!] Skipping {filename} - not a valid file") + continue + outputfilename = f"{filename[:-5]}_{i}.pdf" # make filename unique + outputfilepath = os.path.join(outputpath, outputfilename) + # Use list for subprocess to avoid shell injection + p = subprocess.Popen( + ["djvu2pdf", inputfull], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) output, err = p.communicate() - subprocess.call(["mv", outputfilename, outputfilepath]) + # Use shutil.move instead of shell command for better security + if p.returncode == 0 and os.path.exists(outputfilename): + shutil.move(outputfilename, outputfilepath) print('[-] Processing finished for %s' % filename) print(f"[--] processed {i} file(s) [--]") exit('\n\"Sanity is madness put to good uses.\" - George Santayana\n') elif operationtype == '2': filename = input('What filename to process? (leave blank for example): ') - if 'djvu' in filename: + if filename and 'djvu' in filename: + # Validate filename to prevent path traversal + safe_path = Path(filename).resolve() + if not safe_path.is_file() or not str(safe_path).endswith('.djvu'): + print('[!] Invalid file or not a .djvu file') + exit('Invalid input') print('Processing DJVU to PDF...') - p = subprocess.Popen(["djvu2pdf", filename], stdout=subprocess.PIPE) + p = subprocess.Popen( + ["djvu2pdf", str(safe_path)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) output, err = p.communicate() - print('Processing finished') - exit('Completed sucessfully') + if p.returncode == 0: + print('Processing finished') + exit('Completed successfully') + else: + print(f'[!] Error processing file: {err.decode() if err else "Unknown error"}') + exit('Failed') else: print('No djvu file to process, running sample') print('Processing DJVU to PDF...') - p = subprocess.Popen(["djvu2pdf", "assets/example.djvu"], - stdout=subprocess.PIPE) + sample_file = Path("assets/example.djvu") + if not sample_file.is_file(): + print('[!] Sample file not found') + exit('Sample file missing') + p = subprocess.Popen( + ["djvu2pdf", str(sample_file)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) output, err = p.communicate() - print('Processing finished') - exit('Completed sucessfully') + if p.returncode == 0: + print('Processing finished') + exit('Completed successfully') + else: + print(f'[!] Error: {err.decode() if err else "Unknown error"}') + exit('Failed') elif operationtype == '': diff --git a/python-examples/flask-example.py b/python-examples/flask-example.py index f91c876..56f4a60 100644 --- a/python-examples/flask-example.py +++ b/python-examples/flask-example.py @@ -39,26 +39,57 @@ def hello_world(): @app.route("/upload", methods=["POST"]) def upload_csv() -> str: """Upload CSV example.""" + if "file" not in request.files: + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "No file provided"}), 400 + submitted_file = request.files["file"] - if submitted_file and allowed_filename(submitted_file.filename): - filename = secure_filename(submitted_file.filename) - directory = os.path.join(app.config["UPLOAD_FOLDER"]) - if not os.path.exists(directory): - os.mkdir(directory) - basedir = os.path.abspath(os.path.dirname(__file__)) - submitted_file.save( - os.path.join(basedir, app.config["UPLOAD_FOLDER"], filename) - ) - out = { - "status": HTTPStatus.OK, - "filename": filename, - "message": f"{filename} saved successful.", - } - return jsonify(out) + + if not submitted_file or not submitted_file.filename: + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "No file selected"}), 400 + + if not allowed_filename(submitted_file.filename): + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "File type not allowed"}), 400 + + filename = secure_filename(submitted_file.filename) + + # Additional security check: ensure filename is not empty after sanitization + if not filename: + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "Invalid filename"}), 400 + + basedir = os.path.abspath(os.path.dirname(__file__)) + upload_folder = os.path.abspath(os.path.join(basedir, app.config["UPLOAD_FOLDER"])) + + # Create directory with secure permissions if it doesn't exist + if not os.path.exists(upload_folder): + os.makedirs(upload_folder, mode=0o755, exist_ok=True) + + # Construct full path and verify it's within the upload directory (prevent path traversal) + file_path = os.path.abspath(os.path.join(upload_folder, filename)) + + if not file_path.startswith(upload_folder): + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "Invalid file path"}), 400 + + # Limit file size (optional but recommended) + # submitted_file.seek(0, os.SEEK_END) + # file_size = submitted_file.tell() + # submitted_file.seek(0) + # if file_size > MAX_FILE_SIZE: + # return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "File too large"}), 400 + + submitted_file.save(file_path) + + out = { + "status": HTTPStatus.OK, + "filename": filename, + "message": f"{filename} saved successfully.", + } + return jsonify(out) if __name__ == "__main__": app.config["UPLOAD_FOLDER"] = "flaskme/" - app.run(port=6969, debug=True) + # Debug mode disabled for security - use environment variable to enable in development + debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true" + app.run(port=6969, debug=debug_mode) # curl -X POST localhost:6969/upload -F file=@"assets/archive_name.tar.gz" -i diff --git a/python-examples/grpc_example.proto b/python-examples/grpc_example.proto new file mode 100644 index 0000000..9f02cc1 --- /dev/null +++ b/python-examples/grpc_example.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package grpc_example; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} + // Sends another greeting + rpc SayHelloAgain (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greeting. +message HelloReply { + string message = 1; +} diff --git a/python-examples/grpcio-example.py b/python-examples/grpcio-example.py new file mode 100644 index 0000000..85381e8 --- /dev/null +++ b/python-examples/grpcio-example.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +gRPC Python Example +Based on: https://grpc.io/docs/languages/python/quickstart/ + +This example demonstrates a simple gRPC client-server application using +the Greeter service from the official gRPC Python quickstart. + +Requirements: + pip install grpcio grpcio-tools + +Setup: + Before running, generate the gRPC Python code from the proto file: + python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. python-examples/grpc_example.proto + +To run: + 1. Start the server: python grpcio-example.py server + 2. In another terminal, run the client: python grpcio-example.py client + +Note: This example uses insecure channels for simplicity. In production, +use secure channels with TLS/SSL certificates. +""" + +import sys +import os +import grpc +from concurrent import futures +import time +import subprocess + +# Check if proto files are generated, if not, generate them +if not os.path.exists('python-examples/grpc_example_pb2.py'): + print("Generating gRPC Python code from proto file...") + try: + subprocess.run([ + sys.executable, '-m', 'grpc_tools.protoc', + '-I.', '--python_out=.', '--grpc_python_out=.', + 'python-examples/grpc_example.proto' + ], check=True) + print("Generated successfully!\n") + except subprocess.CalledProcessError as e: + print(f"Error generating proto files: {e}") + print("Make sure grpcio-tools is installed: pip install grpcio-tools") + sys.exit(1) + +# Import generated proto classes +try: + from python-examples import grpc_example_pb2 + from python-examples import grpc_example_pb2_grpc +except ImportError: + # Try alternative import path + try: + sys.path.insert(0, 'python-examples') + import grpc_example_pb2 + import grpc_example_pb2_grpc + except ImportError: + print("Error: Failed to import generated gRPC modules.") + print("Please ensure the proto files are generated correctly.") + sys.exit(1) + + +# Server implementation +class Greeter(grpc_example_pb2_grpc.GreeterServicer): + """Implementation of the Greeter service.""" + + def SayHello(self, request, context): + """Responds to a HelloRequest with a HelloReply.""" + print(f"Server received: {request.name}") + return grpc_example_pb2.HelloReply(message=f"Hello, {request.name}!") + + def SayHelloAgain(self, request, context): + """Another greeting method.""" + print(f"Server received again: {request.name}") + return grpc_example_pb2.HelloReply(message=f"Hello again, {request.name}!") + + +def serve(port=50051): + """Start the gRPC server.""" + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + grpc_example_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) + server.add_insecure_port(f'[::]:{port}') + server.start() + print(f"✓ gRPC Server started on port {port}") + print(" Waiting for client connections...") + print(" Press Ctrl+C to stop\n") + + try: + while True: + time.sleep(86400) # One day + except KeyboardInterrupt: + print("\n\nShutting down server...") + server.stop(0) + print("Server stopped.") + + +def run_client(host='localhost', port=50051): + """Run the gRPC client.""" + print(f"Connecting to gRPC server at {host}:{port}...") + + # Create a channel + with grpc.insecure_channel(f'{host}:{port}') as channel: + # Create a stub (client) + stub = grpc_example_pb2_grpc.GreeterStub(channel) + + # Make a call to SayHello + try: + print("\n1. Calling SayHello RPC...") + response = stub.SayHello(grpc_example_pb2.HelloRequest(name='World')) + print(f" ✓ Client received: {response.message}") + + # Make another call to SayHelloAgain + print("\n2. Calling SayHelloAgain RPC...") + response = stub.SayHelloAgain(grpc_example_pb2.HelloRequest(name='gRPC User')) + print(f" ✓ Client received: {response.message}") + + print("\n✓ All RPC calls completed successfully!\n") + + except grpc.RpcError as e: + print(f"\n✗ RPC failed: {e.code()} - {e.details()}\n") + print(" Make sure the server is running: python grpcio-example.py server") + sys.exit(1) + + +def print_usage(): + """Print usage instructions.""" + print(""" +gRPC Python Example +=================== + +This demonstrates a basic gRPC client-server application based on the +official Python quickstart guide. + +Setup (first time only): + Generate the gRPC Python code from the proto file: + + python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. python-examples/grpc_example.proto + +Usage: + python grpcio-example.py setup - Generate proto files + python grpcio-example.py server [port] - Start the gRPC server (default: 50051) + python grpcio-example.py client [host] [port] - Run the gRPC client + +Examples: + # Setup (only needed once) + python grpcio-example.py setup + + # Start server on default port + python grpcio-example.py server + + # Start server on custom port + python grpcio-example.py server 50052 + + # Run client (connect to localhost:50051) + python grpcio-example.py client + + # Run client with custom host/port + python grpcio-example.py client localhost 50052 + +Demo: + Terminal 1: python grpcio-example.py server + Terminal 2: python grpcio-example.py client +""") + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print_usage() + sys.exit(1) + + command = sys.argv[1].lower() + + if command == 'setup': + print("Generating gRPC Python code from proto file...") + try: + subprocess.run([ + sys.executable, '-m', 'grpc_tools.protoc', + '-I.', '--python_out=.', '--grpc_python_out=.', + 'python-examples/grpc_example.proto' + ], check=True) + print("✓ Generated successfully!") + print("\nYou can now run:") + print(" python grpcio-example.py server") + print(" python grpcio-example.py client") + except subprocess.CalledProcessError as e: + print(f"✗ Error generating proto files: {e}") + sys.exit(1) + elif command == 'server': + port = int(sys.argv[2]) if len(sys.argv) > 2 else 50051 + serve(port) + elif command == 'client': + host = sys.argv[2] if len(sys.argv) > 2 else 'localhost' + port = int(sys.argv[3]) if len(sys.argv) > 3 else 50051 + run_client(host, port) + else: + print(f"Unknown command: {command}") + print_usage() + sys.exit(1) diff --git a/python-examples/pickle_load-example.py b/python-examples/pickle_load-example.py index 7e92b24..45d0439 100644 --- a/python-examples/pickle_load-example.py +++ b/python-examples/pickle_load-example.py @@ -1,8 +1,28 @@ # pickle load example +# WARNING: pickle.load() can execute arbitrary code and should only be used +# with trusted data. For untrusted data, use safer alternatives like JSON. +# See: https://docs.python.org/3/library/pickle.html#module-pickle import pickle import random +import os -with open('assets/discordia.pkl', 'rb') as f: +# Only load pickle files from trusted sources in trusted locations +pickle_file = 'assets/discordia.pkl' + +# Verify the file exists and is in the expected location +if not os.path.exists(pickle_file): + raise FileNotFoundError(f"Pickle file not found: {pickle_file}") + +# Resolve to absolute path to prevent path traversal +pickle_file = os.path.abspath(pickle_file) +expected_dir = os.path.abspath('assets') + +if not pickle_file.startswith(expected_dir): + raise ValueError("Pickle file must be in the assets directory") + +with open(pickle_file, 'rb') as f: + # SECURITY NOTE: This loads a pickle file that must be from a trusted source + # Never load pickle files from untrusted sources (user uploads, internet, etc.) discordia = pickle.load(f) diff --git a/requirements.txt b/requirements.txt index 5432a12..bcefc7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,3 +31,5 @@ requests>=2.31.0 lxml>=5.2.0 pillow>=10.3.0 numpy>=1.26.0 +grpcio>=1.68.0 +grpcio-tools>=1.68.0