fix(sms): inside Docker, send the server's public IP as the relay host, not the container's
Messages went out locally but not from the server. The relay gateway identifies the sending server by `host`, and the owner's ruling (D26) is that this is the server's IP. With SMS_RELAY_HOST unset, the driver sent the machine's first external IPv4. On the bare PHP server that was the real IP; inside the API container it is the container's (172.x). Reproduced: a container on this machine reports 172.17.0.3. - relay.driver: when SMS_RELAY_HOST is unset and the machine address is private (RFC 1918, CGNAT, link-local), the public IPv4 is looked up once (ipify, ifconfig.me, icanhazip in turn) and cached. If every lookup fails, it falls back to the old value. SMS_RELAY_HOST still wins. - SmsService logs the host it will send at boot, and warns when it is still private, so a wrong value is visible instead of silent. - Removed a debug console.log that wrote every recipient's mobile to the server log. - .env.example and SERVER-SETUP-CHECKLIST: SMS_RELAY_HOST = this server's public IP. Tests: relay-host.spec (5). All 254 API tests pass. اپ: پیامک کد ورود دوباره بهدرستی ارسال میشود. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c47d02d377
commit
7f851676b5
+53
-5
@@ -87469,14 +87469,14 @@ var require_sms_driver = __commonJS({
|
||||
"use strict";
|
||||
Object.defineProperty(exports2, "__esModule", { value: true });
|
||||
exports2.postWithTimeout = postWithTimeout;
|
||||
async function postWithTimeout(url, headers, body, timeoutMs) {
|
||||
async function postWithTimeout(url, headers, body, timeoutMs, method = "POST") {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
body: body ?? void 0,
|
||||
signal: controller.signal
|
||||
});
|
||||
return { ok: response.ok, status: response.status, body: await response.text() };
|
||||
@@ -87494,6 +87494,9 @@ var require_relay_driver = __commonJS({
|
||||
Object.defineProperty(exports2, "__esModule", { value: true });
|
||||
exports2.relayDriver = void 0;
|
||||
exports2.serverIpv4 = serverIpv4;
|
||||
exports2.isPrivateIpv4 = isPrivateIpv4;
|
||||
exports2.resolveRelayHost = resolveRelayHost;
|
||||
exports2.resetRelayHostCache = resetRelayHostCache;
|
||||
exports2.relayConfig = relayConfig;
|
||||
exports2.relayRejection = relayRejection;
|
||||
var node_os_1 = require("node:os");
|
||||
@@ -87512,12 +87515,39 @@ var require_relay_driver = __commonJS({
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function isPrivateIpv4(ip) {
|
||||
return !!ip && /^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|169\.254\.)/.test(ip);
|
||||
}
|
||||
var publicIpv4 = null;
|
||||
var PUBLIC_IP_SERVICES = ["https://api.ipify.org", "https://ifconfig.me/ip", "https://icanhazip.com"];
|
||||
async function resolveRelayHost() {
|
||||
if ((0, env_12.envOrNull)("SMS_RELAY_HOST") || publicIpv4)
|
||||
return relayConfig().host;
|
||||
const own = serverIpv4();
|
||||
if (!isPrivateIpv4(own))
|
||||
return own;
|
||||
for (const url of PUBLIC_IP_SERVICES) {
|
||||
try {
|
||||
const reply = await (0, sms_driver_1.postWithTimeout)(url, {}, null, 5e3, "GET");
|
||||
const ip = reply.body.trim();
|
||||
if (reply.ok && /^\d{1,3}(\.\d{1,3}){3}$/.test(ip) && !isPrivateIpv4(ip)) {
|
||||
publicIpv4 = ip;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
return relayConfig().host;
|
||||
}
|
||||
function resetRelayHostCache() {
|
||||
publicIpv4 = null;
|
||||
}
|
||||
function relayConfig() {
|
||||
const platform = (0, env_12.envOr)("SMS_RELAY_PLATFORM", DEFAULT_RELAY_PLATFORM);
|
||||
return {
|
||||
url: (0, env_12.envOr)("SMS_RELAY_URL", DEFAULT_RELAY_URL),
|
||||
secret: (0, env_12.envOr)("SMS_RELAY_SECRET", DEFAULT_RELAY_SECRET),
|
||||
host: (0, env_12.envOrNull)("SMS_RELAY_HOST") ?? serverIpv4(),
|
||||
host: (0, env_12.envOrNull)("SMS_RELAY_HOST") ?? publicIpv4 ?? serverIpv4(),
|
||||
// "default" sends null, so the gateway chooses the line itself.
|
||||
platform: platform.toLowerCase() === "default" ? null : platform
|
||||
};
|
||||
@@ -87551,6 +87581,7 @@ var require_relay_driver = __commonJS({
|
||||
return relayConfig().host ? [] : ["SMS_RELAY_HOST"];
|
||||
},
|
||||
async deliver(mobile, text) {
|
||||
await resolveRelayHost();
|
||||
const c = relayConfig();
|
||||
const reply = await (0, sms_driver_1.postWithTimeout)(c.url, { "Content-Type": "application/json", secret_key: c.secret }, JSON.stringify({ phone: mobile, host: c.host, text, platform: c.platform }), REQUEST_TIMEOUT_MS);
|
||||
if (!reply.ok)
|
||||
@@ -87651,6 +87682,7 @@ var require_sms_service = __commonJS({
|
||||
var prisma_service_1 = require_prisma_service();
|
||||
var env_12 = require_env();
|
||||
var drivers_1 = require_drivers();
|
||||
var relay_driver_1 = require_relay_driver();
|
||||
function smsProvider() {
|
||||
return (0, env_12.envOr)("SMS_PROVIDER", drivers_1.DEFAULT_SMS_PROVIDER).toLowerCase();
|
||||
}
|
||||
@@ -87686,6 +87718,23 @@ var require_sms_service = __commonJS({
|
||||
constructor(prisma) {
|
||||
this.prisma = prisma;
|
||||
}
|
||||
/**
|
||||
* Reports the `host` the relay gateway will receive, so a wrong one shows in
|
||||
* the boot log instead of as silent failures. Never throws and never delays
|
||||
* the boot: the lookup runs in the background.
|
||||
*/
|
||||
onApplicationBootstrap() {
|
||||
if (smsDriver()?.name !== "relay")
|
||||
return;
|
||||
void (0, relay_driver_1.resolveRelayHost)().then((host) => {
|
||||
const source = (0, env_12.envOrNull)("SMS_RELAY_HOST") ? "SMS_RELAY_HOST" : "detected";
|
||||
if ((0, relay_driver_1.isPrivateIpv4)(host)) {
|
||||
this.logger.warn(`SMS relay host is a private address (${host}, ${source}); the gateway may not accept it. Set SMS_RELAY_HOST to this server's public IP.`);
|
||||
} else {
|
||||
this.logger.log(`SMS relay host: ${host ?? "(none)"} (${source})`);
|
||||
}
|
||||
}).catch(() => void 0);
|
||||
}
|
||||
/**
|
||||
* Sends one message. Never throws — a failed SMS must not roll back the
|
||||
* business operation that triggered it, exactly as in the reference.
|
||||
@@ -87693,7 +87742,6 @@ var require_sms_service = __commonJS({
|
||||
async send(mobile, body, options = {}) {
|
||||
const { persist = true, audience = client_1.MessageAudience.SINGLE_NUMBER } = options;
|
||||
const text = body.trim();
|
||||
console.log("mobile ==>", mobile);
|
||||
const result = await this.deliver(mobile, text);
|
||||
if (persist) {
|
||||
await this.record(mobile, text, audience);
|
||||
|
||||
Reference in New Issue
Block a user