1+ import hashlib
12import os
3+ import platform
24import subprocess
5+ from io import BytesIO
36from os import path
47from typing import Optional
8+ from zipfile import ZipFile
59
10+ import requests
611from click import echo
712
813RATHOLE_CLIENT_CONFIG_FILENAME = "rathole-client.toml"
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
2872def 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
58125def 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 )
0 commit comments