mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 14:36:05 +00:00
fix(v2.4.2): iter2 audit fixes
- bot.py: safe_edit_message now accepts disable_web_page_preview (CRIT: was TypeError in cb_pro_confirm success path)
- bot.py: status display uses template_id field (was 'template' — mismatch with save_gotelegram_config, template never showed)
- bot.py: cb_pro_confirm validates tpl_id against [A-Za-z0-9_-]{1,64} before subprocess (defense-in-depth)
- bot.py: cb_lite_domain validates domain shape
- bot.py: asyncio.Lock _BOT_ACTION_LOCK serializes concurrent change-template/change-lite-domain calls
- install.sh: bot_update_config_field uses shell `date -Iseconds` instead of jq's `now|todate` (jq 1.5 compat for Debian 10)
This commit is contained in:
@@ -240,6 +240,24 @@ async def sh(*args, timeout: int = 60) -> Tuple[int, str, str]:
|
|||||||
return (-1, "", str(e))
|
return (-1, "", str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# Per-host mutex preventing concurrent install.sh --action invocations. Two
|
||||||
|
# admins hitting "change template" at the same second could race each other
|
||||||
|
# and corrupt /var/www/gotelegram-site. One global lock is fine — these are
|
||||||
|
# rare operations and should serialize cleanly.
|
||||||
|
_BOT_ACTION_LOCK = asyncio.Lock()
|
||||||
|
|
||||||
|
# Allowed template-id shape: catalog ids are [a-zA-Z0-9_-], never longer than 64.
|
||||||
|
# This is a defense-in-depth check before we hand the value to subprocess.
|
||||||
|
_TPL_ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
|
||||||
|
|
||||||
|
# Allowed Lite mask domain shape — simple DNS hostname, up to 253 chars total.
|
||||||
|
# Each label 1–63 chars, labels separated by dots, alphanumerics + hyphens.
|
||||||
|
_DOMAIN_RE = re.compile(
|
||||||
|
r"^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+"
|
||||||
|
r"(?!-)[A-Za-z0-9-]{2,63}(?<!-)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def run_bot_action(action: str, timeout: int = 300, **params) -> Dict:
|
async def run_bot_action(action: str, timeout: int = 300, **params) -> Dict:
|
||||||
"""Invoke install.sh --action=X --json and parse the JSON result.
|
"""Invoke install.sh --action=X --json and parse the JSON result.
|
||||||
|
|
||||||
@@ -324,12 +342,23 @@ def save_json(path: str, data: Dict) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def safe_edit_message(query, text: str, reply_markup=None, parse_mode=None) -> bool:
|
async def safe_edit_message(
|
||||||
"""Safely edit message, handling cases where message was deleted or not modified."""
|
query,
|
||||||
|
text: str,
|
||||||
|
reply_markup=None,
|
||||||
|
parse_mode=None,
|
||||||
|
disable_web_page_preview: Optional[bool] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Safely edit message, handling cases where message was deleted or not modified.
|
||||||
|
|
||||||
|
`disable_web_page_preview` is forwarded to edit_message_text when set; omitting
|
||||||
|
it keeps Telegram's default (enabled).
|
||||||
|
"""
|
||||||
|
kwargs = {"reply_markup": reply_markup, "parse_mode": parse_mode}
|
||||||
|
if disable_web_page_preview is not None:
|
||||||
|
kwargs["disable_web_page_preview"] = disable_web_page_preview
|
||||||
try:
|
try:
|
||||||
await query.edit_message_text(
|
await query.edit_message_text(text, **kwargs)
|
||||||
text, reply_markup=reply_markup, parse_mode=parse_mode
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
except BadRequest as e:
|
except BadRequest as e:
|
||||||
err_msg = str(e).lower()
|
err_msg = str(e).lower()
|
||||||
@@ -621,11 +650,13 @@ async def get_status_text(user_id: Optional[int] = None) -> str:
|
|||||||
config = load_json(GOTELEGRAM_CONFIG)
|
config = load_json(GOTELEGRAM_CONFIG)
|
||||||
if config:
|
if config:
|
||||||
lines.append(f"<b>{_t(user_id, 'status_mode')}:</b> {html.escape(str(config.get('mode', 'unknown')))}")
|
lines.append(f"<b>{_t(user_id, 'status_mode')}:</b> {html.escape(str(config.get('mode', 'unknown')))}")
|
||||||
if "template" in config:
|
# install.sh/save_gotelegram_config uses "template_id" (not "template")
|
||||||
lines.append(f"<b>{_t(user_id, 'status_template')}:</b> {html.escape(str(config['template']))}")
|
tpl = config.get("template_id") or config.get("template")
|
||||||
if "domain" in config:
|
if tpl:
|
||||||
|
lines.append(f"<b>{_t(user_id, 'status_template')}:</b> {html.escape(str(tpl))}")
|
||||||
|
if config.get("domain"):
|
||||||
lines.append(f"<b>{_t(user_id, 'status_domain')}:</b> {html.escape(str(config['domain']))}")
|
lines.append(f"<b>{_t(user_id, 'status_domain')}:</b> {html.escape(str(config['domain']))}")
|
||||||
if "port" in config:
|
if config.get("port"):
|
||||||
lines.append(f"<b>{_t(user_id, 'status_port')}:</b> {html.escape(str(config['port']))}")
|
lines.append(f"<b>{_t(user_id, 'status_port')}:</b> {html.escape(str(config['port']))}")
|
||||||
|
|
||||||
# Telemt config (v3: [server] port = ..., [censorship] tls_domain = ...)
|
# Telemt config (v3: [server] port = ..., [censorship] tls_domain = ...)
|
||||||
@@ -878,6 +909,13 @@ async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
await query.answer("Invalid domain selection")
|
await query.answer("Invalid domain selection")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Defense-in-depth: LITE_DOMAINS is trusted, but validate the shape anyway
|
||||||
|
# in case someone extends the list with garbage later.
|
||||||
|
if not _DOMAIN_RE.match(domain):
|
||||||
|
logger.warning(f"cb_lite_domain: rejecting malformed domain {domain!r}")
|
||||||
|
await query.answer("Invalid domain")
|
||||||
|
return
|
||||||
|
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||||
@@ -898,6 +936,18 @@ async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Lite active — switch fake-TLS mask domain in place
|
# Lite active — switch fake-TLS mask domain in place
|
||||||
|
if _BOT_ACTION_LOCK.locked():
|
||||||
|
await safe_edit_message(
|
||||||
|
query,
|
||||||
|
"<b>⏳ Другая операция уже выполняется</b>\n\n"
|
||||||
|
"Дождитесь завершения предыдущей смены шаблона/домена и повторите.",
|
||||||
|
reply_markup=InlineKeyboardMarkup(
|
||||||
|
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||||
|
),
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
progress_text = (
|
progress_text = (
|
||||||
"<b>⏳ Меняю маскировочный домен...</b>\n\n"
|
"<b>⏳ Меняю маскировочный домен...</b>\n\n"
|
||||||
f"Новый домен: <code>{html.escape(domain)}</code>\n\n"
|
f"Новый домен: <code>{html.escape(domain)}</code>\n\n"
|
||||||
@@ -905,7 +955,8 @@ async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
)
|
)
|
||||||
await safe_edit_message(query, progress_text, parse_mode="HTML")
|
await safe_edit_message(query, progress_text, parse_mode="HTML")
|
||||||
|
|
||||||
result = await run_bot_action("change-lite-domain", timeout=30, domain=domain)
|
async with _BOT_ACTION_LOCK:
|
||||||
|
result = await run_bot_action("change-lite-domain", timeout=30, domain=domain)
|
||||||
|
|
||||||
if result.get("status") == "success":
|
if result.get("status") == "success":
|
||||||
text = (
|
text = (
|
||||||
@@ -1219,6 +1270,21 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
|
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
|
# Defense-in-depth: even though subprocess.exec uses list args (no shell),
|
||||||
|
# we still enforce the catalog id shape before handing it to install.sh.
|
||||||
|
if not _TPL_ID_RE.match(tpl_id):
|
||||||
|
logger.warning(f"cb_pro_confirm: rejecting malformed tpl_id {tpl_id!r}")
|
||||||
|
await safe_edit_message(
|
||||||
|
query,
|
||||||
|
"<b>❌ Некорректный идентификатор шаблона</b>\n\n"
|
||||||
|
"Выбран неподдерживаемый шаблон. Вернитесь в меню и попробуйте снова.",
|
||||||
|
reply_markup=InlineKeyboardMarkup(
|
||||||
|
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||||
|
),
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Read current config to decide: in-place change-template vs fresh install
|
# Read current config to decide: in-place change-template vs fresh install
|
||||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||||
current_mode = config.get("mode", "")
|
current_mode = config.get("mode", "")
|
||||||
@@ -1240,6 +1306,18 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Pro mode is active — perform change-template in place
|
# Pro mode is active — perform change-template in place
|
||||||
|
if _BOT_ACTION_LOCK.locked():
|
||||||
|
await safe_edit_message(
|
||||||
|
query,
|
||||||
|
"<b>⏳ Другая операция уже выполняется</b>\n\n"
|
||||||
|
"Дождитесь завершения предыдущей смены шаблона/домена и повторите.",
|
||||||
|
reply_markup=InlineKeyboardMarkup(
|
||||||
|
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||||
|
),
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
progress_text = (
|
progress_text = (
|
||||||
"<b>⏳ Меняю шаблон сайта...</b>\n\n"
|
"<b>⏳ Меняю шаблон сайта...</b>\n\n"
|
||||||
f"Шаблон: <code>{html.escape(tpl_id)}</code>\n\n"
|
f"Шаблон: <code>{html.escape(tpl_id)}</code>\n\n"
|
||||||
@@ -1248,8 +1326,10 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
)
|
)
|
||||||
await safe_edit_message(query, progress_text, parse_mode="HTML")
|
await safe_edit_message(query, progress_text, parse_mode="HTML")
|
||||||
|
|
||||||
# Template download + git clone can be slow — generous timeout
|
# Template download + git clone can be slow — generous timeout.
|
||||||
result = await run_bot_action("change-template", timeout=180, template=tpl_id)
|
# Mutex serializes with any concurrent change-lite-domain/change-template.
|
||||||
|
async with _BOT_ACTION_LOCK:
|
||||||
|
result = await run_bot_action("change-template", timeout=180, template=tpl_id)
|
||||||
|
|
||||||
if result.get("status") == "success":
|
if result.get("status") == "success":
|
||||||
domain = result.get("domain", config.get("domain", ""))
|
domain = result.get("domain", config.get("domain", ""))
|
||||||
@@ -2209,9 +2289,10 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
|
|||||||
if not ok:
|
if not ok:
|
||||||
await update.message.reply_text(_t(user_id, info), parse_mode="HTML")
|
await update.message.reply_text(_t(user_id, info), parse_mode="HTML")
|
||||||
return
|
return
|
||||||
# Success — record in GoTelegram config
|
# Success — record in GoTelegram config. Use "template_id" (canonical
|
||||||
|
# field name written by install.sh/save_gotelegram_config).
|
||||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||||
config["template"] = tpl_id
|
config["template_id"] = tpl_id
|
||||||
config["template_source"] = url
|
config["template_source"] = url
|
||||||
save_json(GOTELEGRAM_CONFIG, config)
|
save_json(GOTELEGRAM_CONFIG, config)
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
|
|||||||
11
install.sh
11
install.sh
@@ -1168,17 +1168,20 @@ bot_emit_json() {
|
|||||||
printf '{"status":"%s","message":"%s"%s}\n' "$status" "$msg_esc" "$extra"
|
printf '{"status":"%s","message":"%s"%s}\n' "$status" "$msg_esc" "$extra"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Update a single key in config.json without rewriting the whole file
|
# Update a single key in config.json without rewriting the whole file.
|
||||||
|
# Uses `date -Iseconds` rather than jq's `now | todate` — the latter requires
|
||||||
|
# jq 1.6+ which is not available on Debian 10 or older CentOS.
|
||||||
bot_update_config_field() {
|
bot_update_config_field() {
|
||||||
local key="$1"
|
local key="$1"
|
||||||
local value="$2"
|
local value="$2"
|
||||||
if [ ! -f "$GOTELEGRAM_CONFIG" ]; then
|
if [ ! -f "$GOTELEGRAM_CONFIG" ]; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
local tmp
|
local tmp now
|
||||||
tmp=$(mktemp) || return 1
|
tmp=$(mktemp) || return 1
|
||||||
if jq --arg k "$key" --arg v "$value" \
|
now=$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S%z)
|
||||||
'.[$k] = $v | .updated_at = (now | todate)' \
|
if jq --arg k "$key" --arg v "$value" --arg t "$now" \
|
||||||
|
'.[$k] = $v | .updated_at = $t' \
|
||||||
"$GOTELEGRAM_CONFIG" > "$tmp" 2>/dev/null; then
|
"$GOTELEGRAM_CONFIG" > "$tmp" 2>/dev/null; then
|
||||||
mv "$tmp" "$GOTELEGRAM_CONFIG"
|
mv "$tmp" "$GOTELEGRAM_CONFIG"
|
||||||
chmod 600 "$GOTELEGRAM_CONFIG"
|
chmod 600 "$GOTELEGRAM_CONFIG"
|
||||||
|
|||||||
Reference in New Issue
Block a user