Skip to content

Instantly share code, notes, and snippets.

@bubasik
Last active June 7, 2026 13:19
Show Gist options
  • Select an option

  • Save bubasik/af37247b71ca0b253161b48614aba61a to your computer and use it in GitHub Desktop.

Select an option

Save bubasik/af37247b71ca0b253161b48614aba61a to your computer and use it in GitHub Desktop.
Как вытащить vpn список vless серверов из happ подписки для v2raya, v2rayN приложений и подключить устройств выше лимита по подписке
// Path to store all incoming requests
define('LOG_OUTPUT', __DIR__ . '/requests.log');
$data = sprintf(
"[%s]\n%s %s %s\n\nHTTP HEADERS:\n",
date('c'),
$_SERVER['REQUEST_METHOD'],
$_SERVER['REQUEST_URI'],
$_SERVER['SERVER_PROTOCOL']
);
foreach ($_SERVER as $name => $value) {
if (preg_match('/^HTTP_/',$name)) {
// convert HTTP_HEADER_NAME to Header-Name
$name = strtr(substr($name,5),'_',' ');
$name = ucwords(strtolower($name));
$name = strtr($name,' ','-');
// add to list
$data .= $name . ': ' . $value . "\n";
}
}
$data .= "\nREQUEST BODY:\n" . file_get_contents('php://input') . "\n";
file_put_contents(LOG_OUTPUT, $data, FILE_APPEND|LOCK_EX);
echo("OK!\n");
<?php
/**
* Happ Proxy Subscription Converter
* Для V2RayN / V2RayA
*/
// ================= НАСТРОЙКИ =================
// Ссылка подписки (можно передать через ?url= в запросе)
$subscriptionUrl = 'https://subscription.web.tech/qqwweeerrrttt45';
// Заголовки, имитирующие мобильное приложение Happ
$headers = [
'User-Agent: Happ/3.13.0',
'X-Device-Os: Android',
'X-Device-Locale: ru',
'X-Device-Model: ELP-NX1',
'X-Ver-Os: 15',
'Accept-Encoding: gzip',
'Connection: close',
// Эти два заголовка можно рандомизировать для обхода лимита устройств:
'X-Hwid: 74jf74nf8f4jr5je',
'X-Real-Ip: 101.202.303.404',
'X-Forwarded-For: 101.202.303.404',
];
// Таймаут запроса (сек)
$timeout = 30;
// =============================================
// Если существует кэшированная версия…
if (file_exists("output.cache")) {
if ((time() - '10800') < filemtime("output.cache")) {
$file_cache = "output.cache";
$file_cache_buffer = fopen ($file_cache,'r');
$content_cache = fread( $file_cache_buffer, filesize( $file_cache ) );
fclose($file_cache_buffer);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-cache, max-age=0');
echo $content_cache;
$cache_use="1";
exit;
}}
//если нет кешированной версии то делаем запрос
if (@$cache_use!="1") {
// Инициализация cURL
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $subscriptionUrl,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => true, // При проблемах с сертификатом можно поставить false
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_ENCODING => '', // Автоматическая обработка gzip
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// Обработка ошибок
if ($error || $httpCode !== 200 || !$response) {
http_response_code(502);
die(json_encode([
'error' => 'Failed to fetch subscription',
'details' => $error ?: "HTTP $httpCode",
'hint' => 'Проверьте ссылку, заголовки или попробуйте сменить HWID/IP'
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
// Декодируем Base64 (если нужно)
$decoded = base64_decode($response, true);
$output = $decoded ?: $response; // Если не Base64 — берём как есть
file_put_contents('output.cache', $output);
// Отдаём результат
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-cache, max-age=0');
echo $output;
}
@kirnfs

kirnfs commented Apr 1, 2026

Copy link
Copy Markdown

Спасибо за разработку!
Настроил через uhttpd на openWrt используя v2RayA

@bubasik

bubasik commented Apr 1, 2026

Copy link
Copy Markdown
Author

Спасибо за отзыв! Классно что наработки используют и другие люди ;)

@bubasik

bubasik commented Apr 5, 2026

Copy link
Copy Markdown
Author

Если при парсинга подписки выдает json массив а не построчно vless адреса то нужно в header запросе поменять user-agent на Sing-box.
Должно получится:
'User-Agent: Sing-box/1.8.29',
'X-Device-Os: Android',
'X-Device-Locale: ru',

@bubasik

bubasik commented Apr 5, 2026

Copy link
Copy Markdown
Author

Нашёл панель которую все используют - это Remnawave, можно посмотреть её код чтобы потом лучше парсить данные и обходить ограничение на кол-во устройств:
https://docs.rw/docs/features/hwid-device-limit/
https://docs.rw/docs/learn/quick-start/

Конфиг файлы где есть юзер агенты:
https://github.com/remnawave/subscription-page/blob/main/frontend/public/assets/app-config.json
https://github.com/remnawave/panel/blob/main/src/data/clients.ts

@bubasik

bubasik commented Apr 5, 2026

Copy link
Copy Markdown
Author

Есть ещё клиент xray на питоне, может кому полезно будет для мини-сервера:
https://github.com/Yue2u/xray-client

@kabus1917

Copy link
Copy Markdown

На flask-e с запуском в докере
https://gist.github.com/kabus1917/95656ca13844529b5e7a777a89680a4a

@kirnfs

kirnfs commented Apr 26, 2026

Copy link
Copy Markdown

Привет!
Стал приходить новый формат сообщения, который поддерживает Happ, но v2rayA не понимает, поэтому нужна конвертация.

Приходит массив, содержащий json конфигурации серверов.
Пример:
[{"dns":{"hosts":{"gg1.ru":"222.22.22.111","gg2.ru":"222.22.22.111"},"servers":["https://8.8.8.8/dns-query",{"address":"https://8.8.8.8/dns-query","domains":["geosite:github"]},{"address":"https://77.88.80.80/dns-query","domains":["geosite:private"]}],"queryStrategy":"UseIPv4"},"log":{"loglevel":"warning"},"stats":{},"policy":{"levels":{"8":{"connIdle":300,"handshake":4,"uplinkOnly":1,"downlinkOnly":1}},"system":{"statsOutboundUplink":true,"statsOutboundDownlink":true}},"routing":{"rules":[{"port":53,"outboundTag":"dns-out"},{"domain":["geosite:win-spy"],"outboundTag":"block"},{"ip":["77.88.80.80"],"outboundTag":"direct"},{"ip":["8.8.8.8"],"outboundTag":"proxy"},{"domain":["geosite:github"],"outboundTag":"proxy"},{"domain":["geosite:private"],"outboundTag":"direct"},{"ip":["geoip:private","geoip:direct"],"outboundTag":"direct"}],"domainStrategy":"IPIfNonMatch"},"inbounds":[{"tag":"socks","port":10808,"listen":"127.0.0.1","protocol":"socks","settings":{"udp":true,"auth":"noauth","userLevel":8},"sniffing":{"enabled":true,"destOverride":["http","quic","tls"]}},{"tag":"http","port":10809,"listen":"127.0.0.1","protocol":"http","settings":{"userLevel":8},"sniffing":{"enabled":true,"destOverride":["http","quic","tls"]}}],"outbounds":[{"tag":"proxy","protocol":"vless","settings":{"vnext":[{"address":"111.11.11.111","port":7444,"users":[{"id":"af136882-e7c4-4b2c-a04d-dafff11d244e","encryption":"none","flow":"xtls-rprx-vision"}]}]},"streamSettings":{"network":"tcp","tcpSettings":{},"security":"reality","realitySettings":{"serverName":"test.com","publicKey":"3QAO21ot2H1HcFs_f6EEaQ","shortId":"d33fb333","fingerprint":"chrome"}}},{"tag":"direct","protocol":"freedom"},{"tag":"block","protocol":"blackhole"},{"tag":"dns-out","protocol":"dns","proxySettings":{"tag":"proxy"}}],"remarks":"Пример1"},{"dns":{"hosts":{"gg1.ru":"222.22.22.111","gg2.ru":"222.22.22.111"},"servers":["https://8.8.8.8/dns-query",{"address":"https://8.8.8.8/dns-query","domains":["geosite:github"]},{"address":"https://77.88.80.80/dns-query","domains":["geosite:private"]}],"queryStrategy":"UseIPv4"},"log":{"loglevel":"warning"},"stats":{},"policy":{"levels":{"8":{"connIdle":300,"handshake":4,"uplinkOnly":1,"downlinkOnly":1}},"system":{"statsOutboundUplink":true,"statsOutboundDownlink":true}},"routing":{"rules":[{"port":53,"outboundTag":"dns-out"},{"domain":["geosite:win-spy"],"outboundTag":"block"},{"ip":["77.88.8.8"],"outboundTag":"direct"},{"ip":["8.8.8.8"],"outboundTag":"proxy"},{"domain":["geosite:github"],"outboundTag":"proxy"},{"domain":["geosite:private"],"outboundTag":"direct"},{"ip":["geoip:private","geoip:direct"],"outboundTag":"direct"}],"domainStrategy":"IPIfNonMatch"},"inbounds":[{"tag":"socks","port":10808,"listen":"127.0.0.1","protocol":"socks","settings":{"udp":true,"auth":"noauth","userLevel":8},"sniffing":{"enabled":true,"destOverride":["http","quic","tls"]}},{"tag":"http","port":10809,"listen":"127.0.0.1","protocol":"http","settings":{"userLevel":8},"sniffing":{"enabled":true,"destOverride":["http","quic","tls"]}}],"outbounds":[{"tag":"proxy","protocol":"vless","settings":{"vnext":[{"address":"111.111.111.111","port":7444,"users":[{"id":"af123456-e0c4-1b1c-a01d-dsadas","encryption":"none","flow":"xtls-rprx-vision"}]}]},"streamSettings":{"network":"tcp","tcpSettings":{},"security":"reality","realitySettings":{"serverName":"test.com","publicKey":"8QAO11ot6U7TcGs_f9EE","shortId":"be2ce555","fingerprint":"chrome"}}},{"tag":"direct","protocol":"freedom"},{"tag":"block","protocol":"blackhole"},{"tag":"dns-out","protocol":"dns","proxySettings":{"tag":"proxy"}}],"remarks":"Пример2"}]

В связи с этим пришлось дописывать обработчик
Я не знаток php, поэтому показываю тот вариант который сделал:

$decodJson = json_decode($response, true);
$vlessLinkArray = "";

foreach($decodJson as $item){
try{
$jsonConf = json_decode(json_encode($item));
$outbounds = $jsonConf->outbounds;

foreach($outbounds as $outBound)
{
$protocol = $outBound->protocol;
if(strtolower($protocol) === "vless")
{
$tag = $outBound->tag;
$id = $outBound->settings->vnext[0]->users[0]->id;
$flow = $outBound->settings->vnext[0]->users[0]->flow;
$encryption = $outBound->settings->vnext[0]->users[0]->encryption;
$host = $outBound->settings->vnext[0]->address;
$port = $outBound->settings->vnext[0]->port;
if(is_null($host) || is_null($port)) {continue;}

$networkType = $outBound->streamSettings->network;
$security = $outBound->streamSettings->security;
$sni = $outBound->streamSettings->realitySettings->serverName;
$fp = $outBound->streamSettings->realitySettings->fingerprint;
$pbk = $outBound->streamSettings->realitySettings->publicKey;
$sid = $outBound->streamSettings->realitySettings->shortId;
$connectName = $jsonConf->remarks;

$vlessLink = "vless://".$id.
"@".$host.
":".$port.
"?type=".$networkType.
"&security=".$security.
"&encryption=".$encryption.
"&flow=".$flow.
"&sni=".$sni.
"&fp=".$fp.
"&pbk=".$pbk.
"&sid=".$sid.
"#".$connectName.
"\n"
;
$vlessLinkArray .= $vlessLink;
}
}
}
catch(Throwable $ex){} //Error
}
echo trim($vlessLinkArray);

В целом можно улучшить скрипт и сделать его более универсальным:

  1. Если пришла строка(или base64 строка) в виде уже готовых vless url, то возвращаем их в ответе
  2. Если получаем набор json конфигов, то преобразуем их в vless url, собираем в строки и потом возвращаем

П.С. Кстати совет https://gist.github.com/bubasik/af37247b71ca0b253161b48614aba61a?permalink_comment_id=6082191#gistcomment-6082191
тоже работает, но может со временем этот механизм перестанет работать, тогда придется конвертировать данные...

@rico-x

rico-x commented Apr 26, 2026

Copy link
Copy Markdown

Спасибо, утащил наработки себе в проект и автоматизировал для работы в OpenWRT чтоб обе части запускались прямо на роутере https://github.com/rico-x/tproxy-manager

@bubasik

bubasik commented Apr 26, 2026

Copy link
Copy Markdown
Author

Привет! Стал приходить новый формат сообщения, который поддерживает Happ, но v2rayA не понимает, поэтому нужна конвертация.

@kirnfs Спасибо за предложенный код, у меня тоже вот приходить начал в json но разными скриптами и онлайн сервисами не получилось его превратить в нормальные vless ссылки которые читались v2rayA, а через смену useragent - получилось. (почитал документацию движков которые они используют и что там для разных устройств немного разный код отдаётся)

@bubasik

bubasik commented Apr 26, 2026

Copy link
Copy Markdown
Author

Спасибо, утащил наработки себе в проект и автоматизировал для работы в OpenWRT чтоб обе части запускались прямо на роутере https://github.com/rico-x/tproxy-manager
@rico-x крутой у Вас проект получился, рад что мой код пригодился и приятно видеть такую обратную связь по мини проекту на один вечер

@littlelittlepony

Copy link
Copy Markdown

Подскажите, пожалуйста, в какую сторону копать: вытащенные сервера tcp+reality работают, а вот xhttp+tls - нет. В firefox ошибка "Ошибка при установлении защищённого соединения. Код ошибки: PR_END_OF_FILE_ERROR". В chrome просто "Сайт x.x неожиданно разорвал соединение ERR_CONNECTION_CLOSED".

@kirnfs

kirnfs commented Apr 27, 2026

Copy link
Copy Markdown

@littlelittlepony

  1. Попробовать на других браузерах, хром, опера
  2. Вмешательство VPN или прокси. Некоторые VPN и прокси изменяют сетевые настройки, чтобы перенаправить трафик через защищённые туннели. Если эти настройки вмешиваются в работу протоколов безопасности Firefox, это может вызвать ошибку PR_END_OF_FILE_ERROR

Вообще вопрос звучит неинформативно, не совсем понятно что такое вытащенные сервера (откуда и куда? )))
Ну и в целом здесь мы больше обсуждаем проблему как из happ подписки получить нормальный список vless серверов и скормить его v2rayA сервису...

@Bougakov

Copy link
Copy Markdown

Оказалось что это всё просто можно сделать без ПХП, одним Cloudflare Worker-ом:

export default {
  async fetch(request, env, ctx) {
    // 1. Parse the incoming request URL
    const url = new URL(request.url);

    // 2. Security Check: Validate the query parameter
    // Change "password" and "12345" to whatever key and value you want.
    if (url.searchParams.get("password") !== "12345") {
      return new Response("Unauthorized", { 
        status: 401,
        headers: { "Content-Type": "text/plain" }
      });
    }

    // 3. Define your target URL
    const targetUrl = "https://ссылка_на_подписку";

    // 4. Clone and clean the incoming headers
    const modifiedHeaders = new Headers(request.headers);
    modifiedHeaders.delete("cf-connecting-ip");
    modifiedHeaders.delete("true-client-ip");
    modifiedHeaders.delete("x-forwarded-for");
    modifiedHeaders.set("User-Agent", "Sing-box/1.8.29");
    modifiedHeaders.set("X-Device-Os", "Android");
    modifiedHeaders.set("X-Device-Model", "Generic");
    modifiedHeaders.set("X-Ver-Os", "15");
    modifiedHeaders.set("X-Device-Locale", "ru");
    modifiedHeaders.set("X-Hwid", "64jf75nf8f5jr6je");
    modifiedHeaders.set("X-Real-Ip", "подставьте_свой");
    modifiedHeaders.set("X-Forwarded-For", "подставьте_свой");

    try {
      // 5. Fetch the target page
      const response = await fetch(targetUrl, {
        method: request.method,
        headers: modifiedHeaders,
        body: request.body
      });

      return response;

    } catch (error) {
      return new Response(`Proxy Error: ${error.message}`, { status: 500 });
    }
  },
};

Расшифровывалку ссылок вида happ://crypt5/ххххх брать тут - https://github.com/amurcanov/happ-decrypt-universal/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment