import json import os from flask import Flask, Response, jsonify, redirect, render_template, render_template_string, request, url_for import jinja_partials from jinja2_fragments import render_block import requests from extensions import db, htmx, init_app, login_manager from services import create_http_function_view_model, create_http_functions_view_model from flask_login import current_user, login_required from werkzeug.security import check_password_hash, generate_password_hash import os from dotenv import load_dotenv from routes.timer import timer from routes.test import test from routes.home import home from routes.http import http from routes.llm import llm from routes.auth import auth from routes.settings import settings from constants import DEFAULT_FUNCTION_NAME, DEFAULT_SCRIPT, DEFAULT_ENVIRONMENT from flask_apscheduler import APScheduler import asyncio import aiohttp from concurrent.futures import ThreadPoolExecutor # Load environment variables from .env file in non-production environments if os.environ.get('FLASK_ENV') != 'production': load_dotenv() app = Flask(__name__) app.config.from_pyfile('config.py') app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f') login_manager.init_app(app) login_manager.login_view = "auth.login" jinja_partials.register_extensions(app) # Remove scheduler configuration and initialization init_app(app) app.register_blueprint(timer, url_prefix='/timer') app.register_blueprint(test, url_prefix='/test') app.register_blueprint(home, url_prefix='/home') app.register_blueprint(http, url_prefix='/http') app.register_blueprint(llm, url_prefix='/llm') app.register_blueprint(auth, url_prefix='/auth') app.register_blueprint(settings, url_prefix='/settings') # Swith to inter app routing, which results in speed up from ~400ms to ~270ms # https://stackoverflow.com/questions/76886643/linking-two-not-exposed-dokku-apps NODE_API_URL = os.environ.get('NODE_API_URL', 'http://isolator.web:5000/execute') DENO_API_URL = os.environ.get('DENO_API_URL', 'http://deno-isolator.web:5000/execute') PYTHON_API_URL = os.environ.get('PYTHON_API_URL', 'http://python-isolator.web:5000/execute') def map_isolator_response_to_flask_response(response): """ Maps a Node.js response to a Flask response. :param nodejs_response: The response from Node.js, expected to be a dictionary with keys 'body', 'headers', and 'status'. :return: Flask Response object """ result = response.get('result', {}) body = result.get('body', '') headers = result.get('headers', {}) status = result.get('status', 200) # Convert body to JSON if it's a dictionary body = str(body) return Response(response=body, status=status, headers=headers) @ app.route("/", methods=["GET"]) def landing_page(): return render_template("landing_page.html", name='Try me', script=DEFAULT_SCRIPT, environment_info=DEFAULT_ENVIRONMENT) @app.route("/documentation", methods=["GET"]) def documentation(): return render_template("documentation.html") @app.route('/execute', methods=['POST']) async def execute_code(): try: # Extract code and convert request to a format acceptable by Node.js app code = request.json.get('code') runtime = request.json.get('runtime', 'node') # Default to node if runtime == 'deno': api_url = DENO_API_URL elif runtime == 'python': api_url = PYTHON_API_URL else: api_url = NODE_API_URL request_obj = { 'method': request.method, 'headers': dict(request.headers), 'body': request.json, 'url': request.url, 'path': '/', } environment = request.json.get('environment_info') environment_json = json.loads(environment) # Call the selected isolator API asynchronously async with aiohttp.ClientSession() as session: async with session.post(api_url, json={'code': code, 'request': request_obj, 'environment': environment_json, 'name': "anonymous"}) as response: response_data = await response.json() # check if playground=true is in the query string if request.args.get('playground') == 'true': return response_data # Map the Node.js response to Flask response flask_response = map_isolator_response_to_flask_response(response_data) return flask_response except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/f//', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD']) async def execute_http_function(user_id, function): try: # Split the function_path into the function name and the sub-path parts = function.split('/', 1) function_name = parts[0] sub_path = '/' + parts[1] if len(parts) > 1 else '/' http_function = db.get_http_function(user_id, function_name) if not http_function: return jsonify({'error': 'Function not found'}), 404 code = http_function['script_content'] environment_info = http_function['environment_info'] runtime = http_function.get('runtime', 'node') # Default to node # Ensure environment is a dictionary if isinstance(environment_info, str) and environment_info: try: environment = json.loads(environment_info) except json.JSONDecodeError: environment = {} elif isinstance(environment_info, dict): environment = environment_info else: environment = {} is_public = http_function['is_public'] log_request = http_function['log_request'] log_response = http_function['log_response'] version_number = http_function['version_number'] # Check if the function is public, if not check if the user is authenticated and owns the function if not is_public: is_authorized = False # 1. Session Authentication if current_user.is_authenticated and int(current_user.id) == user_id: is_authorized = True # 2. API Key Authentication elif 'X-API-Key' in request.headers: api_key_value = request.headers.get('X-API-Key') api_key = db.get_api_key(api_key_value) if api_key and api_key['user_id'] == user_id: # Check Scopes scopes = api_key['scopes'] if isinstance(scopes, str): scopes = json.loads(scopes) if "*" in scopes or f"function:{http_function['id']}" in scopes: is_authorized = True db.update_api_key_last_used(api_key['id']) if not is_authorized: if not current_user.is_authenticated: return redirect(url_for('auth.login', next=request.url)) return jsonify({'error': 'Function belongs to another user'}), 404 request_data = { 'method': request.method, 'headers': dict(request.headers), 'url': request.url, 'path': sub_path, } # Add JSON data if it exists if request.is_json: request_data['json'] = request.get_json() # Add form data if it exists if request.form: request_data['form'] = { key.rstrip('[]'): request.form.getlist(key) if key.endswith('[]') or len(request.form.getlist(key)) > 1 else request.form.getlist(key)[0] for key in request.form.keys() } # Add query parameters if they exist if request.args: request_data['query'] = { key.rstrip('[]'): request.args.getlist(key) if key.endswith('[]') or len(request.args.getlist(key)) > 1 else request.args.getlist(key)[0] for key in request.args.keys() } # Add plain text data if it exists if request.data and not request.is_json: request_data['text'] = request.data.decode('utf-8') # Call the Node.js API asynchronously if runtime == 'deno': api_url = DENO_API_URL elif runtime == 'python': api_url = PYTHON_API_URL else: api_url = NODE_API_URL async with aiohttp.ClientSession() as session: async with session.post(api_url, json={'code': code, 'request': request_data, 'environment': environment, 'name': function_name}) as response: response_data = await response.json() db.update_http_function_environment_info_and_invoked_count(user_id, function_name, response_data['environment']) db.add_http_function_invocation( http_function['id'], response_data['status'], request_data if log_request else {}, response_data['result'] if (log_response or response_data['status'] != 'SUCCESS') else {}, response_data['logs'], version_number, response_data.get('execution_time')) if response_data['status'] != 'SUCCESS': return render_template("function_error.html", function_name=function_name ,error=response_data['result'], logs=response_data['logs']) # Map the Node.js response to Flask response flask_response = map_isolator_response_to_flask_response(response_data) return flask_response except Exception as e: return jsonify({'error': str(e)}), 500 if __name__ == '__main__': # Bind to PORT if defined, otherwise default to 5000. port = int(os.environ.get('PORT', 5000)) app.run(host='127.0.0.1', port=port) @app.teardown_appcontext def teardown_db(exception): db.close_conn()