mirror of
https://github.com/by-sonic/tglock.git
synced 2026-11-05 15:21:55 +03:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f82543e44e | |||
| 9529df3fdf | |||
| 05ce9566a8 |
Generated
+1
-1
@@ -3822,7 +3822,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tglock"
|
name = "tglock"
|
||||||
version = "2.0.0-beta.15"
|
version = "2.0.0-beta.16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"cipher",
|
"cipher",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tglock"
|
name = "tglock"
|
||||||
version = "2.0.0-beta.15"
|
version = "2.0.0-beta.16"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
description = "Telegram unblock via local WebSocket tunnel"
|
description = "Telegram unblock via local WebSocket tunnel"
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
|
|
||||||
Если вернулось `not found` — проверь, что путь именно `/apiws`. Если ошибка про `cloudflare:sockets` — у воркера слишком старая дата совместимости, поставь в **Settings → Compatibility date** сегодняшнюю.
|
Если вернулось `not found` — проверь, что путь именно `/apiws`. Если ошибка про `cloudflare:sockets` — у воркера слишком старая дата совместимости, поставь в **Settings → Compatibility date** сегодняшнюю.
|
||||||
|
|
||||||
|
> **Обнови скрипт, если Worker создан или менял compatibility date после 17 марта 2026.** С этой даты Cloudflare отдаёт бинарные WebSocket-кадры как `Blob`, и скрипт до beta.16 закрывал каждый туннель сразу после рукопожатия. В журнале TGLock это строка `WebSocket закрыт: 1003 binary messages required`, в диагностике туннели мигают 0 → 1 → 0. Текущий скрипт выставляет `binaryType = "arraybuffer"` и принимает `Blob` (исправление от [@avost](https://github.com/by-sonic/tglock/pull/61)).
|
||||||
|
|
||||||
> **Обнови скрипт при переходе с beta.14.** Теперь Worker ждёт открытия TCP к Telegram перед ответом 101: отказ возвращает 502, таймаут за 3 секунды — 504. Очередь записи ограничена 1 МиБ / 256 сообщениями; переполнение закрывает соединение с кодом 1009, ошибки сокета — 1011. Старый Worker мог показать успешное соединение ещё до попытки подключения к Telegram и не ограничивал очередь сообщений.
|
> **Обнови скрипт при переходе с beta.14.** Теперь Worker ждёт открытия TCP к Telegram перед ответом 101: отказ возвращает 502, таймаут за 3 секунды — 504. Очередь записи ограничена 1 МиБ / 256 сообщениями; переполнение закрывает соединение с кодом 1009, ошибки сокета — 1011. Старый Worker мог показать успешное соединение ещё до попытки подключения к Telegram и не ограничивал очередь сообщений.
|
||||||
|
|
||||||
Для установки через Wrangler есть [`worker/wrangler.toml`](../worker/wrangler.toml). Из каталога `worker` можно выполнить `npx wrangler deploy` в своём Cloudflare-аккаунте. Эта команда публикует Worker; локальные тесты ничего не публикуют.
|
Для установки через Wrangler есть [`worker/wrangler.toml`](../worker/wrangler.toml). Из каталога `worker` можно выполнить `npx wrangler deploy` в своём Cloudflare-аккаунте. Эта команда публикует Worker; локальные тесты ничего не публикуют.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "tglock-ui",
|
"name": "tglock-ui",
|
||||||
"version": "2.0.0-beta.15",
|
"version": "2.0.0-beta.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tglock-ui",
|
"name": "tglock-ui",
|
||||||
"version": "2.0.0-beta.15",
|
"version": "2.0.0-beta.16",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.8.0"
|
"@tauri-apps/api": "^2.8.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tglock-ui",
|
"name": "tglock-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0-beta.15",
|
"version": "2.0.0-beta.16",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test:worker": "node --experimental-vm-modules --test worker/tglock-worker.test.js",
|
"test:worker": "node --experimental-vm-modules --test worker/tglock-worker.test.js",
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ def ui_dump(label):
|
|||||||
# Dump first, then read the file; never tap coordinates inferred from a
|
# Dump first, then read the file; never tap coordinates inferred from a
|
||||||
# screenshot, a previous Activity, or assumed phone dimensions.
|
# screenshot, a previous Activity, or assumed phone dimensions.
|
||||||
adb("shell", "rm", "-f", "/sdcard/tglock-ui.xml")
|
adb("shell", "rm", "-f", "/sdcard/tglock-ui.xml")
|
||||||
adb("shell", "uiautomator", "dump", "/sdcard/tglock-ui.xml")
|
status = adb("shell", "uiautomator", "dump", "/sdcard/tglock-ui.xml", check=False)
|
||||||
|
if not adb("shell", "ls", "/sdcard/tglock-ui.xml", check=False).endswith("tglock-ui.xml"):
|
||||||
|
# On a loaded emulator UIAutomator intermittently reports "could not get
|
||||||
|
# idle state" and writes nothing. That is the tool, not the app.
|
||||||
|
raise subprocess.SubprocessError(f"uiautomator dump failed: {status}")
|
||||||
text = adb("shell", "cat", "/sdcard/tglock-ui.xml")
|
text = adb("shell", "cat", "/sdcard/tglock-ui.xml")
|
||||||
(EVIDENCE / f"{label}.xml").write_text(text, encoding="utf-8")
|
(EVIDENCE / f"{label}.xml").write_text(text, encoding="utf-8")
|
||||||
return ET.fromstring(text)
|
return ET.fromstring(text)
|
||||||
@@ -58,8 +62,12 @@ def label_node(tree, label):
|
|||||||
|
|
||||||
|
|
||||||
def tap_label(label, stage):
|
def tap_label(label, stage):
|
||||||
for attempt in range(3):
|
for attempt in range(5):
|
||||||
|
try:
|
||||||
point = label_node(ui_dump(f"{stage}-{attempt}"), label)
|
point = label_node(ui_dump(f"{stage}-{attempt}"), label)
|
||||||
|
except (subprocess.SubprocessError, ET.ParseError) as error:
|
||||||
|
(EVIDENCE / f"{stage}-{attempt}.error.txt").write_text(str(error), encoding="utf-8")
|
||||||
|
point = None
|
||||||
if point is not None:
|
if point is not None:
|
||||||
adb("shell", "input", "tap", str(point[0]), str(point[1]))
|
adb("shell", "input", "tap", str(point[0]), str(point[1]))
|
||||||
return
|
return
|
||||||
|
|||||||
+30
-3
@@ -795,9 +795,7 @@ async fn ws_tunnel(
|
|||||||
});
|
});
|
||||||
if !received_payload || transport_error {
|
if !received_payload || transport_error {
|
||||||
let reason = match frame {
|
let reason = match frame {
|
||||||
Some(frame) => {
|
Some(frame) => describe_ws_close(frame.code.into(), &frame.reason),
|
||||||
format!("WebSocket закрыт: {} {}", frame.code, frame.reason)
|
|
||||||
}
|
|
||||||
None => "WebSocket закрыт без ответа Telegram".to_owned(),
|
None => "WebSocket закрыт без ответа Telegram".to_owned(),
|
||||||
};
|
};
|
||||||
return Err(tunnel_route_error(stats, &connected, dc, &reason));
|
return Err(tunnel_route_error(stats, &connected, dc, &reason));
|
||||||
@@ -938,6 +936,24 @@ async fn cdn_tcp_tunnel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Текст для журнала, когда WebSocket закрыла другая сторона.
|
||||||
|
///
|
||||||
|
/// `1003 binary messages required` отвечает только старый скрипт Worker'а: с
|
||||||
|
/// compatibility date 2026-03-17 Cloudflare отдаёт бинарные кадры как Blob, и
|
||||||
|
/// такой скрипт рвал каждый туннель сразу после рукопожатия (by-sonic/tglock#61).
|
||||||
|
/// Обновление TGLock код в чужом Cloudflare-аккаунте не меняет, поэтому
|
||||||
|
/// пользователю надо сказать прямо, что делать.
|
||||||
|
fn describe_ws_close(code: u16, reason: &str) -> String {
|
||||||
|
let mut text = format!("WebSocket закрыт: {code} {reason}");
|
||||||
|
if code == 1003 && reason.contains("binary messages required") {
|
||||||
|
text.push_str(
|
||||||
|
" — скрипт Cloudflare Worker устарел: опубликуйте заново \
|
||||||
|
worker/tglock-worker.js из репозитория",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
fn tunnel_route_error(
|
fn tunnel_route_error(
|
||||||
stats: &Stats,
|
stats: &Stats,
|
||||||
connected: &crate::transport::ConnectedRoute,
|
connected: &crate::transport::ConnectedRoute,
|
||||||
@@ -969,6 +985,17 @@ mod tests {
|
|||||||
use tokio_tungstenite::tungstenite::handshake::server::{Request, Response};
|
use tokio_tungstenite::tungstenite::handshake::server::{Request, Response};
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_outdated_worker_close_says_to_redeploy_it() {
|
||||||
|
let outdated = describe_ws_close(1003, "binary messages required");
|
||||||
|
assert!(outdated.starts_with("WebSocket закрыт: 1003 binary messages required"));
|
||||||
|
assert!(outdated.contains("worker/tglock-worker.js"));
|
||||||
|
assert_eq!(
|
||||||
|
describe_ws_close(1011, "Telegram socket failed"),
|
||||||
|
"WebSocket закрыт: 1011 Telegram socket failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Start the proxy on a kernel-assigned port.
|
/// Start the proxy on a kernel-assigned port.
|
||||||
///
|
///
|
||||||
/// Binding first and reading the port back removes the reserve-then-rebind
|
/// Binding first and reading the port back removes the reserve-then-rebind
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "TGLock",
|
"productName": "TGLock",
|
||||||
"version": "2.0.0-beta.15",
|
"version": "2.0.0-beta.16",
|
||||||
"identifier": "com.bysonic.tglock",
|
"identifier": "com.bysonic.tglock",
|
||||||
"mainBinaryName": "tglock",
|
"mainBinaryName": "tglock",
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
+1
-1
@@ -451,7 +451,7 @@ async function copyDiagnostics(): Promise<void> {
|
|||||||
// Public reports exclude proxy links, worker domains and free-form logs.
|
// Public reports exclude proxy links, worker domains and free-form logs.
|
||||||
const report = {
|
const report = {
|
||||||
application: "TGLock",
|
application: "TGLock",
|
||||||
version: "2.0.0-beta.15",
|
version: "2.0.0-beta.16",
|
||||||
running: status.running,
|
running: status.running,
|
||||||
activeConnections: status.activeConnections,
|
activeConnections: status.activeConnections,
|
||||||
tunnels: status.tunnels,
|
tunnels: status.tunnels,
|
||||||
|
|||||||
+17
-8
@@ -85,6 +85,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [client, server] = Object.values(new WebSocketPair());
|
const [client, server] = Object.values(new WebSocketPair());
|
||||||
|
server.binaryType = "arraybuffer";
|
||||||
server.accept();
|
server.accept();
|
||||||
|
|
||||||
const writer = upstream.writable.getWriter();
|
const writer = upstream.writable.getWriter();
|
||||||
@@ -116,28 +117,36 @@ export default {
|
|||||||
|
|
||||||
server.addEventListener("message", (event) => {
|
server.addEventListener("message", (event) => {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
if (!(event.data instanceof ArrayBuffer) && !ArrayBuffer.isView(event.data)) {
|
const data = event.data;
|
||||||
|
// С compatibility date 2026-03-17 бинарные кадры приходят как Blob, если
|
||||||
|
// binaryType не "arraybuffer". Свойство выставлено выше; Blob всё равно
|
||||||
|
// принимается — на случай рантайма, который его не учитывает. Раньше такой
|
||||||
|
// кадр закрывал туннель кодом 1003 сразу после рукопожатия.
|
||||||
|
const blob = typeof Blob !== "undefined" && data instanceof Blob;
|
||||||
|
if (!blob && !(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
|
||||||
shutdown(1003, "binary messages required");
|
shutdown(1003, "binary messages required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const chunk = event.data instanceof ArrayBuffer
|
const chunk = blob ? null : data instanceof ArrayBuffer
|
||||||
? new Uint8Array(event.data)
|
? new Uint8Array(data)
|
||||||
: new Uint8Array(event.data.buffer, event.data.byteOffset, event.data.byteLength);
|
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
||||||
if (pendingBytes + chunk.byteLength > MAX_PENDING_BYTES || pendingMessages >= MAX_PENDING_MESSAGES) {
|
const size = blob ? data.size : chunk.byteLength;
|
||||||
|
if (pendingBytes + size > MAX_PENDING_BYTES || pendingMessages >= MAX_PENDING_MESSAGES) {
|
||||||
shutdown(1009, "Telegram write queue full");
|
shutdown(1009, "Telegram write queue full");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingBytes += chunk.byteLength;
|
pendingBytes += size;
|
||||||
pendingMessages += 1;
|
pendingMessages += 1;
|
||||||
pending = pending
|
pending = pending
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
|
const bytes = chunk || new Uint8Array(await data.arrayBuffer());
|
||||||
await writer.ready;
|
await writer.ready;
|
||||||
if (!closed) await writer.write(chunk);
|
if (!closed) await writer.write(bytes);
|
||||||
})
|
})
|
||||||
.catch(() => shutdown(1011, "Telegram write failed"))
|
.catch(() => shutdown(1011, "Telegram write failed"))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
pendingBytes -= chunk.byteLength;
|
pendingBytes -= size;
|
||||||
pendingMessages -= 1;
|
pendingMessages -= 1;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ async function fixture({ opened = Promise.resolve(), write, timeout = false } =
|
|||||||
const events = new Map();
|
const events = new Map();
|
||||||
const state = { connects: 0, accepted: false, socketCloses: 0, sent: [], closes: [] };
|
const state = { connects: 0, accepted: false, socketCloses: 0, sent: [], closes: [] };
|
||||||
const server = {
|
const server = {
|
||||||
accept() { state.accepted = true; },
|
accept() { state.accepted = true; state.binaryTypeOnAccept = this.binaryType; },
|
||||||
addEventListener(name, callback) { events.set(name, callback); },
|
addEventListener(name, callback) { events.set(name, callback); },
|
||||||
close(code, reason) { state.closes.push({ code, reason }); },
|
close(code, reason) { state.closes.push({ code, reason }); },
|
||||||
send(chunk) { state.sent.push(Array.from(chunk)); },
|
send(chunk) { state.sent.push(Array.from(chunk)); },
|
||||||
@@ -41,7 +41,7 @@ async function fixture({ opened = Promise.resolve(), write, timeout = false } =
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const context = vm.createContext({
|
const context = vm.createContext({
|
||||||
URL, Uint8Array, ArrayBuffer, setTimeout: timeout ? (fn) => setTimeout(fn, 0) : setTimeout, clearTimeout,
|
URL, Uint8Array, ArrayBuffer, Blob, setTimeout: timeout ? (fn) => setTimeout(fn, 0) : setTimeout, clearTimeout,
|
||||||
Response: class {
|
Response: class {
|
||||||
constructor(body, options) { this.body = body; Object.assign(this, options); }
|
constructor(body, options) { this.body = body; Object.assign(this, options); }
|
||||||
},
|
},
|
||||||
@@ -133,6 +133,27 @@ test("serialize binary writes, preserve bytes and view offsets", async () => {
|
|||||||
f.event("close");
|
f.event("close");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("binary frames arrive as ArrayBuffer on new compatibility dates", async () => {
|
||||||
|
// With compatibility date 2026-03-17+ Cloudflare delivers Blob unless
|
||||||
|
// binaryType is set before accept(); the Worker then closed every tunnel
|
||||||
|
// with 1003 right after the handshake (#61).
|
||||||
|
const f = await fixture();
|
||||||
|
await f.fetch();
|
||||||
|
assert.equal(f.state.binaryTypeOnAccept, "arraybuffer");
|
||||||
|
f.event("close");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Blob frames are still relayed in order, never closed as text", async () => {
|
||||||
|
const f = await fixture();
|
||||||
|
await f.fetch();
|
||||||
|
f.message(new Blob([new Uint8Array([7, 8])]));
|
||||||
|
f.message(new Uint8Array([9]).buffer);
|
||||||
|
for (let i = 0; i < 5; i++) await tick();
|
||||||
|
assert.deepEqual(f.writes, [[7, 8], [9]]);
|
||||||
|
assert.deepEqual(f.state.closes, []);
|
||||||
|
f.event("close");
|
||||||
|
});
|
||||||
|
|
||||||
test("overflow closes socket and discards queued writes", async () => {
|
test("overflow closes socket and discards queued writes", async () => {
|
||||||
const f = await fixture();
|
const f = await fixture();
|
||||||
await f.fetch();
|
await f.fetch();
|
||||||
|
|||||||
Reference in New Issue
Block a user