-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsubscription.php
More file actions
308 lines (276 loc) · 13 KB
/
Copy pathsubscription.php
File metadata and controls
308 lines (276 loc) · 13 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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
<?php
declare(strict_types=1);
require_once __DIR__ . '/functions.php';
require_setup_complete();
$id = trim((string) ($_GET['id'] ?? ''));
if ($id === '') {
if (wants_json_response()) {
error_response('Subscription ID is required.', 400);
}
http_response_code(400);
echo 'Subscription ID is required.';
exit;
}
$subscription = fetch_subscription($id);
if ($subscription === null) {
if (wants_json_response()) {
error_response('Subscription not found.', 404);
}
http_response_code(404);
echo 'Subscription not found.';
exit;
}
$status = subscription_status_detail($subscription);
if (!$status['ok']) {
if (wants_json_response() || isset($_GET['format'])) {
error_response($status['reason'], (int) $status['code']);
}
$branding = get_branding_settings();
render_shell_start('Subscription Unavailable', $status['reason'], false);
?>
<div class="mx-auto max-w-2xl">
<section class="glass rounded-[28px] p-6 sm:p-8">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-slate-500">Access blocked</p>
<h2 class="mt-2 text-2xl font-extrabold text-slate-900"><?= html_escape($subscription['name']) ?></h2>
<p class="mt-3 text-sm text-slate-600"><?= html_escape((string) $status['reason']) ?></p>
<p class="mt-6 text-sm text-slate-500"><?= html_escape($branding['vpn_name']) ?></p>
</section>
</div>
<?php
render_shell_end();
exit;
}
try {
$sourceInfo = fetch_remote_json_source();
$payload = build_default_subscription_payload($subscription, $sourceInfo['decoded']);
$happ = build_happ_config($sourceInfo['decoded']);
$countries = collect_countries_from_source($sourceInfo['decoded']);
} catch (Throwable $exception) {
if (wants_json_response() || isset($_GET['format'])) {
error_response($exception->getMessage(), 502);
}
http_response_code(502);
echo html_escape($exception->getMessage());
exit;
}
$format = strtolower(trim((string) ($_GET['format'] ?? '')));
if ($format === 'source') {
header('Content-Type: application/json; charset=utf-8');
echo $sourceInfo['raw'];
exit;
}
// Default Happ import: many Happ clients expect a base64-encoded list of proxy URIs.
if ($format === 'happ') {
header('Content-Type: text/plain; charset=utf-8');
$serversText = implode("\n", $payload['servers'] ?? []);
$header = build_happ_profile_header($subscription, $payload);
echo base64_encode($header . $serversText);
exit;
}
// Raw happ-compatible sing-box JSON (kept for debugging/advanced clients)
if ($format === 'happ_config' || $format === 'happ_json') {
$happConfig = $happ['config'];
$happConfig['meta'] = $payload['meta'] ?? null;
$happConfig['remarks'] = $payload['remarks'] ?? null;
json_response($happConfig);
}
// Base64-encoded happ JSON (some clients import base64-encoded config)
if ($format === 'happ_base64') {
$happConfig = $happ['config'];
$happConfig['meta'] = $payload['meta'] ?? null;
$happConfig['remarks'] = $payload['remarks'] ?? null;
header('Content-Type: text/plain; charset=utf-8');
echo base64_encode(json_encode($happConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
exit;
}
// Wrapped happ JSON (some clients expect an envelope)
if ($format === 'happ_wrapped') {
$happConfig = $happ['config'];
$happConfig['meta'] = $payload['meta'] ?? null;
$happConfig['remarks'] = $payload['remarks'] ?? null;
json_response(['happ' => $happConfig, 'source' => $sourceInfo['url'] ?? null]);
}
// Combined Happ delivery: base64-encoded URI list + sing-box routing object
if ($format === 'happ_combo') {
$happConfig = $happ['config'];
// NOTE: no meta included here — clients like Happ break when meta is present.
$serversText = implode("\n", $payload['servers'] ?? []);
$header = build_happ_profile_header($subscription, $payload);
$envelope = [
'subscription' => base64_encode($header . $serversText),
'routing' => $happConfig['route'] ?? null,
'source' => $sourceInfo['url'] ?? null,
];
json_response($envelope);
}
// Base64-encoded combined envelope (some clients expect single base64 payload)
if ($format === 'happ_combo_base64') {
$happConfig = $happ['config'];
// NOTE: base64-encoded envelope without meta
$serversText = implode("\n", $payload['servers'] ?? []);
$header = build_happ_profile_header($subscription, $payload);
$envelope = [
'subscription' => base64_encode($header . $serversText),
'routing' => $happConfig['route'] ?? null,
'source' => $sourceInfo['url'] ?? null,
];
header('Content-Type: text/plain; charset=utf-8');
echo base64_encode(json_encode($envelope, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
exit;
}
// Split mode: minimal subscription (base64 URIs) and routing separately (no meta ever present)
if ($format === 'happ_split') {
$serversText = implode("\n", $payload['servers'] ?? []);
$header = build_happ_profile_header($subscription, $payload);
$envelope = [
'subscription' => base64_encode($header . $serversText),
'routing' => $happ['config']['route'] ?? null,
'source' => $sourceInfo['url'] ?? null,
];
json_response($envelope);
}
if ($format === 'happ_split_base64') {
$serversText = implode("\n", $payload['servers'] ?? []);
$header = build_happ_profile_header($subscription, $payload);
$envelope = [
'subscription' => base64_encode($header . $serversText),
'routing' => $happ['config']['route'] ?? null,
'source' => $sourceInfo['url'] ?? null,
];
header('Content-Type: text/plain; charset=utf-8');
echo base64_encode(json_encode($envelope, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
exit;
}
// Happ geo mode: provide base64 subscription and links to geoip/geosite DAT files
if ($format === 'happ_geo') {
$serversText = implode("\n", $payload['servers'] ?? []);
$header = build_happ_profile_header($subscription, $payload);
$envelope = [
'subscription' => base64_encode($header . $serversText),
'geoip' => 'https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/geoip.dat',
'geosite' => 'https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/geosite.dat',
'source' => $sourceInfo['url'] ?? null,
];
json_response($envelope);
}
if ($format === 'happ_geo_base64') {
$serversText = implode("\n", $payload['servers'] ?? []);
$header = build_happ_profile_header($subscription, $payload);
$envelope = [
'subscription' => base64_encode($header . $serversText),
'geoip' => 'https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/geoip.dat',
'geosite' => 'https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/geosite.dat',
'source' => $sourceInfo['url'] ?? null,
];
header('Content-Type: text/plain; charset=utf-8');
echo base64_encode(json_encode($envelope, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
exit;
}
if ($format === '' && wants_json_response()) {
json_response($payload);
}
// Plain newline-separated list of server URIs (useful for simple clients)
if ($format === 'list') {
header('Content-Type: text/plain; charset=utf-8');
echo implode("\n", $payload['servers'] ?? []);
exit;
}
// Base64-encoded subscription (many clients expect base64 of newline-separated URIs)
if ($format === 'base64') {
header('Content-Type: text/plain; charset=utf-8');
$serversText = implode("\n", $payload['servers'] ?? []);
$header = build_happ_profile_header($subscription, $payload);
echo base64_encode($header . $serversText);
exit;
}
$branding = get_branding_settings();
$subscriptionUrl = absolute_app_url('subscription/' . $subscription['id']);
$happUrl = $subscriptionUrl . '?format=happ';
$sourceUrl = $subscriptionUrl . '?format=source';
$jsonUrl = $subscriptionUrl . '?response=json';
render_shell_start($subscription['name'], 'Dynamic subscription page with raw, source, and Happ delivery modes.', false);
?>
<div class="mx-auto max-w-5xl space-y-6">
<section class="glass rounded-[32px] p-6 sm:p-8">
<div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div class="max-w-2xl">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-slate-500"><?= html_escape($branding['vpn_name']) ?></p>
<div class="mt-3 flex flex-wrap items-center gap-3">
<h1 class="text-3xl font-extrabold tracking-tight text-slate-900"><?= html_escape($subscription['name']) ?></h1>
<?php if ((string) $subscription['badge'] !== ''): ?>
<span class="rounded-full bg-slate-900 px-3 py-1 text-xs font-semibold text-white"><?= html_escape($subscription['badge']) ?></span>
<?php endif; ?>
</div>
<p class="mt-4 text-sm leading-7 text-slate-600"><?= html_escape((string) $subscription['description']) ?></p>
<div class="mt-5 flex flex-wrap gap-3 text-sm text-slate-500">
<span>Plan: <?= html_escape((string) ($subscription['plan_name'] ?? 'Custom')) ?></span>
<span>Telegram: <?= html_escape((string) ($subscription['telegram_id'] ?: 'Not linked')) ?></span>
<span>Expires: <?= html_escape($subscription['expires_at']) ?> UTC</span>
</div>
</div>
<div class="glass-dark rounded-[28px] p-5 text-white shadow-xl">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-white/60">Countdown</p>
<p id="countdown" class="mt-3 text-3xl font-extrabold">Calculating...</p>
<p class="mt-2 text-sm text-white/70">RU traffic bypasses VPN directly. All other traffic routes through the proxy selector.</p>
</div>
</div>
</section>
<div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<section class="glass rounded-[28px] p-6">
<div class="mb-5">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-slate-500">Subscription Links</p>
<h2 class="mt-2 text-xl font-extrabold text-slate-900">Import and API targets</h2>
</div>
<div class="space-y-4 text-sm text-slate-600">
<div class="rounded-2xl bg-white/70 p-4">
<p class="font-semibold text-slate-900">Public page / default endpoint</p>
<code class="mt-2 block break-all text-xs"><?= html_escape($subscriptionUrl) ?></code>
</div>
<div class="rounded-2xl bg-white/70 p-4">
<p class="font-semibold text-slate-900">Happ / sing-box config</p>
<code class="mt-2 block break-all text-xs"><?= html_escape($happUrl) ?></code>
</div>
</div>
</section>
<section class="glass rounded-[28px] p-6">
<div class="mb-5">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-slate-500">Happ Import</p>
<h2 class="mt-2 text-xl font-extrabold text-slate-900">Quick instructions</h2>
</div>
<ol class="space-y-3 text-sm text-slate-600">
<li>1. Open your Happ-compatible client and choose import from URL.</li>
<li>2. Paste the Happ link from this page: <code>?format=happ</code>.</li>
<li>3. Keep updates enabled so the client refreshes as the external JSON source changes.</li>
<li>4. If you need the untouched source for debugging, use <code>?format=source</code>.</li>
</ol>
<div class="mt-5 rounded-2xl bg-white/70 p-4 text-sm text-slate-600">
<p class="font-semibold text-slate-900">Detected countries</p>
<div class="mt-3 flex flex-wrap gap-2">
<?php foreach ($countries as $country): ?>
<span class="rounded-full border border-white/70 bg-white px-3 py-1 text-xs font-semibold text-slate-700"><?= html_escape((string) $country) ?></span>
<?php endforeach; ?>
</div>
</div>
</section>
</div>
</div>
<script>
const expiresAt = new Date('<?= html_escape(gmdate('c', strtotime((string) $subscription['expires_at']))) ?>').getTime();
const countdownElement = document.getElementById('countdown');
function updateCountdown() {
const now = Date.now();
const diff = expiresAt - now;
if (diff <= 0) {
countdownElement.textContent = 'Expired';
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24);
const minutes = Math.floor((diff / (1000 * 60)) % 60);
const seconds = Math.floor((diff / 1000) % 60);
countdownElement.textContent = `${days}d ${hours}h ${minutes}m ${seconds}s`;
}
updateCountdown();
setInterval(updateCountdown, 1000);
</script>
<?php render_shell_end();