├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.md ├── README.md ├── install.py ├── preload.py ├── pyproject.toml └── scripts ├── ssh_tunnel.py └── try_cloudflare.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | # End of https://www.toptal.com/developers/gitignore/api/python 174 | 175 | id_rsa 176 | id_rsa.pub 177 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: trailing-whitespace 6 | args: [--markdown-linebreak-ext=md] 7 | - id: end-of-file-fixer 8 | 9 | - repo: https://github.com/asottile/pyupgrade 10 | rev: v3.3.1 11 | hooks: 12 | - id: pyupgrade 13 | args: [--py310-plus] 14 | 15 | - repo: https://github.com/psf/black 16 | rev: 23.1.0 17 | hooks: 18 | - id: black 19 | 20 | - repo: https://github.com/charliermarsh/ruff-pre-commit 21 | # Ruff version. 22 | rev: "v0.0.244" 23 | hooks: 24 | - id: ruff 25 | args: [--fix] 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2023 Bingsu 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sd-webui-tunnels 2 | 3 | Tunneling extension for [AUTOMATIC1111/stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 4 | 5 | ## Usage 6 | 7 | ### [cloudflared](https://try.cloudflare.com/) 8 | 9 | add `--cloudflared` to commandline options. 10 | 11 | ### [localhost.run](https://localhost.run/) 12 | 13 | add `--localhostrun` to commandline options. 14 | 15 | ### [remote.moe](https://github.com/fasmide/remotemoe) 16 | 17 | add `--remotemoe` to commandline options. 18 | 19 | ### [googleusercontent.com](https://colab.research.google.com) 20 | 21 | add `--googleusercontent` to commandline options. 22 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import launch 2 | 3 | if not launch.is_installed("pycloudflared"): 4 | launch.run_pip("install pycloudflared", "pycloudflared") 5 | -------------------------------------------------------------------------------- /preload.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | def preload(parser: argparse.ArgumentParser): 5 | parser.add_argument( 6 | "--cloudflared", 7 | action="store_true", 8 | help="use trycloudflare, alternative to gradio --share", 9 | ) 10 | 11 | parser.add_argument( 12 | "--localhostrun", 13 | action="store_true", 14 | help="use localhost.run, alternative to gradio --share", 15 | ) 16 | 17 | parser.add_argument( 18 | "--remotemoe", 19 | action="store_true", 20 | help="use remote.moe, alternative to gradio --share", 21 | ) 22 | 23 | parser.add_argument( 24 | "--googleusercontent", 25 | action="store_true", 26 | help="use googleusercontent.com, alternative to gradio --share", 27 | ) 28 | 29 | parser.add_argument( 30 | "--multiple", 31 | action="store_true", 32 | help="use cloudflared and remotemoe", 33 | ) 34 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sd-webui-tunnels" 3 | version = "23.2.1" 4 | description = "Tunneling extension for automatic1111 sd-webui" 5 | authors = [ 6 | {name = "dowon", email = "ks2515@naver.com"}, 7 | ] 8 | requires-python = ">=3.8" 9 | readme = "README.md" 10 | license = {text = "MIT"} 11 | 12 | [project.urls] 13 | repository = "https://github.com/Bing-su/sd-webui-tunnels" 14 | 15 | [tool.isort] 16 | profile = "black" 17 | known_first_party = ["modules", "launch"] 18 | 19 | [tool.ruff] 20 | select = ["A", "B", "C4", "E", "F", "I001", "N", "PT", "UP", "W"] 21 | ignore = ["B008", "B905", "E501"] 22 | unfixable = ["F401"] 23 | 24 | [tool.ruff.isort] 25 | known-first-party = ["modules", "launch"] 26 | -------------------------------------------------------------------------------- /scripts/ssh_tunnel.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import re 3 | import shlex 4 | import subprocess 5 | from pathlib import Path 6 | from tempfile import TemporaryDirectory 7 | from typing import Union 8 | from gradio import strings 9 | import os, requests, stat 10 | 11 | from modules.shared import cmd_opts 12 | 13 | #https://github.com/gradio-app/gradio/blob/main/gradio/tunneling.py modified 14 | def kill(self): 15 | if self.proc is not None: 16 | print(f"Killing tunnel 127.0.0.1:7860") 17 | self.proc.terminate() 18 | self.proc = None 19 | 20 | def gradio_tunnel(): 21 | script_path = os.path.dirname(os.path.abspath(__file__)) 22 | binary_path = os.path.join(script_path, "frpc_linux_amd64") 23 | response = requests.get("https://api.gradio.app/v2/tunnel-request") 24 | if response and response.status_code == 200: 25 | try: 26 | payload = response.json()[0] 27 | remote_host, remote_port = payload["host"], int(payload["port"]) 28 | resp = requests.get("https://cdn-media.huggingface.co/frpc-gradio-0.1/frpc_linux_amd64") 29 | with open(binary_path, "wb") as file: 30 | file.write(resp.content) 31 | st = os.stat(binary_path) 32 | os.chmod(binary_path, st.st_mode | stat.S_IEXEC) 33 | command = [binary_path,"http","-n","random","-l","7860","-i","127.0.0.1","--uc","--sd","random","--ue","--server_addr",f"{remote_host}:{remote_port}","--disable_log_color",] 34 | proc = subprocess.Popen( 35 | command, stdout=subprocess.PIPE, stderr=subprocess.PIPE 36 | ) 37 | atexit.register(kill) 38 | url = "" 39 | while url == "": 40 | if proc.stdout is None: 41 | continue 42 | line = proc.stdout.readline() 43 | line = line.decode("utf-8") 44 | if "start proxy success" in line: 45 | result = re.search("start proxy success: (.+)\n", line) 46 | if result is None: 47 | raise ValueError("Could not create share URL") 48 | else: 49 | url = result.group(1) 50 | return url 51 | except Exception as e: 52 | raise RuntimeError(str(e)) 53 | else: 54 | raise RuntimeError("Could not get share link from Gradio API Server.") 55 | 56 | LOCALHOST_RUN = "localhost.run" 57 | REMOTE_MOE = "remote.moe" 58 | localhostrun_pattern = re.compile(r"(?Phttps?://\S+\.lhr\.life)") 59 | remotemoe_pattern = re.compile(r"(?Phttps?://\S+\.remote\.moe)") 60 | 61 | 62 | def gen_key(path: Union[str, Path]) -> None: 63 | path = Path(path) 64 | arg_string = f'ssh-keygen -t rsa -b 4096 -N "" -q -f {path.as_posix()}' 65 | args = shlex.split(arg_string) 66 | subprocess.run(args, check=True) 67 | path.chmod(0o600) 68 | 69 | 70 | def ssh_tunnel(host: str = LOCALHOST_RUN) -> None: 71 | ssh_name = "id_rsa" 72 | ssh_path = Path(__file__).parent.parent / ssh_name 73 | 74 | tmp = None 75 | if not ssh_path.exists(): 76 | try: 77 | gen_key(ssh_path) 78 | # write permission error or etc 79 | except subprocess.CalledProcessError: 80 | tmp = TemporaryDirectory() 81 | ssh_path = Path(tmp.name) / ssh_name 82 | gen_key(ssh_path) 83 | 84 | port = cmd_opts.port if cmd_opts.port else 7860 85 | 86 | arg_string = f"ssh -R 80:127.0.0.1:{port} -o StrictHostKeyChecking=no -i {ssh_path.as_posix()} {host}" 87 | args = shlex.split(arg_string) 88 | 89 | tunnel = subprocess.Popen( 90 | args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8" 91 | ) 92 | 93 | atexit.register(tunnel.terminate) 94 | if tmp is not None: 95 | atexit.register(tmp.cleanup) 96 | 97 | tunnel_url = "" 98 | lines = 27 if host == LOCALHOST_RUN else 5 99 | pattern = localhostrun_pattern if host == LOCALHOST_RUN else remotemoe_pattern 100 | 101 | for _ in range(lines): 102 | line = tunnel.stdout.readline() 103 | if line.startswith("Warning"): 104 | print(line, end="") 105 | 106 | url_match = pattern.search(line) 107 | if url_match: 108 | tunnel_url = url_match.group("url") 109 | if lines == 27: 110 | os.environ['LOCALHOST_RUN'] = tunnel_url 111 | else: 112 | os.environ['REMOTE_MOE'] = tunnel_url 113 | break 114 | else: 115 | raise RuntimeError(f"Failed to run {host}") 116 | 117 | if not cmd_opts.multiple: 118 | strings.en["SHARE_LINK_MESSAGE"] = f"Public WebUI Colab URL: {tunnel_url}" 119 | 120 | def googleusercontent_tunnel(): 121 | colab_url = os.getenv('colab_url') 122 | strings.en["SHARE_LINK_MESSAGE"] = f"WebUI Colab URL: {colab_url}" 123 | 124 | if cmd_opts.localhostrun: 125 | print("localhost.run detected, trying to connect...") 126 | ssh_tunnel(LOCALHOST_RUN) 127 | 128 | if cmd_opts.remotemoe: 129 | print("remote.moe detected, trying to connect...") 130 | ssh_tunnel(REMOTE_MOE) 131 | 132 | if cmd_opts.googleusercontent: 133 | print("googleusercontent.com detected, trying to connect...") 134 | googleusercontent_tunnel() 135 | 136 | if cmd_opts.multiple: 137 | print("all detected, remote.moe trying to connect...") 138 | try: 139 | ssh_tunnel(LOCALHOST_RUN) 140 | except: 141 | pass 142 | # try: 143 | # ssh_tunnel(REMOTE_MOE) 144 | # except: 145 | # pass 146 | try: 147 | os.environ['GRADIO_TUNNEL'] = gradio_tunnel() 148 | except: 149 | pass 150 | strings.en["RUNNING_LOCALLY_SEPARATED"] = f"Public WebUI Colab URL: {os.getenv('REMOTE_MOE')} \nPublic WebUI Colab URL: {os.getenv('GRADIO_TUNNEL')} \nPublic WebUI Colab URL: {os.getenv('LOCALHOST_RUN')}" 151 | strings.en["SHARE_LINK_DISPLAY"] = "Please do not use this link we are getting ERROR: Exception in ASGI application: {}" 152 | -------------------------------------------------------------------------------- /scripts/try_cloudflare.py: -------------------------------------------------------------------------------- 1 | from pycloudflared import try_cloudflare 2 | 3 | from modules.shared import cmd_opts 4 | 5 | from gradio import strings 6 | 7 | import os 8 | 9 | if cmd_opts.cloudflared: 10 | print("cloudflared detected, trying to connect...") 11 | port = cmd_opts.port if cmd_opts.port else 7860 12 | tunnel_url = try_cloudflare(port=port, verbose=False) 13 | os.environ['webui_url'] = tunnel_url.tunnel 14 | colab_url = os.getenv('colab_url') 15 | strings.en["SHARE_LINK_MESSAGE"] = f"Public WebUI Colab URL: {tunnel_url.tunnel}" 16 | strings.en["PUBLIC_SHARE_TRUE"] = f"Public WebUI Colab URL: {tunnel_url.tunnel}" 17 | 18 | if cmd_opts.multiple: 19 | print("all detected, cloudflared trying to connect...") 20 | port = cmd_opts.port if cmd_opts.port else 7860 21 | tunnel_url = try_cloudflare(port=port, verbose=False) 22 | os.environ['webui_url'] = tunnel_url.tunnel 23 | colab_url = os.getenv('colab_url') 24 | strings.en["SHARE_LINK_MESSAGE"] = f"Public WebUI Colab URL: {tunnel_url.tunnel}" 25 | strings.en["PUBLIC_SHARE_TRUE"] = f"Public WebUI Colab URL: {tunnel_url.tunnel}" 26 | --------------------------------------------------------------------------------