mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-17 21:24:48 +03:00
feat(cfw): add Apps Script + Cloudflare Worker alternative backend
This commit is contained in:
@@ -102,6 +102,8 @@ This part is unchanged from the original project. Follow @masterking32's guide o
|
||||
- Who has access: **Anyone**
|
||||
6. Copy the **Deployment ID** (the long random string in the URL).
|
||||
|
||||
> **Alternative backend — Apps Script + Cloudflare Worker.** A variant in [`assets/apps_script/Code.cfw.gs`](assets/apps_script/Code.cfw.gs) + [`assets/cloudflare/worker.js`](assets/cloudflare/worker.js) turns Apps Script into a thin forwarder and offloads the actual fetch to a Cloudflare Worker you deploy. The win on day one is **latency** (~10-50 ms at the CF edge vs ~250-500 ms in Apps Script — visibly snappier for browsing and Telegram). It does **not** reduce your daily 20k Apps Script `UrlFetchApp` count, because today's mhrv-rs always sends single-URL relay requests; the batch path on the GAS+Worker side is wired and ready (`ceil(N/40)` quota per N-URL batch) but no shipping client emits it. Trade-offs: worse for YouTube long-form (30 s wall vs 6 min), no fix for Cloudflare anti-bot, **not compatible with `mode: "full"`** (no tunnel-ops support → won't help WhatsApp/messengers on Android full mode). Full setup and trade-off table in [`assets/cloudflare/README.md`](assets/cloudflare/README.md). mhrv-rs needs no config changes — same `mode: "apps_script"`, same `script_id`, same `auth_key`.
|
||||
|
||||
#### Can't reach `script.google.com` from your network?
|
||||
|
||||
If your ISP is already blocking Google Apps Script (or all of Google), you need Step 1's browser connection to succeed *before* you have a relay to use. `mhrv-rs` ships a `direct` mode for exactly this — SNI-rewrite tunnel only, no Apps Script relay required. (Was named `google_only` before v1.9 — the old name is still accepted in config files.)
|
||||
@@ -499,6 +501,10 @@ Donations cover hosting, self-hosted CI runner costs, and continued maintenance.
|
||||
|
||||
> **نکته:** اگر نمیدانید رمز `AUTH_KEY` چه بگذارید، یک رشتهٔ تصادفی ۱۶ تا ۲۴ کاراکتری بسازید. مهم فقط این است که **دقیقاً همان رشته** را در برنامه هم وارد کنید.
|
||||
|
||||
<!-- -->
|
||||
|
||||
> **پشتیبان جایگزین — `Apps Script` + `Cloudflare Worker`.** نسخهای در [`assets/apps_script/Code.cfw.gs`](assets/apps_script/Code.cfw.gs) بههمراه [`assets/cloudflare/worker.js`](assets/cloudflare/worker.js) وجود دارد که `Apps Script` را به یک رلهٔ نازک تبدیل میکند و کار `fetch` واقعی را به یک `Cloudflare Worker` که خودتان مستقر میکنید میسپارد. سود روز اول این کار **کاهش تأخیر** است (~۱۰ تا ۵۰ میلیثانیه روی لبهٔ `CF` به جای ۲۵۰ تا ۵۰۰ میلیثانیه روی `Apps Script` — برای مرور وب و تلگرام محسوس). سهمیهٔ روزانهٔ `UrlFetchApp` (~۲۰٬۰۰۰) را کاهش **نمیدهد**، چون امروز `mhrv-rs` همیشه درخواست تکآدرسی میفرستد؛ مسیر دستهای روی `GAS+Worker` آماده و سیمکشی شده (`ceil(N/40)` سهمیه بهازای دستهٔ `N` آدرسی) ولی هیچ کلاینتی فعلاً آن را تولید نمیکند. مبادلات: ویدیوی طولانی یوتیوب بدتر میشود (دیوار ۳۰ ثانیه به جای ۶ دقیقه)، ضدبات `Cloudflare` را حل نمیکند، و **با `mode: "full"` سازگار نیست** (پشتیبانی از عملیات تونل ندارد → برای واتساَپ و سایر مسنجرها روی اندرویدِ تونل کامل کمکی نمیکند). راهنمای کامل استقرار و جدول مبادلات در [`assets/cloudflare/README.fa.md`](assets/cloudflare/README.fa.md). در `mhrv-rs` هیچ تنظیمی تغییر نمیکند — همان `mode: "apps_script"`، همان `script_id`، همان `auth_key`.
|
||||
|
||||
#### به `script.google.com` هم دسترسی ندارید؟
|
||||
|
||||
اگر `ISP` شما از قبل `Apps Script` (یا کل گوگل) را مسدود کرده، برای مرحلهٔ ۱ باید مرورگرتان **اول** به `script.google.com` برسد — قبل از اینکه رلهای داشته باشید. `mhrv-rs` یک حالت `direct` دقیقاً برای همین دارد — فقط تونل بازنویسی `SNI`، بدون نیاز به رلهٔ `Apps Script`. (قبل از v1.9 این حالت `google_only` نام داشت — نام قدیمی همچنان در فایل کانفیگ پذیرفته میشود.)
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* DomainFront Relay — Apps Script with Cloudflare Worker exit.
|
||||
*
|
||||
* Variant of Code.gs that off-loads the actual outbound HTTP fetch to
|
||||
* a Cloudflare Worker. Apps Script becomes a thin auth-and-forward
|
||||
* relay; Cloudflare does the work and pays the latency.
|
||||
*
|
||||
* mhrv-rs ──► Apps Script (this file) ──► Cloudflare Worker ──► target
|
||||
* ▲ inbound auth & batch ▲ outbound fetch + base64
|
||||
*
|
||||
* Wire protocol with mhrv-rs is identical to Code.gs:
|
||||
* 1. Single: POST { k, m, u, h, b, ct, r } → { s, h, b }
|
||||
* 2. Batch: POST { k, q: [{m,u,h,b,ct,r}, ...] } → { q: [{s,h,b}, ...] }
|
||||
* Both shapes are forwarded to the Worker as one POST per call
|
||||
* from Apps Script: single mode posts {k, u, m, ...} once, batch
|
||||
* mode posts {k, q: [...]} once. The Worker fans out batches
|
||||
* internally via Promise.all. This is the design choice that
|
||||
* makes Code.cfw.gs actually save GAS UrlFetchApp quota — without
|
||||
* it we'd have to fetchAll(N worker calls) and end up at parity
|
||||
* with the standard Code.gs.
|
||||
*
|
||||
* Trade-off summary (read before deploying):
|
||||
* + Per-call latency drops from ~250-500 ms (Apps Script internal
|
||||
* hop) to ~10-50 ms (CF edge). Visibly snappier for chat-style
|
||||
* workloads (Telegram, page navigation).
|
||||
* + Apps Script *runtime* quota (90 min/day on consumer accounts)
|
||||
* stretches significantly because each call now spends almost all
|
||||
* its time in the network leg to the Worker, not in the body
|
||||
* fetch + base64 + header processing.
|
||||
* + Apps Script *UrlFetchApp count* quota stretches roughly Nx for
|
||||
* an N-URL batch because the batch is sent as a small number of
|
||||
* POSTs to the Worker (one per chunk of WORKER_BATCH_CHUNK URLs),
|
||||
* not fanned out per-URL via fetchAll. For mhrv-rs's typical
|
||||
* 5-30 URL batches that's 1 GAS call (vs N under standard
|
||||
* Code.gs). Single non-batched requests still count 1:1.
|
||||
* - YouTube long-form streaming gets WORSE. Apps Script allows
|
||||
* ~6 min wall per execution; CF Workers cap at 30 s wall. The
|
||||
* SABR cliff hits sooner. For YouTube-heavy use, keep the
|
||||
* standard Code.gs (apps_script mode).
|
||||
* - Batch mode now has a per-batch wall, not per-URL: Promise.all
|
||||
* resolves only when every fetch finishes, so the slowest URL
|
||||
* dominates. mhrv-rs already retries failed batch items
|
||||
* individually, so failure modes are graceful, but it's a real
|
||||
* behavioural change vs Code.gs's per-URL fetchAll wall.
|
||||
* - Cloudflare anti-bot challenges on destination sites can be
|
||||
* stricter — exit IP is now in CF's own range, which CF's
|
||||
* anti-bot fingerprints as a worker-internal request. This is
|
||||
* a different problem than DPI bypass; not solved by either
|
||||
* variant.
|
||||
*
|
||||
* Deployment:
|
||||
* 1. Deploy assets/cloudflare/worker.js to Cloudflare Workers first
|
||||
* (set its AUTH_KEY to a strong secret).
|
||||
* 2. Note the *.workers.dev URL of that Worker.
|
||||
* 3. Open https://script.google.com → New project, delete default code.
|
||||
* 4. Paste THIS entire file.
|
||||
* 5. Set AUTH_KEY (must match the Worker's AUTH_KEY and your mhrv-rs
|
||||
* config's auth_key — all three identical).
|
||||
* 6. Set WORKER_URL to your *.workers.dev URL (must include https://).
|
||||
* 7. Deploy → New deployment → Web app
|
||||
* Execute as: Me | Who has access: Anyone
|
||||
* 8. Copy the Deployment ID into mhrv-rs config.json as "script_id".
|
||||
* mhrv-rs does not need to know about Cloudflare; it talks to
|
||||
* Apps Script the same way it always has.
|
||||
*
|
||||
* CHANGE THESE TWO CONSTANTS BELOW.
|
||||
*
|
||||
* Upstream credit for the GAS-→-Worker pattern: github.com/denuitt1/mhr-cfw.
|
||||
* This file inherits the hardening (decoy-on-bad-auth, hop-loop guard)
|
||||
* from the standard Code.gs.
|
||||
*/
|
||||
|
||||
const AUTH_KEY = "CHANGE_ME_TO_A_STRONG_SECRET";
|
||||
|
||||
// Full https://… URL of the Cloudflare Worker you deployed using
|
||||
// assets/cloudflare/worker.js. Must include the scheme.
|
||||
const WORKER_URL = "https://CHANGE_ME.workers.dev";
|
||||
|
||||
// ── Sentinels — DO NOT EDIT ─────────────────────────────────
|
||||
// These two constants are NOT configuration. They are the literal
|
||||
// template-default values used by the fail-closed check in doPost so
|
||||
// that a forgotten edit (AUTH_KEY or WORKER_URL still set to the
|
||||
// placeholder) returns a loud error instead of silently accepting the
|
||||
// placeholder secret or POSTing to a bogus URL. Configure AUTH_KEY
|
||||
// and WORKER_URL above; leave these alone.
|
||||
const DEFAULT_AUTH_KEY = "CHANGE_ME_TO_A_STRONG_SECRET";
|
||||
const DEFAULT_WORKER_URL = "https://CHANGE_ME.workers.dev";
|
||||
|
||||
// Must match the Worker's MAX_BATCH_SIZE. Batches larger than this
|
||||
// are split into chunks of this size and dispatched via fetchAll —
|
||||
// each chunk costs 1 GAS UrlFetchApp call, so an N-URL batch costs
|
||||
// ceil(N/CHUNK) calls (still much cheaper than the per-URL cost
|
||||
// under standard Code.gs's fetchAll).
|
||||
const WORKER_BATCH_CHUNK = 40;
|
||||
|
||||
// Active-probing defense — same semantics as Code.gs. Bad-auth and
|
||||
// malformed POST bodies receive a decoy HTML page that looks like a
|
||||
// placeholder Apps Script web app instead of the JSON `{e}` error,
|
||||
// so probes can't fingerprint the deployment as a relay endpoint.
|
||||
// Flip to `true` only during initial setup if you need to debug an
|
||||
// "unauthorized" loop, then flip back before sharing the deployment.
|
||||
const DIAGNOSTIC_MODE = false;
|
||||
|
||||
const SKIP_HEADERS = {
|
||||
host: 1, connection: 1, "content-length": 1,
|
||||
"transfer-encoding": 1, "proxy-connection": 1, "proxy-authorization": 1,
|
||||
"priority": 1, te: 1,
|
||||
};
|
||||
|
||||
const DECOY_HTML =
|
||||
'<!DOCTYPE html><html><head><title>Web App</title></head>' +
|
||||
'<body><p>The script completed but did not return anything.</p>' +
|
||||
'</body></html>';
|
||||
|
||||
// ── Request Handlers ────────────────────────────────────────
|
||||
|
||||
function _decoyOrError(jsonBody) {
|
||||
if (DIAGNOSTIC_MODE) return _json(jsonBody);
|
||||
return ContentService
|
||||
.createTextOutput(DECOY_HTML)
|
||||
.setMimeType(ContentService.MimeType.HTML);
|
||||
}
|
||||
|
||||
function doPost(e) {
|
||||
try {
|
||||
// Fail-closed if either constant is still the template default.
|
||||
// Without this, a forgotten edit would either accept the placeholder
|
||||
// secret as valid auth or POST to a literal "CHANGE_ME" URL — both
|
||||
// are silent failure modes a deploy might miss. Surface them loud.
|
||||
if (AUTH_KEY === DEFAULT_AUTH_KEY) {
|
||||
return _json({ e: "configure AUTH_KEY in Code.cfw.gs" });
|
||||
}
|
||||
if (WORKER_URL === DEFAULT_WORKER_URL) {
|
||||
return _json({ e: "configure WORKER_URL in Code.cfw.gs" });
|
||||
}
|
||||
|
||||
var req = JSON.parse(e.postData.contents);
|
||||
if (req.k !== AUTH_KEY) return _decoyOrError({ e: "unauthorized" });
|
||||
|
||||
if (Array.isArray(req.q)) return _doBatch(req.q);
|
||||
return _doSingle(req);
|
||||
} catch (err) {
|
||||
return _decoyOrError({ e: String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
function doGet(e) {
|
||||
return ContentService
|
||||
.createTextOutput(DECOY_HTML)
|
||||
.setMimeType(ContentService.MimeType.HTML);
|
||||
}
|
||||
|
||||
// ── Worker Forwarding ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Strip headers that must not be forwarded (hop-by-hop / Apps-Script-
|
||||
* managed). Returns a fresh header map; the input is never mutated.
|
||||
*/
|
||||
function _scrubHeaders(rawHeaders) {
|
||||
var out = {};
|
||||
if (rawHeaders && typeof rawHeaders === "object") {
|
||||
for (var k in rawHeaders) {
|
||||
if (rawHeaders.hasOwnProperty(k) && !SKIP_HEADERS[k.toLowerCase()]) {
|
||||
out[k] = rawHeaders[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize one request item into the shape the Worker expects.
|
||||
* Used for both single and batch paths — single mode wraps this in
|
||||
* `{k, ...item}`; batch mode wraps it in `{k, q: [item, ...]}`.
|
||||
* Auth key is added at envelope level by callers, not per-item.
|
||||
*/
|
||||
function _normalizeItem(item) {
|
||||
return {
|
||||
u: item.u,
|
||||
m: (item.m || "GET").toUpperCase(),
|
||||
h: _scrubHeaders(item.h),
|
||||
b: item.b || null,
|
||||
ct: item.ct || null,
|
||||
r: item.r !== false,
|
||||
};
|
||||
}
|
||||
|
||||
function _workerFetchOptions(payload) {
|
||||
return {
|
||||
url: WORKER_URL,
|
||||
method: "post",
|
||||
contentType: "application/json",
|
||||
payload: JSON.stringify(payload),
|
||||
muteHttpExceptions: true,
|
||||
followRedirects: true,
|
||||
validateHttpsCertificates: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Single Request ─────────────────────────────────────────
|
||||
|
||||
function _doSingle(req) {
|
||||
if (!req.u || typeof req.u !== "string" || !req.u.match(/^https?:\/\//i)) {
|
||||
return _json({ e: "bad url" });
|
||||
}
|
||||
|
||||
var item = _normalizeItem(req);
|
||||
var envelope = {
|
||||
k: AUTH_KEY,
|
||||
u: item.u,
|
||||
m: item.m,
|
||||
h: item.h,
|
||||
b: item.b,
|
||||
ct: item.ct,
|
||||
r: item.r,
|
||||
};
|
||||
var opts = _workerFetchOptions(envelope);
|
||||
// muteHttpExceptions covers HTTP-level errors (4xx/5xx come back as
|
||||
// a normal HTTPResponse). It does NOT cover network-level failures
|
||||
// — DNS resolution failure, TLS handshake failure, connection
|
||||
// timeout to *.workers.dev, etc. — those throw. Catch and surface
|
||||
// them as `{e}` so the operator debugging "why isn't my deployment
|
||||
// responding?" gets a useful signal instead of the doPost outer
|
||||
// catch returning the decoy HTML page (which makes the deployment
|
||||
// look like a bad-auth probe to the client). Auth has already
|
||||
// passed at this point so the probe-defence argument doesn't apply.
|
||||
var resp;
|
||||
try {
|
||||
resp = UrlFetchApp.fetch(opts.url, opts);
|
||||
} catch (err) {
|
||||
return _json({ e: "worker unreachable: " + String(err) });
|
||||
}
|
||||
return _json(_parseWorkerJson(resp));
|
||||
}
|
||||
|
||||
// ── Batch Request ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Forward a batch to the Worker, chunking when needed. Each chunk
|
||||
* becomes ONE POST to the Worker; the Worker fans out across the URLs
|
||||
* in the chunk via Promise.all and returns `{q: [...]}` in the same
|
||||
* order. Multiple chunks fire in parallel via UrlFetchApp.fetchAll.
|
||||
*
|
||||
* Quota cost: ceil(N / WORKER_BATCH_CHUNK) GAS UrlFetchApp calls for
|
||||
* an N-URL batch. For typical mhrv-rs batches of 5-30 URLs this is
|
||||
* exactly 1 call (vs N under standard Code.gs's fetchAll). Larger
|
||||
* batches gracefully degrade to a few calls instead of failing under
|
||||
* the Worker's own MAX_BATCH_SIZE soft cap.
|
||||
*
|
||||
* Bad-URL items are filtered locally so the Worker only sees valid
|
||||
* inputs, then re-interleaved into the result array in original order
|
||||
* so mhrv-rs's batch-index assumptions hold.
|
||||
*/
|
||||
function _doBatch(items) {
|
||||
var validItems = [];
|
||||
var errorMap = {};
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
if (!item.u || typeof item.u !== "string" || !item.u.match(/^https?:\/\//i)) {
|
||||
errorMap[i] = "bad url";
|
||||
continue;
|
||||
}
|
||||
validItems.push(_normalizeItem(item));
|
||||
}
|
||||
|
||||
var workerResults = [];
|
||||
if (validItems.length > 0) {
|
||||
// Split into chunks ≤ WORKER_BATCH_CHUNK so each Worker call stays
|
||||
// under the Worker's MAX_BATCH_SIZE cap. Single-chunk fast path
|
||||
// avoids the fetchAll overhead for the common case.
|
||||
var chunks = [];
|
||||
for (var c = 0; c < validItems.length; c += WORKER_BATCH_CHUNK) {
|
||||
chunks.push(validItems.slice(c, c + WORKER_BATCH_CHUNK));
|
||||
}
|
||||
|
||||
var fetchOpts = chunks.map(function(chunk) {
|
||||
return _workerFetchOptions({ k: AUTH_KEY, q: chunk });
|
||||
});
|
||||
|
||||
// muteHttpExceptions covers HTTP-level errors. Network-level
|
||||
// failures (DNS, TLS, connection timeout to *.workers.dev) still
|
||||
// throw — catch and convert to per-chunk `{e}` errors that get
|
||||
// spread across each chunk's slots. mhrv-rs's per-item retry
|
||||
// then handles them individually instead of getting the decoy
|
||||
// HTML page from the doPost outer catch. See _doSingle for why
|
||||
// the probe-defence argument doesn't apply post-auth.
|
||||
var responses;
|
||||
try {
|
||||
if (fetchOpts.length === 1) {
|
||||
responses = [UrlFetchApp.fetch(fetchOpts[0].url, fetchOpts[0])];
|
||||
} else {
|
||||
responses = UrlFetchApp.fetchAll(fetchOpts);
|
||||
}
|
||||
} catch (err) {
|
||||
var unreachable = { e: "worker unreachable: " + String(err) };
|
||||
for (var u = 0; u < validItems.length; u++) workerResults.push(unreachable);
|
||||
// Skip the per-response loop below by returning early through the
|
||||
// reassembly code path.
|
||||
responses = null;
|
||||
}
|
||||
|
||||
for (var r = 0; responses && r < responses.length; r++) {
|
||||
var parsed = _parseWorkerJson(responses[r]);
|
||||
if (parsed && Array.isArray(parsed.q)) {
|
||||
for (var k = 0; k < parsed.q.length; k++) {
|
||||
workerResults.push(parsed.q[k]);
|
||||
}
|
||||
} else {
|
||||
// Per-chunk failure (worker error, parse failure, auth, etc).
|
||||
// Spread the same error to every slot in this chunk so mhrv-rs
|
||||
// retries each item individually rather than masking the
|
||||
// failure. Other chunks are unaffected.
|
||||
var slotErr = (parsed && parsed.e)
|
||||
? { e: parsed.e }
|
||||
: { e: "worker batch failure" };
|
||||
for (var s = 0; s < chunks[r].length; s++) workerResults.push(slotErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reassemble into the original order: validated slots get their
|
||||
// worker result; invalid slots get their pre-flight error.
|
||||
var results = [];
|
||||
var wi = 0;
|
||||
for (var j = 0; j < items.length; j++) {
|
||||
if (errorMap.hasOwnProperty(j)) {
|
||||
results.push({ e: errorMap[j] });
|
||||
} else {
|
||||
results.push(workerResults[wi++] || { e: "missing worker response" });
|
||||
}
|
||||
}
|
||||
return _json({ q: results });
|
||||
}
|
||||
|
||||
// ── Worker response handling ───────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse the Worker's JSON envelope. Worker errors come back as
|
||||
* `{e: "..."}` — pass them through to the client unchanged so mhrv-rs
|
||||
* sees the same error-shape it would for a direct-fetch failure in
|
||||
* Code.gs. On HTTP errors from the Worker itself (auth failure, 5xx,
|
||||
* etc.), wrap into `{e}` so the client gets a useful message instead
|
||||
* of a parse-failure.
|
||||
*/
|
||||
function _parseWorkerJson(resp) {
|
||||
var code = resp.getResponseCode();
|
||||
var text = resp.getContentText();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (err) {
|
||||
return { e: "worker " + code + ": " + (text.length > 200 ? text.substring(0, 200) + "…" : text) };
|
||||
}
|
||||
}
|
||||
|
||||
function _json(obj) {
|
||||
return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(
|
||||
ContentService.MimeType.JSON
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
# Apps Script source (mirrored)
|
||||
# Apps Script source
|
||||
|
||||
The file `Code.gs` next to this README is a verbatim snapshot of the upstream script you deploy in your own Google Apps Script project:
|
||||
Three deploy-ready Apps Script files live here. They all speak the same `{k, m, u, h, b, ct, r}` wire protocol with `mhrv-rs`, so the client just points its `script_id` at whichever deployment you want — no mode change required.
|
||||
|
||||
- Upstream: <https://github.com/masterking32/MasterHttpRelayVPN/blob/python_testing/apps_script/Code.gs>
|
||||
- Raw link: <https://raw.githubusercontent.com/masterking32/MasterHttpRelayVPN/refs/heads/python_testing/apps_script/Code.gs>
|
||||
## Variants and origins
|
||||
|
||||
This copy lives in our repo for two reasons:
|
||||
- **`Code.gs`** — standard relay. **Verbatim mirror of upstream.** Apps Script does the outbound fetch itself. This is the default choice for most users.
|
||||
- Upstream: <https://github.com/masterking32/MasterHttpRelayVPN/blob/python_testing/apps_script/Code.gs>
|
||||
- Credit: [@masterking32](https://github.com/masterking32). We do not modify this file.
|
||||
- The mirror lives here so that (a) users on networks where `raw.githubusercontent.com` is unreachable can still deploy from a `git clone` / ZIP, and (b) we have a snapshot to diff against if upstream changes silently break the informal relay protocol.
|
||||
|
||||
1. **Survives upstream outages**: if the user is on a network where raw.githubusercontent.com is temporarily unreachable but they can clone or ZIP this repo, they still have the deploy-ready file.
|
||||
2. **Pins what we tested against**: the relay protocol between `mhrv-rs` and the script is informal; upstream changes can silently break us. Keeping a snapshot here lets us diff and see if a spec drift is responsible for any reported breakage.
|
||||
- **`CodeFull.gs`** — superset of `Code.gs` that additionally proxies raw-TCP / UDP via `tunnel-node` (used by `mode: "full"`). **Maintained in this repo** — written for this Rust port and not present upstream. Deploy this if you want full-tunnel mode; details in the file's header comment.
|
||||
|
||||
All credit for `Code.gs` goes to [@masterking32](https://github.com/masterking32) — we do not modify it. If you're using mhrv-rs, follow the upstream deploy instructions in the script's header comment. The only edit **you** must make is the `AUTH_KEY` constant — set it to a strong secret and reuse that exact string in your `mhrv-rs` config.
|
||||
- **`Code.cfw.gs`** — Apps Script becomes a thin auth+forward layer; the actual outbound fetch happens on a Cloudflare Worker you also deploy ([`assets/cloudflare/`](../cloudflare/)). **Derivative work — not unmodified upstream.** The pattern of forwarding through a Cloudflare Worker came from [denuitt1/mhr-cfw](https://github.com/denuitt1/mhr-cfw); this file inherits hardening from `Code.gs` (decoy-on-bad-auth, fail-closed sentinels) and adds chunked batch forwarding (`Promise.all` on the Worker side, `ceil(N/40)` GAS calls per batch) that the upstream `mhr-cfw` does not have. Faster per-call latency, worse YouTube long-form, no fix for Cloudflare anti-bot. Read [`assets/cloudflare/README.md`](../cloudflare/README.md) before choosing this one.
|
||||
|
||||
## What you must edit before deploying
|
||||
|
||||
For every variant: change `AUTH_KEY` from its placeholder to a strong secret, and use that same string in your `mhrv-rs` config's `auth_key`. `Code.cfw.gs` additionally requires setting `WORKER_URL` to your deployed Cloudflare Worker URL; `CodeFull.gs` additionally requires `TUNNEL_SERVER_URL` and `TUNNEL_AUTH_KEY` for the tunnel-node leg.
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<div dir="rtl">
|
||||
|
||||
# خروجی Cloudflare Worker (پشتیبان جایگزین برای Apps Script)
|
||||
|
||||
> *English: [README.md](README.md)*
|
||||
|
||||
این پوشه یک **Cloudflare Worker** ارائه میکند که همراه با [`assets/apps_script/Code.cfw.gs`](../apps_script/Code.cfw.gs) شکل متفاوتی از حالت `apps_script` به شما میدهد:
|
||||
|
||||
```
|
||||
mhrv-rs ──► Apps Script (Code.cfw.gs) ──► Cloudflare Worker ──► مقصد
|
||||
▲ فقط احراز هویت و فوروارد ▲ گرفتن داده + base64
|
||||
```
|
||||
|
||||
پشتیبان استاندارد ([`assets/apps_script/Code.gs`](../apps_script/Code.gs)) خودِ `Apps Script` کار `fetch` به مقصد را انجام میدهد. این نسخهٔ جایگزین، `Apps Script` را به یک رلهٔ نازک تبدیل میکند و کارِ اصلی را به لبهٔ `Cloudflare` میسپارد. **خود `mhrv-rs` تغییر نمیکند** — همان پاکت `JSON` روی سیم، همان `mode: "apps_script"` در `config.json`، همان `script_id`. تنها تفاوت این است که `Apps Script` مستقر شدهٔ شما بعد از احراز هویت چه میکند.
|
||||
|
||||
ایدهٔ اصلی: <https://github.com/denuitt1/mhr-cfw>. این کپی یک بررسی `AUTH_KEY` روی خود `Worker` اضافه میکند، رفتار «صفحهٔ تقلبی برای کلید نامعتبر» را از `Code.gs` به ارث میبرد، و یک محافظ در برابر حلقهشدن دارد.
|
||||
|
||||
## چهوقت ارزش راهاندازی دارد؟
|
||||
|
||||
✅ مرور وب، باز کردن صفحات جدید، ترافیک گفتگومحور — بهطور محسوسی سریعتر میشود. تأخیر هر تماس از کف ۲۵۰ تا ۵۰۰ میلیثانیهٔ `Apps Script` به ۱۰ تا ۵۰ میلیثانیهٔ لبهٔ `Cloudflare` کاهش مییابد.
|
||||
|
||||
✅ تلگرام بلادرنگ — پیامهای کوتاه و مکرر بیشترین سود را میبرند.
|
||||
|
||||
✅ شبکههایی که در آنها ابتدا سهمیهٔ **زمان اجرای `Apps Script`** (۹۰ دقیقه در روز برای حسابهای مصرفی گوگل) تمام میشود، نه شمارش `URL fetch`. در این حالت `GAS` تقریباً هیچ زمانی صرف هر تماس نمیکند.
|
||||
|
||||
❌ **امروز هیچ کاهشی در شمارش روزانهٔ `UrlFetchApp` به دست نمیآورید.** مسیر رلهٔ `HTTP` در `mhrv-rs` همیشه فقط یک پاکت تکآدرسی میفرستد و هیچگاه شکل دستهای `q: [...]` را تولید نمیکند، پس هر درخواست کاربر همچنان یک `UrlFetchApp` در `GAS` مصرف میکند — مستقل از اینکه کدام نسخهٔ `Code.gs` را مستقر کرده باشید. مسیر `Code.cfw.gs` به سمت `Worker` *قابلیت* پشتیبانی از دسته را دارد (قطعهبندی ۴۰تایی، پخشسازی روی `Worker` با `Promise.all`، هزینهٔ `ceil(N / 40)` به جای `N`)، ولی این شاخه از هیچ کلاینت موجودی فراخوانی نمیشود. **تا زمانی که `mhrv-rs` خودش `HTTP relay` را دستهبندی نکند، سقف روزانهٔ ~۲۰٬۰۰۰ مصرف نسبت به `Code.gs` تغییر نمیکند.** این پشتیبانی برای سازگاری آینده در کد نگه داشته شده — هزینهای ندارد و روزی که کلاینتِ دستهبندیکننده برسد، خود به خود فعال میشود.
|
||||
|
||||
❌ ویدیوهای طولانی یوتیوب — **بدتر** میشود، نه بهتر. `Apps Script` تا حدود ۶ دقیقه دیوار اجرا (`wall`) به ازای هر فراخوانی میدهد؛ `Cloudflare Workers` در ۳۰ ثانیه قطع میکنند. صخرهٔ `SABR` زودتر فرا میرسد. برای استفادهٔ یوتیوبمحور، روی `Code.gs` بمانید.
|
||||
|
||||
❌ سایتهایی که پشت ضدبات `Cloudflare` هستند (توییتر/`X`، `OpenAI`، …) — `IP` خروجی حالا داخل خود `Cloudflare` است، که ضدبات `Cloudflare` آن را بهعنوان «درخواست داخلی `Worker`» انگشتنگاری میکند. اغلب **سختگیرانهتر** از `IP` گوگل برخورد میشود. این مشکلی جدا از عبور از `DPI` است و هیچکدام از این دو نسخه آن را حل نمیکنند.
|
||||
|
||||
❌ اگر/زمانی که `HTTP relay` دستهای فعال شود، سقف ۳۰ ثانیهٔ `Cloudflare` روی **کندترین آدرس در هر قطعه** اعمال خواهد شد، نه بهازای هر `URL` — یک مقصد قفلشده میتواند کل قطعهٔ ۴۰ آدرسی را به `timeout` بکشاند. تلاش مجدد تکبهتک در `mhrv-rs` این را پوشش میدهد، اما تفاوت رفتاری نسبت به دیوار `per-URL` در `fetchAll` استانداردِ `Code.gs` است. (امروز بیاثر است چون کلاینت دسته نمیفرستد.)
|
||||
|
||||
## راهاندازی
|
||||
|
||||
سه رشتهٔ همخوان نیاز دارید: یک `AUTH_KEY` که بین `worker.js`، `Code.cfw.gs` و `config.json` خود `mhrv-rs` مشترک است. یک رمز تصادفی قوی انتخاب کنید و در هر سه جا paste کنید.
|
||||
|
||||
### ۱. استقرار `Worker`
|
||||
|
||||
۱. وارد <https://dash.cloudflare.com/> شوید → **`Workers & Pages`** → **`Create`** → **`Hello World`** → **`Deploy`**.
|
||||
۲. روی **`Edit code`** بزنید، کد پیشفرض را پاک کنید و محتوای [`worker.js`](worker.js) را paste کنید.
|
||||
۳. ثابت `AUTH_KEY` در بالای فایل را به رمز قوی خودتان تغییر دهید.
|
||||
۴. روی **`Deploy`** بزنید. آدرس `*.workers.dev` را کپی کنید — در مرحلهٔ بعد لازم است.
|
||||
|
||||
### ۲. استقرار `Apps Script`
|
||||
|
||||
۱. وارد <https://script.google.com> با حساب گوگلتان شوید → **`New project`** → کد پیشفرض را پاک کنید.
|
||||
|
||||
۲. محتوای [`../apps_script/Code.cfw.gs`](../apps_script/Code.cfw.gs) را paste کنید.
|
||||
|
||||
۳. هر دو ثابت بالای فایل را تنظیم کنید:
|
||||
- مقدار `AUTH_KEY` را همان رمزی بگذارید که در `worker.js` گذاشتید.
|
||||
- مقدار `WORKER_URL` را آدرس کامل `https://…workers.dev` همان `Worker` که الان مستقر کردید بگذارید (حتماً با پیشوند `https://`).
|
||||
|
||||
۴. از مسیر **`Deploy → New deployment → Web app`** استقرار را شروع کنید: مقدار `Execute as` را روی **`Me`** و `Who has access` را روی **`Anyone`** بگذارید.
|
||||
|
||||
۵. سپس **`Deployment ID`** را کپی کنید.
|
||||
|
||||
### ۳. اشاره دادن `mhrv-rs` به این `Apps Script`
|
||||
|
||||
در `config.json` (یا از طریق فرم `UI`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "apps_script",
|
||||
"script_id": "PASTE_DEPLOYMENT_ID_HERE",
|
||||
"auth_key": "SAME_SECRET_AS_BOTH_FILES_ABOVE"
|
||||
}
|
||||
```
|
||||
|
||||
تمام. `mhrv-rs` لازم نیست بداند `Cloudflare` در کار است؛ از نگاه او این `script_id` مثل هر `Deployment` دیگری رفتار میکند. اگر چند `Deployment` دارید (بعضی استاندارد، بعضی `CFW`)، میتوانید همه را در `script_ids: [...]` بگذارید — `round-robin` و `parallel-relay` همچنان روی همهشان کار میکند.
|
||||
|
||||
## چرا هر سه `AUTH_KEY` باید یکی باشند؟
|
||||
|
||||
- **بین `mhrv-rs` و `Apps Script`**: جلوی این را میگیرد که هر `POST` تصادفی روی آدرس `*.googleusercontent.com` شما رله شود. درخواستهایی که این کلید را نداشته باشند، یک صفحهٔ `HTML` تقلبی میگیرند (بهخاطر `DIAGNOSTIC_MODE = false` در `Code.cfw.gs`) و `Deployment` شما بهجای یک تونل، شبیه یک پروژهٔ فراموششده دیده میشود.
|
||||
- **بین `Apps Script` و `Worker`**: اگر آدرس `Worker` لو برود، جلوی این را میگیرد که به یک رلهٔ `HTTP` باز برای مهاجم تبدیل شود. بدون این بررسی، `Worker` شما برای هر کسی که `URL` را پیدا کند، قابل سوءاستفاده است. نسخهٔ بالادست `mhr-cfw` این بررسی را ندارد؛ این کپی آن را اضافه میکند.
|
||||
|
||||
اگر میخواهید برای امنیت بیشتر روی هر بخش رمز جدا داشته باشید، `Code.cfw.gs` را ویرایش کنید تا یک `k` متفاوت از آن چیزی که از `mhrv-rs` میگیرد به `Worker` بفرستد. تنظیم تکرمز سادهترین حالتِ درست است.
|
||||
|
||||
## بررسی اینکه کار میکند
|
||||
|
||||
همان روش پشتیبان استاندارد: <https://ipleak.net> را از طریق پروکسی باز کنید. باید یک `IP` متعلق به `Cloudflare` ببینید (چون `fetch` واقعی حالا از شبکهٔ `Cloudflare` خارج میشود)، نه یک `IP` متعلق به گوگل که با `Code.gs` میدیدید. اگر `IP` واقعی خودتان را ببینید، پروکسی استفاده نمیشود؛ اگر `IP` گوگل ببینید، اشتباهاً `Code.gs` را بهجای `Code.cfw.gs` مستقر کردهاید.
|
||||
|
||||
دکمهٔ **`Test`** در `UI` دسکتاپ همچنان کار میکند — یک درخواست `HEAD` از طریق هر `Apps Script Deployment` که تنظیم کردهاید رله میکند.
|
||||
|
||||
## جدول مقایسه در یک نگاه
|
||||
|
||||
| محور | `Code.gs` (استاندارد) | `Code.cfw.gs` (این نسخه) |
|
||||
|---|---|---|
|
||||
| کف تأخیر هر تماس | ۲۵۰–۵۰۰ میلیثانیه (هاپ داخلی `GAS`) | ۱۰–۵۰ میلیثانیه (لبهٔ `CF`) |
|
||||
| سهمیهٔ `UrlFetchApp` در روز، **آنچه `mhrv-rs` امروز میفرستد** | ۱ سهمیه بهازای هر درخواست | ۱ سهمیه بهازای هر درخواست — یکسان (`mhrv-rs` فقط پاکت تکآدرسی تولید میکند) |
|
||||
| سهمیهٔ `UrlFetchApp` در روز، **اگر کلاینتی در آینده دسته بفرستد** | تعداد `N` سهمیه (یکی برای هر آدرس از طریق `fetchAll`) | تعداد `ceil(N / 40)` سهمیه (قطعهبندی ۴۰تایی؛ پخشسازی روی `Worker` با `Promise.all`) |
|
||||
| سقف درخواست `Cloudflare Workers` در روز (پلن رایگان) | ندارد | ۱۰۰٬۰۰۰ — بسیار بالاتر از چیزی که `GAS` میتواند تغذیهاش کند؛ گلوگاه نیست |
|
||||
| سهمیهٔ زمان اجرای `Apps Script` در روز | ۹۰ دقیقه، اغلب گلوگاه | ۹۰ دقیقه، بهندرت گلوگاه |
|
||||
| دیوار اجرای هر فراخوانی | ~۶ دقیقه، بهازای هر آدرس | ۳۰ ثانیه، بهازای هر تماس (اگر دستهبندی فعال شود، بهازای هر قطعه) |
|
||||
| سقف اندازهٔ پاسخ | ~۵۰ مگابایت (مستندات `Apps Script`) | محدود به حافظهٔ `Worker` (۱۲۸ مگابایت در پلن رایگان)؛ در عمل با تبدیل `base64` چند ده مگابایت |
|
||||
| حروف بزرگ/کوچک هدرهای پاسخ | همانطور که مبدأ فرستاده | کاملاً کوچک میشود (`Headers.forEach` در `Workers` نرمال میکند). فقط برای ابزارهای پاییندستی که نام هدر را حساس به حروف مقایسه میکنند مهم است؛ `mhrv-rs` خود حساس به حروف نیست. |
|
||||
| پخش ویدیوی طولانی یوتیوب | قابل قبول (صخرهٔ ۶ دقیقه) | بدتر (صخرهٔ ۳۰ ثانیه) |
|
||||
| سرعت تلگرام / گفتگو | پایه | محسوساً بهتر |
|
||||
| ضدبات `Cloudflare` روی مقصد | یک `IP` دیتاسنتر | یک `IP` داخلی `Worker` (اغلب سختگیرانهتر) |
|
||||
| کش پاسخ روی `Spreadsheet` | موجود (اختیاری) | در این نسخه نیست |
|
||||
| پیچیدگی استقرار | ۱ چیز برای نگهداری | ۲ چیز که باید همگام بمانند |
|
||||
|
||||
اگر این مبادلات به نفع شماست، این نسخه را مستقر کنید. اگر نیست — یا حساب `Cloudflare` ندارید — روی `Code.gs` بمانید.
|
||||
|
||||
## محدودیت مهم: این نسخه با `mode: "full"` کار نمیکند
|
||||
|
||||
این فایل فقط مسیر **رلهٔ `HTTP`** (حالتهای ۱ و ۲ در `CodeFull.gs`) را پورت میکند. عملیات تونل `TCP/UDP` خام (حالتهای ۳ و ۴ در `CodeFull.gs` که برای `mode: "full"` و کاربری اپلیکیشنهای موبایل مثل واتساَپ روی اندروید لازماند) در `Code.cfw.gs` پشتیبانی نمیشوند. اگر در حالت `full` هستید و `WhatsApp` کند است، این تغییر کمکی نمیکند — این مسئلهٔ متفاوتی است که نیاز به طراحی جداگانه دارد.
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,97 @@
|
||||
# Cloudflare Worker exit (alternative Apps Script backend)
|
||||
|
||||
> *فارسی: [README.fa.md](README.fa.md)*
|
||||
|
||||
This directory ships a **Cloudflare Worker** that pairs with [`assets/apps_script/Code.cfw.gs`](../apps_script/Code.cfw.gs) to give you a different shape of `apps_script` mode:
|
||||
|
||||
```
|
||||
mhrv-rs ──► Apps Script (Code.cfw.gs) ──► Cloudflare Worker ──► target
|
||||
▲ thin auth + forward ▲ outbound fetch + base64
|
||||
```
|
||||
|
||||
The standard backend (`assets/apps_script/Code.gs`) does the outbound fetch from inside Apps Script directly. This variant makes Apps Script a thin relay and pushes the actual fetch to Cloudflare's edge. **mhrv-rs itself is unchanged** — same JSON envelope on the wire, same `mode: "apps_script"` in `config.json`, same `script_id`. The only thing that's different is what your deployed Apps Script does after it authenticates the request.
|
||||
|
||||
Original idea: <https://github.com/denuitt1/mhr-cfw>. This copy adds an `AUTH_KEY` check on the Worker, the decoy-on-bad-auth treatment from `Code.gs`, and a hop-loop guard.
|
||||
|
||||
## When this is worth it
|
||||
|
||||
✅ Browsing, page navigation, chat-style traffic — visibly snappier. Per-call latency drops from the ~250-500 ms Apps Script floor to ~10-50 ms at the CF edge.
|
||||
✅ Telegram realtime — small frequent messages benefit most.
|
||||
✅ Networks where the Apps Script *runtime* quota (90 min/day on consumer Google accounts) is what you hit before the URL-fetch count cap. GAS spends almost no time per call here.
|
||||
|
||||
❌ **No `UrlFetchApp` daily-count relief today.** mhrv-rs's HTTP relay path emits a single-URL envelope per request, never the `q: [...]` batch shape, so each user request still consumes one GAS UrlFetchApp call regardless of which `Code.gs` variant is deployed. The `Code.cfw.gs` ↔ Worker path *is* batch-aware (chunks at 40, Worker fans out via `Promise.all`, costs `ceil(N / 40)` per batch instead of N), but that branch is unreachable from any shipping client. **Until/unless mhrv-rs grows HTTP-relay batching, the daily 20k-fetch ceiling is unchanged from `Code.gs`.** The ready batching support is left in place for forward compatibility — it costs nothing and goes live the day a batching client lands.
|
||||
❌ YouTube long-form video — gets **worse**, not better. Apps Script allows ~6 min wall per execution; CF Workers cap at 30 s. The SABR cliff arrives sooner. Stay on `Code.gs` for YouTube-heavy use.
|
||||
❌ Sites behind Cloudflare anti-bot (Twitter/X, OpenAI, etc.) — exit IP becomes a Workers IP, which CF's own anti-bot fingerprints as a worker-internal request. Often *stricter* than a Google IP. This is a separate problem from DPI bypass and neither variant fixes it.
|
||||
❌ When/if HTTP-relay batching ships, the 30 s wall would apply to **the slowest URL in each chunk**, not per-URL — a single hung target could drag a 40-URL chunk to timeout. mhrv-rs's existing per-item retry would absorb this, but it's a behavioral change vs the per-URL `fetchAll` wall under `Code.gs`. (Inert today since no batching client exists.)
|
||||
|
||||
## Setup
|
||||
|
||||
You need three matching strings: an `AUTH_KEY` shared between `worker.js`, `Code.cfw.gs`, and your `mhrv-rs` `config.json`. Pick a strong random secret once and paste it into all three.
|
||||
|
||||
### 1. Deploy the Worker
|
||||
|
||||
1. Open <https://dash.cloudflare.com/> → **Workers & Pages** → **Create** → **Hello World** → **Deploy**.
|
||||
2. Click **Edit code**, delete the template, and paste the contents of [`worker.js`](worker.js).
|
||||
3. Change the `AUTH_KEY` constant near the top of the file to your strong secret.
|
||||
4. **Deploy**. Copy the `*.workers.dev` URL — you'll need it next.
|
||||
|
||||
### 2. Deploy the Apps Script
|
||||
|
||||
1. Open <https://script.google.com> while signed into your Google account → **New project** → delete the default code.
|
||||
2. Paste the contents of [`../apps_script/Code.cfw.gs`](../apps_script/Code.cfw.gs).
|
||||
3. Set both constants at the top:
|
||||
- `AUTH_KEY` — the same secret you set in `worker.js`.
|
||||
- `WORKER_URL` — the full `https://…workers.dev` URL of the Worker you just deployed (must include the scheme).
|
||||
4. **Deploy → New deployment → Web app**: *Execute as* = **Me**, *Who has access* = **Anyone**.
|
||||
5. Copy the **Deployment ID**.
|
||||
|
||||
### 3. Point mhrv-rs at the Apps Script
|
||||
|
||||
In `config.json` (or via the UI's config form):
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "apps_script",
|
||||
"script_id": "PASTE_DEPLOYMENT_ID_HERE",
|
||||
"auth_key": "SAME_SECRET_AS_BOTH_FILES_ABOVE"
|
||||
}
|
||||
```
|
||||
|
||||
That's it. mhrv-rs doesn't need to know Cloudflare exists; from its perspective, the `script_id` deployment behaves like any other. If you have multiple deployments (some plain, some CFW), `script_ids: [...]` round-robins across all of them and the parallel-relay fan-out still works.
|
||||
|
||||
## Why three matching `AUTH_KEY`s
|
||||
|
||||
- **mhrv-rs ↔ Apps Script**: prevents random POSTs to your `*.googleusercontent.com` deployment from being relayed. Probes that don't carry the key get the decoy HTML page (`DIAGNOSTIC_MODE = false` in `Code.cfw.gs`), so the deployment looks like a forgotten placeholder rather than a tunnel.
|
||||
- **Apps Script ↔ Worker**: prevents random POSTs to your `*.workers.dev` Worker from being relayed if the Worker URL ever leaks. Without this check the Worker becomes an open HTTP-relay for arbitrary attackers. The upstream `mhr-cfw` Worker omits it; this copy adds it back.
|
||||
|
||||
If you want compartmentalization (different secret on each leg), edit `Code.cfw.gs` to send a different `k` to the Worker than the one it accepts from mhrv-rs. The single-secret setup is the simplest correct configuration.
|
||||
|
||||
## Verifying it works
|
||||
|
||||
Same procedure as the standard backend: open <https://ipleak.net> through the proxy. You should see a Cloudflare-owned IP (since the actual fetch now exits Cloudflare's network), not a Google-owned one as you would with `Code.gs`. If you see your real IP, the proxy isn't being used; if you see a Google IP, you deployed `Code.gs` instead of `Code.cfw.gs`.
|
||||
|
||||
The `Test` button in the desktop UI still works — it does a HEAD relay through whichever Apps Script deployment you configured.
|
||||
|
||||
## Trade-off table at a glance
|
||||
|
||||
| Axis | `Code.gs` (standard) | `Code.cfw.gs` (this variant) |
|
||||
|---|---|---|
|
||||
| Per-call latency floor | ~250-500 ms (GAS internal hop) | ~10-50 ms (CF edge) |
|
||||
| Apps Script `UrlFetchApp`/day, **what mhrv-rs sends today** | 1 quota / request | 1 quota / request — same (mhrv-rs only emits single-URL envelopes) |
|
||||
| Apps Script `UrlFetchApp`/day, **if a future client batches** | N quota (one per URL via `fetchAll`) | `ceil(N / 40)` quota (chunks at 40, Worker fans out via `Promise.all`) |
|
||||
| CF Workers requests/day (free tier) | n/a | 100 000 — far above what GAS can feed it; not the binding ceiling |
|
||||
| Apps Script runtime/day | 90 min, often binding | 90 min, rarely binding |
|
||||
| Per-execution wall budget | ~6 min, per-URL | 30 s, per-call (would become per-chunk if batching ships) |
|
||||
| Per-response size cap | ~50 MB (Apps Script doc'd) | bounded by Worker memory (128 MB free tier); ~tens of MB in practice with the base64 conversion |
|
||||
| Response header casing | preserved as origin sent it | lowercased (Workers' `Headers.forEach` normalises). Matters only for downstream tools that compare header names case-sensitively; mhrv-rs is case-insensitive. |
|
||||
| YouTube long-form playback | OK (6-min cliff) | WORSE (30-s cliff) |
|
||||
| Telegram / chat snappiness | baseline | noticeably better |
|
||||
| Cloudflare anti-bot on target | datacenter IP | worker-internal IP (often stricter) |
|
||||
| Spreadsheet response cache | available (opt-in) | not in this variant |
|
||||
| Deployment complexity | 1 thing to maintain | 2 things to keep in sync |
|
||||
|
||||
If those trade-offs land on the right side for you, deploy this variant. If not — or if you don't have a Cloudflare account — stay on `Code.gs`.
|
||||
|
||||
## Important limitation: not compatible with `mode: "full"`
|
||||
|
||||
`Code.cfw.gs` only ports the HTTP-relay path (modes 1 + 2 in `CodeFull.gs`). The raw-TCP/UDP tunnel ops that `mode: "full"` depends on (modes 3 + 4 in `CodeFull.gs` — required for Android full-mode coverage of WhatsApp / Telegram / messengers / any non-HTTPS-MITM-able app) are **not** ported. If you're on full mode and looking for messenger speed-ups, this variant won't help — that's a different design that would need to ride on top of Cloudflare's TCP Sockets API + Durable Objects, with no equivalent for UDP. See the discussion in [issue #380](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/380) for context.
|
||||
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* MHR-CFW Exit Worker — Cloudflare Workers companion to Code.cfw.gs.
|
||||
*
|
||||
* Architecture (alternative backend, opt-in):
|
||||
* mhrv-rs → Apps Script (Code.cfw.gs) → THIS Worker → target site
|
||||
*
|
||||
* Apps Script in this configuration is a thin relay: it authenticates
|
||||
* the inbound request from mhrv-rs, then forwards to this Worker. The
|
||||
* Worker does the actual outbound fetch(es), base64-encodes the body,
|
||||
* and returns the same JSON envelope shape the standard Code.gs would
|
||||
* have returned. The mhrv-rs client is unaware that the work happened
|
||||
* on Cloudflare — same `{u, m, h, b, ct, r}` request, same `{s, h, b}`
|
||||
* response.
|
||||
*
|
||||
* Two request shapes are accepted:
|
||||
* 1. Single: { k, u, m, h, b, ct, r } → { s, h, b }
|
||||
* 2. Batch: { k, q: [{u,m,h,b,ct,r}, ...] } → { q: [{s,h,b} | {e}, ...] }
|
||||
*
|
||||
* The batch shape is what makes this design actually save Apps Script
|
||||
* UrlFetchApp quota. Without it, Code.cfw.gs would have to do
|
||||
* `UrlFetchApp.fetchAll(N worker calls)` to fan out an N-URL batch,
|
||||
* which costs N quota — same as the standard Code.gs. With it,
|
||||
* Code.cfw.gs does ONE fetch to this Worker (1 quota) and we fan out
|
||||
* inside the Worker via Promise.all. For a typical mhrv-rs batch of
|
||||
* 5-30 URLs that's a 5-30x reduction in GAS daily quota.
|
||||
*
|
||||
* Why bother:
|
||||
* - Faster per-call latency (~10-50 ms at CF edge vs ~250-500 ms in
|
||||
* Apps Script), which matters most for many small requests
|
||||
* (Telegram realtime, page navigation chatter).
|
||||
* - Apps Script *runtime* quota (90 min/day on consumer accounts)
|
||||
* stretches further because GAS spends each call almost entirely
|
||||
* on its single forward to the Worker rather than on body fetch
|
||||
* + base64 + header munging.
|
||||
* - With the batch shape (above), Apps Script *UrlFetchApp count*
|
||||
* quota also stretches roughly Nx for an N-URL batch — typically
|
||||
* 5-30x for mhrv-rs.
|
||||
*
|
||||
* What this does NOT change:
|
||||
* - Cloudflare anti-bot challenges on the destination. The exit IP
|
||||
* becomes a Workers IP (inside Cloudflare's network), which CF's
|
||||
* own anti-bot can fingerprint as a worker-internal request —
|
||||
* often *stricter* than a Google IP. This is a different problem
|
||||
* than DPI bypass; see docs.
|
||||
* - YouTube long-form streaming gets WORSE, not better. Apps Script
|
||||
* allows ~6 min wall per execution; CF Workers cap at 30s wall.
|
||||
* The SABR cliff arrives sooner. Keep the standard `apps_script`
|
||||
* mode (Code.gs) for YouTube-heavy use.
|
||||
* - The 30s wall now applies to the *slowest URL in the batch*
|
||||
* because Promise.all only resolves once every fetch finishes.
|
||||
* mhrv-rs already retries failed batch items individually, so a
|
||||
* single slow target degrades to a per-item timeout rather than
|
||||
* a hard failure — but it's a real behavioural difference vs the
|
||||
* per-URL wall under the standard Code.gs path.
|
||||
*
|
||||
* Deployment:
|
||||
* 1. Cloudflare dashboard → Workers & Pages → Create → Hello World
|
||||
* 2. Edit code → delete the template, paste this entire file
|
||||
* 3. Change AUTH_KEY below to the same value you set in Code.cfw.gs
|
||||
* AND in your mhrv-rs config.json (auth_key). All three must match.
|
||||
* 4. Deploy. Note the *.workers.dev URL; paste it into Code.cfw.gs as
|
||||
* WORKER_URL.
|
||||
*
|
||||
* SECURITY NOTE: this Worker accepts unauthenticated POSTs from anyone
|
||||
* who knows the URL unless AUTH_KEY is changed. The check below is
|
||||
* cheap; do not skip it. The point of the AUTH_KEY is to keep the
|
||||
* Worker from becoming an open HTTP-relay for arbitrary attackers if
|
||||
* its URL leaks. Same secret as Code.cfw.gs by convention — if you
|
||||
* want compartmentalisation, use a different one and have Code.cfw.gs
|
||||
* forward both keys.
|
||||
*
|
||||
* Hardened over the upstream mhr-cfw worker.js by adding the AUTH_KEY
|
||||
* check and batch handling. Upstream credit: github.com/denuitt1/mhr-cfw.
|
||||
*/
|
||||
|
||||
const AUTH_KEY = "CHANGE_ME_TO_A_STRONG_SECRET";
|
||||
const DEFAULT_AUTH_KEY = "CHANGE_ME_TO_A_STRONG_SECRET";
|
||||
|
||||
// Loop-prevention tag. The Worker tags its OUTBOUND request to the
|
||||
// target with `x-relay-hop: 1` (see processOne). If a subsequent
|
||||
// request comes back into the Worker with that header set, the Worker
|
||||
// has been chained back to itself somehow — most likely the user's
|
||||
// `item.u` resolved to this Worker's own URL. Bail out instead of
|
||||
// fetching to avoid a stack-overflow loop.
|
||||
//
|
||||
// Note: Code.cfw.gs does NOT set this header on its GAS→Worker call
|
||||
// (and could not check for it on inbound anyway — Apps Script's
|
||||
// doPost event doesn't expose request headers). So this guard
|
||||
// catches Worker-↔-Worker cycles, not GAS-↔-Worker cycles. The
|
||||
// `targetUrl.hostname === selfHost` check in processOne is the
|
||||
// primary defence for the common misconfiguration.
|
||||
const RELAY_HOP_HEADER = "x-relay-hop";
|
||||
|
||||
// Soft cap on batch size. Cloudflare Workers allow up to 50
|
||||
// subrequests per invocation on the free tier (1000 on paid). We
|
||||
// keep a margin for retries and internal CF traffic. mhrv-rs's
|
||||
// typical batches are 5-30 URLs so this is rarely the binding limit.
|
||||
//
|
||||
// **Must match `WORKER_BATCH_CHUNK` in Code.cfw.gs.** If the GAS side
|
||||
// chunks at a different size, oversized chunks here return a top-level
|
||||
// error and the entire chunk's slots fail. Tune both together.
|
||||
const MAX_BATCH_SIZE = 40;
|
||||
|
||||
// Hop-by-hop headers and headers Cloudflare manages itself. Stripped
|
||||
// before forwarding so the inbound request doesn't poison the outbound.
|
||||
// Kept in sync with Code.cfw.gs / Code.gs SKIP_HEADERS so the Worker
|
||||
// is correct as a defence-in-depth even when called directly (the
|
||||
// AUTH_KEY check is the primary gate, but GAS scrubs first in the
|
||||
// normal flow).
|
||||
const SKIP_HEADERS = new Set([
|
||||
"host",
|
||||
"connection",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
"proxy-connection",
|
||||
"proxy-authorization",
|
||||
"priority",
|
||||
"te",
|
||||
]);
|
||||
|
||||
export default {
|
||||
async fetch(request) {
|
||||
// Fail-closed if the deployer forgot to change AUTH_KEY from the
|
||||
// template default. Without this guard a forgotten edit would
|
||||
// accept any client that also happens to send the placeholder —
|
||||
// effectively running as an open relay. Prefer a loud 500 over
|
||||
// a silent open door.
|
||||
if (AUTH_KEY === DEFAULT_AUTH_KEY) {
|
||||
return json({ e: "configure AUTH_KEY in worker.js" }, 500);
|
||||
}
|
||||
|
||||
if (request.method !== "POST") {
|
||||
return json({ e: "method not allowed" }, 405);
|
||||
}
|
||||
|
||||
if (request.headers.get(RELAY_HOP_HEADER) === "1") {
|
||||
return json({ e: "loop detected" }, 508);
|
||||
}
|
||||
|
||||
let req;
|
||||
try {
|
||||
req = await request.json();
|
||||
} catch (_err) {
|
||||
return json({ e: "bad json" }, 400);
|
||||
}
|
||||
|
||||
if (!req || req.k !== AUTH_KEY) {
|
||||
// Same shape as Code.cfw.gs unauthorized so downstream errors are
|
||||
// uniform. The Worker URL is generally not user-discoverable; the
|
||||
// GAS in front of it is the public surface, and probes hit GAS
|
||||
// first. We don't bother with the decoy-HTML treatment here.
|
||||
return json({ e: "unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const selfHost = new URL(request.url).hostname;
|
||||
|
||||
// Batch mode: { k, q: [{u,m,h,b,ct,r}, ...] }. Process all items in
|
||||
// parallel via Promise.all. Per-item failures are per-item `{e}`s in
|
||||
// the response array; the envelope itself stays 200 unless the batch
|
||||
// is malformed at the top level.
|
||||
if (Array.isArray(req.q)) {
|
||||
if (req.q.length === 0) return json({ q: [] });
|
||||
if (req.q.length > MAX_BATCH_SIZE) {
|
||||
return json({
|
||||
e: "batch too large (" + req.q.length + " > " + MAX_BATCH_SIZE + ")",
|
||||
}, 400);
|
||||
}
|
||||
const results = await Promise.all(
|
||||
req.q.map((item) => processOne(item, selfHost).catch((err) => ({
|
||||
e: "fetch failed: " + String(err),
|
||||
})))
|
||||
);
|
||||
return json({ q: results });
|
||||
}
|
||||
|
||||
// Single mode: { k, u, m, h, b, ct, r }
|
||||
let result;
|
||||
try {
|
||||
result = await processOne(req, selfHost);
|
||||
} catch (err) {
|
||||
return json({ e: "fetch failed: " + String(err) }, 502);
|
||||
}
|
||||
if (result.e) {
|
||||
// Per-item validation errors get HTTP 400 in single mode so
|
||||
// mhrv-rs sees the same shape as in standard Code.gs ("bad url"
|
||||
// etc are already client-error-coded there).
|
||||
return json(result, 400);
|
||||
}
|
||||
return json(result);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Process one item, whether it came in as the top-level single
|
||||
* request or as one slot of a batch. Returns a plain object — never
|
||||
* throws to the caller; Promise.all's .catch above only triggers on
|
||||
* exceptions from this function's own internals (programmer error).
|
||||
*
|
||||
* Result shape mirrors what Code.gs would return for the same item:
|
||||
* - Success: { s: status, h: {...}, b: base64Body }
|
||||
* - Validation / fetch failure: { e: "..." }
|
||||
*/
|
||||
async function processOne(item, selfHost) {
|
||||
if (!item || typeof item !== "object") {
|
||||
return { e: "bad item" };
|
||||
}
|
||||
if (!item.u || typeof item.u !== "string" || !/^https?:\/\//i.test(item.u)) {
|
||||
return { e: "bad url" };
|
||||
}
|
||||
|
||||
let targetUrl;
|
||||
try {
|
||||
targetUrl = new URL(item.u);
|
||||
} catch (_err) {
|
||||
return { e: "bad url" };
|
||||
}
|
||||
if (targetUrl.hostname === selfHost) {
|
||||
return { e: "self-fetch blocked" };
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
if (item.h && typeof item.h === "object") {
|
||||
for (const [k, v] of Object.entries(item.h)) {
|
||||
if (SKIP_HEADERS.has(k.toLowerCase())) continue;
|
||||
try {
|
||||
headers.set(k, v);
|
||||
} catch (_err) {
|
||||
// Worker rejects some headers (e.g. forbidden ones); skip
|
||||
// rather than fail the whole item.
|
||||
}
|
||||
}
|
||||
}
|
||||
headers.set(RELAY_HOP_HEADER, "1");
|
||||
|
||||
const method = (item.m || "GET").toUpperCase();
|
||||
const fetchOptions = {
|
||||
method,
|
||||
headers,
|
||||
redirect: item.r === false ? "manual" : "follow",
|
||||
};
|
||||
|
||||
// Code.gs/UrlFetchApp tolerates a body on GET/HEAD (browsers don't
|
||||
// do this, but custom clients sometimes do); Workers' native fetch
|
||||
// throws TypeError if you set a body on a body-prohibited method.
|
||||
// To match Code.gs's permissiveness, silently drop the body for
|
||||
// those methods rather than failing the whole item.
|
||||
const bodyAllowed = method !== "GET" && method !== "HEAD";
|
||||
if (item.b && bodyAllowed) {
|
||||
try {
|
||||
const binary = Uint8Array.from(atob(item.b), (c) => c.charCodeAt(0));
|
||||
fetchOptions.body = binary;
|
||||
if (item.ct && !headers.has("content-type")) {
|
||||
headers.set("content-type", item.ct);
|
||||
}
|
||||
} catch (_err) {
|
||||
return { e: "bad body base64" };
|
||||
}
|
||||
}
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(targetUrl.toString(), fetchOptions);
|
||||
} catch (err) {
|
||||
return { e: "fetch failed: " + String(err) };
|
||||
}
|
||||
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const uint8 = new Uint8Array(buffer);
|
||||
|
||||
// Avoid call-stack overflow from String.fromCharCode.apply on big
|
||||
// bodies — chunk the conversion.
|
||||
let binary = "";
|
||||
const chunkSize = 0x8000;
|
||||
for (let i = 0; i < uint8.length; i += chunkSize) {
|
||||
binary += String.fromCharCode.apply(null, uint8.subarray(i, i + chunkSize));
|
||||
}
|
||||
const base64 = btoa(binary);
|
||||
|
||||
// Note: Headers.forEach delivers keys lowercased per the Fetch
|
||||
// spec, whereas Code.gs's getAllHeaders preserves the origin's
|
||||
// casing. mhrv-rs treats headers case-insensitively, but anything
|
||||
// downstream that does a case-sensitive string compare will see
|
||||
// a backend-dependent difference. There is no Workers API to
|
||||
// recover the origin casing, so we accept the divergence.
|
||||
const responseHeaders = {};
|
||||
resp.headers.forEach((v, k) => {
|
||||
responseHeaders[k] = v;
|
||||
});
|
||||
|
||||
return {
|
||||
s: resp.status,
|
||||
h: responseHeaders,
|
||||
b: base64,
|
||||
};
|
||||
}
|
||||
|
||||
function json(obj, status = 200) {
|
||||
return new Response(JSON.stringify(obj), {
|
||||
status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user