Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch from SSH tunneling to FRP #2509

Merged
merged 52 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5decf9f
FRP Poc (#2396)
XciD Oct 21, 2022
160e610
Merge branch 'main' into frp
abidlabs Oct 21, 2022
a1cd740
2509
abidlabs Oct 21, 2022
71cfff9
updated url to testing.gradiodash.com
abidlabs Oct 21, 2022
7f8f70b
gradiotesting
abidlabs Oct 21, 2022
4926921
format, version
abidlabs Oct 21, 2022
2dc5fdf
gradio.live
abidlabs Oct 21, 2022
889b6b8
temp fix for https
abidlabs Oct 21, 2022
45eec71
remove unnecessary tests
abidlabs Oct 22, 2022
5144929
version
abidlabs Oct 22, 2022
90a041a
Merge branch 'main' into frp
abidlabs Oct 24, 2022
a5f261a
updated tunnel logic
abidlabs Oct 24, 2022
0523e4f
formatting and tests
abidlabs Oct 24, 2022
3945568
load testing
aliabid94 Oct 24, 2022
6c3c8d7
changes
aliabid94 Oct 24, 2022
ffb60aa
Make private method + generate privilege key (#2519)
Wauplin Oct 25, 2022
448e6b8
rm load test
aliabid94 Oct 25, 2022
dbf48ec
frp
abidlabs Oct 28, 2022
ecf54a0
formatting
abidlabs Oct 28, 2022
d55e809
merge
abidlabs Oct 28, 2022
744b2ef
Update run.py
abidlabs Oct 28, 2022
f05ecce
Update run.py
abidlabs Oct 28, 2022
aeb269e
merge
abidlabs Oct 30, 2022
49813cc
Merge branch 'main' into frp
abidlabs Nov 2, 2022
d267770
updated message
abidlabs Nov 3, 2022
8b94ec4
Merge branch 'main' into frp
abidlabs Nov 3, 2022
72cb34b
Merge branch 'main' into frp
abidlabs Nov 4, 2022
c27ab48
share=True
abidlabs Nov 29, 2022
5de1f8d
[DO NOT MERGE] Add pymux for FRP (#2747)
XciD Dec 6, 2022
34dc12f
merged main
abidlabs Dec 6, 2022
0084733
removed share=True
abidlabs Dec 6, 2022
b778507
formatting
abidlabs Dec 6, 2022
6c619d8
hello world notebook
abidlabs Dec 6, 2022
d8e0c1c
version
abidlabs Dec 6, 2022
eea49b1
fixes
abidlabs Dec 9, 2022
e94ff92
Merge branch 'main' into frp
abidlabs Dec 9, 2022
500b89a
Merge branch 'frp' of github.com:gradio-app/gradio into frp
abidlabs Dec 9, 2022
730b069
formatting
abidlabs Dec 9, 2022
0bfffca
testing tunneling exists
abidlabs Dec 9, 2022
f6a7db7
tests
abidlabs Dec 9, 2022
81a27ed
formatting
abidlabs Dec 9, 2022
9f12be3
lint
abidlabs Dec 9, 2022
c0b6801
Remove asyncio + kill proc on exit
Wauplin Dec 9, 2022
6c771f0
Merge branch 'main' into frp
abidlabs Dec 10, 2022
1b73287
version
abidlabs Dec 10, 2022
0c7d487
version
abidlabs Dec 10, 2022
510f8f7
Merge branch 'frp' of github.com:gradio-app/gradio into frp
abidlabs Dec 10, 2022
9d0100e
Merge branch 'main' into frp
abidlabs Dec 10, 2022
c3da682
Merge branch 'main' into frp
abidlabs Dec 13, 2022
7e67489
Merge branch 'main' into frp
abidlabs Dec 14, 2022
4875c41
update changelog
abidlabs Dec 14, 2022
18e7ae3
explicit message about reporting
abidlabs Dec 14, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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