Skip to content

Commit 64c3593

Browse files
authored
Merge pull request #724 from splitgraph/tunnel-issues-2-CU-38f3r2e
Download rathole client binary for platform if necessary
2 parents 00c8a02 + 378bee4 commit 64c3593

File tree

4 files changed

+138
-11
lines changed

4 files changed

+138
-11
lines changed

splitgraph/cloud/tunnel_client.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import hashlib
12
import os
3+
import platform
24
import subprocess
5+
from io import BytesIO
36
from os import path
47
from typing import Optional
8+
from zipfile import ZipFile
59

10+
import requests
611
from click import echo
712

813
RATHOLE_CLIENT_CONFIG_FILENAME = "rathole-client.toml"
@@ -24,6 +29,45 @@
2429
2530
"""
2631

32+
# Download URL and SHA256 hash of rathole build ZIP archive for each supported platform.
33+
RATHOLE_BUILDS = {
34+
"Darwin": (
35+
"https://github.com/rapiz1/rathole/releases/download/v0.4.4/rathole-x86_64-apple-darwin.zip",
36+
"c1e6d0a41a0af8589303ab6940937d9183b344a62283ff6033a17e82c357ce17",
37+
),
38+
"Windows": (
39+
"https://github.com/rapiz1/rathole/releases/download/v0.4.4/rathole-x86_64-pc-windows-msvc.zip",
40+
"92cc3feb57149c0b4dba7ec198dbda26c4831cde0a7c74a7d9f51e0002f65ead",
41+
),
42+
"Linux-x86_64-glibc": (
43+
"https://github.com/rapiz1/rathole/releases/download/v0.4.4/rathole-x86_64-unknown-linux-gnu.zip",
44+
"fef39ed9d25e944711e2a27d5a9c812163ab184bf3f703827fca6bbf54504fbf",
45+
),
46+
"Linux-x86_64-musl": (
47+
"https://github.com/rapiz1/rathole/releases/download/v0.4.4/rathole-x86_64-unknown-linux-musl.zip",
48+
"fc6b0a57727383a1491591f8e9ee76b1e0e25ecf7c2736b803d8f4411f651a15",
49+
),
50+
}
51+
52+
53+
def get_sha256(stream):
54+
return hashlib.sha256(stream.read()).hexdigest()
55+
56+
57+
def get_rathole_build_key():
58+
system = platform.system()
59+
# Currently only x86_64 macos builds exist for Windows and MacOS.
60+
# It works on Apple Silicon due to Rosetta 2.
61+
if system in ["Windows", "Darwin"]:
62+
return system
63+
if system == "Linux":
64+
# python 3.8 sometimes reports '' instead of 'musl' for musl libc (https://bugs.python.org/issue43248)
65+
return "Linux-%s-%s" % (
66+
platform.machine(),
67+
"glibc" if platform.libc_ver()[0] == "glibc" else "musl",
68+
)
69+
return None
70+
2771

2872
def get_rathole_client_config(
2973
tunnel_connect_address: str,
@@ -51,8 +95,31 @@ def get_config_dir():
5195
return os.path.dirname(get_singleton(CONFIG, "SG_CONFIG_FILE"))
5296

5397

54-
def get_rathole_client_binary_path():
55-
return os.path.join(get_config_dir(), "rathole")
98+
def get_rathole_client_binary_path(config_dir: Optional[str] = None) -> str:
99+
filename = "rathole.exe" if get_rathole_build_key() == "Windows" else "rathole"
100+
return os.path.join(config_dir or get_config_dir(), filename)
101+
102+
103+
def get_config_filename(config_dir: Optional[str] = None) -> str:
104+
return path.join(config_dir or get_config_dir(), RATHOLE_CLIENT_CONFIG_FILENAME)
105+
106+
107+
def download_rathole_binary(
108+
build: Optional[str] = None, rathole_path: Optional[str] = None
109+
) -> None:
110+
rathole_binary_path = rathole_path or get_rathole_client_binary_path()
111+
(url, sha256) = RATHOLE_BUILDS.get(build or get_rathole_build_key(), ("", ""))
112+
if not url:
113+
raise Exception("No rathole build found for this architecture")
114+
content = BytesIO(requests.get(url).content)
115+
assert get_sha256(content) == sha256
116+
content.seek(0)
117+
zipfile = ZipFile(content)
118+
assert len(zipfile.filelist) == 1
119+
assert zipfile.filelist[0].filename == path.basename(rathole_binary_path)
120+
zipfile.extract(path.basename(rathole_binary_path), path.dirname(rathole_binary_path))
121+
if get_rathole_build_key() != "Windows":
122+
os.chmod(rathole_binary_path, 0o500)
56123

57124

58125
def write_rathole_client_config(
@@ -64,7 +131,7 @@ def write_rathole_client_config(
64131
tls_hostname: Optional[str],
65132
) -> str:
66133
# If a specific root CA file is used (eg: for self-signed hosts), reference
67-
# it in the rathole client config.
134+
# it in the rathole client config. Otherwise use system default trust root.
68135
trusted_root = os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("SSL_CERT_FILE")
69136
rathole_client_config = get_rathole_client_config(
70137
tunnel_connect_address=f"{tunnel_connect_host}:{tunnel_connect_port}",
@@ -74,13 +141,23 @@ def write_rathole_client_config(
74141
section_id=section_id,
75142
trusted_root=trusted_root,
76143
)
77-
config_filename = path.join(get_config_dir(), RATHOLE_CLIENT_CONFIG_FILENAME)
144+
config_filename = get_config_filename()
78145
with open(config_filename, "w") as f:
79146
f.write(rathole_client_config)
80147
return config_filename
81148

82149

83-
def launch_rathole_client(rathole_client_config_path: str) -> None:
150+
def launch_rathole_client(
151+
rathole_client_binary_path: Optional[str] = None,
152+
rathole_client_config_path: Optional[str] = None,
153+
) -> None:
154+
binary_path = rathole_client_binary_path or get_rathole_client_binary_path()
155+
if not path.isfile(binary_path):
156+
download_rathole_binary()
84157
echo("launching rathole client")
85-
command = [get_rathole_client_binary_path(), "--client", rathole_client_config_path]
158+
command = [
159+
binary_path,
160+
"--client",
161+
rathole_client_config_path or get_config_filename(),
162+
]
86163
subprocess.check_call(command)

splitgraph/commandline/cloud.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,7 +1238,7 @@ def start_repository_tunnel(
12381238
section_id = f"{repository.namespace}/{repository.repository}"
12391239
local_address = f"{external.params['host']}:{external.params['port']}"
12401240

1241-
rathole_client_config_path = write_rathole_client_config(
1241+
write_rathole_client_config(
12421242
section_id,
12431243
secret_token,
12441244
tunnel_connect_host,
@@ -1247,7 +1247,7 @@ def start_repository_tunnel(
12471247
tunnel_connect_host,
12481248
)
12491249

1250-
launch_rathole_client(rathole_client_config_path)
1250+
launch_rathole_client()
12511251

12521252

12531253
def start_ephemeral_tunnel(remote: str, local_address: str) -> None:
@@ -1262,7 +1262,7 @@ def start_ephemeral_tunnel(remote: str, local_address: str) -> None:
12621262
private_address_port,
12631263
) = client.provision_ephemeral_tunnel()
12641264

1265-
rathole_client_config_path = write_rathole_client_config(
1265+
write_rathole_client_config(
12661266
private_address_host,
12671267
secret_token,
12681268
tunnel_connect_host,
@@ -1273,7 +1273,7 @@ def start_ephemeral_tunnel(remote: str, local_address: str) -> None:
12731273
click.echo(
12741274
f"To connect to {local_address} from Splitgraph, use the following connection parameters:\nHost: {private_address_host}\nPort: {private_address_port}"
12751275
)
1276-
launch_rathole_client(rathole_client_config_path)
1276+
launch_rathole_client()
12771277

12781278

12791279
@click.command("tunnel")

splitgraph/config/management.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import os
33
from pathlib import Path
4+
from typing import cast
45

56
from splitgraph.config.config import patch_config
67
from splitgraph.config.export import overwrite_config
@@ -12,7 +13,11 @@ def patch_and_save_config(config, patch):
1213
config_path = config["SG_CONFIG_FILE"]
1314
if not config_path:
1415
# Default to creating a config in the user's homedir rather than local.
15-
config_dir = Path(os.environ["HOME"]) / Path(HOME_SUB_DIR)
16+
homedir = os.environ.get("HOME")
17+
# on Windows, HOME is not a standard env var
18+
if homedir is None and os.name == "nt":
19+
homedir = f"{os.environ['HOMEDRIVE']}{os.environ['HOMEPATH']}"
20+
config_dir = Path(cast(str, homedir)) / Path(HOME_SUB_DIR)
1621
config_path = config_dir / Path(".sgconfig")
1722
logging.debug("No config file detected, creating one at %s" % config_path)
1823
config_dir.mkdir(exist_ok=True, parents=True)

test/splitgraph/commandline/test_cloud.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
from httpretty.core import HTTPrettyRequest
2828

2929
from splitgraph.__version__ import __version__
30+
from splitgraph.cloud.project.models import External
31+
from splitgraph.cloud.tunnel_client import get_config_filename
3032
from splitgraph.commandline import cli
3133
from splitgraph.commandline.cloud import (
3234
add_c,
@@ -37,6 +39,7 @@
3739
register_c,
3840
sql_c,
3941
stub_c,
42+
tunnel_c,
4043
)
4144
from splitgraph.config import create_config_dict
4245
from splitgraph.config.config import patch_config
@@ -624,3 +627,45 @@ def test_commandline_stub(snapshot):
624627
assert result.exit_code == 0
625628

626629
snapshot.assert_match(result.stdout, "sgr_cloud_stub.yml")
630+
631+
632+
def test_rathole_client_config():
633+
runner = CliRunner(mix_stderr=False)
634+
external = External(
635+
tunnel=True, plugin="asdf", params={"host": "127.0.0.1", "port": 5432}, tables={}
636+
)
637+
638+
def mock_provision_tunnel(a, b):
639+
print("asdf", a, b)
640+
return ("foo", "bar", 1)
641+
642+
with patch(
643+
"splitgraph.commandline.cloud._get_external_from_yaml", return_value=(external,)
644+
), patch(
645+
"splitgraph.cloud.GQLAPIClient.provision_repository_tunnel",
646+
new_callable=PropertyMock,
647+
return_value=mock_provision_tunnel,
648+
), patch(
649+
"splitgraph.commandline.cloud.launch_rathole_client", return_value=None
650+
):
651+
result = runner.invoke(
652+
tunnel_c,
653+
[
654+
"test/repo",
655+
],
656+
catch_exceptions=False,
657+
)
658+
assert result.exit_code == 0
659+
with open(get_config_filename(), "r") as f:
660+
non_empty_lines = [line.strip() for line in f if line.strip() != ""]
661+
assert non_empty_lines == [
662+
"[client]",
663+
'remote_addr = "bar:1"',
664+
"[client.transport]",
665+
'type = "tls"',
666+
"[client.transport.tls]",
667+
'hostname = "bar"',
668+
'[client.services."test/repo"]',
669+
'local_addr = "127.0.0.1:5432"',
670+
'token = "foo"',
671+
]

0 commit comments

Comments
 (0)