Skip to content

Commit

Permalink
Switch from SSH tunneling to FRP (#2509)
Browse files Browse the repository at this point in the history
* FRP Poc (#2396)

* FRP Poc

* Gracefully handle exceptions in thread tunneling

* comments

* Fix share error message when files are built locally (#2502)

* fix share error message

* changelog

* formatting

* tunneling rename

* version

* formatting

* remove test

* changelog

* version

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
Co-authored-by: Wauplin <lucainp@gmail.com>

* 2509

* updated url to testing.gradiodash.com

* gradiotesting

* format, version

* gradio.live

* temp fix for https

* remove unnecessary tests

* version

* updated tunnel logic

* formatting and tests

* load testing

* changes

* Make private method + generate privilege key (#2519)

* rm load test

* frp

* formatting

* Update run.py

* Update run.py

* updated message

* share=True

* [DO NOT MERGE] Add pymux for FRP (#2747)

* Add pymux for FRP

* Cleaning pyamux

* Cleaning pyamux + make it work

* Forgot the thread

* Reformat

* some logs to be removed afterwards

* added share to hello world

* Transform into object

* I guess it's cleaner now

* Handle 404 + Transform to object

* Fix params names

* Add debug

* windows fix

Co-authored-by: Wauplin <lucainp@gmail.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>

* removed share=True

* formatting

* hello world notebook

* version

* fixes

* formatting

* testing tunneling exists

* tests

* formatting

* lint

* Remove asyncio + kill proc on exit

* version

* version

* update changelog

* explicit message about reporting

Co-authored-by: Adrien <adrien@xcid.fr>
Co-authored-by: Wauplin <lucainp@gmail.com>
Co-authored-by: Ali Abid <aabid94@gmail.com>
  • Loading branch information
4 people authored Dec 14, 2022
1 parent 5182460 commit 53005ab
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 143 deletions.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ workspace.code-workspace
*.h5

# log files
.pnpm-debug.log
.pnpm-debug.log

# Local virtualenv for devs
.venv*

# FRP
gradio/frpc_*
17 changes: 15 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
# Upcoming Release

## New Features:
No changes to highlight.

### New Shareable Links

Replaces tunneling logic based on ssh port-forwarding to that based on `frp` by [XciD](https://github.com/XciD) and [Wauplin](https://github.com/Wauplin) in [PR 2509](https://github.com/gradio-app/gradio/pull/2509)

You don't need to do anything differently, but when you set `share=True` in `launch()`,
you'll get this message and a public link that look a little bit different:

```
Setting up a public link... we have recently upgraded the way public links are generated. If you encounter any problems, please downgrade to gradio version 3.13.0
.
Running on public URL: https://bec81a83-5b5c-471e.gradio.live
```

These links are a more secure and scalable way to create shareable demos!

## Bug Fixes:
* Allows `gr.Dataframe()` to take a `pandas.DataFrame` that includes numpy array and other types as its initial value, by [@abidlabs](https://github.com/abidlabs) in [PR 2804](https://github.com/gradio-app/gradio/pull/2804)
Expand Down Expand Up @@ -654,7 +668,6 @@ No changes to highlight.
try to use `Series` or `Parallel` with `Blocks` by [@abidlabs](https://github.com/abidlabs) in [PR 2543](https://github.com/gradio-app/gradio/pull/2543)
* Adds support for audio samples that are in `float64`, `float16`, or `uint16` formats by [@abidlabs](https://github.com/abidlabs) in [PR 2545](https://github.com/gradio-app/gradio/pull/2545)


## Contributors Shoutout:
No changes to highlight.

Expand Down
2 changes: 1 addition & 1 deletion demo/hello_world/run.ipynb
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: hello_world\n", "### The simplest possible Gradio demo. It wraps a 'Hello {name}!' function in an Interface that accepts and returns text.\n", " "]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "def greet(name):\n", " return \"Hello \" + name + \"!\"\n", "\n", "demo = gr.Interface(fn=greet, inputs=\"text\", outputs=\"text\")\n", " \n", "demo.launch() "]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: hello_world\n", "### The simplest possible Gradio demo. It wraps a 'Hello {name}!' function in an Interface that accepts and returns text.\n", " "]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "def greet(name):\n", " return \"Hello \" + name + \"!\"\n", "\n", "demo = gr.Interface(fn=greet, inputs=\"text\", outputs=\"text\")\n", " \n", "if __name__ == \"__main__\":\n", " demo.launch() "]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
3 changes: 2 additions & 1 deletion demo/hello_world/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ def greet(name):

demo = gr.Interface(fn=greet, inputs="text", outputs="text")

demo.launch()
if __name__ == "__main__":
demo.launch()
13 changes: 11 additions & 2 deletions gradio/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from gradio.deprecation import check_deprecated_parameters
from gradio.documentation import document, set_documentation_group
from gradio.exceptions import DuplicateBlockError, InvalidApiName
from gradio.tunneling import CURRENT_TUNNELS
from gradio.utils import (
TupleNoPrint,
check_function_inputs_match,
Expand Down Expand Up @@ -1415,8 +1416,14 @@ def reverse(text):
raise RuntimeError("Share is not supported when you are in Spaces")
try:
if self.share_url is None:
share_url = networking.setup_tunnel(self.server_port, None)
self.share_url = share_url
print(
"\nSetting up a public link... we have recently upgraded the "
"way public links are generated. If you encounter any "
"problems, please report the issue and downgrade to gradio version 3.13.0\n."
)
self.share_url = networking.setup_tunnel(
self.server_name, self.server_port
)
print(strings.en["SHARE_LINK_DISPLAY"].format(self.share_url))
if not (quiet):
print(strings.en["SHARE_LINK_MESSAGE"])
Expand Down Expand Up @@ -1606,6 +1613,8 @@ def block_thread(
except (KeyboardInterrupt, OSError):
print("Keyboard interruption in main thread... closing server.")
self.server.close()
for tunnel in CURRENT_TUNNELS:
tunnel.kill()

def attach_load_events(self):
"""Add a load event for every component whose initial value should be randomized."""
Expand Down
17 changes: 9 additions & 8 deletions gradio/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import uvicorn

from gradio.routes import App
from gradio.tunneling import create_tunnel
from gradio.tunneling import Tunnel

if TYPE_CHECKING: # Only import for type checking (to avoid circular imports).
from gradio.blocks import Blocks
Expand All @@ -26,7 +26,7 @@
INITIAL_PORT_VALUE = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
TRY_NUM_PORTS = int(os.getenv("GRADIO_NUM_PORTS", "100"))
LOCALHOST_NAME = os.getenv("GRADIO_SERVER_NAME", "127.0.0.1")
GRADIO_API_SERVER = "https://api.gradio.app/v1/tunnel-request"
GRADIO_API_SERVER = "https://api.gradio.app/v2/tunnel-request"


class Server(uvicorn.Server):
Expand Down Expand Up @@ -157,14 +157,15 @@ def start_server(
return server_name, port, path_to_local_server, app, server


def setup_tunnel(local_server_port: int, endpoint: str) -> str:
response = requests.get(
endpoint + "/v1/tunnel-request" if endpoint is not None else GRADIO_API_SERVER
)
def setup_tunnel(local_host: str, local_port: int) -> str:
response = requests.get(GRADIO_API_SERVER)
if response and response.status_code == 200:
try:
payload = response.json()[0]
return create_tunnel(payload, LOCALHOST_NAME, local_server_port)
remote_host, remote_port = payload["host"], int(payload["port"])
tunnel = Tunnel(remote_host, remote_port, local_host, local_port)
address = tunnel.start_tunnel()
return address
except Exception as e:
raise RuntimeError(str(e))
else:
Expand All @@ -174,11 +175,11 @@ def setup_tunnel(local_server_port: int, endpoint: str) -> str:
def url_ok(url: str) -> bool:
try:
for _ in range(5):
time.sleep(0.500)
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
r = requests.head(url, timeout=3, verify=False)
if r.status_code in (200, 401, 302): # 401 or 302 if auth is set
return True
time.sleep(0.500)
except (ConnectionError, requests.exceptions.ConnectionError):
return False
199 changes: 98 additions & 101 deletions gradio/tunneling.py
Original file line number Diff line number Diff line change
@@ -1,101 +1,98 @@
"""
This file provides remote port forwarding functionality using paramiko package,
Inspired by: https://github.com/paramiko/paramiko/blob/master/demos/rforward.py
"""

import select
import socket
import sys
import threading
import warnings
from io import StringIO

from cryptography.utils import CryptographyDeprecationWarning

with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning)
import paramiko


def handler(chan, host, port):
sock = socket.socket()
try:
sock.connect((host, port))
except Exception as e:
verbose(f"Forwarding request to {host}:{port} failed: {e}")
return

verbose(
"Connected! Tunnel open "
f"{chan.origin_addr} -> {chan.getpeername()} -> {(host, port)}"
)

while True:
r, w, x = select.select([sock, chan], [], [])
if sock in r:
data = sock.recv(1024)
if len(data) == 0:
break
chan.send(data)
if chan in r:
data = chan.recv(1024)
if len(data) == 0:
break
sock.send(data)
chan.close()
sock.close()
verbose(f"Tunnel closed from {chan.origin_addr}")


def reverse_forward_tunnel(server_port, remote_host, remote_port, transport):
transport.request_port_forward("", server_port)
while True:
chan = transport.accept(1000)
if chan is None:
continue
thr = threading.Thread(target=handler, args=(chan, remote_host, remote_port))
thr.setDaemon(True)
thr.start()


def verbose(s, debug_mode=False):
if debug_mode:
print(s)


def create_tunnel(payload, local_server, local_server_port):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.WarningPolicy())

verbose(f'Conecting to ssh host {payload["host"]}:{payload["port"]} ...')
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
client.connect(
hostname=payload["host"],
port=int(payload["port"]),
username=payload["user"],
pkey=paramiko.RSAKey.from_private_key(StringIO(payload["key"])),
)
except Exception as e:
print(f'*** Failed to connect to {payload["host"]}:{payload["port"]}: {e}')
sys.exit(1)

verbose(
f'Now forwarding remote port {payload["remote_port"]}'
f"to {local_server}:{local_server_port} ..."
)

thread = threading.Thread(
target=reverse_forward_tunnel,
args=(
int(payload["remote_port"]),
local_server,
local_server_port,
client.get_transport(),
),
daemon=True,
)
thread.start()

return payload["share_url"]
import atexit
import os
import platform
import re
import subprocess
from typing import List

VERSION = "0.1"
CURRENT_TUNNELS: List["Tunnel"] = []


class Tunnel:
def __init__(self, remote_host, remote_port, local_host, local_port):
self.proc = None
self.url = None
self.remote_host = remote_host
self.remote_port = remote_port
self.local_host = local_host
self.local_port = local_port

@staticmethod
def download_binary():
machine = platform.machine()
if machine == "x86_64":
machine = "amd64"

# Check if the file exist
binary_name = f"frpc_{platform.system().lower()}_{machine.lower()}"
binary_path = os.path.join(os.path.dirname(__file__), binary_name)

extension = ".exe" if os.name == "nt" else ""

if not os.path.exists(binary_path):
import stat

import requests

binary_url = f"https://cdn-media.huggingface.co/frpc-gradio-{VERSION}/{binary_name}{extension}"
resp = requests.get(binary_url)

if resp.status_code == 403:
raise OSError(
f"Cannot set up a share link as this platform is incompatible. Please "
f"create a GitHub issue with information about your platform: {platform.uname()}"
)

resp.raise_for_status()

# Save file data to local copy
with open(binary_path, "wb") as file:
file.write(resp.content)
st = os.stat(binary_path)
os.chmod(binary_path, st.st_mode | stat.S_IEXEC)

return binary_path

def start_tunnel(self) -> str:
binary_path = self.download_binary()
self.url = self._start_tunnel(binary_path)
return self.url

def kill(self):
if self.proc is not None:
print(f"Killing tunnel {self.local_host}:{self.local_port} <> {self.url}")
self.proc.terminate()
self.proc = None

def _start_tunnel(self, binary: str) -> str:
CURRENT_TUNNELS.append(self)
command = [
binary,
"http",
"-n",
"random",
"-l",
str(self.local_port),
"-i",
self.local_host,
"--uc",
"--sd",
"random",
"--ue",
"--server_addr",
f"{self.remote_host}:{self.remote_port}",
"--disable_log_color",
]

self.proc = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
atexit.register(self.kill)
url = ""
while url == "":
line = self.proc.stdout.readline()
line = line.decode("utf-8")
if "start proxy success" in line:
url = re.search("start proxy success: (.+)\n", line).group(1)
return url
2 changes: 1 addition & 1 deletion gradio/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.13.0
3.13.0
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ matplotlib
numpy
orjson
pandas
paramiko
pillow
pycryptodome
python-multipart
Expand Down
6 changes: 0 additions & 6 deletions test/test_networking.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Contains tests for networking.py and app.py"""

import os
import unittest.mock as mock
import urllib
import warnings

Expand Down Expand Up @@ -75,11 +74,6 @@ def test_start_server(self):


class TestURLs:
def test_setup_tunnel(self):
networking.create_tunnel = mock.MagicMock(return_value="test")
res = networking.setup_tunnel(None, None)
assert res == "test"

def test_url_ok(self):
res = networking.url_ok("https://www.gradio.app")
assert res
Loading

0 comments on commit 53005ab

Please sign in to comment.