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