├── .gitignore ├── LICENSE ├── README.md ├── examples ├── flask_cloudflared_example.ipynb └── flask_cloudflared_example.py ├── flask_cloudflared.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Ralf Rademacher 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask-cloudflared 2 | 3 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/flask-cloudflared) [![Run it button](https://img.shields.io/badge/-Run%20it%20now-brightgreen.svg?longCache=true&style=flat&logo=data:image/png;base64,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)](https://colab.research.google.com/github/UWUplus/flask-cloudflared/blob/main/examples/flask_cloudflared_example.ipynb) 4 | 5 | Start a [TryCloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/trycloudflare) tunnel to your flask app right from code. 6 | This requires at least `Python 3.6` 7 | 8 | ## Behavior 9 | The Flask app will run on port 5000 by default and start the Cloudflared metrics page on a random port between 8100 and 9000. 10 | This can be changed by passing the `port` and `metrics_port` arguments to the `app.run()` function after using the `run_with_cloudflared` decorator. 11 | 12 | ### Custom tunnel domain 13 | By default, the tunnel will be created with a random subdomain of `trycloudflare.com`. 14 | To use custom domains, follow [this tutorial by Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/local/) and pass either the `tunnel_id` or `config_path` arguments to the `app.run()` function after using the `run_with_cloudflared` decorator. For an example check out [examples/flask_cloudflared_example.py](https://github.com/UWUplus/flask-cloudflared/blob/main/examples/flask_cloudflared_example.py#L13-L14). 15 | 16 | ### Users on Apple Silicon 17 | Because [cloudflared](https://github.com/cloudflare/cloudflared) doesn't support Darwin arm64 natively yet, Rosetta 2 is used to create a compatibility layer. If you don't have Rosetta 2 installed yet, please check [Apple's support page](https://support.apple.com/en-us/HT211861). 18 | 19 | ## Acknowledgements 20 | 21 | This project is based on [flask-ngrok](https://github.com/gstaff/flask-ngrok). 22 | -------------------------------------------------------------------------------- /examples/flask_cloudflared_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "flask-cloudflared.ipynb", 7 | "provenance": [] 8 | }, 9 | "kernelspec": { 10 | "name": "python3", 11 | "display_name": "Python 3" 12 | }, 13 | "language_info": { 14 | "name": "python" 15 | } 16 | }, 17 | "cells": [ 18 | { 19 | "cell_type": "code", 20 | "execution_count": 1, 21 | "metadata": { 22 | "colab": { 23 | "base_uri": "https://localhost:8080/" 24 | }, 25 | "id": "jVIPz454lB0y", 26 | "outputId": "5aaabf38-00be-4585-c614-9b058f3a8d53" 27 | }, 28 | "outputs": [ 29 | { 30 | "output_type": "stream", 31 | "name": "stdout", 32 | "text": [ 33 | "Collecting flask-cloudflared\n", 34 | " Downloading flask_cloudflared-0.0.6-py3-none-any.whl (3.7 kB)\n", 35 | "Requirement already satisfied: requests in /usr/local/lib/python3.7/dist-packages (from flask-cloudflared) (2.23.0)\n", 36 | "Requirement already satisfied: Flask>=0.8 in /usr/local/lib/python3.7/dist-packages (from flask-cloudflared) (1.1.4)\n", 37 | "Requirement already satisfied: itsdangerous<2.0,>=0.24 in /usr/local/lib/python3.7/dist-packages (from Flask>=0.8->flask-cloudflared) (1.1.0)\n", 38 | "Requirement already satisfied: Jinja2<3.0,>=2.10.1 in /usr/local/lib/python3.7/dist-packages (from Flask>=0.8->flask-cloudflared) (2.11.3)\n", 39 | "Requirement already satisfied: Werkzeug<2.0,>=0.15 in /usr/local/lib/python3.7/dist-packages (from Flask>=0.8->flask-cloudflared) (1.0.1)\n", 40 | "Requirement already satisfied: click<8.0,>=5.1 in /usr/local/lib/python3.7/dist-packages (from Flask>=0.8->flask-cloudflared) (7.1.2)\n", 41 | "Requirement already satisfied: MarkupSafe>=0.23 in /usr/local/lib/python3.7/dist-packages (from Jinja2<3.0,>=2.10.1->Flask>=0.8->flask-cloudflared) (2.0.1)\n", 42 | "Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests->flask-cloudflared) (2.10)\n", 43 | "Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests->flask-cloudflared) (3.0.4)\n", 44 | "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests->flask-cloudflared) (2021.10.8)\n", 45 | "Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests->flask-cloudflared) (1.24.3)\n", 46 | "Installing collected packages: flask-cloudflared\n", 47 | "Successfully installed flask-cloudflared-0.0.6\n" 48 | ] 49 | } 50 | ], 51 | "source": [ 52 | "!pip install flask-cloudflared" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "source": [ 58 | "from flask import Flask\n", 59 | "from flask_cloudflared import run_with_cloudflared\n", 60 | "\n", 61 | "app = Flask(__name__)\n", 62 | "run_with_cloudflared(app) # Open a Cloudflare Tunnel when app is run\n", 63 | "\n", 64 | "@app.route(\"/\")\n", 65 | "def home(): \n", 66 | " return \"Hello World from Google Colab!\" # Serve Hello World from Google Colab\n", 67 | "\n", 68 | "if __name__ == '__main__':\n", 69 | " app.run()" 70 | ], 71 | "metadata": { 72 | "colab": { 73 | "base_uri": "https://localhost:8080/" 74 | }, 75 | "id": "DhQpQMnalYvv", 76 | "outputId": "7457f110-572c-4589-e2a9-82f250b2e9a9" 77 | }, 78 | "execution_count": 3, 79 | "outputs": [ 80 | { 81 | "output_type": "stream", 82 | "name": "stdout", 83 | "text": [ 84 | " * Serving Flask app \"__main__\" (lazy loading)\n", 85 | " * Environment: production\n", 86 | "\u001b[31m WARNING: This is a development server. Do not use it in a production deployment.\u001b[0m\n", 87 | "\u001b[2m Use a production WSGI server instead.\u001b[0m\n", 88 | " * Debug mode: off\n" 89 | ] 90 | }, 91 | { 92 | "output_type": "stream", 93 | "name": "stderr", 94 | "text": [ 95 | " * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)\n" 96 | ] 97 | }, 98 | { 99 | "output_type": "stream", 100 | "name": "stdout", 101 | "text": [ 102 | " * Running on https://socket-graduated-mono-iowa.trycloudflare.com\n", 103 | " * Traffic stats available on http://127.0.0.1:8099/metrics\n" 104 | ] 105 | }, 106 | { 107 | "output_type": "stream", 108 | "name": "stderr", 109 | "text": [ 110 | "127.0.0.1 - - [12/Apr/2022 11:46:03] \"\u001b[37mGET / HTTP/1.1\u001b[0m\" 200 -\n", 111 | "127.0.0.1 - - [12/Apr/2022 11:46:04] \"\u001b[33mGET /favicon.ico HTTP/1.1\u001b[0m\" 404 -\n" 112 | ] 113 | } 114 | ] 115 | } 116 | ] 117 | } -------------------------------------------------------------------------------- /examples/flask_cloudflared_example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_cloudflared import run_with_cloudflared 3 | 4 | app = Flask(__name__) 5 | run_with_cloudflared(app) # Open a Cloudflare Tunnel when app is run 6 | 7 | @app.route("/") 8 | def home(): 9 | return "Hello World!" # Serve Hello World 10 | 11 | if __name__ == '__main__': 12 | # app.run(port=1337, metrics_port=1338) # Run the app on port 1337 and metrics on port 1338 13 | # app.run(config_path="/path/to/config.yml") # Run the app with a Cloudflare Tunnel config 14 | # app.run(tunnel_id="my-tunnel-id") # Run the app with a Cloudflare Tunnel ID 15 | app.run() # Run the app -------------------------------------------------------------------------------- /flask_cloudflared.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import requests 3 | import subprocess 4 | import tarfile 5 | import tempfile 6 | import shutil 7 | import os 8 | import platform 9 | import time 10 | import re 11 | from random import randint 12 | from threading import Timer 13 | from pathlib import Path 14 | from tqdm.auto import tqdm 15 | 16 | CLOUDFLARED_CONFIG = { 17 | ("Windows", "AMD64"): { 18 | "command": "cloudflared-windows-amd64.exe", 19 | "url": "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe", 20 | }, 21 | ("Windows", "x86"): { 22 | "command": "cloudflared-windows-386.exe", 23 | "url": "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-386.exe", 24 | }, 25 | ("Linux", "x86_64"): { 26 | "command": "cloudflared-linux-amd64", 27 | "url": "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64", 28 | }, 29 | ("Linux", "i386"): { 30 | "command": "cloudflared-linux-386", 31 | "url": "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-386", 32 | }, 33 | ("Linux", "arm"): { 34 | "command": "cloudflared-linux-arm", 35 | "url": "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm", 36 | }, 37 | ("Linux", "arm64"): { 38 | "command": "cloudflared-linux-arm64", 39 | "url": "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64", 40 | }, 41 | ("Linux", "aarch64"): { 42 | "command": "cloudflared-linux-arm64", 43 | "url": "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64", 44 | }, 45 | ("Darwin", "x86_64"): { 46 | "command": "cloudflared", 47 | "url": "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz", 48 | }, 49 | ("Darwin", "arm64"): { 50 | "command": "cloudflared", 51 | "url": "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz", 52 | }, 53 | } 54 | 55 | 56 | def _get_command(system, machine): 57 | try: 58 | return CLOUDFLARED_CONFIG[(system, machine)]["command"] 59 | except KeyError: 60 | raise Exception(f"{machine} is not supported on {system}") 61 | 62 | 63 | def _get_url(system, machine): 64 | try: 65 | return CLOUDFLARED_CONFIG[(system, machine)]["url"] 66 | except KeyError: 67 | raise Exception(f"{machine} is not supported on {system}") 68 | 69 | 70 | # Needed for the darwin package 71 | def _extract_tarball(tar_path, filename): 72 | tar = tarfile.open(tar_path + "/" + filename, "r") 73 | for item in tar: 74 | tar.extract(item, tar_path) 75 | if item.name.find(".tgz") != -1 or item.name.find(".tar") != -1: 76 | extract(item.name, "./" + item.name[: item.name.rfind("/")]) 77 | 78 | 79 | def extract(filename, path): 80 | tar = tarfile.open(filename, "r") 81 | for item in tar: 82 | tar.extract(item, path) 83 | if item.name.find(".tgz") != -1 or item.name.find(".tar") != -1: 84 | extract(item.name, "./" + item.name[: item.name.rfind("/")]) 85 | 86 | 87 | def _download_cloudflared(cloudflared_path, command): 88 | system, machine = platform.system(), platform.machine() 89 | if Path(cloudflared_path, command).exists(): 90 | executable = ( 91 | (cloudflared_path + "/" + "cloudflared") 92 | if (system == "Darwin" and machine in ["x86_64", "arm64"]) 93 | else (cloudflared_path + "/" + command) 94 | ) 95 | update_cloudflared = subprocess.Popen( 96 | [executable, "update"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT 97 | ) 98 | return 99 | print(f" * Downloading cloudflared for {system} {machine}...") 100 | url = _get_url(system, machine) 101 | _download_file(url) 102 | 103 | 104 | def _download_file(url): 105 | local_filename = url.split("/")[-1] 106 | r = requests.get(url, stream=True) 107 | r.raise_for_status() 108 | download_path = str(Path(tempfile.gettempdir(), local_filename)) 109 | with open(download_path, "wb") as f: 110 | file_size = int(r.headers.get("content-length", 50000000)) # type: ignore 111 | chunk_size = 1024 112 | with tqdm( 113 | desc=" * Downloading", 114 | total=file_size, 115 | unit="B", 116 | unit_scale=True, 117 | unit_divisor=1024, 118 | ) as pbar: 119 | for chunk in r.iter_content(chunk_size=chunk_size): 120 | f.write(chunk) 121 | pbar.update(chunk_size) 122 | return download_path 123 | 124 | 125 | def _run_cloudflared(port, metrics_port, tunnel_id=None, config_path=None): 126 | system, machine = platform.system(), platform.machine() 127 | command = _get_command(system, machine) 128 | cloudflared_path = str(Path(tempfile.gettempdir())) 129 | if system == "Darwin": 130 | _download_cloudflared(cloudflared_path, "cloudflared-darwin-amd64.tgz") 131 | _extract_tarball(cloudflared_path, "cloudflared-darwin-amd64.tgz") 132 | else: 133 | _download_cloudflared(cloudflared_path, command) 134 | 135 | executable = str(Path(cloudflared_path, command)) 136 | os.chmod(executable, 0o777) 137 | 138 | cloudflared_command = [ 139 | executable, 140 | "tunnel", 141 | "--metrics", 142 | f"127.0.0.1:{metrics_port}", 143 | ] 144 | if config_path: 145 | cloudflared_command += ["--config", config_path, "run"] 146 | elif tunnel_id: 147 | cloudflared_command += ["--url", f"http://127.0.0.1:{port}", "run", tunnel_id] 148 | else: 149 | cloudflared_command += ["--url", f"http://127.0.0.1:{port}"] 150 | 151 | if system == "Darwin" and machine == "arm64": 152 | cloudflared = subprocess.Popen( 153 | ["arch", "-x86_64"] + cloudflared_command, 154 | stdout=subprocess.DEVNULL, 155 | stderr=subprocess.STDOUT, 156 | ) 157 | else: 158 | cloudflared = subprocess.Popen( 159 | cloudflared_command, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT 160 | ) 161 | 162 | atexit.register(cloudflared.terminate) 163 | localhost_url = f"http://127.0.0.1:{metrics_port}/metrics" 164 | 165 | for _ in range(10): 166 | try: 167 | metrics = requests.get(localhost_url).text 168 | if tunnel_id or config_path: 169 | # If tunnel_id or config_path is provided, we check for cloudflared_tunnel_ha_connections, as no tunnel URL is available in the metrics 170 | if re.search(r"cloudflared_tunnel_ha_connections\s\d", metrics): 171 | # No tunnel URL is available in the metrics, so we return a generic text 172 | tunnel_url = "preconfigured tunnel URL" 173 | break 174 | else: 175 | # If neither tunnel_id nor config_path is provided, we check for the tunnel URL in the metrics 176 | tunnel_url = re.search( 177 | r"(?Phttps?:\/\/[^\s]+.trycloudflare.com)", metrics 178 | ) 179 | if tunnel_url: 180 | tunnel_url = tunnel_url.group("url") 181 | break 182 | except: 183 | time.sleep(3) 184 | else: 185 | raise Exception(f"! Can't connect to Cloudflare Edge") 186 | 187 | return tunnel_url 188 | 189 | 190 | def start_cloudflared(port, metrics_port, tunnel_id=None, config_path=None): 191 | cloudflared_address = _run_cloudflared(port, metrics_port, tunnel_id, config_path) 192 | print(f" * Running on {cloudflared_address}") 193 | print(f" * Traffic stats available on http://127.0.0.1:{metrics_port}/metrics") 194 | 195 | 196 | def run_with_cloudflared(app): 197 | old_run = app.run 198 | 199 | def new_run(*args, **kwargs): 200 | print(" * Starting Cloudflared tunnel...") 201 | port = kwargs.get("port", 5000) 202 | 203 | metrics_port = kwargs.pop("metrics_port", randint(8100, 9000)) 204 | tunnel_id = kwargs.pop("tunnel_id", None) 205 | config_path = kwargs.pop("config_path", None) 206 | 207 | # Starting the Cloudflared tunnel in a separate thread. 208 | tunnel_args = (port, metrics_port, tunnel_id, config_path) 209 | thread = Timer(2, start_cloudflared, args=tunnel_args) 210 | thread.daemon = True 211 | thread.start() 212 | 213 | # Running the Flask app. 214 | old_run(*args, **kwargs) 215 | 216 | app.run = new_run 217 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="flask_cloudflared", 8 | version="0.0.14", 9 | author="Ralf Rademacher", 10 | description="Start a TryCloudflare Tunnel from your flask app.", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="https://github.com/UWUplus/flask-cloudflared", 14 | classifiers=[ 15 | "Programming Language :: Python :: 3.6", 16 | "License :: OSI Approved :: Apache Software License", 17 | "Operating System :: OS Independent", 18 | ], 19 | keywords='flask cloudflared', 20 | install_requires=['Flask>=0.8', 'requests', 'tqdm'], 21 | py_modules=['flask_cloudflared'] 22 | ) --------------------------------------------------------------------------------