root.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. """Main bot page"""
  2. # ©️ Dan Gazizullin, 2021-2023
  3. # This file is a part of Hikka Userbot
  4. # 🌐 https://github.com/hikariatama/Hikka
  5. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  6. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  7. import asyncio
  8. import collections
  9. import functools
  10. import logging
  11. import os
  12. import re
  13. import string
  14. import time
  15. import aiohttp_jinja2
  16. import requests
  17. from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
  18. from aiohttp import web
  19. from hikkatl.errors import (
  20. FloodWaitError,
  21. PasswordHashInvalidError,
  22. PhoneCodeExpiredError,
  23. PhoneCodeInvalidError,
  24. SessionPasswordNeededError,
  25. YouBlockedUserError,
  26. )
  27. from hikkatl.password import compute_check
  28. from hikkatl.sessions import MemorySession
  29. from hikkatl.tl.functions.account import GetPasswordRequest
  30. from hikkatl.tl.functions.auth import CheckPasswordRequest
  31. from hikkatl.tl.functions.contacts import UnblockRequest
  32. from hikkatl.utils import parse_phone
  33. from .. import database, main, utils
  34. from .._internal import restart
  35. from ..tl_cache import CustomTelegramClient
  36. from ..version import __version__
  37. DATA_DIR = (
  38. "/data"
  39. if "DOCKER" in os.environ
  40. else os.path.normpath(os.path.join(utils.get_base_dir(), ".."))
  41. )
  42. logger = logging.getLogger(__name__)
  43. class Web:
  44. def __init__(self, **kwargs):
  45. self.sign_in_clients = {}
  46. self._pending_client = None
  47. self._qr_login = None
  48. self._qr_task = None
  49. self._2fa_needed = None
  50. self._sessions = []
  51. self._ratelimit = {}
  52. self.api_token = kwargs.pop("api_token")
  53. self.data_root = kwargs.pop("data_root")
  54. self.connection = kwargs.pop("connection")
  55. self.proxy = kwargs.pop("proxy")
  56. self.app.router.add_get("/", self.root)
  57. self.app.router.add_put("/set_api", self.set_tg_api)
  58. self.app.router.add_post("/send_tg_code", self.send_tg_code)
  59. self.app.router.add_post("/check_session", self.check_session)
  60. self.app.router.add_post("/web_auth", self.web_auth)
  61. self.app.router.add_post("/tg_code", self.tg_code)
  62. self.app.router.add_post("/finish_login", self.finish_login)
  63. self.app.router.add_post("/custom_bot", self.custom_bot)
  64. self.app.router.add_post("/init_qr_login", self.init_qr_login)
  65. self.app.router.add_post("/get_qr_url", self.get_qr_url)
  66. self.app.router.add_post("/qr_2fa", self.qr_2fa)
  67. self.app.router.add_post("/can_add", self.can_add)
  68. self.api_set = asyncio.Event()
  69. self.clients_set = asyncio.Event()
  70. async def schedule_restart():
  71. # Yeah-yeah, ikr, but it's the only way to restart
  72. await asyncio.sleep(1)
  73. restart()
  74. @property
  75. def _platform_emoji(self) -> str:
  76. return {
  77. "vds": "https://github.com/hikariatama/assets/raw/master/waning-crescent-moon_1f318.png",
  78. "lavhost": "https://github.com/hikariatama/assets/raw/master/victory-hand_270c-fe0f.png",
  79. "termux": "https://github.com/hikariatama/assets/raw/master/smiling-face-with-sunglasses_1f60e.png",
  80. "docker": "https://github.com/hikariatama/assets/raw/master/spouting-whale_1f433.png",
  81. }[(
  82. "lavhost"
  83. if "LAVHOST" in os.environ
  84. else (
  85. "termux"
  86. if "com.termux" in os.environ.get("PREFIX", "")
  87. else "docker" if "DOCKER" in os.environ else "vds"
  88. )
  89. )]
  90. @aiohttp_jinja2.template("root.jinja2")
  91. async def root(self, _):
  92. return {
  93. "skip_creds": self.api_token is not None,
  94. "tg_done": bool(self.client_data),
  95. "lavhost": "LAVHOST" in os.environ,
  96. "platform_emoji": self._platform_emoji,
  97. }
  98. async def check_session(self, request: web.Request) -> web.Response:
  99. return web.Response(body=("1" if self._check_session(request) else "0"))
  100. def wait_for_api_token_setup(self):
  101. return self.api_set.wait()
  102. def wait_for_clients_setup(self):
  103. return self.clients_set.wait()
  104. def _check_session(self, request: web.Request) -> bool:
  105. return (
  106. request.cookies.get("session", None) in self._sessions
  107. if main.hikka.clients
  108. else True
  109. )
  110. async def _check_bot(
  111. self,
  112. client: CustomTelegramClient,
  113. username: str,
  114. ) -> bool:
  115. async with client.conversation("@BotFather", exclusive=False) as conv:
  116. try:
  117. m = await conv.send_message("/token")
  118. except YouBlockedUserError:
  119. await client(UnblockRequest(id="@BotFather"))
  120. m = await conv.send_message("/token")
  121. r = await conv.get_response()
  122. await m.delete()
  123. await r.delete()
  124. if not hasattr(r, "reply_markup") or not hasattr(r.reply_markup, "rows"):
  125. return False
  126. for row in r.reply_markup.rows:
  127. for button in row.buttons:
  128. if username != button.text.strip("@"):
  129. continue
  130. m = await conv.send_message("/cancel")
  131. r = await conv.get_response()
  132. await m.delete()
  133. await r.delete()
  134. return True
  135. async def custom_bot(self, request: web.Request) -> web.Response:
  136. if not self._check_session(request):
  137. return web.Response(status=401)
  138. text = await request.text()
  139. client = self._pending_client
  140. db = database.Database(client)
  141. await db.init()
  142. text = text.strip("@")
  143. if any(
  144. litera not in (string.ascii_letters + string.digits + "_")
  145. for litera in text
  146. ) or not text.lower().endswith("bot"):
  147. return web.Response(body="OCCUPIED")
  148. try:
  149. await client.get_entity(f"@{text}")
  150. except ValueError:
  151. pass
  152. else:
  153. if not await self._check_bot(client, text):
  154. return web.Response(body="OCCUPIED")
  155. db.set("hikka.inline", "custom_bot", text)
  156. return web.Response(body="OK")
  157. async def set_tg_api(self, request: web.Request) -> web.Response:
  158. if not self._check_session(request):
  159. return web.Response(status=401, body="Authorization required")
  160. text = await request.text()
  161. if len(text) < 36:
  162. return web.Response(
  163. status=400,
  164. body="API ID and HASH pair has invalid length",
  165. )
  166. api_id = text[32:]
  167. api_hash = text[:32]
  168. if any(c not in string.hexdigits for c in api_hash) or any(
  169. c not in string.digits for c in api_id
  170. ):
  171. return web.Response(
  172. status=400,
  173. body="You specified invalid API ID and/or API HASH",
  174. )
  175. main.save_config_key("api_id", int(api_id))
  176. main.save_config_key("api_hash", api_hash)
  177. self.api_token = collections.namedtuple("api_token", ("ID", "HASH"))(
  178. api_id,
  179. api_hash,
  180. )
  181. self.api_set.set()
  182. return web.Response(body="ok")
  183. async def _qr_login_poll(self):
  184. logged_in = False
  185. self._2fa_needed = False
  186. logger.debug("Waiting for QR login to complete")
  187. while not logged_in:
  188. try:
  189. logged_in = await self._qr_login.wait(10)
  190. except asyncio.TimeoutError:
  191. logger.debug("Recreating QR login")
  192. try:
  193. await self._qr_login.recreate()
  194. except SessionPasswordNeededError:
  195. self._2fa_needed = True
  196. return
  197. except SessionPasswordNeededError:
  198. self._2fa_needed = True
  199. break
  200. logger.debug("QR login completed. 2FA needed: %s", self._2fa_needed)
  201. self._qr_login = True
  202. async def init_qr_login(self, request: web.Request) -> web.Response:
  203. if self.client_data and "LAVHOST" in os.environ:
  204. return web.Response(status=403, body="Forbidden by host EULA")
  205. if not self._check_session(request):
  206. return web.Response(status=401)
  207. if self._pending_client is not None:
  208. self._pending_client = None
  209. self._qr_login = None
  210. if self._qr_task:
  211. self._qr_task.cancel()
  212. self._qr_task = None
  213. self._2fa_needed = False
  214. logger.debug("QR login cancelled, new session created")
  215. client = self._get_client()
  216. self._pending_client = client
  217. await client.connect()
  218. self._qr_login = await client.qr_login()
  219. self._qr_task = asyncio.ensure_future(self._qr_login_poll())
  220. return web.Response(body=self._qr_login.url)
  221. async def get_qr_url(self, request: web.Request) -> web.Response:
  222. if not self._check_session(request):
  223. return web.Response(status=401)
  224. if self._qr_login is True:
  225. if self._2fa_needed:
  226. return web.Response(status=403, body="2FA")
  227. await main.hikka.save_client_session(
  228. self._pending_client, delay_restart=True
  229. )
  230. asyncio.ensure_future(self.schedule_restart())
  231. return web.Response(status=200, body="SUCCESS")
  232. if self._qr_login is None:
  233. await self.init_qr_login(request)
  234. if self._qr_login is None:
  235. return web.Response(
  236. status=500,
  237. body="Internal Server Error: Unable to initialize QR login",
  238. )
  239. return web.Response(status=201, body=self._qr_login.url)
  240. def _get_client(self) -> CustomTelegramClient:
  241. return CustomTelegramClient(
  242. MemorySession(),
  243. self.api_token.ID,
  244. self.api_token.HASH,
  245. connection=self.connection,
  246. proxy=self.proxy,
  247. connection_retries=None,
  248. device_model=main.get_app_name(),
  249. system_version="Windows 10",
  250. app_version=".".join(map(str, __version__)) + " x64",
  251. lang_code="en",
  252. system_lang_code="en-US",
  253. )
  254. async def can_add(self, request: web.Request) -> web.Response:
  255. if self.client_data and "LAVHOST" in os.environ:
  256. return web.Response(status=403, body="Forbidden by host EULA")
  257. return web.Response(status=200, body="Yes")
  258. async def send_tg_code(self, request: web.Request) -> web.Response:
  259. if not self._check_session(request):
  260. return web.Response(status=401, body="Authorization required")
  261. if self.client_data and "LAVHOST" in os.environ:
  262. return web.Response(status=403, body="Forbidden by host EULA")
  263. if self._pending_client:
  264. return web.Response(status=208, body="Already pending")
  265. text = await request.text()
  266. phone = parse_phone(text)
  267. if not phone:
  268. return web.Response(status=400, body="Invalid phone number")
  269. client = self._get_client()
  270. self._pending_client = client
  271. await client.connect()
  272. try:
  273. await client.send_code_request(phone)
  274. except FloodWaitError as e:
  275. return web.Response(status=429, body=self._render_fw_error(e))
  276. return web.Response(body="ok")
  277. @staticmethod
  278. def _render_fw_error(e: FloodWaitError) -> str:
  279. seconds, minutes, hours = (
  280. e.seconds % 3600 % 60,
  281. e.seconds % 3600 // 60,
  282. e.seconds // 3600,
  283. )
  284. seconds, minutes, hours = (
  285. f"{seconds} second(-s)",
  286. f"{minutes} minute(-s) " if minutes else "",
  287. f"{hours} hour(-s) " if hours else "",
  288. )
  289. return (
  290. f"You got FloodWait for {hours}{minutes}{seconds}. Wait the specified"
  291. " amount of time and try again."
  292. )
  293. async def qr_2fa(self, request: web.Request) -> web.Response:
  294. if not self._check_session(request):
  295. return web.Response(status=401)
  296. text = await request.text()
  297. logger.debug("2FA code received for QR login: %s", text)
  298. try:
  299. await self._pending_client._on_login(
  300. (
  301. await self._pending_client(
  302. CheckPasswordRequest(
  303. compute_check(
  304. await self._pending_client(GetPasswordRequest()),
  305. text.strip(),
  306. )
  307. )
  308. )
  309. ).user
  310. )
  311. except PasswordHashInvalidError:
  312. logger.debug("Invalid 2FA code")
  313. return web.Response(
  314. status=403,
  315. body="Invalid 2FA password",
  316. )
  317. except FloodWaitError as e:
  318. logger.debug("FloodWait for 2FA code")
  319. return web.Response(
  320. status=421,
  321. body=(self._render_fw_error(e)),
  322. )
  323. logger.debug("2FA code accepted, logging in")
  324. await main.hikka.save_client_session(self._pending_client, delay_restart=True)
  325. asyncio.ensure_future(self.schedule_restart())
  326. return web.Response()
  327. async def tg_code(self, request: web.Request) -> web.Response:
  328. if not self._check_session(request):
  329. return web.Response(status=401)
  330. text = await request.text()
  331. if len(text) < 6:
  332. return web.Response(status=400)
  333. split = text.split("\n", 2)
  334. if len(split) not in (2, 3):
  335. return web.Response(status=400)
  336. code = split[0]
  337. phone = parse_phone(split[1])
  338. password = split[2]
  339. if (
  340. (len(code) != 5 and not password)
  341. or any(c not in string.digits for c in code)
  342. or not phone
  343. ):
  344. return web.Response(status=400)
  345. if not password:
  346. try:
  347. await self._pending_client.sign_in(phone, code=code)
  348. except SessionPasswordNeededError:
  349. return web.Response(
  350. status=401,
  351. body="2FA Password required",
  352. )
  353. except PhoneCodeExpiredError:
  354. return web.Response(status=404, body="Code expired")
  355. except PhoneCodeInvalidError:
  356. return web.Response(status=403, body="Invalid code")
  357. except FloodWaitError as e:
  358. return web.Response(
  359. status=421,
  360. body=(self._render_fw_error(e)),
  361. )
  362. else:
  363. try:
  364. await self._pending_client.sign_in(phone, password=password)
  365. except PasswordHashInvalidError:
  366. return web.Response(
  367. status=403,
  368. body="Invalid 2FA password",
  369. )
  370. except FloodWaitError as e:
  371. return web.Response(
  372. status=421,
  373. body=(self._render_fw_error(e)),
  374. )
  375. await main.hikka.save_client_session(self._pending_client, delay_restart=True)
  376. asyncio.ensure_future(self.schedule_restart())
  377. return web.Response()
  378. async def finish_login(self, request: web.Request) -> web.Response:
  379. if not self._check_session(request):
  380. return web.Response(status=401)
  381. if not self._pending_client:
  382. return web.Response(status=400)
  383. first_session = not bool(main.hikka.clients)
  384. # Client is ready to pass in to dispatcher
  385. main.hikka.clients = list(set(main.hikka.clients + [self._pending_client]))
  386. self._pending_client = None
  387. self.clients_set.set()
  388. if not first_session:
  389. restart()
  390. return web.Response()
  391. async def web_auth(self, request: web.Request) -> web.Response:
  392. if self._check_session(request):
  393. return web.Response(body=request.cookies.get("session", "unauthorized"))
  394. token = utils.rand(8)
  395. markup = InlineKeyboardMarkup()
  396. markup.add(
  397. InlineKeyboardButton(
  398. "🔓 Authorize user",
  399. callback_data=f"authorize_web_{token}",
  400. )
  401. )
  402. ips = request.headers.get("X-FORWARDED-FOR", None) or request.remote
  403. cities = []
  404. for ip in re.findall(r"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}", ips):
  405. if ip not in self._ratelimit:
  406. self._ratelimit[ip] = []
  407. if (
  408. len(
  409. list(
  410. filter(lambda x: time.time() - x < 3 * 60, self._ratelimit[ip])
  411. )
  412. )
  413. >= 3
  414. ):
  415. return web.Response(status=429)
  416. self._ratelimit[ip] = list(
  417. filter(lambda x: time.time() - x < 3 * 60, self._ratelimit[ip])
  418. )
  419. self._ratelimit[ip] += [time.time()]
  420. try:
  421. res = (
  422. await utils.run_sync(
  423. requests.get,
  424. f"https://freegeoip.app/json/{ip}",
  425. )
  426. ).json()
  427. cities += [
  428. f"<i>{utils.get_lang_flag(res['country_code'])} {res['country_name']} {res['region_name']} {res['city']} {res['zip_code']}</i>"
  429. ]
  430. except Exception:
  431. pass
  432. cities = (
  433. ("<b>🏢 Possible cities:</b>\n\n" + "\n".join(cities) + "\n")
  434. if cities
  435. else ""
  436. )
  437. ops = []
  438. for user in self.client_data.values():
  439. try:
  440. bot = user[0].inline.bot
  441. msg = await bot.send_message(
  442. user[1].tg_id,
  443. (
  444. "🌘🔐 <b>Click button below to confirm web application"
  445. f" ops</b>\n\n<b>Client IP</b>: {ips}\n{cities}\n<i>If you did"
  446. " not request any codes, simply ignore this message</i>"
  447. ),
  448. disable_web_page_preview=True,
  449. reply_markup=markup,
  450. )
  451. ops += [
  452. functools.partial(
  453. bot.delete_message,
  454. chat_id=msg.chat.id,
  455. message_id=msg.message_id,
  456. )
  457. ]
  458. except Exception:
  459. pass
  460. session = f"hikka_{utils.rand(16)}"
  461. if not ops:
  462. # If no auth message was sent, just leave it empty
  463. # probably, request was a bug and user doesn't have
  464. # inline bot or did not authorize any sessions
  465. return web.Response(body=session)
  466. if not await main.hikka.wait_for_web_auth(token):
  467. for op in ops:
  468. await op()
  469. return web.Response(body="TIMEOUT")
  470. for op in ops:
  471. await op()
  472. self._sessions += [session]
  473. return web.Response(body=session)