refactor: remove Val Town references and update exit node documentation

This commit is contained in:
Abolfazl
2026-05-04 22:46:37 +03:30
parent 2afdf3c7bb
commit b3df1d3828
6 changed files with 56 additions and 158 deletions
+10 -12
View File
@@ -174,15 +174,14 @@ Some websites block Google datacenter IPs when traffic exits directly from Apps
To fix that, configure an exit node so traffic path becomes: To fix that, configure an exit node so traffic path becomes:
```text ```text
Browser -> Local Proxy -> Apps Script -> Exit Node (Val Town / Cloudflare / Deno) -> Target website Browser -> Local Proxy -> Apps Script -> Exit Node (Cloudflare / Deno / VPS) -> Target website
``` ```
You can deploy any one of these free exit-node templates: You can deploy any one of these exit-node backends:
1. Val Town: [`apps_script/valtown.ts`](apps_script/valtown.ts) 1. Cloudflare Workers: [`apps_script/cloudflare_worker.js`](apps_script/cloudflare_worker.js)
2. Cloudflare Workers: [`apps_script/cloudflare_worker.js`](apps_script/cloudflare_worker.js) 2. Deno Deploy: [`apps_script/deno_deploy.ts`](apps_script/deno_deploy.ts)
3. Deno Deploy: [`apps_script/deno_deploy.ts`](apps_script/deno_deploy.ts) 3. Your own VPS server
4. Your own VPS server
Full step-by-step deployment guide (all providers): Full step-by-step deployment guide (all providers):
- [docs/exit-node/EXIT_NODE_DEPLOYMENT.md](docs/exit-node/EXIT_NODE_DEPLOYMENT.md) - [docs/exit-node/EXIT_NODE_DEPLOYMENT.md](docs/exit-node/EXIT_NODE_DEPLOYMENT.md)
@@ -194,8 +193,8 @@ Then configure provider switching like this:
```json ```json
"exit_node": { "exit_node": {
"enabled": true, "enabled": true,
"provider": "valtown", "provider": "cloudflare",
"url": "https://YOUR-NAME.web.val.run", "url": "https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev",
"psk": "CHANGE_ME_TO_A_STRONG_SECRET", "psk": "CHANGE_ME_TO_A_STRONG_SECRET",
"mode": "full", "mode": "full",
"hosts": [ "hosts": [
@@ -218,7 +217,7 @@ Production recommendation:
- Keep `verify_ssl: true` - Keep `verify_ssl: true`
- Keep `listen_host: 127.0.0.1` unless LAN sharing is explicitly needed - Keep `listen_host: 127.0.0.1` unless LAN sharing is explicitly needed
- Rotate both secrets periodically - Rotate both secrets periodically
- Never publish your live val URL with valid PSK - Never publish your live exit-node URL with valid PSK
### Step 4: Run ### Step 4: Run
@@ -311,7 +310,7 @@ By default, the proxy only listens on `127.0.0.1` (localhost), meaning only your
## Modes Overview ## Modes Overview
This project is centered on the **Apps Script** relay (free, no VPS needed). For destinations that block Google egress, you can optionally chain a free edge exit node (Val Town, Cloudflare Workers, or Deno Deploy). This project is centered on the **Apps Script** relay (free, no VPS needed). For destinations that block Google egress, you can optionally chain an edge exit node (Cloudflare Workers, Deno Deploy, or your own VPS).
--- ---
@@ -350,7 +349,7 @@ This project is centered on the **Apps Script** relay (free, no VPS needed). For
| `direct_google_exclude` | see [config.example.json](config.example.json) | Google apps that must use the MITM relay path instead of the fast direct tunnel. | | `direct_google_exclude` | see [config.example.json](config.example.json) | Google apps that must use the MITM relay path instead of the fast direct tunnel. |
| `hosts` | `{}` | Manual DNS override: map a hostname to a specific IP. | | `hosts` | `{}` | Manual DNS override: map a hostname to a specific IP. |
| `youtube_via_relay` | `false` | Route YouTube (`youtube.com`, `youtu.be`, `youtube-nocookie.com`) through the Apps Script relay instead of the SNI-rewrite path. The SNI-rewrite path uses Google's frontend IP which enforces SafeSearch and can cause **"Video Unavailable"** errors. Setting this to `true` fixes playback at the cost of using more Apps Script executions and slightly higher latency. | | `youtube_via_relay` | `false` | Route YouTube (`youtube.com`, `youtu.be`, `youtube-nocookie.com`) through the Apps Script relay instead of the SNI-rewrite path. The SNI-rewrite path uses Google's frontend IP which enforces SafeSearch and can cause **"Video Unavailable"** errors. Setting this to `true` fixes playback at the cost of using more Apps Script executions and slightly higher latency. |
| `exit_node.provider` | `valtown` | Selected exit-node backend: `valtown`, `cloudflare`, `deno`, or `custom`. | | `exit_node.provider` | `cloudflare` | Selected exit-node backend: `cloudflare`, `deno`, `vps`, or `custom`. |
| `exit_node.url` | `""` | Beginner-friendly single URL for the selected provider. | | `exit_node.url` | `""` | Beginner-friendly single URL for the selected provider. |
### Optional Dependencies ### Optional Dependencies
@@ -472,7 +471,6 @@ MasterHttpRelayVPN/
├── requirements.txt # Python dependencies ├── requirements.txt # Python dependencies
├── apps_script/ ├── apps_script/
│ ├── Code.gs # The relay script you deploy to Google Apps Script │ ├── Code.gs # The relay script you deploy to Google Apps Script
│ ├── valtown.ts # Exit node template for val.town
│ ├── cloudflare_worker.js # Exit node template for Cloudflare Workers │ ├── cloudflare_worker.js # Exit node template for Cloudflare Workers
│ └── deno_deploy.ts # Exit node template for Deno Deploy │ └── deno_deploy.ts # Exit node template for Deno Deploy
├── ca/ # Generated MITM CA (do NOT share) ├── ca/ # Generated MITM CA (do NOT share)
+8 -10
View File
@@ -137,15 +137,14 @@ cp config.example.json config.json
برای حل این مورد، نود خروجی (exit node) را فعال کنید تا مسیر این‌گونه شود: برای حل این مورد، نود خروجی (exit node) را فعال کنید تا مسیر این‌گونه شود:
```text ```text
مرورگر -> پراکسی محلی -> Apps Script -> Exit Node (Val Town / Cloudflare / Deno) -> سایت مقصد مرورگر -> پراکسی محلی -> Apps Script -> Exit Node (Cloudflare / Deno / VPS) -> سایت مقصد
``` ```
می‌توانید یکی از این template های رایگان را deploy کنید: می‌توانید یکی از این backend های نود خروجی را deploy کنید:
1. Val Town: [apps_script/valtown.ts](apps_script/valtown.ts) 1. Cloudflare Workers: [apps_script/cloudflare_worker.js](apps_script/cloudflare_worker.js)
2. Cloudflare Workers: [apps_script/cloudflare_worker.js](apps_script/cloudflare_worker.js) 2. Deno Deploy: [apps_script/deno_deploy.ts](apps_script/deno_deploy.ts)
3. Deno Deploy: [apps_script/deno_deploy.ts](apps_script/deno_deploy.ts) 3. سرور VPS شخصی
4. سرور VPS شخصی
راهنمای کامل مرحله‌به‌مرحله برای هر provider: راهنمای کامل مرحله‌به‌مرحله برای هر provider:
- [docs/exit-node/EXIT_NODE_DEPLOYMENT_FA.md](docs/exit-node/EXIT_NODE_DEPLOYMENT_FA.md) (فارسی) - [docs/exit-node/EXIT_NODE_DEPLOYMENT_FA.md](docs/exit-node/EXIT_NODE_DEPLOYMENT_FA.md) (فارسی)
@@ -158,8 +157,8 @@ cp config.example.json config.json
```json ```json
"exit_node": { "exit_node": {
"enabled": true, "enabled": true,
"provider": "valtown", "provider": "cloudflare",
"url": "https://YOUR-NAME.web.val.run", "url": "https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev",
"psk": "CHANGE_ME_TO_A_STRONG_SECRET", "psk": "CHANGE_ME_TO_A_STRONG_SECRET",
"mode": "full", "mode": "full",
"hosts": [ "hosts": [
@@ -294,7 +293,7 @@ json
| `bypass_hosts` | `["localhost", ".local", ".lan", ".home.arpa"]` | هاست‌هایی که مستقیم می‌روند (بدون MITM و بدون رله). برای منابع داخلی شبکه یا سایت‌هایی که با MITM مشکل دارند. | | `bypass_hosts` | `["localhost", ".local", ".lan", ".home.arpa"]` | هاست‌هایی که مستقیم می‌روند (بدون MITM و بدون رله). برای منابع داخلی شبکه یا سایت‌هایی که با MITM مشکل دارند. |
| `direct_google_exclude` | مراجعه به [config.example.json](config.example.json) | اپ‌های Google که باید از مسیر MITM برای رله استفاده کنند به‌جای tunnel مستقیم. | | `direct_google_exclude` | مراجعه به [config.example.json](config.example.json) | اپ‌های Google که باید از مسیر MITM برای رله استفاده کنند به‌جای tunnel مستقیم. |
| `youtube_via_relay` | `false` | مسیردهی YouTube (`youtube.com`، `youtu.be`، `youtube-nocookie.com`) از طریق رله Apps Script به‌جای مسیر SNI-rewrite. مسیر SNI-rewrite از IP فرانت‌اند Google عبور می‌کند که SafeSearch را اجباری می‌کند و می‌تواند باعث خطای **«ویدیو در دسترس نیست»** شود. با فعال کردن این گزینه، پخش ویدیو درست می‌شود اما تعداد اجراهای Apps Script بیشتر و تأخیر اندکی بالاتر می‌رود. | | `youtube_via_relay` | `false` | مسیردهی YouTube (`youtube.com`، `youtu.be`، `youtube-nocookie.com`) از طریق رله Apps Script به‌جای مسیر SNI-rewrite. مسیر SNI-rewrite از IP فرانت‌اند Google عبور می‌کند که SafeSearch را اجباری می‌کند و می‌تواند باعث خطای **«ویدیو در دسترس نیست»** شود. با فعال کردن این گزینه، پخش ویدیو درست می‌شود اما تعداد اجراهای Apps Script بیشتر و تأخیر اندکی بالاتر می‌رود. |
| `exit_node.provider` | `valtown` | backend انتخاب‌شده برای exit node: `valtown`، `cloudflare`، `deno` یا `custom`. | | `exit_node.provider` | `cloudflare` | backend انتخاب‌شده برای exit node: `cloudflare`، `deno`، `vps` یا `custom`. |
| `exit_node.url` | `""` | آدرس ساده و اصلی برای provider انتخاب‌شده. | | `exit_node.url` | `""` | آدرس ساده و اصلی برای provider انتخاب‌شده. |
### وابستگی‌های اختیاری ### وابستگی‌های اختیاری
@@ -411,7 +410,6 @@ MasterHttpRelayVPN/
├── requirements.txt # وابستگی‌های اختیاری پایتون ├── requirements.txt # وابستگی‌های اختیاری پایتون
├── apps_script/ ├── apps_script/
│ ├── Code.gs # اسکریپت رله روی Google Apps Script │ ├── Code.gs # اسکریپت رله روی Google Apps Script
│ ├── valtown.ts # template نود خروجی برای val.town
│ ├── cloudflare_worker.js # template نود خروجی برای Cloudflare Workers │ ├── cloudflare_worker.js # template نود خروجی برای Cloudflare Workers
│ └── deno_deploy.ts # template نود خروجی برای Deno Deploy │ └── deno_deploy.ts # template نود خروجی برای Deno Deploy
├── ca/ # گواهی MITM (هرگز به اشتراک نگذارید) ├── ca/ # گواهی MITM (هرگز به اشتراک نگذارید)
-88
View File
@@ -1,88 +0,0 @@
const PSK = "CHANGE_ME_TO_A_STRONG_SECRET";
const STRIP_HEADERS = new Set([
"host",
"connection",
"content-length",
"transfer-encoding",
"proxy-connection",
"proxy-authorization",
"x-forwarded-for",
"x-forwarded-host",
"x-forwarded-proto",
"x-forwarded-port",
"x-real-ip",
"forwarded",
"via",
]);
function decodeBase64ToBytes(input: string): Uint8Array {
const bin = atob(input);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
function encodeBytesToBase64(bytes: Uint8Array): string {
let bin = "";
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
function sanitizeHeaders(h: unknown): Record<string, string> {
const out: Record<string, string> = {};
if (!h || typeof h !== "object") return out;
for (const [k, v] of Object.entries(h as Record<string, unknown>)) {
if (!k) continue;
if (STRIP_HEADERS.has(k.toLowerCase())) continue;
out[k] = String(v ?? "");
}
return out;
}
export default async function(req: Request): Promise<Response> {
try {
if (req.method !== "POST") {
return Response.json({ e: "method_not_allowed" }, { status: 405 });
}
const body = await req.json();
if (!body || typeof body !== "object") {
return Response.json({ e: "bad_json" }, { status: 400 });
}
const k = String((body as any).k ?? "");
const u = String((body as any).u ?? "");
const m = String((body as any).m ?? "GET").toUpperCase();
const h = sanitizeHeaders((body as any).h);
const b64 = (body as any).b;
if (k !== PSK) return Response.json({ e: "unauthorized" }, { status: 401 });
if (!/^https?:\/\//i.test(u)) return Response.json({ e: "bad url" }, { status: 400 });
let payload: Uint8Array | undefined;
if (typeof b64 === "string" && b64.length > 0) payload = decodeBase64ToBytes(b64);
const resp = await fetch(u, {
method: m,
headers: h,
body: payload,
redirect: "manual",
});
const data = new Uint8Array(await resp.arrayBuffer());
const respHeaders: Record<string, string> = {};
resp.headers.forEach((value, key) => {
respHeaders[key] = value;
});
return Response.json({
s: resp.status,
h: respHeaders,
b: encodeBytesToBase64(data),
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return Response.json({ e: message }, { status: 500 });
}
}
+8 -22
View File
@@ -1,4 +1,4 @@
# Exit Node Deployment Guide (Val Town / Cloudflare / Deno / VPS) # Exit Node Deployment Guide (Cloudflare / Deno / VPS)
This guide explains how to deploy an exit node for MasterHttpRelayVPN on free platforms or your own VPS server. This guide explains how to deploy an exit node for MasterHttpRelayVPN on free platforms or your own VPS server.
@@ -10,7 +10,6 @@ Use this when destinations block Google datacenter egress.
## 1) Choose One Provider ## 1) Choose One Provider
- Val Town (free, no server required)
- Cloudflare Workers (free tier available) - Cloudflare Workers (free tier available)
- Deno Deploy (free, not fully tested) - Deno Deploy (free, not fully tested)
- **Your Own VPS** (full control, Linux server — automated installer included) - **Your Own VPS** (full control, Linux server — automated installer included)
@@ -29,19 +28,7 @@ Important:
- Use the same PSK in your local config under exit_node.psk. - Use the same PSK in your local config under exit_node.psk.
- Never share your deployed URL together with a valid PSK. - Never share your deployed URL together with a valid PSK.
## 3) Deploy On Val Town ## 3) Deploy On Cloudflare Workers
Source file: apps_script/valtown.ts
Steps:
1. Sign in at https://www.val.town
2. Create a new Val (TypeScript HTTP endpoint).
3. Paste content from apps_script/valtown.ts.
4. Set the PSK constant in the code.
5. Save and Add HTTP trigger.
6. Copy your public URL, usually like https://YOUR-NAME.web.val.run
## 4) Deploy On Cloudflare Workers
Source file: apps_script/cloudflare_worker.js Source file: apps_script/cloudflare_worker.js
@@ -54,7 +41,7 @@ Steps:
6. Deploy. 6. Deploy.
7. Copy URL, usually like https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev 7. Copy URL, usually like https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev
## 5) Deploy On Deno Deploy (It's not tested Yet) ## 4) Deploy On Deno Deploy (It's not tested Yet)
Source file: apps_script/deno_deploy.ts Source file: apps_script/deno_deploy.ts
@@ -66,7 +53,7 @@ Steps:
5. Deploy. 5. Deploy.
6. Copy URL, usually like https://YOUR-PROJECT.deno.net 6. Copy URL, usually like https://YOUR-PROJECT.deno.net
## 6) Deploy On Your Own VPS (Linux only) ## 5) Deploy On Your Own VPS (Linux only)
Source files: Source files:
- `apps_script/vps_exit_node.py` — the relay server - `apps_script/vps_exit_node.py` — the relay server
@@ -94,16 +81,16 @@ The script automatically downloads `vps_exit_node.py` from GitHub, so no `git cl
Note: Note:
- To rotate the PSK, edit `/etc/exit-node.env` and restart: `systemctl restart exit-node`. - To rotate the PSK, edit `/etc/exit-node.env` and restart: `systemctl restart exit-node`.
## 7) Configure MasterHttpRelayVPN ## 6) Configure MasterHttpRelayVPN
Update `config.json`: Update `config.json`:
For Val Town / Cloudflare / Deno: For Cloudflare / Deno:
```json ```json
"exit_node": { "exit_node": {
"enabled": true, "enabled": true,
"provider": "valtown", "provider": "cloudflare",
"url": "https://YOUR-NAME.web.val.run", "url": "https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev",
"psk": "CHANGE_ME_TO_A_STRONG_SECRET", "psk": "CHANGE_ME_TO_A_STRONG_SECRET",
"mode": "full", "mode": "full",
"hosts": [ "hosts": [
@@ -133,7 +120,6 @@ For your own VPS:
``` ```
Provider values: Provider values:
- `valtown`
- `cloudflare` - `cloudflare`
- `deno` - `deno`
- `vps` - `vps`
+29 -19
View File
@@ -1,4 +1,4 @@
# راهنمای نصب نود خروجی (Val Town / Cloudflare / Deno) # راهنمای نصب نود خروجی (Cloudflare / Deno / VPS)
این راهنما توضیح می‌دهد چطور یک نود خروجی رایگان برای MasterHttpRelayVPN راه‌اندازی کنید. این راهنما توضیح می‌دهد چطور یک نود خروجی رایگان برای MasterHttpRelayVPN راه‌اندازی کنید.
@@ -12,9 +12,9 @@
## ۱) یک Provider انتخاب کنید ## ۱) یک Provider انتخاب کنید
- Val Town
- Cloudflare Workers - Cloudflare Workers
- Deno Deploy - Deno Deploy
- VPS شخصی
فقط به یکی از این‌ها نیاز دارید. فقط به یکی از این‌ها نیاز دارید.
@@ -32,19 +32,7 @@ const PSK = "CHANGE_ME_TO_A_STRONG_SECRET";
- همین PSK را در `config.json` زیر `exit_node.psk` وارد کنید. - همین PSK را در `config.json` زیر `exit_node.psk` وارد کنید.
- URL عمومی را هرگز همراه با PSK معتبر به اشتراک نگذارید. - URL عمومی را هرگز همراه با PSK معتبر به اشتراک نگذارید.
## ۳) نصب روی Val Town ## ۳) نصب روی Cloudflare Workers
فایل: `apps_script/valtown.ts`
مراحل:
1. در [https://www.val.town](https://www.val.town) ثبت‌نام کنید.
2. یک Val جدید بسازید (TypeScript HTTP endpoint).
3. محتوای `apps_script/valtown.ts` را paste کنید.
4. مقدار ثابت PSK را در کد تنظیم کنید.
5. ذخیره و deploy کنید. (Add HTTP trigger را فراموش نکنید)
6. URL عمومی خود را کپی کنید؛ معمولاً به شکل `https://YOUR-NAME.web.val.run`
## ۴) نصب روی Cloudflare Workers
فایل: `apps_script/cloudflare_worker.js` فایل: `apps_script/cloudflare_worker.js`
@@ -57,7 +45,7 @@ const PSK = "CHANGE_ME_TO_A_STRONG_SECRET";
6. Deploy کنید. 6. Deploy کنید.
7. URL را کپی کنید؛ معمولاً به شکل `https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev` 7. URL را کپی کنید؛ معمولاً به شکل `https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev`
## ۵) نصب روی Deno Deploy (هنوز تست نشده) ## ۴) نصب روی Deno Deploy (هنوز تست نشده)
فایل: `apps_script/deno_deploy.ts` فایل: `apps_script/deno_deploy.ts`
@@ -70,6 +58,28 @@ const PSK = "CHANGE_ME_TO_A_STRONG_SECRET";
6. Deploy کنید. 6. Deploy کنید.
7. URL را کپی کنید؛ معمولاً به شکل `https://YOUR-PROJECT.deno.dev` 7. URL را کپی کنید؛ معمولاً به شکل `https://YOUR-PROJECT.deno.dev`
## ۵) نصب روی VPS شخصی (فقط Linux)
فایل‌ها:
- `apps_script/vps_exit_node.py` (سرور نود خروجی)
- `apps_script/setup_vps_exit_node.sh` (نصب خودکار - پیشنهادی)
نیازمندی‌ها:
- یک VPS لینوکسی با دسترسی root/sudo
- Python 3.10+
دستور نصب سریع:
```bash
curl -fsSL https://raw.githubusercontent.com/masterking32/MasterHttpRelayVPN/python_testing/apps_script/setup_vps_exit_node.sh | sudo bash
```
یا:
```bash
wget -qO- https://raw.githubusercontent.com/masterking32/MasterHttpRelayVPN/python_testing/apps_script/setup_vps_exit_node.sh | sudo bash
```
## ۶) تنظیم MasterHttpRelayVPN ## ۶) تنظیم MasterHttpRelayVPN
فایل `config.json` را ویرایش کنید: فایل `config.json` را ویرایش کنید:
@@ -77,8 +87,8 @@ const PSK = "CHANGE_ME_TO_A_STRONG_SECRET";
```json ```json
"exit_node": { "exit_node": {
"enabled": true, "enabled": true,
"provider": "valtown", "provider": "cloudflare",
"url": "https://YOUR-NAME.web.val.run", "url": "https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev",
"psk": "CHANGE_ME_TO_A_STRONG_SECRET", "psk": "CHANGE_ME_TO_A_STRONG_SECRET",
"mode": "full", "mode": "full",
"hosts": [ "hosts": [
@@ -91,9 +101,9 @@ const PSK = "CHANGE_ME_TO_A_STRONG_SECRET";
``` ```
مقادیر provider: مقادیر provider:
- `valtown`
- `cloudflare` - `cloudflare`
- `deno` - `deno`
- `vps`
اگر `mode` برابر `selective` باشد، فقط دامنه‌های داخل `hosts` از نود خروجی عبور می‌کنند. اگر `mode` برابر `selective` باشد، فقط دامنه‌های داخل `hosts` از نود خروجی عبور می‌کنند.
اگر `mode` برابر `full` باشد، تمام ترافیک relay‌شده از نود خروجی عبور می‌کند. اگر `mode` برابر `full` باشد، تمام ترافیک relay‌شده از نود خروجی عبور می‌کند.
+1 -7
View File
@@ -1116,8 +1116,6 @@ class DomainFronter:
def _normalize_exit_node_provider(raw: object) -> str: def _normalize_exit_node_provider(raw: object) -> str:
provider = str(raw or "custom").strip().lower() provider = str(raw or "custom").strip().lower()
aliases = { aliases = {
"val": "valtown",
"val-town": "valtown",
"cloudflare_worker": "cloudflare", "cloudflare_worker": "cloudflare",
"worker": "cloudflare", "worker": "cloudflare",
"cf": "cloudflare", "cf": "cloudflare",
@@ -1150,11 +1148,7 @@ class DomainFronter:
if direct: if direct:
return direct return direct
if provider == "valtown": if provider == "cloudflare":
selected = _pick_from(en_cfg, "valtown_url", "val_url") or _pick_from(
providers, "valtown", "val_town", "val",
)
elif provider == "cloudflare":
selected = _pick_from( selected = _pick_from(
en_cfg, "cloudflare_url", "worker_url", "cf_url", en_cfg, "cloudflare_url", "worker_url", "cf_url",
) or _pick_from( ) or _pick_from(