Upload 5 files
Browse files- colab_tunnel/__init__.py +12 -1
- colab_tunnel/_config.py +5 -1
- colab_tunnel/_ssh.py +175 -0
- colab_tunnel/_tunnels.py +63 -10
- colab_tunnel/_utils.py +18 -11
colab_tunnel/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
"""
|
| 2 |
colab-proxy — утилиты для туннелирования веб-интерфейсов в Google Colab.
|
| 3 |
|
|
@@ -16,6 +17,9 @@ from ._tunnels import (
|
|
| 16 |
get_localt_url,
|
| 17 |
get_gradio_url,
|
| 18 |
get_native_url,
|
|
|
|
|
|
|
|
|
|
| 19 |
)
|
| 20 |
|
| 21 |
__version__ = '1.0.0'
|
|
@@ -29,6 +33,9 @@ __all__ = [
|
|
| 29 |
'get_localt_url',
|
| 30 |
'get_gradio_url',
|
| 31 |
'get_native_url',
|
|
|
|
|
|
|
|
|
|
| 32 |
'download',
|
| 33 |
'run',
|
| 34 |
'WORK_FOLDER',
|
|
@@ -41,6 +48,9 @@ proxies_functions: dict = {
|
|
| 41 |
'localt': get_localt_url,
|
| 42 |
'gradio': get_gradio_url,
|
| 43 |
'native': get_native_url,
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
|
|
@@ -61,7 +71,8 @@ def get_share_link(host: str, port: int) -> str:
|
|
| 61 |
|
| 62 |
Args:
|
| 63 |
host: Имя провайдера: 'cloudflared', 'tmole', 'tunwg',
|
| 64 |
-
'localt', 'gradio', 'native'
|
|
|
|
| 65 |
port: Локальный порт веб-интерфейса.
|
| 66 |
|
| 67 |
Returns:
|
|
|
|
| 1 |
+
# ./colab_tunnel/__init__.py
|
| 2 |
"""
|
| 3 |
colab-proxy — утилиты для туннелирования веб-интерфейсов в Google Colab.
|
| 4 |
|
|
|
|
| 17 |
get_localt_url,
|
| 18 |
get_gradio_url,
|
| 19 |
get_native_url,
|
| 20 |
+
get_mmar_url,
|
| 21 |
+
get_tunnelite_url,
|
| 22 |
+
get_beeceptor_url,
|
| 23 |
)
|
| 24 |
|
| 25 |
__version__ = '1.0.0'
|
|
|
|
| 33 |
'get_localt_url',
|
| 34 |
'get_gradio_url',
|
| 35 |
'get_native_url',
|
| 36 |
+
'get_mmar_url',
|
| 37 |
+
'get_tunnelite_url',
|
| 38 |
+
'get_beeceptor_url',
|
| 39 |
'download',
|
| 40 |
'run',
|
| 41 |
'WORK_FOLDER',
|
|
|
|
| 48 |
'localt': get_localt_url,
|
| 49 |
'gradio': get_gradio_url,
|
| 50 |
'native': get_native_url,
|
| 51 |
+
'mmar': get_mmar_url,
|
| 52 |
+
'tunnelite': get_tunnelite_url,
|
| 53 |
+
'beeceptor': get_beeceptor_url,
|
| 54 |
}
|
| 55 |
|
| 56 |
|
|
|
|
| 71 |
|
| 72 |
Args:
|
| 73 |
host: Имя провайдера: 'cloudflared', 'tmole', 'tunwg',
|
| 74 |
+
'localt', 'gradio', 'native', 'mmar', 'tunnelite',
|
| 75 |
+
'beeceptor' — или 'all' для всех сразу.
|
| 76 |
port: Локальный порт веб-интерфейса.
|
| 77 |
|
| 78 |
Returns:
|
colab_tunnel/_config.py
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
|
|
| 1 |
from os import environ
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
# Переопределяется переменной окружения colab_tunnel_WORK_DIR
|
| 5 |
WORK_FOLDER = Path(environ.get('TUNNEL_WORK_DIR', '/content/.config'))
|
| 6 |
|
| 7 |
-
|
| 8 |
tmole_bin = WORK_FOLDER / 'tmole'
|
| 9 |
tunwg_bin = WORK_FOLDER / 'tunwg'
|
| 10 |
go_localt_bin = WORK_FOLDER / 'go_localt'
|
| 11 |
gradio_bin = WORK_FOLDER / 'frpc_linux_amd64'
|
|
|
|
|
|
|
|
|
|
| 12 |
colab_native_url = WORK_FOLDER / 'colab_url.txt'
|
| 13 |
links_file = WORK_FOLDER / 'links.txt'
|
| 14 |
|
|
|
|
| 1 |
+
# ./colab_tunnel/_config.py
|
| 2 |
from os import environ
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
# Переопределяется переменной окружения colab_tunnel_WORK_DIR
|
| 6 |
WORK_FOLDER = Path(environ.get('TUNNEL_WORK_DIR', '/content/.config'))
|
| 7 |
|
| 8 |
+
cloudflare_bin = WORK_FOLDER / 'cloudflared'
|
| 9 |
tmole_bin = WORK_FOLDER / 'tmole'
|
| 10 |
tunwg_bin = WORK_FOLDER / 'tunwg'
|
| 11 |
go_localt_bin = WORK_FOLDER / 'go_localt'
|
| 12 |
gradio_bin = WORK_FOLDER / 'frpc_linux_amd64'
|
| 13 |
+
mmar_bin = WORK_FOLDER / 'mmar'
|
| 14 |
+
tunnelite_bin = WORK_FOLDER / 'tunnelite'
|
| 15 |
+
beeceptor_bin = WORK_FOLDER / 'beeceptor-cli'
|
| 16 |
colab_native_url = WORK_FOLDER / 'colab_url.txt'
|
| 17 |
links_file = WORK_FOLDER / 'links.txt'
|
| 18 |
|
colab_tunnel/_ssh.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from atexit import register as exit_register
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from re import search
|
| 5 |
+
from select import select
|
| 6 |
+
from subprocess import DEVNULL, PIPE, Popen
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
from colab_tunnel._utils import terminate_process, run
|
| 10 |
+
|
| 11 |
+
# Глобальный реестр для предотвращения Garbage Collection (сборки мусора) процессов и их труб
|
| 12 |
+
_active_tunnels: list[Popen] = []
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def ensure_ssh_key() -> None:
|
| 16 |
+
"""
|
| 17 |
+
Гарантирует наличие приватного SSH-ключа в системе.
|
| 18 |
+
"""
|
| 19 |
+
ssh_dir = Path.home() / '.ssh'
|
| 20 |
+
ssh_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
| 21 |
+
key_file = ssh_dir / 'id_ed25519'
|
| 22 |
+
|
| 23 |
+
if not key_file.exists():
|
| 24 |
+
import subprocess
|
| 25 |
+
subprocess.run(
|
| 26 |
+
['ssh-keygen', '-t', 'ed25519', '-N', '', '-f', str(key_file)],
|
| 27 |
+
stdout=DEVNULL,
|
| 28 |
+
stderr=DEVNULL
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_ssh_tunnel_url(
|
| 33 |
+
start_commands: list[str],
|
| 34 |
+
process_name: str,
|
| 35 |
+
url_pattern: str,
|
| 36 |
+
timeout: float = 15.0,
|
| 37 |
+
) -> str:
|
| 38 |
+
"""
|
| 39 |
+
Запускает SSH-процесс с открытым stdin=PIPE для поддержания соединения.
|
| 40 |
+
"""
|
| 41 |
+
ensure_ssh_key()
|
| 42 |
+
|
| 43 |
+
# Завершаем старые процессы
|
| 44 |
+
run(f'pkill -f "{process_name}"')
|
| 45 |
+
|
| 46 |
+
# Важно: stdin=PIPE оставляем ОТКРЫТЫМ, чтобы SSH не получал EOF и не выходил
|
| 47 |
+
process = Popen(start_commands, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
| 48 |
+
|
| 49 |
+
stdout_raw = process.stdout.raw
|
| 50 |
+
stderr_raw = process.stderr.raw
|
| 51 |
+
|
| 52 |
+
# Делаем сырые дескрипторы неблокирующими
|
| 53 |
+
for raw_stream in (stdout_raw, stderr_raw):
|
| 54 |
+
try:
|
| 55 |
+
os.set_blocking(raw_stream.fileno(), False)
|
| 56 |
+
except Exception:
|
| 57 |
+
pass
|
| 58 |
+
|
| 59 |
+
start_time = time.time()
|
| 60 |
+
buffer = b""
|
| 61 |
+
|
| 62 |
+
while time.time() - start_time < timeout:
|
| 63 |
+
ready_readers, _, _ = select([stdout_raw, stderr_raw], [], [], 0.1)
|
| 64 |
+
|
| 65 |
+
read_any = False
|
| 66 |
+
for reader in ready_readers:
|
| 67 |
+
try:
|
| 68 |
+
chunk = reader.read(1024)
|
| 69 |
+
if chunk:
|
| 70 |
+
buffer += chunk
|
| 71 |
+
read_any = True
|
| 72 |
+
except (BlockingIOError, ValueError):
|
| 73 |
+
pass
|
| 74 |
+
except Exception:
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
decoded_text = buffer.decode('utf-8', errors='ignore')
|
| 78 |
+
match = search(url_pattern, decoded_text)
|
| 79 |
+
if match:
|
| 80 |
+
# Регистрируем завершение при выходе из Python
|
| 81 |
+
exit_register(terminate_process, process_name, process)
|
| 82 |
+
# Добавляем в глобальный список, чтобы сохранить процесс и stdin-трубу открытыми
|
| 83 |
+
_active_tunnels.append(process)
|
| 84 |
+
return match.group()
|
| 85 |
+
|
| 86 |
+
if not read_any and process.poll() is not None:
|
| 87 |
+
break
|
| 88 |
+
|
| 89 |
+
time.sleep(0.05)
|
| 90 |
+
|
| 91 |
+
process.terminate()
|
| 92 |
+
run(f'pkill -f "{process_name}"')
|
| 93 |
+
|
| 94 |
+
final_output = buffer.decode('utf-8', errors='ignore')
|
| 95 |
+
raise RuntimeError(
|
| 96 |
+
f'Не удалось получить SSH-ссылку за {timeout} сек. Вывод процесса:\n{final_output}'
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ---------------------------------------------------------------------------
|
| 101 |
+
# Провайдеры
|
| 102 |
+
# ---------------------------------------------------------------------------
|
| 103 |
+
|
| 104 |
+
def get_optimistix_url(port: int) -> str:
|
| 105 |
+
cmd = [
|
| 106 |
+
'ssh',
|
| 107 |
+
'-o', 'StrictHostKeyChecking=no',
|
| 108 |
+
'-o', 'UserKnownHostsFile=/dev/null',
|
| 109 |
+
'-o', 'ExitOnForwardFailure=yes',
|
| 110 |
+
'-o', 'ServerAliveInterval=10',
|
| 111 |
+
'-o', 'ServerAliveCountMax=3',
|
| 112 |
+
'-p', '1122',
|
| 113 |
+
'-R', f'80:127.0.0.1:{port}',
|
| 114 |
+
'ssh.optimistixtunnel.com'
|
| 115 |
+
]
|
| 116 |
+
return get_ssh_tunnel_url(
|
| 117 |
+
start_commands=cmd,
|
| 118 |
+
process_name='ssh.optimistixtunnel.com',
|
| 119 |
+
url_pattern=r'https://\S+\.otnl\.link',
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def get_srvus_url(port: int) -> str:
|
| 124 |
+
cmd = [
|
| 125 |
+
'ssh',
|
| 126 |
+
'-o', 'StrictHostKeyChecking=no',
|
| 127 |
+
'-o', 'UserKnownHostsFile=/dev/null',
|
| 128 |
+
'-o', 'ExitOnForwardFailure=yes',
|
| 129 |
+
'-o', 'ServerAliveInterval=10',
|
| 130 |
+
'-o', 'ServerAliveCountMax=3',
|
| 131 |
+
'-R', f'1:127.0.0.1:{port}',
|
| 132 |
+
'srv.us'
|
| 133 |
+
]
|
| 134 |
+
return get_ssh_tunnel_url(
|
| 135 |
+
start_commands=cmd,
|
| 136 |
+
process_name='srv.us',
|
| 137 |
+
url_pattern=r'https://[a-z0-9]{10,}\.srv\.us/?',
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def get_serveo_url(port: int) -> str:
|
| 142 |
+
cmd = [
|
| 143 |
+
'ssh',
|
| 144 |
+
'-o', 'StrictHostKeyChecking=no',
|
| 145 |
+
'-o', 'UserKnownHostsFile=/dev/null',
|
| 146 |
+
'-o', 'ExitOnForwardFailure=yes',
|
| 147 |
+
'-o', 'ServerAliveInterval=10',
|
| 148 |
+
'-o', 'ServerAliveCountMax=3',
|
| 149 |
+
'-R', f'80:127.0.0.1:{port}',
|
| 150 |
+
'serveo.net'
|
| 151 |
+
]
|
| 152 |
+
return get_ssh_tunnel_url(
|
| 153 |
+
start_commands=cmd,
|
| 154 |
+
process_name='serveo.net',
|
| 155 |
+
url_pattern=r'https://\S+\.serveousercontent\.com',
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def get_localhostrun_url(port: int) -> str:
|
| 160 |
+
cmd = [
|
| 161 |
+
'ssh',
|
| 162 |
+
'-o', 'StrictHostKeyChecking=no',
|
| 163 |
+
'-o', 'UserKnownHostsFile=/dev/null',
|
| 164 |
+
'-o', 'ExitOnForwardFailure=yes',
|
| 165 |
+
'-o', 'ServerAliveInterval=10',
|
| 166 |
+
'-o', 'ServerAliveCountMax=3',
|
| 167 |
+
'-R', f'80:127.0.0.1:{port}',
|
| 168 |
+
'nokey@localhost.run'
|
| 169 |
+
]
|
| 170 |
+
return get_ssh_tunnel_url(
|
| 171 |
+
start_commands=cmd,
|
| 172 |
+
process_name='localhost.run',
|
| 173 |
+
url_pattern=r'https://(?!admin\b)[a-zA-Z0-9-]+\.(?:localhost\.run|lhr\.life)',
|
| 174 |
+
)
|
| 175 |
+
|
colab_tunnel/_tunnels.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from atexit import register as exit_register
|
| 2 |
from pathlib import Path
|
| 3 |
from re import search
|
|
@@ -8,8 +9,9 @@ from requests import get as get_url
|
|
| 8 |
|
| 9 |
from ._config import (
|
| 10 |
WORK_FOLDER,
|
| 11 |
-
|
| 12 |
-
go_localt_bin, gradio_bin,
|
|
|
|
| 13 |
)
|
| 14 |
from ._utils import (
|
| 15 |
download, unpack_archive, run,
|
|
@@ -27,24 +29,31 @@ def get_revproxy_url(
|
|
| 27 |
lines_to_read: int,
|
| 28 |
url_pattern: str = r'https://\S+',
|
| 29 |
write_link: bool = False,
|
|
|
|
| 30 |
) -> str:
|
| 31 |
if not bin_path.exists():
|
| 32 |
-
files_before = {f
|
| 33 |
|
| 34 |
if need_unpack:
|
| 35 |
unpack_archive(download(bin_url, progress=False), WORK_FOLDER, rm_archive=True)
|
| 36 |
else:
|
| 37 |
download(bin_url, save_path=bin_path.parent, progress=False)
|
| 38 |
|
| 39 |
-
files_after = {f
|
| 40 |
-
new_files = files_after - files_before
|
| 41 |
|
| 42 |
if len(new_files) == 1:
|
| 43 |
-
move_path(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
else:
|
| 45 |
raise RuntimeError(
|
| 46 |
f'ошибка при определении нового файла после распаковки!\n'
|
| 47 |
-
f'ожидалс
|
| 48 |
)
|
| 49 |
|
| 50 |
bin_path.chmod(0o777)
|
|
@@ -52,7 +61,14 @@ def get_revproxy_url(
|
|
| 52 |
if is_process_running(bin_path.name):
|
| 53 |
run(f'pkill -f {bin_path.name}')
|
| 54 |
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
stream = process.stderr if read_from_stderr else process.stdout
|
| 57 |
lines: list[str] = []
|
| 58 |
|
|
@@ -107,8 +123,8 @@ def get_cloudflared_url(port: int) -> str:
|
|
| 107 |
return get_revproxy_url(
|
| 108 |
bin_url='https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64',
|
| 109 |
need_unpack=False,
|
| 110 |
-
bin_path=
|
| 111 |
-
start_commands=[str(
|
| 112 |
read_from_stderr=True,
|
| 113 |
lines_to_read=6,
|
| 114 |
url_pattern=r'(?P<url>https?://\S+\.trycloudflare\.com)',
|
|
@@ -160,6 +176,43 @@ def get_gradio_url(port: int) -> str:
|
|
| 160 |
raise RuntimeError(f'после {max_attempts} попыток поднять туннель градио не удалось: {last_error}')
|
| 161 |
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
def get_native_url(port: int) -> str:
|
| 164 |
try:
|
| 165 |
from google.colab.output import eval_js # type: ignore[import]
|
|
|
|
| 1 |
+
# ./colab_tunnel/_tunnels.py
|
| 2 |
from atexit import register as exit_register
|
| 3 |
from pathlib import Path
|
| 4 |
from re import search
|
|
|
|
| 9 |
|
| 10 |
from ._config import (
|
| 11 |
WORK_FOLDER,
|
| 12 |
+
cloudflare_bin, tmole_bin, tunwg_bin,
|
| 13 |
+
go_localt_bin, gradio_bin, mmar_bin,
|
| 14 |
+
tunnelite_bin, beeceptor_bin, links_file
|
| 15 |
)
|
| 16 |
from ._utils import (
|
| 17 |
download, unpack_archive, run,
|
|
|
|
| 29 |
lines_to_read: int,
|
| 30 |
url_pattern: str = r'https://\S+',
|
| 31 |
write_link: bool = False,
|
| 32 |
+
stdin_input: str | None = None,
|
| 33 |
) -> str:
|
| 34 |
if not bin_path.exists():
|
| 35 |
+
files_before = {f for f in WORK_FOLDER.iterdir() if f.is_file()}
|
| 36 |
|
| 37 |
if need_unpack:
|
| 38 |
unpack_archive(download(bin_url, progress=False), WORK_FOLDER, rm_archive=True)
|
| 39 |
else:
|
| 40 |
download(bin_url, save_path=bin_path.parent, progress=False)
|
| 41 |
|
| 42 |
+
files_after = {f for f in WORK_FOLDER.iterdir() if f.is_file()}
|
| 43 |
+
new_files = list(files_after - files_before)
|
| 44 |
|
| 45 |
if len(new_files) == 1:
|
| 46 |
+
move_path(new_files[0], bin_path)
|
| 47 |
+
elif len(new_files) > 1:
|
| 48 |
+
largest_file = max(new_files, key=lambda f: f.stat().st_size)
|
| 49 |
+
move_path(largest_file, bin_path)
|
| 50 |
+
for f in new_files:
|
| 51 |
+
if f != largest_file:
|
| 52 |
+
f.unlink(missing_ok=True)
|
| 53 |
else:
|
| 54 |
raise RuntimeError(
|
| 55 |
f'ошибка при определении нового файла после распаковки!\n'
|
| 56 |
+
f'ожидалось появление новых файлов в директории {WORK_FOLDER}'
|
| 57 |
)
|
| 58 |
|
| 59 |
bin_path.chmod(0o777)
|
|
|
|
| 61 |
if is_process_running(bin_path.name):
|
| 62 |
run(f'pkill -f {bin_path.name}')
|
| 63 |
|
| 64 |
+
# Если требуется отправка ввода, открываем канал stdin
|
| 65 |
+
stdin_channel = PIPE if stdin_input is not None else None
|
| 66 |
+
process = Popen(start_commands, stdout=PIPE, stderr=PIPE, stdin=stdin_channel, text=True)
|
| 67 |
+
|
| 68 |
+
if stdin_input is not None and process.stdin:
|
| 69 |
+
process.stdin.write(stdin_input)
|
| 70 |
+
process.stdin.flush()
|
| 71 |
+
|
| 72 |
stream = process.stderr if read_from_stderr else process.stdout
|
| 73 |
lines: list[str] = []
|
| 74 |
|
|
|
|
| 123 |
return get_revproxy_url(
|
| 124 |
bin_url='https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64',
|
| 125 |
need_unpack=False,
|
| 126 |
+
bin_path=cloudflare_bin,
|
| 127 |
+
start_commands=[str(cloudflare_bin), 'tunnel', '--url', f'http://127.0.0.1:{port}'],
|
| 128 |
read_from_stderr=True,
|
| 129 |
lines_to_read=6,
|
| 130 |
url_pattern=r'(?P<url>https?://\S+\.trycloudflare\.com)',
|
|
|
|
| 176 |
raise RuntimeError(f'после {max_attempts} попыток поднять туннель градио не удалось: {last_error}')
|
| 177 |
|
| 178 |
|
| 179 |
+
def get_mmar_url(port: int) -> str:
|
| 180 |
+
return get_revproxy_url(
|
| 181 |
+
bin_url='https://github.com/yusuf-musleh/mmar/releases/latest/download/mmar_Linux_x86_64.tar.gz',
|
| 182 |
+
need_unpack=True,
|
| 183 |
+
bin_path=mmar_bin,
|
| 184 |
+
start_commands=[str(mmar_bin), 'client', '--local-port', str(port)],
|
| 185 |
+
read_from_stderr=True,
|
| 186 |
+
lines_to_read=10,
|
| 187 |
+
url_pattern=r'(?P<url>https?://\S+\.mmar\.dev)',
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def get_tunnelite_url(port: int) -> str:
|
| 192 |
+
return get_revproxy_url(
|
| 193 |
+
bin_url='https://github.com/hehepiska/tunnellite-linux/releases/download/latest/tunnelite',
|
| 194 |
+
need_unpack=False,
|
| 195 |
+
bin_path=tunnelite_bin,
|
| 196 |
+
start_commands=[str(tunnelite_bin), f'http://127.0.0.1:{port}'],
|
| 197 |
+
read_from_stderr=False,
|
| 198 |
+
lines_to_read=60,
|
| 199 |
+
url_pattern=r'(?P<url>https?://\S+\.tunnelite\.com)',
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def get_beeceptor_url(port: int) -> str:
|
| 204 |
+
return get_revproxy_url(
|
| 205 |
+
bin_url='https://cdn.beeceptor.com/downloads/cli/latest/beeceptor-cli-linux-x64.tar.gz',
|
| 206 |
+
need_unpack=True,
|
| 207 |
+
bin_path=beeceptor_bin,
|
| 208 |
+
start_commands=[str(beeceptor_bin), '-p', str(port), '-y'],
|
| 209 |
+
read_from_stderr=False,
|
| 210 |
+
lines_to_read=15,
|
| 211 |
+
url_pattern=r'(?P<url>https?://\S+\.beeceptor\.com)',
|
| 212 |
+
stdin_input='\n', # Симулируем нажатие клавиши Enter для выбора первого пункта
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
def get_native_url(port: int) -> str:
|
| 217 |
try:
|
| 218 |
from google.colab.output import eval_js # type: ignore[import]
|
colab_tunnel/_utils.py
CHANGED
|
@@ -149,7 +149,6 @@ def terminate_process(process_name: str, process_obj: Popen) -> None:
|
|
| 149 |
|
| 150 |
def move_path(old_path: Path | str, new_path: Path | str) -> None:
|
| 151 |
old, new = Path(old_path), Path(new_path)
|
| 152 |
-
# BUGFIX: оригинал проверял old_path.exists() — падало если old_path это str
|
| 153 |
if not old.exists():
|
| 154 |
raise RuntimeError(f'не найден исходный путь для перемещения: {old}')
|
| 155 |
try:
|
|
@@ -175,15 +174,19 @@ def is_ipv4(address: str) -> bool:
|
|
| 175 |
return bool(pattern.match(address))
|
| 176 |
|
| 177 |
|
| 178 |
-
def is_valid_url(url: str) -> bool:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
for attempt in range(2):
|
| 180 |
try:
|
| 181 |
-
with get_url(url, headers=
|
| 182 |
if 200 <= resp.status_code < 300:
|
| 183 |
return True
|
| 184 |
except Exception:
|
| 185 |
try:
|
| 186 |
-
resp = get_head(url, headers=
|
| 187 |
if 200 <= resp.status_code < 300:
|
| 188 |
return True
|
| 189 |
except Exception:
|
|
@@ -214,21 +217,25 @@ def download(
|
|
| 214 |
save_path: str | Path | None = None,
|
| 215 |
progress: bool = True,
|
| 216 |
) -> Path:
|
| 217 |
-
# Поддержка --header= суффикса
|
| 218 |
extra_headers: dict | None = None
|
| 219 |
url_parts = url.replace('"', '').replace("'", '').split('--header=')
|
| 220 |
if len(url_parts) > 1:
|
| 221 |
url = url_parts[0].strip()
|
| 222 |
-
key, _, value = url_parts[1].partition(':')
|
| 223 |
extra_headers = {key.strip(): value.strip()}
|
| 224 |
|
| 225 |
-
|
| 226 |
-
|
|
|
|
| 227 |
|
| 228 |
save_path = Path(save_path) if save_path else Path.cwd()
|
| 229 |
save_path.mkdir(parents=True, exist_ok=True)
|
| 230 |
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
file_size = int(req.headers.get('content-length', 0))
|
| 233 |
file_name = filename or get_filename_from_headers(req.headers) or Path(urlparse(req.url).path).name
|
| 234 |
file_path = save_path / file_name
|
|
@@ -241,7 +248,7 @@ def download(
|
|
| 241 |
for chunk in req.iter_content(chunk_size=chunk_size):
|
| 242 |
if chunk:
|
| 243 |
fp.write(chunk)
|
| 244 |
-
if progress and file_size:
|
| 245 |
downloaded += len(chunk)
|
| 246 |
elapsed = time() - start
|
| 247 |
print(
|
|
@@ -252,5 +259,5 @@ def download(
|
|
| 252 |
raise RuntimeError(f'не удалось загрузить файл по ссылке {url}:\n{e}')
|
| 253 |
|
| 254 |
if progress and file_size:
|
| 255 |
-
print()
|
| 256 |
return file_path
|
|
|
|
| 149 |
|
| 150 |
def move_path(old_path: Path | str, new_path: Path | str) -> None:
|
| 151 |
old, new = Path(old_path), Path(new_path)
|
|
|
|
| 152 |
if not old.exists():
|
| 153 |
raise RuntimeError(f'не найден исходный путь для перемещения: {old}')
|
| 154 |
try:
|
|
|
|
| 174 |
return bool(pattern.match(address))
|
| 175 |
|
| 176 |
|
| 177 |
+
def is_valid_url(url: str, custom_headers: dict | None = None) -> bool:
|
| 178 |
+
req_headers = HEADERS.copy()
|
| 179 |
+
if custom_headers:
|
| 180 |
+
req_headers.update(custom_headers)
|
| 181 |
+
|
| 182 |
for attempt in range(2):
|
| 183 |
try:
|
| 184 |
+
with get_url(url, headers=req_headers, allow_redirects=True, stream=True, timeout=10) as resp:
|
| 185 |
if 200 <= resp.status_code < 300:
|
| 186 |
return True
|
| 187 |
except Exception:
|
| 188 |
try:
|
| 189 |
+
resp = get_head(url, headers=req_headers, allow_redirects=True, timeout=10)
|
| 190 |
if 200 <= resp.status_code < 300:
|
| 191 |
return True
|
| 192 |
except Exception:
|
|
|
|
| 217 |
save_path: str | Path | None = None,
|
| 218 |
progress: bool = True,
|
| 219 |
) -> Path:
|
|
|
|
| 220 |
extra_headers: dict | None = None
|
| 221 |
url_parts = url.replace('"', '').replace("'", '').split('--header=')
|
| 222 |
if len(url_parts) > 1:
|
| 223 |
url = url_parts[0].strip()
|
| 224 |
+
key, _, value = url_parts[1].partition(':')
|
| 225 |
extra_headers = {key.strip(): value.strip()}
|
| 226 |
|
| 227 |
+
# Проверка валидности с учетом переданных заголовков
|
| 228 |
+
if not is_valid_url(url, custom_headers=extra_headers):
|
| 229 |
+
raise RuntimeError(f'недействительная ссылка на файл или доступ ограничен: {url}')
|
| 230 |
|
| 231 |
save_path = Path(save_path) if save_path else Path.cwd()
|
| 232 |
save_path.mkdir(parents=True, exist_ok=True)
|
| 233 |
|
| 234 |
+
req_headers = HEADERS.copy()
|
| 235 |
+
if extra_headers:
|
| 236 |
+
req_headers.update(extra_headers)
|
| 237 |
+
|
| 238 |
+
with get_url(url, stream=True, allow_redirects=True, headers=req_headers) as req:
|
| 239 |
file_size = int(req.headers.get('content-length', 0))
|
| 240 |
file_name = filename or get_filename_from_headers(req.headers) or Path(urlparse(req.url).path).name
|
| 241 |
file_path = save_path / file_name
|
|
|
|
| 248 |
for chunk in req.iter_content(chunk_size=chunk_size):
|
| 249 |
if chunk:
|
| 250 |
fp.write(chunk)
|
| 251 |
+
if progress and file_size:
|
| 252 |
downloaded += len(chunk)
|
| 253 |
elapsed = time() - start
|
| 254 |
print(
|
|
|
|
| 259 |
raise RuntimeError(f'не удалось загрузить файл по ссылке {url}:\n{e}')
|
| 260 |
|
| 261 |
if progress and file_size:
|
| 262 |
+
print()
|
| 263 |
return file_path
|