§ WèÄj´ºã óJ—UddlZddlZddlZddlZddlZddlZddlZddlmZddl m Z ddl m Z m Z ddlZddlZddlmZddlmZddlmZddlmZdd lmZdd lmZdd lmZdd lm Z m!Z!m"Z"dd l#m$Z$m%Z%ddl&m'Z'ej(e)¦«Z*dZ+e,e-d<dZ.e,e-d<dZ/e,e-d<e!j0Z1e2e-d<dZ3e,e-d<dZ4e,e-d<ddiZ5de2fd„Z6Gd„de j7¦«Z8Gd „d!e j7¦«Z9e:hd"£¦«Z;Gd#„d$e j7¦«Z<Gd%„d&e j=e>¦«Z?d'e2d(e2de?fd)„Z@Gd*„d+¦«ZAd,e2d-e,dzde2fd.„ZBGd/„d0e¦«ZCe1fd1e2dejDfd2„ZEe"jFd3eg¦«e1e+fd1e2d4eGfd5„¦«ZHed'e2d6e fd7„¦«ZIejJGd8„d9¦«¦«ZKdYd'e2d:e2d;e2dzdeKfd<„ZLd=ZMd>ZNejOd?¦«d@fejOdA¦«dBfejOdC¦«dDfejOdE¦«dFffZPdGe2dzde2dzfdH„ZQdIe2dJe2de2dzfdK„ZRdIe2dJe2deSfdL„ZTd'e2dJe2deSfdM„ZUdJe2deSfdN„ZVdYd'e2dJe2d;e2dzdeSfdO„ZWd'e2dJe2deSfdP„ZXd'e2dQe2fdR„ZYd'e2dSe2d1e2d;e2dzd4eGdeKf dT„ZZ dZd'e2dUe2dSe2d;e2dzdVe2dzdeKf dW„Z[ dZd'e2dUe2d;e2dzdVe2dzdeKf dX„Z\dS)[éN)Úcontextmanager)ÚPath)ÚurlsplitÚ urlunsplit)Údrop_privileges)Údocroot)Ú HTTPAdapter)ÚChunkedEncodingError)Ú disable_quota)ÚReadTimeoutError)Úgettext)Ú exceptionsÚ probe_targetsÚutils)Úcanonical_variantsÚis_allowed_ini_path)Úverify_user_owns_domainé ÚTIMEOUTéÚ_CONNECT_TIMEOUTéÚ_SWEEP_BUDGET_SECONDSÚ _PIN_ADDRESSiÚ_MAX_RESPONSE_BYTESi Ú _CHUNK_BYTESz User-AgentzjMozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13) Gecko/20101209 CentOS/3.6-2.el5.centos Firefox/3.6.13Úreturncó@—td¦«t|¦«zS)uÙThe user-facing "website not accessible" text. Shared by every terminal probe failure path so the message the tenant sees — and its translation — is identical regardless of which failure produced it. z™Unable to detect php version for website because it is not accessible. Try again and contact an administrator if the issue persists. Original error: %s. )Ú_Ústr)Úerrors úR/opt/cloudlinux/venv/lib64/python3.11/site-packages/xray/internal/phpinfo_utils.pyÚ_unreachable_messager#Gs+€õ ð ñ ô õ ˆE‰ Œ ñ  ðócó"‡—eZdZdZˆfd„ZˆxZS)ÚWebsiteNotRespondingucNothing usable answered the probe on this connect target and scheme. ``url`` and ``details`` drive the caller's scheme + connect-candidate fallback. But this exception can also ESCAPE get_php_configuration — when every candidate failed on both schemes — and the CLI then renders it with ``print(e)`` (console_utils/base_app.py), i.e. XRayError.__str__, which reads attributes that ONLY XRayError.__init__ sets. So the base MUST be initialised: without it `str(e)` raised AttributeError('type_flag') and the tenant got an unhandled traceback instead of the error message. The message is the same "website not accessible" text every other terminal probe failure uses, so a caller that lets this escape produces exactly the output an unreachable site produced before connect failures were routed through this class. c󀕗||_||_t¦« t |¦«¦«dS©N)ÚurlÚdetailsÚsuperÚ__init__r#)Úselfr)r*Ú __class__s €r"r,zWebsiteNotResponding.__init__fs8ø€ØˆŒØˆŒ Ý ‰Œ×ÒÕ-¨gÑ6Ô6Ñ7Ô7Ð7Ð7Ð7r$)Ú__name__Ú __module__Ú __qualname__Ú__doc__r,Ú __classcell__©r.s@r"r&r&UsBø€€€€€ððð 8ð8ð8ð8ð8ð8ð8ð8ð8r$r&có—eZdZdZdS)ÚWebsiteTooSlowuéA webserver accepted the connection on this address but did not finish responding within the read timeout. Kept distinct from WebsiteNotResponding (nothing answered at all) because the two mean different things in a support log. It is NOT treated as terminal: a timeout says a webserver answered, but says nothing about WHICH vhost did — and on the very topology this module exists for, the loopback connection is accepted by the address bucket's DEFAULT vhost. Aborting the sweep there would reproduce the CLOS-6825 symptom whenever that default vhost is slow, so the sweep advances and `_SWEEP_BUDGET_SECONDS` (not terminality) is what stops a slow site from multiplying the stall across candidates. N©r/r0r1r2©r$r"r6r6ló€€€€€ð ð ð ð r$r6>Ú phpversionÚ include_pathÚ ini_scan_dirÚ php_sapi_namecó—eZdZdZdS)ÚProbeBodyNotUnderstoodu¶The fetch succeeded, but the body is not this probe's output. A 200 does NOT prove the right vhost answered. On a per-IP-vhost host the address bucket's default vhost may carry a front controller or catch-all that serves its own page with status 200 instead of 404 — so the sweep must treat an unparseable body as "this candidate failed" and keep going, or it stops on a wrong-but-successful answer before ever reaching the account's real vhost address (the CLOS-6825 symptom, from the other direction). Raised instead of the bare ValueError/TypeError that `_parse_configuration` used to leak: those are not XRayErrors, so the CLI could not render them. Nr7r8r$r"r?r?€r9r$r?có—eZdZdZdS)ÚIniScanDirRejecteduFThe probe's ini_scan_dir did not pass validation for the privileged write. An XRayError so the CLI can RENDER it. This used to be a bare ValueError, which `console_utils/base_app.py` neither prints nor reports: it catches only XRayError, so a rejected ini_scan_dir surfaced as an unhandled traceback and never reached `capture_exception`. That is what made CLOS-6825 look unfixed after its connect-address fix shipped — the probe succeeded on the vhost's own address, this gate discarded the result, and the newest renderable line left in manager.log was the UNRELATED "failed against 127.0.0.1" INFO from the sweep's first candidate. Support and the customer both read that as the original 404 bug persisting. Deliberately XRayError and NOT XRayManagerError, even though every other error on this path is the latter. `manager/cpanel.get_domain_info` wraps `check_domain` — which is where `absolute_ini_scan_dir` is evaluated — in `except XRayManagerError` and retries the whole thing against `resolve_alias(domain)`. As an XRayManagerError this would be swallowed there: the probe would re-run (a second temp-file write into a docroot plus a second connect sweep) and the surfaced error would be the alias attempt's, not the real cause — reintroducing the exact misdiagnosis this class exists to end. XRayError renders identically (`XRayError.__str__` does the work; base_app catches XRayManagerExit then XRayError) and the only other broad `except XRayError`, in `manager/base.stop`, re-raises anything without the `_internal_missing_domain` marker. Still a ValueError as well: callers and tests that predate this class treat a rejected ini_scan_dir as one, and `manager/base.get_ini_path` catches ValueError around the neighbouring ini properties. Nr7r8r$r"rArA�s€€€€€ððððr$rAÚusernameÚdetailcór—t d||¦«ttd¦«¦«S)ugLog the technical detail, and return an error that does NOT carry it. ``detail`` keeps the exact wording these rejections have always had — a support engineer greping manager.log for "outside allowed paths" or "not bound to user" still finds it, and now finds it as a log line of its own rather than only inside a traceback. It deliberately does NOT reach the returned exception. ``ini_scan_dir`` is attacker-controlled (the probe body is served from the tenant's own docroot, and on a CGI/FastCGI SAPI PHP_INI_SCAN_DIR is reachable from the vhost's environment), and the message is rendered to an unprivileged tenant by the panel plugin. Echoing the resolved path back — and distinguishing "does not resolve to an existing dir" from "outside allowed paths" from "not bound to user" — would make the root daemon a path-existence oracle for arbitrary absolute paths. So every reason yields ONE uniform message and the detail stays in the root-only log. Same reasoning as the missing-vs-foreign domain unification in `user_plugin_utils.user_mode_verification`. z4Rejected probe-reported ini_scan_dir for user %s: %sz™Unable to detect php configuration for website because its PHP configuration directory is not supported. Contact an administrator if the issue persists. )ÚloggerÚwarningrAr)rBrCs r"Ú_reject_ini_scan_dirrG¯s@€õ& ‡N‚NÐIÈ8ÐU[Ñ\Ô\Ð\Ý Ý ð ?ñ ô ñ ô ðr$có"—eZdZdZdZdefd„ZdS)Ú_BoundedResponsezDA drop-in replacement exposing `.text` from a bounded streamed read.©ÚtextrKcó—||_dSr(rJ)r-rKs r"r,z_BoundedResponse.__init__Ñs €ØˆŒ ˆ ˆ r$N)r/r0r1r2Ú __slots__r r,r8r$r"rIrIÌs:€€€€€ØNÐNà€Ið˜Sððððððr$rIÚaddressÚportcó0—d|vrd|›d�n|}|€|n|›d|›�S)zGBuild the netloc for a pinned connect target, bracketing IPv6 literals.ú:ú[ú]r8)rNrOÚhosts r"Ú_pinned_netlocrUÕs7€à  G˜^˜^ˆ>ˆwˆ>ˆ>ˆ>ˆ>°€DØ�<ˆ4ˆ4¨Ð%5Ð%5¨tÐ%5Ð%5Ð5r$có6‡—eZdZdZefdefˆfd„ Zˆfd„ZˆxZS)Ú_LocalAddressPinnedAdapteru$requests adapter that pins every connection to one local address. The vhost is hosted on this machine; we must not follow the tenant-controlled public-DNS record. We rewrite the connect target to ``pin_address`` — loopback, or an address the caller has already verified is configured on this machine — while keeping the original hostname in the Host header so the local webserver routes to the correct vhost. The TLS SNI/cert mismatch that results is tolerated because the caller already requests verify=False. Ú pin_addresscóH•—||_t¦«j|i|¤ŽdSr()rXr+r,)r-rXÚargsÚkwargsr.s €r"r,z#_LocalAddressPinnedAdapter.__init__çs-ø€Ø&ˆÔØ�‰ŒÔ˜$Ð) &Ð)Ð)Ð)Ð)Ð)r$c ó•—t|j¦«}|j}|�O|j|jd<t | t|j|j ¦«¬¦«¦«|_t¦«j |fi|¤ŽS)NÚHost)Únetloc) rr)Úhostnamer^ÚheadersrÚ_replacerUrXrOr+Úsend)r-Úrequestr[ÚsplitÚ original_hostr.s €r"rbz_LocalAddressPinnedAdapter.sendësyø€Ý˜œÑ%Ô%ˆØœˆ Ø Ð $Ø&+¤lˆGŒO˜FÑ #Ý$ U§^¢^½>È$ÔJZÐ\aÔ\fÑ;gÔ;g ^Ñ%hÔ%hÑiÔiˆGŒKØ�u‰wŒwŒ|˜GÐ.Ð. vÐ.Ð.Ð.r$) r/r0r1r2rr r,rbr3r4s@r"rWrWÛsqø€€€€€ð ð ð+7ð*ð* Cð*ð*ð*ð*ð*ð*ð/ð/ð/ð/ð/ð/ð/ð/ð/r$rWrXcó¢—tj¦«}t|¦«}| d|¦«| d|¦«|S)Núhttp://úhttps://)ÚrequestsÚSessionrWÚmount)rXÚsessionÚadapters r"Ú_pinned_sessionrnôsJ€ÝÔÑ Ô €GÝ(¨Ñ5Ô5€GØ ‡M‚M�)˜WÑ%Ô%Ð%Ø ‡M‚M�*˜gÑ&Ô&Ð&Ø €Nr$éÚ read_timeoutc óŠ—|tj¦«vr%tjt d|›d�¦«¦«‚ t |¦«5}| |t|fdtdd¬¦«5}|j sd|j cxkrdkrnnt|d|j ›�¬ ¦«‚|  ¦«t¦«}| t¬ ¦«D]m}|sŒt!|¦«t!|¦«zt"kr+tjt%d ¦«|t"fz¦«‚| |¦«Œn|jpd } | |d ¬¦«}n'#t,$r| d d ¬¦«}YnwxYwddd¦«n #1swxYwYddd¦«n #1swxYwYnê#t.jjt0f$rf} | jr sometimes error happens due to network issues/glitch zrefusing to probe z*: not an address configured on this serverFT)ÚtimeoutÚverifyr`ÚstreamÚallow_redirectsi,i�zprobe returned redirect status )r*)Ú chunk_sizezVphpinfo response for %s exceeds %d bytes; refusing to buffer attacker-controlled body.zutf-8Úreplace)ÚerrorsNr)!rÚlocal_addressesrÚXRayManagerErrorr#rnÚgetrÚHEADERSÚ is_redirectÚ status_coder&Úraise_for_statusÚ bytearrayÚ iter_contentrÚlenrrÚextendÚencodingÚdecodeÚ LookupErrorriÚConnectionErrorrZÚ isinstancer r6r Ú ReadTimeoutÚRequestExceptionrI) r)rXrprlÚresponseÚbufÚchunkr„rKÚes r"Ú _request_urlr�üs�€ð �-Ô7Ñ9Ô9Ð9Ð9ÝÔ)Ý Ð!m°kÐ!mÐ!mÐ!mÑ nÔ nñ ô ð ðNCõ ˜KÑ (Ô (ð+ =Ø,3Ø �KŠKØÝ)¨<Ð8ØÝØØ %ð ñ ô ð+ =ðàÔ#ð r s¨hÔ.BÐ'HÐ'HÒ'HÐ'HÀSÒ'HÐ'HÐ'HÐ'HÐ'Hõ +¨3Ð8pÐZbÔZnÐ8pÐ8pÐqÑqÔqÐqØ × %Ò %Ñ 'Ô 'Ð 'Ý‘+”+ˆCØ!×.Ò.½,Ð.ÑGÔGð "ð "�ØðØÝ�s‘8”8�c %™jœjÑ(Õ+>Ò>Ð>Ý$Ô5ÝÐrÑsÔsØÕ 3Ð4ñ5ñôðð— ’ ˜5Ñ!Ô!Ð!Ð!ð  Ô(Ð3¨GˆHð =Ø—z’z (°9�zÑ=Ô=��øÝð =ð =ð =ð —z’z '°)�zÑ<Ô<���ð  =øøøðK+ =ð+ =ð+ =ñ+ =ô+ =ð+ =ð+ =ð+ =ð+ =ð+ =ð+ =øøøð+ =ð+ =ð+ =ð+ =ð+ =ð+ =ð+ =ñ+ =ô+ =ð+ =ð+ =ð+ =ð+ =ð+ =ð+ =øøøð+ =ð+ =ð+ =ð+ =øøõX Ô Ô /µÐ Að8ð8ð8ð Œ6ð :•j ¤¨¤Õ,<Ñ=Ô=ð :õ!Õ!5°aÑ!8Ô!8Ñ9Ô9Ð 9Ý" 3µ°A±´Ð7Ñ7Ô7Ð7øøøøÝ Ô Ô *ð6ð6ð6õÕ1°!Ñ4Ô4Ñ5Ô5Ð5øøøøÝ Ô $ðCðCðCÝÔ)Õ*>¸qÑ*AÔ*AÑBÔBÐBøøøøðCøøøõ ˜DÑ !Ô !Ð!s�¼G Á (GÁ3C*F)ÅE6Å5F)Å6!FÆF)ÆFÆF)Æ GÆ)F- Æ-GÆ0F- Æ1GÆ4 G ÇGÇG ÇGÇG Ç J3Ç'A!IÉJ3ÉI;É;J3Ê !J.Ê.J3Ú document_rootc#ópK—d}dtjd¦«›d�}||z }t|¦«5t¦«5| |¦«ddd¦«n #1swxYwYddd¦«n #1swxYwY |V—| ¦«dS#| ¦«wxYw)Na¹ —d|jvr d|j›�S|›|j›�S)Nz/opt/altzalt-php)r;r¤)r-r¥s r"Úget_full_php_versionz%PhpConfiguration.get_full_php_versionœs5€Ø ˜Ô*Ð *Ð *Ø5˜TÔ3Ð5Ð5Ð 5Ø Ð: $Ô"8Ð:Ð:Ð:r$cóâ—d|jvrt|j|j¦«}n|j}|rÁ tt |¦« d¬¦«¦«}n-#t tf$rt|jd|›�¦«‚wxYwt|¦«st|jd|›�¦«‚t|j||j ¦«s t|jd|j›d|›�¦«‚|S)Nz link/confT©Ústrictz2ini_scan_dir does not resolve to an existing dir: z$ini_scan_dir outside allowed paths: zini_scan_dir not bound to user z: ) r<Ú_resolve_ini_path_in_cagefsrBr rÚresolveÚOSErrorÚ RuntimeErrorrGrÚ_ini_scan_dir_belongs_to_userrž)r-Úresolveds r"Úabsolute_ini_scan_dirz&PhpConfiguration.absolute_ini_scan_dir¡s$€ð ˜$Ô+Ð +Ð +Ý2°4´=À$ÔBSÑTÔTˆHˆHàÔ(ˆHà ð ð Ý�t H™~œ~×5Ò5¸TÐ5ÑBÔBÑCÔC��øÝ�\Ð*ð ð ð Ý*Ø”MÐ#dÐX`Ð#dÐ#dñôðð øøøõ' xÑ0Ô0ð oÝ*¨4¬=Ð:mÐaiÐ:mÐ:mÑnÔnÐnõ1°´ÀÈ$ÔJbÑcÔcð Ý*Ø”MÐ#dÀTÄ]Ð#dÐ#dÐX`Ð#dÐ#dñôððˆs ¯0A Á *B có—|jdkS)Nzfpm-fcgi)r=r£s r"Ú is_php_fpmzPhpConfiguration.is_php_fpmÈs€àÔ! ZÒ/Ð/r$) r/r0r1r Ú__annotations__ržÚpropertyr¤r§r±r³r8r$r"r�r�€sÜ€€€€€€ð€M€M�Mà€O€O�OàÐÐÑàÐÐÑàÐÐÑð'+И˜t™Ð*Ð*Ñ*à ð7 3ð7ð7ð7ñ„Xð7ð;°3ð;ð;ð;ð;ð ð$ð$ñ„Xð$ðLð0ð0ñ„Xð0ð0ð0r$r�r‹ržc ó®—i}| d¦«D]R}| d¦«\}}}|sŒ| ¦«}|tvr| ¦«||<ŒSt| ¦«z }|r?t t dd t|¦«¦«›�¦«¦«‚td||dœ|¤ŽS)uParse the probe payload, or raise ProbeBodyNotUnderstood. Deliberately lenient per line and strict overall: unusable lines are skipped and only `_REQUIRED_PROBE_KEYS` decides whether this really is our payload. That single gate replaces three ways this used to leak a non-XRayError the CLI could not render — `line.split('=')` raising ValueError on a line with no `=` (a default vhost's HTML) or with two (`include_path=.:/a=b`), and `PhpConfiguration(**config)` raising TypeError on an unexpected key. ú ú=zprobe response is missing z, )rBržr8) rdÚ partitionÚstripÚ_REQUIRED_PROBE_KEYSÚkeysr?r#r¢Úsortedr�) rBr‹ržÚconfigÚlineÚkeyÚ separatorÚvalueÚmissings r"Ú_parse_configurationrÄÍsç€ð€FØ—’˜tÑ$Ô$ð (ð (ˆØ $§¢¨sÑ 3Ô 3шˆY˜ðð Ø Ø�iŠi‰kŒkˆð Õ&Ð &Ð &ØŸ+š+™-œ-ˆF�3‰Køå" V§[¢[¡]¤]Ñ2€GØðvÝ$Õ%9Ð:sÐW[×W`ÒW`ÕagÐhoÑapÔapÑWqÔWqÐ:sÐ:sÑ%tÔ%tÑuÔuÐuå Ð a XÐCVÐ aÐ aÐZ`Ð aÐ aÐar$ú /var/cagefs/)z /etc/users/z/etc/configs/users/z^ea-php(?P\d{2})$z$/opt/cpanel/ea-php{v}/root/etc/php.dz^alt-php(?P\d{2})$z/opt/alt/php{v}/link/confz!^plesk-php(?P\d)(?P\d)$z$/opt/plesk/php/{maj}.{min}/etc/php.dz^php(?P\d{2})$z /usr/local/php{v}/lib/php.conf.dÚtrusted_handlercó²—|sdStD]J\}}| t|¦«¦«}|r!|jdi| ¦«¤ŽcSŒKdS)uÃBuild the single global INI_LOCATION for a TRUSTED, FULL PHP handler. ``trusted_handler`` is the account's REAL provisioned handler carrying its distro family ('ea-php80', 'alt-php80', 'plesk-php80', 'php80'). Returns the ONE server-wide per-distro+version dir that handler's PHP scans, keyed BY the distro family — so the derived dir's family always equals the trusted handler's family, never the family of whatever dir the probe reported. Returns None when ``trusted_handler`` is empty or carries no distro family (a bare number such as custom's '80'): the expected dir cannot be uniquely derived, so the caller default-denies the global per-version accept branch. Nr8)Ú_HANDLER_INI_LOCATION_TEMPLATESÚmatchr ÚformatÚ groupdict)rÆÚpatternÚtemplaterÉs r"Ú_expected_ini_locationrÎ9sx€ð ð؈tÝ<ð8ð8ш�Ø— ’ �c /Ñ2Ô2Ñ3Ô3ˆØ ð 8Ø"�8”?Ð7Ð7 U§_¢_Ñ%6Ô%6Ð7Ð7Ð 7Ð 7Ð 7ð 8à ˆ4r$Úrootr°cóΗt|¦«D]T}| d¦«}||krdS| |dz¦«r|t|¦«dzd…cSŒUdS)aÿ``resolved`` relative to ``root`` ('' for root itself), or None if outside. The trailing-separator boundary stops /var/cagefs/01/aliceEVIL from being treated as inside alice's /var/cagefs/01/alice subtree. ``resolved`` is a realpath, so ``root`` is matched in its canonical spelling as well as the literal one: on a host where /var/cagefs is a symlink (the documented relocation layout) the tenant's dir resolves to /home/cagefs/..., which the literal root would miss (CLOS-7045). ú/r éN)rÚrstripÚ startswithr‚)rÏr°Ú candidates r"Ú_relative_to_rootrÖOs†€õ(¨Ñ-Ô-ð2ð2ˆ Ø×$Ò$ SÑ)Ô)ˆ Ø �yÒ Ð Ø�2�2Ø × Ò ˜y¨3™Ñ /Ô /ð 2Ø�C  ™NœN¨QÑ.Ð0Ð0Ô1Ð 1Ð 1Ð 1ð 2à ˆ4r$có&—t||¦«duS)zETrue if ``resolved`` is ``root`` itself or a path strictly inside it.N)rÖ)rÏr°s r"Ú _is_withinrØcs€å ˜T 8Ñ ,Ô ,°DÐ 8Ð8r$có‚—tD]6}t||¦«}|�"| d¦«d|krdSŒ7dS)u{True if ``resolved`` is inside ``username``'s OWN per-domain ini scan dir. One helper for every gate that has to answer this question — the phpinfo binding gate below, `manager/custom.py._validate_ini_path` and `types.Task._ini_dir_validator`'s write-time inode binding. They each used to inline the same `/etc/users/` containment check, which is precisely how /etc/configs/users ended up supported by none of them: an operator layout has to be added in one place, not three. Compare the first relative path segment to ``username``; never append caller data to a root before canonicalizing it. NrÑrTF)Ú_PER_DOMAIN_INI_ROOTSrÖr¹)rBr°rÏÚrels r"Ú_is_own_per_domain_ini_dirrÜhsO€õ&ððˆÝ  hÑ/Ô/ˆØ ˆ?˜sŸ}š}¨SÑ1Ô1°!Ô4¸Ò@Ð@Ø�4�4øØ ˆ5r$cóD‡—tˆfd„tD¦«¦«S)aHTrue if ``resolved`` is inside ANY per-domain ini scan root. Recognises "this is some tenant's per-domain dir" without saying whose. A path here that is not the owner's own subtree belongs to a FOREIGN tenant and must be rejected outright, never fall through to a root-ownership check that would accept it. c3ó8•K—|]}t|‰¦«V—ŒdSr()rØ)Ú.0rÏr°s €r"ú z0_is_under_per_domain_ini_root..‚s-øèè€ÐLÐL¨d�z˜$ Ñ)Ô)ÐLÐLÐLÐLÐLÐLr$)ÚanyrÚ)r°s`r"Ú_is_under_per_domain_ini_rootrâzs)ø€õ ÐLÐLÐLÐLÕ6KÐLÑLÔLÑ LÔ LÐLr$có—t||¦«rdSt||¦«rdSt|¦«}|�R tt |¦« d¬¦«¦«}n#t tf$rYdSwxYw||krdSdS)u’ Confirm a resolved (realpath'd) ini_scan_dir is one the tenant may drive. ``resolved`` has already been realpath'd and prefix-checked against ALLOWED_INI_PREFIXES. That prefix list is intentionally broad and admits global root-owned dirs, so this is the default-deny binding gate: accept ONLY paths provably tied to the principal, reject everything else. In phpinfo mode the legitimate ini_scan_dir is one of: * the tenant's own CageFS subtree (the link/conf branch is re-rooted there by _resolve_ini_path_in_cagefs(self.username, ...)); * the tenant's own per-domain ini dir, //... for any root in _PER_DOMAIN_INI_ROOTS (cPanel's /etc/users, DirectAdmin's /etc/configs/users); * for a NON-CageFS tenant (ea-php / Plesk / native), the global, root-owned per-distro+version INI_LOCATION for the account's PHP handler — e.g. /opt/cpanel/ea-php80/root/etc/php.d (see xray-ini-placement.md). This is legitimate, so rejecting it outright would regress those tenants. The danger is that ``ini_scan_dir`` AND the probe ``phpversion`` are both served from the tenant's own docroot, so a tenant could report a global dir its account does NOT actually run. A bare two-digit version compare is not enough: it is distro-BLIND, so an ea-php80 tenant could report the same-number alt-php dir /opt/alt/php80/link/conf and steer the root xray.ini write into a DIFFERENT distro family's server-wide, all-tenant-shared dir (cross-distro spoof) — on a mixed alt-php+ea-php server that dir exists, so strict resolve passes. To close that AND the cross-version spoof, the global branch accepts ONLY the EXACT INI_LOCATION derived from ``trusted_php_version`` — here the account's REAL provisioned FULL handler carrying its distro family (cPanel domain['version'] 'ea-php80', Plesk resolve_lsphp_version(handler), DA opts[...]['ver'] 'php80'), NOT the probe-reported version and NOT a distro-blind number — and requires ``resolved`` to equal it. Accepted: * the tenant's OWN CageFS subtree (/var/cagefs///...); * the tenant's OWN per-domain ini dir (//..., any root in _PER_DOMAIN_INI_ROOTS); * the SINGLE INI_LOCATION == _expected_ini_location(trusted_php_version). Rejected (default-deny): global config dirs with no version segment (/etc/php.d, /etc/php.scan.d, /etc/cl.php.d, /usr/share/cagefs[-skeleton] roots), a per-version dir of a DIFFERENT distro family (cross-distro spoof) or a DIFFERENT version (cross-version spoof) than the trusted handler, a trusted handler that carries no distro family (a bare number — expected dir cannot be derived), another tenant's CageFS subtree, another account's per-domain ini dir under another account's // subtree, and any other dir not matched above. TNr©F)Ú_is_own_cagefs_subtreerÜrÎr rr¬r­r®)rBr°ržÚexpecteds r"r¯r¯…s¶€õb˜h¨Ñ1Ô1ð؈tõ" (¨HÑ5Ô5ð؈tõ&Ð&9Ñ:Ô:€HØÐð Ý�4 ™>œ>×1Ò1¸Ð1Ñ>Ô>Ñ?Ô?ˆHˆHøÝ�Ð&ð ð ð ð�5�5ð øøøð �xÒ Ð Ø�4à ˆ5s·0A(Á(A=Á<A=cóF—tt|¦«}|€dS| d¦«} tj|¦«}n#t $rd}YnwxYw|r+t |¦«dko|d|ko |d|kSt |¦«dko |d|kS)aáTrue if ``resolved`` is inside the tenant's OWN /var/cagefs subtree. Prefer the exact /var/cagefs// root when the server can supply the CageFS prefix (cagefsctl). When the prefix is unavailable, fall back to a structural check that still binds to the principal: the path must be /var/cagefs//[/...], i.e. ``username`` must occupy the per-user segment directly under a single shard directory. This never accepts another tenant's subtree (the username segment is pinned and realpath has already been applied) and avoids failing closed on a legitimate own-subtree path when a transient cagefsctl lookup fails. Note such a /var/cagefs path only reaches here when the prefix WAS resolvable in _resolve_ini_path_in_cagefs, so the fallback covers a mid-request race, not a routine "no prefix" case. Both the prefix and username are compared as relative path segments so neither can alter the canonicalized root. NFrÑrrrÒ)rÖÚ _CAGEFS_ROOTrdrÚcagefsctl_get_prefixÚ Exceptionr‚)rBr°rÛÚpartsÚprefixs r"räräÕs¾€õ �L¨(Ñ 3Ô 3€CØ €{؈uØ �IŠI�c‰NŒN€EðÝÔ+¨HÑ5Ô5ˆˆøÝ ðððØˆˆˆðøøøà ðOõ �5‰zŒz˜QŠÐN 5¨¤8¨vÒ#5ÐN¸%À¼(ÀhÒ:NÐNõ ˆu‰:Œ:˜Š?Ð 3˜u Qœx¨8Ò3Ð3s°AÁ AÁAÚpathcó˜— tj|¦«n#t$rYdSwxYwd|d|g} tj|dtj¬¦« ¦«}n#tj$rYdSwxYw| d¦«rhttps answers the http probe with a 3xx (surfaced as WebsiteNotResponding and never followed — F-01), so the probe is re-issued directly over https against the SAME address. Parsing happens HERE, inside the candidate loop, not after it. A 200 is not proof that the account's vhost answered, so a body that is not our payload has to count as this candidate failing — otherwise a default vhost that serves its own page with status 200 ends the sweep before the right address is tried. rgrh)rž)r�r&rÄrK)rBrùrXržrpr‹s r"Ú_probe_one_candidaterûsy€ð&bÝ  Ð,DÑ DÀkÐS_Ñ`Ô`ˆˆøÝ ðbðbðbõ   Ð-EÑ EÀ{ÐT`ÑaÔaˆˆˆðbøøøõ  ¨(¬-ÐM`Ð aÑ aÔ aÐas ‚—8·8ÚdomainÚ panel_addressc ó¦—tj|||¬¦«}t d||¦«t j¦«t z}d}|D]Ñ}|t j¦«z } | dkr#t d|t ¦«n‘ t||||tt| ¦«¬¦«} t d||| j | j ¦«| cS#tj$r+} t d||| ¦«|€| }Yd} ~ ŒÊd} ~ wwxYw|�|‚tjtd t ›d �¦«¦«‚) u†Fetch and parse the probe file, trying each validated connect target in turn. A failure against one address says nothing about the others. The defining symptom of a per-IP-vhost host (CLOS-6825) is that the request DOES reach the local webserver, but on an address whose bucket holds no matching vhost, so that bucket's default vhost answers from the server's own DocumentRoot — with a 404, or with its own page if it has a catch-all. An error status, a refused connection, a read timeout and an unparseable body therefore all advance to the next candidate: none of them tells us which vhost answered. Two things bound the cost of that, since nothing is terminal any more: * `_CONNECT_TIMEOUT` caps the connect phase per attempt — every candidate is on-box, so a slow connect is a firewall DROP, not a slow site; * `_SWEEP_BUDGET_SECONDS` caps the whole sweep's wall-clock, shrinking each attempt's read budget to what is left. The temp probe file lives in the tenant's docroot for this whole window, so the ceiling matters. When no candidate serves the probe the FIRST error is re-raised, not the last: loopback is the canonical attempt, so its error is the one a support ticket can recognise. Surfacing the last one would report, say, "connection refused" against some unrelated interface address on a multi-IP host. )rýz3phpinfo probe for %s will try connect candidates %sNrz@phpinfo probe for %s gave up after %ds; candidates left unprobed)rpzBphpinfo probe for %s succeeded against %s: php %s, ini_scan_dir %sz*phpinfo probe for %s failed against %s: %sz,no connect candidate could be probed within Ús)rÚconnect_candidatesrEÚinfoÚtimeÚ monotonicrrFrûÚminrr:r<rrzr#) rBrürùržrýÚ candidatesÚdeadlineÚ first_errorrXÚ remainingÚ configurationÚ probe_errors r"Ú_probe_candidatesr 4s–€õBÔ1°(¸FÐR_Ð`Ñ`Ô`€Jõ  ‡K‚KÐEÀvÈzÑZÔZÐZÝŒ~ÑÔÕ"7Ñ7€HØ€Kà!ð!ð!ˆ Ø�tœ~Ñ/Ô/Ñ/ˆ Ø ˜Š>ˆ>Ý �NŠNØRØÝ%ñ ô ð ð ˆEð !Ý0ØØ(ØØ#Ý ¥¨)Ñ4Ô4ð ñôˆMõ �KŠKØTØØØÔ(ØÔ*ñ  ô ð ð!Ð Ð Ð øõÔ*ð *ð *ð *Ý �KŠKÐDÀfÈkÐ[fÑ gÔ gÐ gØÐ"Ø)� øøøøøøøøøð *øøøðÐØÐõ Ô %ÝÐdÕLaÐdÐdÐdÑeÔeñ ô ðsÂ'C(Ã(D"Ã7!DÄD"có4—t||¦«tjtjj¦«t |t t|¦«d¦«¦«5}|dz|z}t|||||¬¦«cddd¦«S#1swxYwYdS)ac Writes temporary phpinfo-like file to document root and executes request to website to retrieve the current php version and configuration ``trusted_php_version`` is the account's REAL provisioned PHP version as known to the panel (server-side, not the probe). The caller passes it so a global per-version ini_scan_dir can be version-bound in absolute_ini_scan_dir; None disables that accept branch for the panel. ``panel_address`` is the address the panel bound THIS domain's vhost to, when the caller can resolve it better than `probe_targets` can. DirectAdmin needs this: it records the address in the PARENT domain's conf, and only the manager knows that mapping for a subdomain or an alias. Still validated against this machine's own addresses before use; None falls back to `probe_targets`' own lookup. rrÑ)ržrýN) rÚurllib3Údisable_warningsrÚInsecureRequestWarningr›rrr )rBrüržrýÚ php_info_filerùs r"Úget_php_configurationrŠsã€õ>˜F HÑ-Ô-Ð-õ Ô�WÔ/ÔFÑGÔGÐGå  ­4µ¸±´ÀÔ0BÑ+CÔ+CÑ DÔ Dð  È Ø#)¨C¡<°-Ñ#?Ð õ!Ø Ø Ø $Ø 3Ø'ð  ñ ô ð  ð  ð  ð  ñ  ô  ð  ð  ð  ð  ð  ð  øøøð  ð  ð  ð  ð  ð  sÁ$B  BÂBr()NN)]Ú dataclassesÚloggingrïÚrer”ròrÚ contextlibrÚpathlibrÚ urllib.parserrrir Úclcommon.clpwdrÚclcommon.cpapirÚrequests.adaptersr Úrequests.exceptionsr Úsecureior Úurllib3.exceptionsr Úxrayr rÚ xray.internalrrrÚxray.internal.constantsrrÚxray.internal.user_plugin_utilsrÚ getLoggerr/rErÚintr´rrÚLOOPBACK_ADDRESSrr rrr|r#rzr&r6Ú frozensetr»r?Ú XRayErrorrörArGrIrUrWrjrnÚretry_on_exceptionsÚfloatr�r›Ú dataclassr�rÄrçrÚÚcompilerÈrÎrÖÚboolrØrÜrâr¯rär«rûr rr8r$r"úr,s¾ðð ÐÐÐÐØ€€€Ø € € € Ø € € € Ø€€€ØÐÐÐØ € € € Ø%Ð%Ð%Ð%Ð%Ð%ØÐÐÐÐÐØ-Ð-Ð-Ð-Ð-Ð-Ð-Ð-à€€€Ø€€€Ø*Ð*Ð*Ð*Ð*Ð*Ø"Ð"Ð"Ð"Ð"Ð"Ø)Ð)Ð)Ð)Ð)Ð)Ø4Ð4Ð4Ð4Ð4Ð4Ø"Ð"Ð"Ð"Ð"Ð"Ø/Ð/Ð/Ð/Ð/Ð/àÐÐÐÐÐØ:Ð:Ð:Ð:Ð:Ð:Ð:Ð:Ð:Ð:ØKÐKÐKÐKÐKÐKÐKÐKØCÐCÐCÐCÐCÐCà ˆÔ ˜8Ñ $Ô $€ð€ˆÐÐÑð Ð�#ÐÐÑð  Ð�sÐÐÑð"Ô2€ ˆcÐ2Ð2Ñ2ð 'Ð�SÐ&Ð&Ñ&Ø€ ˆcÐÐÑàð<ð €ð  3ð ð ð ð ð8ð8ð8ð8ð8˜:Ô6ñ8ô8ð8ð. ð ð ð ð �ZÔ0ñ ô ð ð"!�yÐ!`Ð!`Ð!`ÑaÔaÐð ð ð ð ð ˜ZÔ8ñ ô ð ððððð˜Ô-¨zñôðð@ 3ð°ðÐ8Jððððð:ððððñôðð6˜Cð6 s¨T¡zð6°cð6ð6ð6ð6ð /ð/ð/ð/ð/ ñ/ô/ð/ð2(4ðð ð¸Ô8Hððððð€Ô˜1Ð3Ð4Ñ5Ô5Ø)5ÈWðc"ðc" 3ðc"ÀUðc"ðc"ðc"ñ6Ô5ðc"ðLð cð¸$ðððñ„ðð8 ÔðI0ðI0ðI0ðI0ðI0ñI0ôI0ñÔðI0ðXbðb 3ðb°#ðbÈCÐRVÉJðbÐbrðbðbðbðbð@€ ðP?Ðð4€R„ZÐ'Ñ(Ô(Ð*PÐQà€R„ZÐ(Ñ)Ô)Ð+FÐGà€R„ZÐ4Ñ5Ô5Ð7]Ð^à€R„ZÐ$Ñ%Ô%Ð'IÐJð #Ðð¨C°$©Jð¸3À¹:ððððð,˜Cð¨3ð°3¸±:ððððð(9�Sð9 Cð9¨Dð9ð9ð9ð9ð ¨ð¸ðÀððððð$M¨CðM°DðMðMðMðMðMðM¨CðM¸3ðMÐUXÐ[_ÑU_ðMÐkoðMðMðMðMð`!4 Sð!4°Cð!4¸Dð!4ð!4ð!4ð!4ðH¨#ð°Sððððð8bØðbà!ðbððbð˜t™ð bð ð bð ð bðbðbðbðF'+Ø $ð SðSØðSà ðSð"ðSð˜t™ð S𠘑:ð Sð ð SðSðSðSðr'+Ø $ð / ð/ Øð/ à ð/ ð˜t™ð/ ð˜‘:ð / ð ð / ð/ ð/ ð/ ð/ ð/ r$