gitscope
https://dev.kronoz.win/r/marzneshin.git
app/utils/share.py · v0.6.4
Source
1 a47eb4c Dawsh 2y ago import base64
2 24112b4 Dawsh 2y ago import ipaddress
3 a47eb4c Dawsh 2y ago import json
4 a47eb4c Dawsh 2y ago import random
5 a47eb4c Dawsh 2y ago import secrets
6 19a42f0 Dawsh 2y ago from collections import defaultdict
7 a47eb4c Dawsh 2y ago from datetime import datetime as dt, timedelta
8 406d607 dawsh 2y ago from importlib import resources
9 e422d72 Dawsh 2y ago from typing import Literal, Union, List, Type
10 a47eb4c Dawsh 2y ago from uuid import UUID
11 a47eb4c Dawsh 2y ago
12 a47eb4c Dawsh 2y ago from jdatetime import date as jd
13 f1ab16c Dawsh 2y ago from v2share import (
14 f1ab16c Dawsh 2y ago V2Data,
15 f1ab16c Dawsh 2y ago SingBoxConfig,
16 f1ab16c Dawsh 2y ago ClashConfig,
17 f1ab16c Dawsh 2y ago ClashMetaConfig,
18 f1ab16c Dawsh 2y ago XrayConfig,
19 0f3112a Dawsh 2y ago WireGuardConfig,
20 f1ab16c Dawsh 2y ago )
21 e422d72 Dawsh 2y ago from v2share.base import BaseConfig
22 d6600c5 Dawsh 2y ago from v2share.links import LinksConfig
23 a47eb4c Dawsh 2y ago
24 8111eb7 Dawsh 2y ago from app.config.env import (
25 8111eb7 Dawsh 2y ago XRAY_SUBSCRIPTION_TEMPLATE,
26 8111eb7 Dawsh 2y ago SINGBOX_SUBSCRIPTION_TEMPLATE,
27 8111eb7 Dawsh 2y ago CLASH_SUBSCRIPTION_TEMPLATE,
28 fb4b69d Dawsh 2y ago SUBSCRIPTION_PAGE_TEMPLATE,
29 8111eb7 Dawsh 2y ago )
30 f446990 Dawsh 2y ago from app.db import GetDB, crud
31 01cdf2e Dawsh 2y ago from app.models.proxy import (
32 01cdf2e Dawsh 2y ago InboundHostSecurity,
33 01cdf2e Dawsh 2y ago )
34 fb4b69d Dawsh 2y ago from app.models.settings import SubscriptionSettings
35 d377c4c Dawsh 2y ago from app.models.user import UserResponse, UserExpireStrategy
36 fb4b69d Dawsh 2y ago from app.templates import render_template
37 24112b4 Dawsh 2y ago from app.utils.keygen import gen_uuid, gen_password, generate_curve25519_pbk
38 a47eb4c Dawsh 2y ago from app.utils.system import get_public_ip, readable_size
39 a47eb4c Dawsh 2y ago
40 a47eb4c Dawsh 2y ago SERVER_IP = get_public_ip()
41 a47eb4c Dawsh 2y ago
42 fb4b69d Dawsh 2y ago ACTIVITY_EMOJIS = {
43 fb4b69d Dawsh 2y ago True: "✅",
44 fb4b69d Dawsh 2y ago False: "❌",
45 a47eb4c Dawsh 2y ago }
46 a47eb4c Dawsh 2y ago
47 e422d72 Dawsh 2y ago subscription_handlers: dict[str, Type[BaseConfig]] = {
48 0178858 Dawsh 2y ago "links": LinksConfig,
49 d6600c5 Dawsh 2y ago "xray": XrayConfig,
50 d6600c5 Dawsh 2y ago "clash-meta": ClashMetaConfig,
51 d6600c5 Dawsh 2y ago "clash": ClashConfig,
52 d6600c5 Dawsh 2y ago "sing-box": SingBoxConfig,
53 0f3112a Dawsh 2y ago "wireguard": WireGuardConfig,
54 d6600c5 Dawsh 2y ago }
55 a47eb4c Dawsh 2y ago
56 406d607 dawsh 2y ago handlers_templates = {
57 406d607 dawsh 2y ago LinksConfig: None,
58 0f3112a Dawsh 2y ago WireGuardConfig: None,
59 406d607 dawsh 2y ago XrayConfig: XRAY_SUBSCRIPTION_TEMPLATE
60 406d607 dawsh 2y ago or resources.files("app.templates") / "xray.json",
61 406d607 dawsh 2y ago ClashConfig: CLASH_SUBSCRIPTION_TEMPLATE,
62 406d607 dawsh 2y ago ClashMetaConfig: CLASH_SUBSCRIPTION_TEMPLATE,
63 406d607 dawsh 2y ago SingBoxConfig: SINGBOX_SUBSCRIPTION_TEMPLATE
64 406d607 dawsh 2y ago or resources.files("app.templates") / "sing-box.json",
65 406d607 dawsh 2y ago }
66 406d607 dawsh 2y ago
67 a47eb4c Dawsh 2y ago
68 fb4b69d Dawsh 2y ago def generate_subscription_template(
69 fb4b69d Dawsh 2y ago db_user, subscription_settings: SubscriptionSettings
70 fb4b69d Dawsh 2y ago ):
71 fb4b69d Dawsh 2y ago links = generate_subscription(
72 fb4b69d Dawsh 2y ago user=db_user,
73 fb4b69d Dawsh 2y ago config_format="links",
74 fb4b69d Dawsh 2y ago use_placeholder=not db_user.is_active
75 fb4b69d Dawsh 2y ago and subscription_settings.placeholder_if_disabled,
76 fb4b69d Dawsh 2y ago placeholder_remark=subscription_settings.placeholder_remark,
77 fb4b69d Dawsh 2y ago shuffle=subscription_settings.shuffle_configs,
78 fb4b69d Dawsh 2y ago ).split()
79 fb4b69d Dawsh 2y ago return render_template(
80 fb4b69d Dawsh 2y ago SUBSCRIPTION_PAGE_TEMPLATE,
81 fb4b69d Dawsh 2y ago {"user": UserResponse.model_validate(db_user), "links": links},
82 fb4b69d Dawsh 2y ago )
83 fb4b69d Dawsh 2y ago
84 fb4b69d Dawsh 2y ago
85 a47eb4c Dawsh 2y ago def generate_subscription(
86 a47eb4c Dawsh 2y ago user: "UserResponse",
87 0178858 Dawsh 2y ago config_format: Literal["links", "xray", "clash-meta", "clash", "sing-box"],
88 d6600c5 Dawsh 2y ago as_base64: bool = False,
89 fb4b69d Dawsh 2y ago use_placeholder: bool = False,
90 fb4b69d Dawsh 2y ago placeholder_remark: str = "disabled",
91 fb4b69d Dawsh 2y ago shuffle: bool = False,
92 a47eb4c Dawsh 2y ago ) -> str:
93 db68a58 Dawsh 2y ago extra_data = UserResponse.model_validate(user).model_dump(
94 db68a58 Dawsh 2y ago exclude={"subscription_url", "services", "inbounds"}
95 db68a58 Dawsh 2y ago )
96 d6600c5 Dawsh 2y ago format_variables = setup_format_variables(extra_data)
97 d6600c5 Dawsh 2y ago
98 d6600c5 Dawsh 2y ago if config_format not in subscription_handlers.keys():
99 a47eb4c Dawsh 2y ago raise ValueError(f'Unsupported format "{config_format}"')
100 a47eb4c Dawsh 2y ago
101 406d607 dawsh 2y ago subscription_handler_class = subscription_handlers[config_format]
102 406d607 dawsh 2y ago if template_path := handlers_templates[subscription_handler_class]:
103 f1ab16c Dawsh 2y ago subscription_handler = subscription_handler_class(
104 f1ab16c Dawsh 2y ago template_path=template_path
105 f1ab16c Dawsh 2y ago )
106 406d607 dawsh 2y ago else:
107 406d607 dawsh 2y ago subscription_handler = subscription_handler_class()
108 406d607 dawsh 2y ago
109 fb4b69d Dawsh 2y ago if use_placeholder:
110 fb4b69d Dawsh 2y ago placeholder_config = V2Data(
111 fb4b69d Dawsh 2y ago "vmess",
112 fb4b69d Dawsh 2y ago placeholder_remark.format_map(format_variables),
113 fb4b69d Dawsh 2y ago "127.0.0.1",
114 fb4b69d Dawsh 2y ago 80,
115 fb4b69d Dawsh 2y ago )
116 fb4b69d Dawsh 2y ago configs = [placeholder_config]
117 fb4b69d Dawsh 2y ago
118 fb4b69d Dawsh 2y ago else:
119 fb4b69d Dawsh 2y ago configs = generate_user_configs(
120 fb4b69d Dawsh 2y ago user.inbounds,
121 fb4b69d Dawsh 2y ago user.key,
122 24112b4 Dawsh 2y ago user.id,
123 fb4b69d Dawsh 2y ago format_variables,
124 fb4b69d Dawsh 2y ago )
125 fb4b69d Dawsh 2y ago
126 fb4b69d Dawsh 2y ago subscription_handler.add_proxies(configs)
127 e422d72 Dawsh 2y ago config = subscription_handler.render(sort=True, shuffle=shuffle)
128 a47eb4c Dawsh 2y ago
129 f1ab16c Dawsh 2y ago return (
130 f1ab16c Dawsh 2y ago config if not as_base64 else base64.b64encode(config.encode()).decode()
131 f1ab16c Dawsh 2y ago )
132 a47eb4c Dawsh 2y ago
133 a47eb4c Dawsh 2y ago
134 a47eb4c Dawsh 2y ago def format_time_left(seconds_left: int) -> str:
135 a47eb4c Dawsh 2y ago if not seconds_left or seconds_left <= 0:
136 a47eb4c Dawsh 2y ago return "∞"
137 a47eb4c Dawsh 2y ago
138 a47eb4c Dawsh 2y ago minutes, seconds = divmod(seconds_left, 60)
139 a47eb4c Dawsh 2y ago hours, minutes = divmod(minutes, 60)
140 a47eb4c Dawsh 2y ago days, hours = divmod(hours, 24)
141 a47eb4c Dawsh 2y ago months, days = divmod(days, 30)
142 a47eb4c Dawsh 2y ago
143 a47eb4c Dawsh 2y ago result = []
144 a47eb4c Dawsh 2y ago if months:
145 a47eb4c Dawsh 2y ago result.append(f"{months}m")
146 a47eb4c Dawsh 2y ago if days:
147 a47eb4c Dawsh 2y ago result.append(f"{days}d")
148 a47eb4c Dawsh 2y ago if hours and (days < 7):
149 a47eb4c Dawsh 2y ago result.append(f"{hours}h")
150 a47eb4c Dawsh 2y ago if minutes and not (months or days):
151 a47eb4c Dawsh 2y ago result.append(f"{minutes}m")
152 a47eb4c Dawsh 2y ago if seconds and not (months or days):
153 a47eb4c Dawsh 2y ago result.append(f"{seconds}s")
154 a47eb4c Dawsh 2y ago return " ".join(result)
155 a47eb4c Dawsh 2y ago
156 01cdf2e Dawsh 2y ago
157 0f3112a Dawsh 2y ago def calculate_client_address(interface_address: str, user_id: int) -> str:
158 0f3112a Dawsh 2y ago try:
159 0f3112a Dawsh 2y ago interface = ipaddress.ip_interface(interface_address)
160 0f3112a Dawsh 2y ago address = interface.ip
161 0f3112a Dawsh 2y ago network = interface.network
162 0f3112a Dawsh 2y ago except ValueError:
163 0f3112a Dawsh 2y ago return ""
164 0f3112a Dawsh 2y ago user_address = network[user_id]
165 0f3112a Dawsh 2y ago if user_address == network[0]:
166 0f3112a Dawsh 2y ago user_address = network.broadcast_address - 1
167 0f3112a Dawsh 2y ago if user_address == address:
168 0f3112a Dawsh 2y ago user_address = network.broadcast_address - 2
169 0f3112a Dawsh 2y ago return user_address.compressed + "/" + str(user_address.max_prefixlen)
170 0f3112a Dawsh 2y ago
171 0f3112a Dawsh 2y ago
172 a47eb4c Dawsh 2y ago def setup_format_variables(extra_data: dict) -> dict:
173 d377c4c Dawsh 2y ago expire_strategy = extra_data.get("expire_strategy")
174 d377c4c Dawsh 2y ago expire_datetime = extra_data.get("expire_date")
175 d377c4c Dawsh 2y ago usage_duration = extra_data.get("usage_duration")
176 a47eb4c Dawsh 2y ago
177 d377c4c Dawsh 2y ago if expire_strategy != UserExpireStrategy.START_ON_FIRST_USE:
178 f446990 Dawsh 2y ago if expire_datetime is not None:
179 f446990 Dawsh 2y ago seconds_left = (expire_datetime - dt.utcnow()).total_seconds()
180 a47eb4c Dawsh 2y ago expire_date = expire_datetime.date()
181 a47eb4c Dawsh 2y ago jalali_expire_date = jd.fromgregorian(
182 f1ab16c Dawsh 2y ago year=expire_date.year,
183 f1ab16c Dawsh 2y ago month=expire_date.month,
184 f1ab16c Dawsh 2y ago day=expire_date.day,
185 a47eb4c Dawsh 2y ago ).strftime("%Y-%m-%d")
186 a47eb4c Dawsh 2y ago days_left = (expire_datetime - dt.utcnow()).days + 1
187 a47eb4c Dawsh 2y ago time_left = format_time_left(seconds_left)
188 a47eb4c Dawsh 2y ago else:
189 d377c4c Dawsh 2y ago days_left, time_left, expire_date, jalali_expire_date = "∞" * 4
190 a47eb4c Dawsh 2y ago else:
191 d377c4c Dawsh 2y ago if usage_duration is not None and usage_duration >= 0:
192 d377c4c Dawsh 2y ago days_left = timedelta(seconds=usage_duration).days
193 d377c4c Dawsh 2y ago time_left = format_time_left(usage_duration)
194 a47eb4c Dawsh 2y ago expire_date = "-"
195 a47eb4c Dawsh 2y ago jalali_expire_date = "-"
196 a47eb4c Dawsh 2y ago else:
197 d377c4c Dawsh 2y ago days_left, time_left, expire_date, jalali_expire_date = "∞" * 4
198 a47eb4c Dawsh 2y ago
199 a47eb4c Dawsh 2y ago if extra_data.get("data_limit"):
200 a47eb4c Dawsh 2y ago data_limit = readable_size(extra_data["data_limit"])
201 a47eb4c Dawsh 2y ago data_left = extra_data["data_limit"] - extra_data["used_traffic"]
202 a47eb4c Dawsh 2y ago if data_left < 0:
203 a47eb4c Dawsh 2y ago data_left = 0
204 a47eb4c Dawsh 2y ago data_left = readable_size(data_left)
205 a47eb4c Dawsh 2y ago else:
206 fb4b69d Dawsh 2y ago data_limit, data_left = "∞" * 2
207 a47eb4c Dawsh 2y ago
208 fb4b69d Dawsh 2y ago status_emoji = ACTIVITY_EMOJIS.get(extra_data.get("is_active")) or ""
209 a47eb4c Dawsh 2y ago
210 19a42f0 Dawsh 2y ago format_variables = defaultdict(
211 19a42f0 Dawsh 2y ago lambda: "<missing>",
212 19a42f0 Dawsh 2y ago {
213 19a42f0 Dawsh 2y ago "SERVER_IP": SERVER_IP,
214 19a42f0 Dawsh 2y ago "USERNAME": extra_data.get("username", "{USERNAME}"),
215 19a42f0 Dawsh 2y ago "DATA_USAGE": readable_size(extra_data.get("used_traffic")),
216 19a42f0 Dawsh 2y ago "DATA_LIMIT": data_limit,
217 19a42f0 Dawsh 2y ago "DATA_LEFT": data_left,
218 19a42f0 Dawsh 2y ago "DAYS_LEFT": days_left,
219 19a42f0 Dawsh 2y ago "EXPIRE_DATE": expire_date,
220 19a42f0 Dawsh 2y ago "JALALI_EXPIRE_DATE": jalali_expire_date,
221 19a42f0 Dawsh 2y ago "TIME_LEFT": time_left,
222 19a42f0 Dawsh 2y ago "STATUS_EMOJI": status_emoji,
223 19a42f0 Dawsh 2y ago },
224 19a42f0 Dawsh 2y ago )
225 a47eb4c Dawsh 2y ago
226 a47eb4c Dawsh 2y ago return format_variables
227 a47eb4c Dawsh 2y ago
228 a47eb4c Dawsh 2y ago
229 fb4b69d Dawsh 2y ago def generate_user_configs(
230 a47eb4c Dawsh 2y ago inbounds: list,
231 d6600c5 Dawsh 2y ago key: str,
232 24112b4 Dawsh 2y ago user_id: int,
233 a47eb4c Dawsh 2y ago format_variables: dict,
234 a47eb4c Dawsh 2y ago ) -> Union[List, str]:
235 a47eb4c Dawsh 2y ago salt = secrets.token_hex(8)
236 fb4b69d Dawsh 2y ago configs = []
237 01cdf2e Dawsh 2y ago
238 a47eb4c Dawsh 2y ago for inb in inbounds:
239 01cdf2e Dawsh 2y ago inb_id, inbound, protocol, tag = (
240 01cdf2e Dawsh 2y ago inb.id,
241 01cdf2e Dawsh 2y ago json.loads(inb.config),
242 01cdf2e Dawsh 2y ago inb.protocol,
243 01cdf2e Dawsh 2y ago inb.tag,
244 01cdf2e Dawsh 2y ago )
245 a47eb4c Dawsh 2y ago
246 a47eb4c Dawsh 2y ago format_variables.update({"PROTOCOL": protocol.name})
247 a47eb4c Dawsh 2y ago if not inbound:
248 a47eb4c Dawsh 2y ago continue
249 a47eb4c Dawsh 2y ago
250 dd32f70 Dawsh 2y ago format_variables.update(
251 dd32f70 Dawsh 2y ago {"TRANSPORT": inbound.get("network", "<missing>")}
252 dd32f70 Dawsh 2y ago )
253 a47eb4c Dawsh 2y ago with GetDB() as db:
254 a47eb4c Dawsh 2y ago hosts = crud.get_inbound_hosts(db, inb_id)
255 f446990 Dawsh 2y ago
256 a47eb4c Dawsh 2y ago for host in hosts:
257 a88ed47 Mardin 2y ago if host.is_disabled:
258 a88ed47 Mardin 2y ago continue
259 fe923dc Dawsh 2y ago host_snis = host.sni.split(",") if host.sni else []
260 dd32f70 Dawsh 2y ago sni_list = host_snis or inbound.get("sni", [])
261 a47eb4c Dawsh 2y ago if sni_list:
262 a47eb4c Dawsh 2y ago sni = random.choice(sni_list).replace("*", salt)
263 fe923dc Dawsh 2y ago else:
264 fe923dc Dawsh 2y ago sni = ""
265 a47eb4c Dawsh 2y ago
266 fe923dc Dawsh 2y ago host_hosts = host.host.split(",") if host.host else []
267 dd32f70 Dawsh 2y ago req_host_list = host_hosts or inbound.get("host", [])
268 a47eb4c Dawsh 2y ago if req_host_list:
269 a47eb4c Dawsh 2y ago req_host = random.choice(req_host_list).replace("*", salt)
270 fe923dc Dawsh 2y ago else:
271 fe923dc Dawsh 2y ago req_host = ""
272 01cdf2e Dawsh 2y ago
273 01cdf2e Dawsh 2y ago host_tls = (
274 01cdf2e Dawsh 2y ago None
275 01cdf2e Dawsh 2y ago if host.security == InboundHostSecurity.inbound_default
276 01cdf2e Dawsh 2y ago else host.security.value
277 01cdf2e Dawsh 2y ago )
278 f446990 Dawsh 2y ago data = V2Data(
279 f446990 Dawsh 2y ago protocol.value,
280 f446990 Dawsh 2y ago host.remark.format_map(format_variables),
281 f446990 Dawsh 2y ago host.address.format_map(format_variables),
282 f446990 Dawsh 2y ago host.port or inbound["port"],
283 dd32f70 Dawsh 2y ago transport_type=inbound.get("network"),
284 f446990 Dawsh 2y ago sni=sni,
285 f446990 Dawsh 2y ago host=req_host,
286 dd32f70 Dawsh 2y ago tls=host_tls or inbound.get("tls"),
287 8825243 Dawsh 2y ago header_type=inbound.get("header_type"),
288 244c9d8 Dawsh 2y ago alpn=host.alpn if host.alpn != "none" else None,
289 f446990 Dawsh 2y ago path=(
290 f446990 Dawsh 2y ago host.path.format_map(format_variables)
291 f446990 Dawsh 2y ago if host.path
292 d6600c5 Dawsh 2y ago else inbound.get("path")
293 f446990 Dawsh 2y ago ),
294 8825243 Dawsh 2y ago fingerprint=host.fingerprint.value or inbound.get("fp"),
295 8825243 Dawsh 2y ago reality_pbk=inbound.get("pbk"),
296 8825243 Dawsh 2y ago reality_sid=inbound.get("sid"),
297 0f3112a Dawsh 2y ago client_address=calculate_client_address(
298 0f3112a Dawsh 2y ago inbound.get("address"), user_id
299 24112b4 Dawsh 2y ago ),
300 8825243 Dawsh 2y ago flow=inbound.get("flow"),
301 d636f3f Dawsh 2y ago dns_servers=(
302 d636f3f Dawsh 2y ago host.dns_servers.split(",") if host.dns_servers else []
303 d636f3f Dawsh 2y ago ),
304 d636f3f Dawsh 2y ago mtu=host.mtu,
305 b584c6c Dawsh 2y ago allowed_ips=(
306 b584c6c Dawsh 2y ago list(map(str.strip, host.allowed_ips.split(",")))
307 b584c6c Dawsh 2y ago if host.allowed_ips
308 b584c6c Dawsh 2y ago else None
309 b584c6c Dawsh 2y ago ),
310 f446990 Dawsh 2y ago allow_insecure=host.allowinsecure,
311 d6600c5 Dawsh 2y ago uuid=UUID(gen_uuid(key)),
312 d6600c5 Dawsh 2y ago password=gen_password(key),
313 24112b4 Dawsh 2y ago ed25519=generate_curve25519_pbk(key),
314 ab8556b Dawsh 2y ago enable_mux=host.mux,
315 03039e3 Dawsh 2y ago http_headers={
316 03039e3 Dawsh 2y ago "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.3"
317 03039e3 Dawsh 2y ago },
318 248ca9c Dawsh 2y ago shadowsocks_method="chacha20-ietf-poly1305",
319 70b35e2 Dawsh 1y ago shadowtls_version=inbound.get("shadowtls_version"),
320 e422d72 Dawsh 2y ago weight=host.weight,
321 a47eb4c Dawsh 2y ago )
322 ab8556b Dawsh 2y ago if host.fragment:
323 ab8556b Dawsh 2y ago data.fragment = True
324 ab8556b Dawsh 2y ago data.fragment_packets = host.fragment["packets"]
325 ab8556b Dawsh 2y ago data.fragment_length = host.fragment["length"]
326 ab8556b Dawsh 2y ago data.fragment_interval = host.fragment["interval"]
327 fb4b69d Dawsh 2y ago configs.append(data)
328 fb4b69d Dawsh 2y ago
329 fb4b69d Dawsh 2y ago return configs
330 a47eb4c Dawsh 2y ago
331 a47eb4c Dawsh 2y ago
332 a47eb4c Dawsh 2y ago def encode_title(text: str) -> str:
333 a47eb4c Dawsh 2y ago return f"base64:{base64.b64encode(text.encode()).decode()}"