-
Notifications
You must be signed in to change notification settings - Fork 104
Expand file tree
/
Copy pathsetup.py
More file actions
208 lines (171 loc) · 6.16 KB
/
setup.py
File metadata and controls
208 lines (171 loc) · 6.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env python3
"""Interactive setup wizard for MasterHttpRelayVPN.
Writes a ready-to-use config.json by prompting only for the values
the user really has to choose. Everything else gets a sane default.
Run:
python setup.py
"""
from __future__ import annotations
import json
import os
import secrets
import shutil
import string
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
CONFIG_PATH = HERE / "config.json"
EXAMPLE_PATH = HERE / "config.example.json"
def _c(code: str, text: str) -> str:
if os.environ.get("NO_COLOR") or not sys.stdout.isatty():
return text
return f"\033[{code}m{text}\033[0m"
def bold(t: str) -> str: return _c("1", t)
def cyan(t: str) -> str: return _c("36", t)
def green(t: str) -> str: return _c("32", t)
def yellow(t: str) -> str: return _c("33", t)
def red(t: str) -> str: return _c("31", t)
def dim(t: str) -> str: return _c("2", t)
def prompt(question: str, default: str | None = None) -> str:
suffix = f" [{dim(default)}]" if default else ""
while True:
try:
raw = input(f"{cyan('?')} {question}{suffix}: ").strip()
except EOFError:
print()
sys.exit(1)
if not raw and default is not None:
return default
if raw:
return raw
print(red(" value required"))
def prompt_yes_no(question: str, default: bool = True) -> bool:
hint = "Y/n" if default else "y/N"
while True:
raw = input(f"{cyan('?')} {question} [{hint}]: ").strip().lower()
if not raw:
return default
if raw in ("y", "yes"):
return True
if raw in ("n", "no"):
return False
def random_auth_key(length: int = 32) -> str:
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length))
def load_base_config() -> dict:
if EXAMPLE_PATH.exists():
try:
with EXAMPLE_PATH.open() as f:
return json.load(f)
except Exception:
pass
return {
"mode": "apps_script",
"google_ip": "216.239.38.120",
"front_domain": "www.google.com",
"listen_host": "127.0.0.1",
"listen_port": 8085,
"socks5_enabled": True,
"socks5_port": 1080,
"log_level": "INFO",
"verify_ssl": True,
"lan_sharing": False,
"relay_timeout": 25,
"tls_connect_timeout": 15,
"tcp_connect_timeout": 10,
"max_response_body_bytes": 200 * 1024 * 1024,
"chunked_download_min_size": 5 * 1024 * 1024,
"chunked_download_chunk_size": 512 * 1024,
"chunked_download_max_parallel": 8,
"chunked_download_max_chunks": 256,
"hosts": {},
}
def configure_apps_script(cfg: dict) -> dict:
print()
print(bold("Google Apps Script setup"))
print(dim(" 1. Open https://script.google.com -> New project"))
print(dim(" 2. Paste apps_script/Code.gs from this repo into the editor"))
print(dim(" 3. Set AUTH_KEY in Code.gs to the password below"))
print(dim(" 4. Deploy -> New deployment -> Web app"))
print(dim(" Execute as: Me | Who has access: Anyone"))
print(dim(" 5. Copy the Deployment ID and paste it here"))
print()
ids_raw = prompt(
"Deployment ID(s) - comma-separated for load balancing",
default=None,
)
ids = [x.strip() for x in ids_raw.split(",") if x.strip()]
if len(ids) == 1:
cfg["script_id"] = ids[0]
cfg.pop("script_ids", None)
else:
cfg["script_ids"] = ids
cfg.pop("script_id", None)
return cfg
def configure_network(cfg: dict) -> dict:
print()
print(bold("Network settings") + dim(" (press enter to accept defaults)"))
cfg["lan_sharing"] = prompt_yes_no(
"Enable LAN sharing?",
default=bool(cfg.get("lan_sharing", False)),
)
default_host = str(cfg.get("listen_host", "127.0.0.1"))
if cfg["lan_sharing"] and default_host == "127.0.0.1":
default_host = "0.0.0.0"
cfg["listen_host"] = prompt("Listen host", default=default_host)
port = prompt("HTTP proxy port", default=str(cfg.get("listen_port", 8085)))
try:
cfg["listen_port"] = int(port)
except ValueError:
cfg["listen_port"] = 8085
socks5 = prompt_yes_no("Enable SOCKS5 proxy?", default=bool(cfg.get("socks5_enabled", True)))
cfg["socks5_enabled"] = socks5
if socks5:
sport = prompt("SOCKS5 port", default=str(cfg.get("socks5_port", 1080)))
try:
cfg["socks5_port"] = int(sport)
except ValueError:
cfg["socks5_port"] = 1080
return cfg
def write_config(cfg: dict) -> None:
if CONFIG_PATH.exists():
backup = CONFIG_PATH.with_suffix(".json.bak")
shutil.copy2(CONFIG_PATH, backup)
print(yellow(f" existing config.json backed up to {backup.name}"))
with CONFIG_PATH.open("w", encoding="utf-8") as f:
json.dump(cfg, f, indent=2)
f.write("\n")
def main() -> int:
print()
print(bold("MasterHttpRelayVPN - setup wizard"))
print(dim("Answer a few questions and we'll write config.json for you."))
if CONFIG_PATH.exists():
if not prompt_yes_no("config.json already exists. Overwrite?", default=False):
print(dim("Nothing changed."))
return 0
cfg = load_base_config()
cfg["mode"] = "apps_script"
suggested_key = random_auth_key()
print()
print(bold("Shared password (auth_key)"))
print(dim(" Must match AUTH_KEY inside apps_script/Code.gs."))
cfg["auth_key"] = prompt("auth_key", default=suggested_key)
cfg = configure_apps_script(cfg)
cfg = configure_network(cfg)
write_config(cfg)
print()
print(green(f"[OK] wrote {CONFIG_PATH.name}"))
print()
print(bold("Next step:"))
print(f" python main.py")
print()
print(yellow("Reminder: the AUTH_KEY inside apps_script/Code.gs must match the auth_key"))
print(yellow("you just entered - otherwise the relay will return 'unauthorized'."))
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
print()
print(dim("Cancelled."))
sys.exit(130)