mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 11:26:03 +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))
|
||||
|
||||
|
||||
# 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:
|
||||
"""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
|
||||
|
||||
|
||||
async def safe_edit_message(query, text: str, reply_markup=None, parse_mode=None) -> bool:
|
||||
"""Safely edit message, handling cases where message was deleted or not modified."""
|
||||
async def safe_edit_message(
|
||||
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:
|
||||
await query.edit_message_text(
|
||||
text, reply_markup=reply_markup, parse_mode=parse_mode
|
||||
)
|
||||
await query.edit_message_text(text, **kwargs)
|
||||
return True
|
||||
except BadRequest as e:
|
||||
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)
|
||||
if config:
|
||||
lines.append(f"<b>{_t(user_id, 'status_mode')}:</b> {html.escape(str(config.get('mode', 'unknown')))}")
|
||||
if "template" in config:
|
||||
lines.append(f"<b>{_t(user_id, 'status_template')}:</b> {html.escape(str(config['template']))}")
|
||||
if "domain" in config:
|
||||
# install.sh/save_gotelegram_config uses "template_id" (not "template")
|
||||
tpl = config.get("template_id") or config.get("template")
|
||||
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']))}")
|
||||
if "port" in config:
|
||||
if config.get("port"):
|
||||
lines.append(f"<b>{_t(user_id, 'status_port')}:</b> {html.escape(str(config['port']))}")
|
||||
|
||||
# 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")
|
||||
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()
|
||||
|
||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||
@@ -898,6 +936,18 @@ async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
return
|
||||
|
||||
# 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 = (
|
||||
"<b>⏳ Меняю маскировочный домен...</b>\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")
|
||||
|
||||
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":
|
||||
text = (
|
||||
@@ -1219,6 +1270,21 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
|
||||
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
|
||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||
current_mode = config.get("mode", "")
|
||||
@@ -1240,6 +1306,18 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
return
|
||||
|
||||
# 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 = (
|
||||
"<b>⏳ Меняю шаблон сайта...</b>\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")
|
||||
|
||||
# Template download + git clone can be slow — generous timeout
|
||||
result = await run_bot_action("change-template", timeout=180, template=tpl_id)
|
||||
# Template download + git clone can be slow — generous timeout.
|
||||
# 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":
|
||||
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:
|
||||
await update.message.reply_text(_t(user_id, info), parse_mode="HTML")
|
||||
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["template"] = tpl_id
|
||||
config["template_id"] = tpl_id
|
||||
config["template_source"] = url
|
||||
save_json(GOTELEGRAM_CONFIG, config)
|
||||
await update.message.reply_text(
|
||||
|
||||
Reference in New Issue
Block a user