晋太元中,武陵人捕鱼为业。缘溪行,忘路之远近。忽逢桃花林,夹岸数百步,中无杂树,芳草鲜美,落英缤纷。渔人甚异之,复前行,欲穷其林。 林尽水源,便得一山,山有小口,仿佛若有光。便舍船,从口入。初极狭,才通人。复行数十步,豁然开朗。土地平旷,屋舍俨然,有良田、美池、桑竹之属。阡陌交通,鸡犬相闻。其中往来种作,男女衣着,悉如外人。黄发垂髫,并怡然自乐。 见渔人,乃大惊,问所从来。具答之。便要还家,设酒杀鸡作食。村中闻有此人,咸来问讯。自云先世避秦时乱,率妻子邑人来此绝境,不复出焉,遂与外人间隔。问今是何世,乃不知有汉,无论魏晋。此人一一为具言所闻,皆叹惋。余人各复延至其家,皆出酒食。停数日,辞去。此中人语云:“不足为外人道也。”(间隔 一作:隔绝) 既出,得其船,便扶向路,处处志之。及郡下,诣太守,说如此。太守即遣人随其往,寻向所志,遂迷,不复得路。 南阳刘子骥,高尚士也,闻之,欣然规往。未果,寻病终。后遂无问津者。
| DIR:/opt/imunify360/venv/lib64/python3.11/site-packages/defence360agent/internals/ |
| Current File : //opt/imunify360/venv/lib64/python3.11/site-packages/defence360agent/internals/iaid.py |
import asyncio
import grp
import json
import os
import random
import time
from contextlib import suppress
from dataclasses import dataclass
from logging import getLogger
from pathlib import Path
from typing import Callable
from urllib.parse import urljoin
from urllib.request import Request
from defence360agent.api.server import API, APIError
from defence360agent.contracts.license import LicenseCLN
from defence360agent.utils import atomic_rewrite
from defence360agent.utils.common import DAY
from defence360agent.internals.global_scope import g
from defence360agent.internals.deadlock_detecting_lock import (
DeadlockDetectingLock,
DeadlockError,
)
logger = getLogger(__name__)
_MAX_TRIES = 10
_TIMEOUT_MULTIPLICATOR = 2
"""
>>> _MAX_TRIES_FOR_DOWNLOAD = 10
>>> _TIMEOUT_MULTIPLICATOR = 2
>>> [(1 << i) * _TIMEOUT_MULTIPLICATOR for i in range(1, _MAX_TRIES_FOR_DOWNLOAD)] # noqa
[4, 8, 16, 32, 64, 128, 256, 512, 1024]
"""
_ACTIVATE_MINIMUM_TIMEOUT = 60
class IAIDTokenError(RuntimeError):
"""Can't get iaid token for any reason."""
class IndependentAgentIDAPI(API):
API_PATH = "/api/auth/agent/{}"
REGISTER_URL = urljoin(API._BASE_URL, API_PATH.format("register"))
ACTIVATE_URL = urljoin(API._BASE_URL, API_PATH.format("activate"))
LOGIN_URL = urljoin(API._BASE_URL, API_PATH.format("login"))
TOKEN_INFO = urljoin(API._BASE_URL, API_PATH.format("token-info"))
IAID_DIR = Path("/var/imunify360")
IAID_FILE = IAID_DIR / "iaid"
IAID_PASSWORD_FILE = IAID_DIR / "iaid-password"
IAID_TOKEN_FILE = IAID_DIR / "iaid-token"
IAID_ACTIVATED_FILE = IAID_DIR / "iaid-activated"
_tasks = {
"register": [],
"activate": [],
"login": [],
}
_register_lock = DeadlockDetectingLock()
_activate_lock = asyncio.Lock()
@dataclass(frozen=True)
class TokenInfo:
__slots__ = [
"valid",
"iaid",
"license_status",
"server_id",
"need_renew",
]
valid: bool
iaid: str
license_status: str # "ok", "ok-av", "ok-avp", "ok-trial"
server_id: str
need_renew: bool
@staticmethod
async def _retry_on_error(coro: Callable, *args, attempt, timeout=0):
# Exponential backoff retry
await asyncio.sleep(
timeout + random.randrange(1 << attempt) * _TIMEOUT_MULTIPLICATOR
)
await coro(*args)
@classmethod
def _add_task(cls, type, coro: Callable, *args, attempt, timeout=0):
cls._tasks[type] = [
task for task in cls._tasks[type] if not task.done()
]
if len(cls._tasks[type]) <= 1:
loop = asyncio.get_event_loop()
cls._tasks[type].append(
loop.create_task(
cls._retry_on_error(
coro, *args, attempt=attempt, timeout=timeout
)
)
)
else:
logger.info("Task %s already in retry queue", type)
@classmethod
def add_initial_task(cls):
cls._add_task("activate", cls.activate, attempt=0)
@classmethod
async def shutdown(cls):
for type, tasks in cls._tasks.items():
for task in tasks:
if not task.done():
task.cancel()
with suppress(asyncio.CancelledError):
await task
logger.info("Retry task %s was canceled.", type)
@staticmethod
def _gid():
return grp.getgrnam("_imunify").gr_gid
@classmethod
def get_iaid(cls):
if cls.IAID_FILE.exists():
return cls.IAID_FILE.read_text()
return None
@staticmethod
def _request(url, headers=None, method="POST", **kwargs):
_headers = {"Content-Type": "application/json"}
if headers is not None:
_headers.update(headers)
return Request(
url,
method=method,
headers=_headers,
data=json.dumps(kwargs).encode() if kwargs else None,
)
@classmethod
def is_registered(cls):
return all(
iaid_file.exists()
for iaid_file in (cls.IAID_FILE, cls.IAID_PASSWORD_FILE)
)
@classmethod
async def get_token(cls):
"""Ensure that iaid token is up to date
Return iaid token or raise IAIDTokenError."""
if IndependentAgentIDAPI.is_token_expired():
await IndependentAgentIDAPI.login()
if IndependentAgentIDAPI.is_token_expired():
raise IAIDTokenError("IAID token is expired")
try:
token = cls.IAID_TOKEN_FILE.read_text(encoding="ascii").strip()
if not token:
raise IAIDTokenError("IAID_TOKEN_FILE is empty")
return token
except Exception as e:
raise IAIDTokenError(f"Can't get iaid token, reason: {e}") from e
@classmethod
async def _get_token_info(cls) -> TokenInfo:
iaid_token = await cls.get_token()
headers = {"X-Auth": iaid_token}
request = cls._request(cls.TOKEN_INFO, headers=headers, method="GET")
result = await cls.async_request(request)
token = result.get("token_info")
if token is None:
raise APIError("wrong response %r", result)
return cls.TokenInfo(**token)
@classmethod
def is_token_expired(cls):
try:
stat = os.stat(cls.IAID_TOKEN_FILE)
except FileNotFoundError:
st_mtime = 0.0
else:
st_mtime = stat.st_mtime
return time.time() - st_mtime > DAY
@classmethod
async def register(cls, force=False, tried_credentials_ts=None, attempt=1):
# In case of Unauthorized 401 for login/activate, iaid initiates force registration.
# Prevent multiple force registrations by checking if the lock was already acquired.
# This approach also works if the lock was already acquired by non-force registration,
# as the non-force registration is currently only triggered if iaid wasn't registered.
was_waiting = cls._register_lock.locked()
try:
async with cls._register_lock:
if cls.is_registered():
if not force or was_waiting:
return
# If credentials were already updated - no need to register again.
# This check makes the above `was_waiting` check obsolete in most cases,
# but some old filesystems have second precision of for a file mtime.
# Both checks are kept to decrease a chance of race conditions.
if (
tried_credentials_ts is not None
and tried_credentials_ts < cls._get_credentials_ts()
):
return
payload = dict()
server_id = LicenseCLN.get_server_id()
if server_id:
payload["server_id"] = server_id
request = cls._request(cls.REGISTER_URL, **payload)
try:
result = await cls.async_request(request)
cls.IAID_ACTIVATED_FILE.unlink(missing_ok=True)
except APIError as e:
logger.warning(
"Something went wrong on register %r - attempt %s",
e,
attempt,
)
if (
e.status_code is None
or e.status_code >= 500
or e.status_code == 402
) and attempt < _MAX_TRIES:
# internal error we may try again
cls._add_task(
"register",
cls.register,
force,
tried_credentials_ts,
attempt + 1,
attempt=attempt,
)
else:
logger.error(
"Failed to register (%s) after %s attempts: %r",
request.full_url,
attempt,
e,
)
return
else:
atomic_rewrite(
str(cls.IAID_FILE),
result["iaid"],
backup=cls.IAID_FILE.exists(),
uid=-1,
gid=cls._gid(),
permissions=0o640,
)
atomic_rewrite(
str(cls.IAID_PASSWORD_FILE),
result["password"],
backup=cls.IAID_PASSWORD_FILE.exists(),
permissions=0o600,
)
await cls.activate()
except DeadlockError:
logger.error(
"Received incorrect credentials on register after %s attempts",
attempt,
)
@classmethod
async def ensure_is_activated_and_valid(cls):
"""Check whether the agent activated"""
if not cls.IAID_ACTIVATED_FILE.exists():
await cls.activate()
return
lic = LicenseCLN.get_token()
token = await cls._get_token_info()
if token.license_status != lic.get(
"status"
) or token.server_id != lic.get("id"):
logger.error("Got a corrupted token: %r", token)
await cls.reactivate()
return
iaid = cls.IAID_FILE.read_text()
if not token.valid or token.iaid != iaid or token.need_renew:
await cls.login()
@classmethod
def _get_credentials_ts(cls):
return cls.IAID_PASSWORD_FILE.stat().st_mtime
@classmethod
async def activate(cls, attempt=1):
if cls.IAID_ACTIVATED_FILE.exists():
g["iaid"] = cls.get_iaid()
return
if not cls.is_registered():
logger.warning("need to register first before activate")
await cls.register()
return
if LicenseCLN.is_free():
g["iaid"] = cls.get_iaid()
if cls.is_token_expired():
await cls.login()
return
lic = LicenseCLN.get_token()
if not lic:
logger.warning(
"Can't continue iaid activation: no valid license is found"
)
return
async with cls._activate_lock:
iaid = cls.IAID_FILE.read_text()
password = cls.IAID_PASSWORD_FILE.read_text()
credentials_ts = cls._get_credentials_ts()
request = cls._request(
cls.ACTIVATE_URL, iaid=iaid, password=password, license=lic
)
g["iaid"] = iaid
need_to_register = False
try:
await cls.async_request(request)
cls.IAID_ACTIVATED_FILE.touch()
except APIError as e:
logger.warning(
"Something went wrong on activate %r attempt %s",
e,
attempt,
)
if e.status_code and e.status_code == 401:
# need to register again, do it outside of lock
need_to_register = True
elif (
e.status_code
and (e.status_code >= 500 or e.status_code == 402)
and attempt < _MAX_TRIES
):
# internal error we may try again
# 402 - if it is fresh registration it may take
# time to sync CLN db
cls._add_task(
"activate",
cls.activate,
attempt + 1,
attempt=attempt,
timeout=_ACTIVATE_MINIMUM_TIMEOUT,
)
else:
logger.error(
"Failed to activate (%s) after %s attempts: %r",
request.full_url,
attempt,
e,
)
else:
cls.IAID_TOKEN_FILE.unlink(missing_ok=True)
await cls.login()
if need_to_register:
await cls.register(force=True, tried_credentials_ts=credentials_ts)
@classmethod
async def reactivate(cls):
cls.IAID_ACTIVATED_FILE.unlink(missing_ok=True)
await cls.activate()
@classmethod
async def login(cls, attempt=1):
if not cls.is_registered():
logger.error("need to register first before login")
return
iaid = cls.IAID_FILE.read_text()
password = cls.IAID_PASSWORD_FILE.read_text()
credentials_ts = cls._get_credentials_ts()
request = cls._request(cls.LOGIN_URL, iaid=iaid, password=password)
try:
result = await cls.async_request(request)
except APIError as e:
logger.warning(
"Something wrong happened on login %r attempt %s", e, attempt
)
if attempt < _MAX_TRIES:
if e.status_code is None or e.status_code >= 500:
# internal error we may try again
cls._add_task(
"login", cls.login, attempt + 1, attempt=attempt
)
elif e.status_code == 401:
await cls.register(
force=True, tried_credentials_ts=credentials_ts
)
else:
logger.error(
"Failed to login (%s) after %s attempts: %r",
request.full_url,
attempt,
e,
)
else:
atomic_rewrite(
str(cls.IAID_TOKEN_FILE),
result["token"],
backup=cls.IAID_TOKEN_FILE.exists(),
uid=-1,
gid=cls._gid(),
permissions=0o640,
)
|