PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/imunify360/venv/lib/python3.11/site-packages/clcommon/cpapi/plugins
Viewing File: /opt/imunify360/venv/lib/python3.11/site-packages/clcommon/cpapi/plugins/directadmin.py
# -*- coding: utf-8 -*- """ CloudLinux API for DirectAdmin control panel """ import glob import os import re import subprocess import sys import syslog from traceback import format_exc from typing import Dict, List, Tuple # NOQA from urllib.parse import urlparse import requests from clcommon.clconfpars import ( WebConfigMissing, WebConfigParsingError, apache_conf_parser, load_fast, nginx_conf_parser, read_unicode_file_with_decode_fallback, ) from clcommon.clconfpars import load as loadconfig from clcommon.clpwd import ClPwd from clcommon.cpapi.cpapicustombin import ( _docroot_under_user_via_custom_bin, get_domains_via_custom_binary, ) from clcommon.cpapi.cpapiexceptions import ( CpApiTypeError, NoDBAccessData, NoDomain, NoPanelUser, ParsingError, ReadFileError, ) from clcommon.cpapi.GeneralPanel import ( DomainDescription, GeneralPanelPluginV1, PHPDescription, ) from clcommon.cpapi.plugins.universal import ( get_admin_email as universal_get_admin_email, ) from clcommon.features import Feature from clcommon.utils import ( ExternalProgramFailed, find_module_param_in_config, get_file_lines, grep, ) __cpname__ = 'DirectAdmin' DA_DIR = '/usr/local/directadmin' DA_CONF = os.path.join(DA_DIR, 'conf/directadmin.conf') DA_DATA_DIR = os.path.join(DA_DIR, 'data') DA_DB_CONF = os.path.join(DA_DIR, 'conf/mysql.conf') DA_USERS_PATH = os.path.join(DA_DATA_DIR, 'users') DA_OPT_PATH = os.path.join(DA_DIR, 'custombuild', 'options.conf') USER_CONF = 'user.conf' DOMAINOWNERS = '/etc/virtual/domainowners' ADMIN_DIR = os.path.join(DA_DATA_DIR, 'admin') RESELLERS_LIST = os.path.join(ADMIN_DIR, 'reseller.list') ADMINS_LIST = os.path.join(ADMIN_DIR, 'admin.list') USER_PATTERN = re.compile(rf'.+/(.+)/{re.escape(USER_CONF)}') # WARN: Probably will be deprecated for our "official" plugins. # See pluginlib.detect_panel_fast() def detect(): return os.path.isfile('/usr/local/directadmin/directadmin') or \ os.path.isfile('/usr/local/directadmin/custombuild/build') def db_access(): access = {} try: login_data = loadconfig(DA_DB_CONF) access['login'] = login_data['user'] access['pass'] = login_data['passwd'] except IOError as err: raise NoDBAccessData( 'Can not open file with data to database access; ' + str(err) ) from err except KeyError as err: raise NoDBAccessData( f'Can not get database access data from file {DA_DB_CONF}' ) from err return access def cpusers(): match_list = [USER_PATTERN.match(path) for path in glob.glob(os.path.join(DA_USERS_PATH, '*', USER_CONF))] users_list = [match.group(1) for match in match_list if match] return tuple(users_list) def resellers(): with open(RESELLERS_LIST, encoding='utf-8') as f: resellers_list = [line.strip() for line in f] return tuple(resellers_list) def admins(): with open(ADMINS_LIST, encoding='utf-8') as f: admins_list = [line.strip() for line in f] return set(admins_list) def dblogin_cplogin_pairs(cplogin_lst=None, with_system_users=False): from clcommon.cpapi.plugins.universal import _dblogin_cplogin_pairs # pylint: disable=import-outside-toplevel access = db_access() data = _dblogin_cplogin_pairs(cplogin_lst=cplogin_lst, access=access) if with_system_users: data += tuple(get_da_user(DA_USERS_PATH).items()) return data def get_da_user(path, quiet=True): users = {} cur_dir = os.getcwd() os.chdir(path) dir_list = glob.glob('./*') for user_dir in dir_list: if os.path.isdir(user_dir): file_domains = path + '/' + user_dir + '/domains.list' try: with open(file_domains, encoding='utf-8') as f: if len(f.readline()) > 0: user_name = user_dir[2:] users[user_name] = user_name except IOError: if not quiet: sys.stderr.write("No file " + file_domains) os.chdir(cur_dir) return users def cpinfo(cpuser=None, keyls=('cplogin', 'package', 'mail', 'reseller', 'dns', 'locale'), search_sys_users=True): returned = [] if isinstance(cpuser, str): cpusers_list = [cpuser] elif isinstance(cpuser, (list, tuple)): cpusers_list = tuple(cpuser) elif cpuser is None: cpusers_list = cpusers() else: raise CpApiTypeError(funcname='cpinfo', supportedtypes='str|unicode|list|tuple', received_type=type(cpuser).__name__) def _get_reseller(config): if config.get('usertype') == 'reseller': return config.get('username') return config.get('creator') _user_conf_map = {'cplogin': lambda config: config.get('username'), 'package': lambda config: config.get('package'), 'mail': lambda config: config.get('email'), 'reseller': lambda config: _get_reseller(config), 'dns': lambda config: config.get('domain'), 'locale': lambda config: config.get('language')} keyls_ = [_user_conf_map[key] for key in keyls] for username in cpusers_list: user_conf_file = os.path.join(DA_USERS_PATH, username, USER_CONF) if os.path.exists(user_conf_file): user_config = load_fast(user_conf_file) returned.append([key(user_config) for key in keyls_]) return returned def _docroot_under_root(domain): # type: (str) -> Tuple[str, str] """ Old method for getting doc_root for domain under root Method reads DA config :return: (doc_root, username) cortege """ user_name = None # Load /etc/virtual/domainowners _domain_to_user_map = _load_domains_owners() # Find supposed owner of domain for main_domain in list(_domain_to_user_map.keys()): if domain == main_domain or domain.endswith(f'.{main_domain}'): # Parent domain found user_name = _domain_to_user_map[main_domain] break if user_name is None: domains_list = [] else: domains_list = userdomains(user_name) for d in domains_list: if domain in d: return d[1], user_name def _docroot_under_user_old_mechanism(domain): # type: (str) -> Tuple[str, str] """ Old method for getting doc_root for domain under user Method parses /home/<username>/domains directory :return: (doc_root, username) cortege """ clpwd = ClPwd() user_pw = clpwd.get_pw_by_uid(os.getuid())[0] list_domains_and_doc_roots = _get_domains_list_as_user(user_pw.pw_dir) for domain_data in list_domains_and_doc_roots: if domain_data['server_name'] == domain: return domain_data['document_root'], user_pw.pw_name def docroot(domain): # type: (str) -> Tuple[str, str] """ Retrieves document root for domain :param domain: Domain to determine doc_root :return: Cortege: (doc_root, domain_user) """ res = None domain = domain.strip() uid = os.getuid() euid = os.geteuid() if euid == 0 and uid == 0: res = _docroot_under_root(domain) else: res = _docroot_under_user_via_custom_bin(domain) # If there was successful result, res object will have # (doc_root, domain_user) format. If there wasn't found any correct # doc_roots, res will be None. if res is not None: return res raise NoDomain(f"Can't obtain document root for domain '{domain}'") def _is_nginx_installed(): """ Check if nginx is installed via custombuild; """ config = loadconfig(DA_CONF) return bool(int(config.get('nginx', 0)) or int(config.get('nginx_proxy', 0))) def _get_domains_list_as_root(user_path): """ Get domains list for user from httpd or nginx config as root :param user_path: path to DA directory of user's profile :return: parsed httpd or nginx config :rtype: list """ try: if _is_nginx_installed(): httpd_conf = nginx_conf_parser(os.path.join(user_path, 'nginx.conf')) else: httpd_conf = apache_conf_parser(os.path.join(user_path, 'httpd.conf')) except WebConfigParsingError as e: raise ParsingError(e.message) from e except WebConfigMissing: return [] return httpd_conf def _get_domains_list_as_user(user_home): # type: (str) -> List[Dict[str, str, bool]] """ Get domains list for user from ~/domains directory as user. Method DOESN'T search subdomains, because it's almost impossible detect by user's folders without privileges escalation :param user_home: path to user home :return: list of dictionaries {'server_name': 'domain', 'document_root': 'doc_root', 'ssl': False} """ domains_dir = 'domains' doc_root_dir = 'public_html' domains_list = [] domains_path = os.path.join(user_home, domains_dir) # Searching main domains # All directories of main domains are saved in ~/domains directory for domain_dir in os.listdir(domains_path): domain_path = os.path.join(domains_path, domain_dir) doc_root_path = os.path.join(domains_path, domain_dir, doc_root_dir) if os.path.isdir(domain_path) and os.path.isdir(doc_root_path): domains_list.append({ 'server_name': domain_dir, 'document_root': doc_root_path, 'ssl': False, }) else: continue return domains_list def userdomains(cpuser, as_root=False): # type: (str, bool) -> List[Tuple[str, str]] """ Get user's domains list :return list: domain names Example: [('cltest1.com', '/home/cltest1/domains/cltest1.com/public_html'), ('mk.cltest1.com', '/home/cltest1/domains/cltest1.com/public_html/mk'), ('cltest11.com', '/home/cltest1/domains/cltest11.com/public_html') ] """ domains_list = [] user_path = os.path.join(DA_USERS_PATH, cpuser) euid = os.geteuid() # old method to get list of user's domains main_domain_path = '' if not os.path.exists(user_path): return [] user_home = os.path.expanduser('~' + cpuser) public_path = os.path.join(user_home, 'public_html') if os.path.exists(public_path) and os.path.islink(public_path): main_domain_path = os.path.realpath(public_path) if euid == 0 or as_root: httpd_conf = _get_domains_list_as_root(user_path) for domain in httpd_conf: if domain['ssl'] is True: continue # Put main domain in start of list if domain['server_name'] in main_domain_path: domains_list.insert(0, (domain['server_name'], domain['document_root'])) else: domains_list.append((domain['server_name'], domain['document_root'])) return domains_list # this case works the same as above but through the rights escalation binary wrapper # call path: here -> binary -> python(diradmin euid) -> userdomains(as_root=True) -> print json result to stdout rc, res = get_domains_via_custom_binary() if rc == 0: return res elif rc == 11: raise NoPanelUser(f'User {cpuser} not found in the database') else: raise ExternalProgramFailed(f'Failed to get userdomains: {res}') def homedirs(): """ Detects and returns list of folders contained the home dirs of users of the DirectAdmin :return: list of folders, which are parent of home dirs of users of the panel """ home_dirs = set() clpwd = ClPwd() users_dict = clpwd.get_user_dict() for user_name, pw_user in list(users_dict.items()): conf_file = os.path.join(DA_USERS_PATH, user_name, USER_CONF) if os.path.exists(conf_file): home_dir = os.path.dirname(pw_user.pw_dir) home_dirs.add(home_dir) return list(home_dirs) def domain_owner(domain): """ Return domain's owner :param domain: Domain/sub-domain/add-domain name :return: user name or None if domain not found """ return _load_domains_owners().get(domain, None) @GeneralPanelPluginV1.cache_call(panel_parker=[DOMAINOWNERS]) def _load_domains_owners() -> Dict[str, str]: """ Get domain<->user map from /etc/virtual/domainowners file """ # 1. Load DA data file try: domains_lines = read_unicode_file_with_decode_fallback(DOMAINOWNERS).splitlines() except (OSError, IOError) as e: raise ReadFileError(str(e)) from e # 2. File loaded successfully, parse data and fill dictionaries _domain_to_user_map = {} for line_ in domains_lines: line_ = line_.strip() # pass empty line if not line_: continue domain_, user_ = line_.split(':') domain_ = domain_.strip() user_ = user_.strip() # Fill domain to user map _domain_to_user_map[domain_] = user_ return _domain_to_user_map def reseller_users(resellername): """ Return list of reseller users :param resellername: reseller name; return empty list if None :return list[str]: user names list """ if resellername is None: return [] all_users_dict = ClPwd().get_user_dict() users_list_file = os.path.join(DA_USERS_PATH, resellername, 'users.list') try: with open(users_list_file, encoding='utf-8') as users_list: users_list = [item.strip() for item in users_list] users_list.append(resellername) # performing intersection to avoid returning non-existing users # that are still present in config file for some reason return list(set(all_users_dict) & set(users_list)) except (IOError, OSError): return [] def reseller_domains(resellername=None): """ Get pairs user <=> domain for given reseller; Empty list if cannot get or no users found; :type resellername: str :return list[tuple[str, str]]: tuple[username, main_domain] """ if resellername is None: return [] users = reseller_users(resellername) return dict(cpinfo(users, keyls=('cplogin', 'dns'))) def get_admin_email(): admin_user_file = os.path.join(DA_USERS_PATH, 'admin', USER_CONF) cnf = loadconfig(admin_user_file) return cnf.get('email', universal_get_admin_email()) def is_reseller(username): """ Check if given user is reseller; :type username: str :rtype: bool :raise: ParsingError, ReadFileError """ user_config = os.path.join(DA_USERS_PATH, username, USER_CONF) if os.path.exists(user_config): try: return loadconfig(user_config)['usertype'] == 'reseller' except IndexError as e: raise ParsingError('User config exists, but no usertype given') from e return False def get_user_login_url(domain): return f'http://{domain}:2222' def _get_da_php_config(): """ Return map (PHP_DA_CODE:{PHP_HANDLER, PHP_VERSION}) :return: """ _php_da_map = {} try: php_cfg = loadconfig(DA_OPT_PATH) except (IOError, OSError): return None # iterate through custombuild options.conf php_mode and php_release options i = 1 while f'php{i}_mode' in php_cfg and f'php{i}_release' in php_cfg: _php_da_map[str(i)] = {} _php_da_map[str(i)]['handler_type'] = php_cfg[f'php{i}_mode'] _php_da_map[str(i)]['php_version_id'] = php_cfg[f'php{i}_release'] i += 1 return _php_da_map def _get_php_code_info_for_domain(domain, owner): """ Return php code from domain config :param domain: :param owner: :return: string '1' or '2' - php code in DA """ domain_config_file = os.path.join(DA_USERS_PATH, str(owner), 'domains', str(domain) + '.conf') try: domain_config = loadconfig(domain_config_file) except (IOError, OSError): return '1' domain_php = domain_config.get('php1_select') # None - DA custombuild has only one php version # '0' - it means that user selected default version PHP of DA custombuild if domain_php is None or domain_php == '0': domain_php = '1' return domain_php def _get_subdomains(all_domains, mapped_all_domains): subdomains = [] for domain in all_domains: if domain[0] in mapped_all_domains.keys(): continue subdomains.append(domain[0]) return subdomains def get_domains_php_info(): """ Return php version information for each domain :return: domain to php info mapping Example output: {'cltest.com': {'handler_type': 'mod_php', 'php_version_id': '7.1', 'username': 'cltest'}, 'cltest2.com': {'handler_type': 'fastcgi', 'php_version_id': '7.3', 'username': 'kek_2'}, 'cltest3.com': {'handler_type': 'suphp', 'php_version_id': '5.5', 'username': 'cltest3'}, 'omg.kek': {'handler_type': 'php-fpm', 'php_version_id': '5.2', 'username': 'cltest'}} :rtype: dict[str, dict] """ # returns only main domains map_domain_user = _load_domains_owners() result_map = {} php_da_map = _get_da_php_config() if php_da_map is None: return result_map owner_to_domains: dict[str, list[str]] = {} for domain, owner in map_domain_user.items(): owner_to_domains.setdefault(owner, []).append(domain) for owner, domains in owner_to_domains.items(): all_domains_in_httpd_file = userdomains(owner) # get safely to not break something to other teams try: subdomains = _get_subdomains(all_domains_in_httpd_file, map_domain_user) except Exception: subdomains = [] for domain in domains: php_info_code = _get_php_code_info_for_domain(domain, owner) if php_info_code not in php_da_map \ or php_da_map[php_info_code]['php_version_id'] == 'no': # 'no' means that php_release specified in user's config # does not exist in custombuild options.conf php_info_code = '1' php_info = php_da_map[php_info_code] try: domain_aliases = _useraliases(owner, domain) except Exception: domain_aliases = [] # https://forum.directadmin.com/threads/sub-domain-different-php-version.58426/ # subdomain version should be the same as main domain for domain_entity in [domain] + subdomains + domain_aliases: result_map[domain_entity] = DomainDescription( username=owner, php_version_id=php_info['php_version_id'], handler_type=php_info['handler_type'], display_version=f'php{php_info["php_version_id"].replace(".", "")}' ) return result_map def _get_installed_alt_php_versions(): """ Gets installed alt-phpXY - could be chosen via CloudLinux PHP Selector w/o being compiled via custombuild """ installed_list = [] alt_phps_directory = '/opt/alt/' pattern = re.compile(r'^php\d+$') for item in os.listdir(alt_phps_directory): item_path = os.path.join(alt_phps_directory, item) # Check if the item is a directory and its name matches the pattern if os.path.isdir(item_path) and pattern.match(item) and os.path.exists(f'{item_path}/usr/bin/php'): version = item.replace('php', '') installed_list.append(PHPDescription( identifier=f'alt-{item}', version=f'{version[:1]}.{version[1:]}', dir=f'{item_path}/', modules_dir=os.path.join(item_path, 'usr/lib64/php/modules/'), bin=os.path.join(item_path, 'usr/bin/php'), ini=os.path.join(item_path, 'link/conf/default.ini'), )) return installed_list def _get_da_php_extension_dir(directadmin_php_dir): return subprocess.run( [f'{directadmin_php_dir}bin/php-config', '--extension-dir'], text=True, capture_output=True, check=False, ).stdout def _get_compiled_custombuild_versions(): """ Gets compiled phpXY - could be chosen via DirectAdmin PHP Selector """ php_da_map = _get_da_php_config() if php_da_map is None: return [] # {'1': {'handler_type': 'php-fpm', 'php_version_id': '7.4'}, # '2': {'handler_type': 'php-fpm', 'php_version_id': '8.0'}, # '3': {'handler_type': 'php-fpm', 'php_version_id': 'no'}, # '4': {'handler_type': 'php-fpm', 'php_version_id': 'no'}} installed_php_data = php_da_map.values() installed_list = [] # obtain php version compiled via custombuild: phpXY for version_info in installed_php_data: version = version_info['php_version_id'] if version == 'no': continue directadmin_php_dir = f'/usr/local/php{version.replace(".", "")}/' if not os.path.exists(directadmin_php_dir): continue modules_dir_path = _get_da_php_extension_dir(directadmin_php_dir) if modules_dir_path: modules_dir_path = modules_dir_path.strip() installed_list.append(PHPDescription( identifier=f'php{version.replace(".", "")}', version=version, dir=os.path.join(directadmin_php_dir), modules_dir=modules_dir_path, bin=os.path.join(directadmin_php_dir, 'bin/php'), ini=os.path.join(directadmin_php_dir, 'lib/php.ini'), )) return installed_list def _get_aliases(path): """ Parse user aliases file and return data """ if not os.path.exists(path): return [] data = [] try: with open(path, encoding='utf-8') as f: data = f.readlines() except IOError as e: syslog.syslog(syslog.LOG_WARNING, f'Can`t open file "{path}" due to : "{e}"') return [record.strip().split('=')[0] for record in data] def _useraliases(cpuser, domain): """ Return aliases from user domain :param str|unicode cpuser: user login :param str|unicode domain: :return list of aliases """ path = f'/usr/local/directadmin/data/users/{cpuser}/domains/{domain}.pointers' data = _get_aliases(path) return data class PanelPlugin(GeneralPanelPluginV1): HTTPD_CONFIG_FILE = '/etc/httpd/conf/httpd.conf' HTTPD_MPM_CONFIG = '/etc/httpd/conf/extra/httpd-mpm.conf' HTTPD_INFO_CONFIG = '/etc/httpd/conf/extra/httpd-info.conf' def __init__(self): super().__init__() self.ADMINS_LIST = os.path.join(ADMIN_DIR, 'admin.list') def getCPName(self): """ Return panel name :return: """ return __cpname__ def get_cp_description(self): """ Retrieve panel name and it's version :return: dict: { 'name': 'panel_name', 'version': 'panel_version', 'additional_info': 'add_info'} or None if can't get info """ try: with subprocess.Popen( ['/usr/local/directadmin/directadmin', 'v'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) as p: out, _ = p.communicate() # output may differ (depending on version): # 'Version: DirectAdmin v.1.642' # 'DirectAdmin v.1.643 55acaa256ec6ed99b9aaec1050de793b298f62b0' # 'DirectAdmin 1.644 55acaa256ec6ed99b9aaec1050de793b298f62b0' version_words = (word.lstrip('v.') for word in out.split()) def _is_float(s): return s.replace('.', '').isdigit() version = next(filter(_is_float, version_words), '') return {'name': __cpname__, 'version': version, 'additional_info': None} except Exception: return None def db_access(self): """ Getting root access to mysql database. For example {'login': 'root', 'db': 'mysql', 'host': 'localhost', 'pass': '9pJUv38sAqqW'} :return: root access to mysql database :rtype: dict :raises: NoDBAccessData """ return db_access() def cpusers(self): """ Generates a list of cpusers registered in the control panel :return: list of cpusers registered in the control panel :rtype: tuple """ return cpusers() def resellers(self): """ Generates a list of resellers in the control panel :return: list of cpusers registered in the control panel :rtype: tuple """ return resellers() def is_reseller(self, username): """ Check if given user is reseller; :type username: str :rtype: bool """ return is_reseller(username) # unlike admins(), this method works fine in post_create_user # hook; looks like directadmin updates admins.list a little bit later # then calls post_create_user.sh def is_admin(self, username): """ Return True if username is in admin names :param str username: user to check :return: bool """ user_conf_file = os.path.join(DA_USERS_PATH, username, USER_CONF) if not os.path.exists(user_conf_file): return False user_config = load_fast(user_conf_file) return user_config['usertype'] == 'admin' def dblogin_cplogin_pairs(self, cplogin_lst=None, with_system_users=False): """ Get mapping between system and DB users @param cplogin_lst :list: list with usernames for generate mapping @param with_system_users :bool: add system users to result list or no. default: False """ return dblogin_cplogin_pairs(cplogin_lst, with_system_users) def cpinfo(self, cpuser=None, keyls=('cplogin', 'package', 'mail', 'reseller', 'dns', 'locale'), search_sys_users=True): """ Retrieves info about panel user(s) :param str|unicode|list|tuple|None cpuser: user login :param keyls: list of data which is necessary to obtain the user, the valuescan be: cplogin - name/login user control panel mail - Email users reseller - name reseller/owner users locale - localization of the user account package - User name of the package dns - domain of the user :param bool search_sys_users: search for cpuser in sys_users or in control panel users (e.g. for Plesk) :return: returns a tuple of tuples of data in the same sequence as specified keys in keylst :rtype: tuple """ return cpinfo(cpuser, keyls, search_sys_users=search_sys_users) def get_admin_email(self): """ Retrieve admin email address :return: Host admin's email """ return get_admin_email() def docroot(self, domain): """ Return document root for domain :param str|unicode domain: :return Cortege: (document_root, owner) """ return docroot(domain) @staticmethod def useraliases(cpuser, domain): return _useraliases(cpuser, domain) def userdomains(self, cpuser): """ Return domain and document root pairs for control panel user first domain is main domain :param str|unicode cpuser: user login :return list of tuples (domain_name, documen_root) """ return userdomains(cpuser) def homedirs(self): """ Detects and returns list of folders contained the home dirs of users of the cPanel :return: list of folders, which are parent of home dirs of users of the panel """ return homedirs() def reseller_users(self, resellername=None): """ Return reseller users :param resellername: reseller name; autodetect name if None :return list[str]: user names list """ return reseller_users(resellername) def reseller_domains(self, resellername=None): """ Get dict[user, domain] :param reseller_name: reseller's name :rtype: dict[str, str|None] :raises DomainException: if cannot obtain domains """ return reseller_domains(resellername) def get_user_login_url(self, domain): """ Get login url for current panel; :type domain: str :rtype: str """ return get_user_login_url(domain) def admins(self): """ List all admins names in given control panel :return: list of strings """ return admins() def domain_owner(self, domain): """ Return domain's owner :param domain: Domain/sub-domain/add-domain name :rtype: str :return: user name or None if domain not found """ return domain_owner(domain) def get_domains_php_info(self): """ Return php version information for each domain :return: domain to php info mapping :rtype: dict[str, dict] """ return get_domains_php_info() @staticmethod def _get_da_skin_name(): """ Retrieve current DA skin name :return: Current DA skin name. None if unknown """ config = loadconfig(DA_CONF) # starting from DA 1.664 `docsroot` option was replaced by `system_skin` if 'system_skin' in config: return config['system_skin'] # grep '^docsroot=' /usr/local/directadmin/conf/directadmin.conf | cut -d/ -f4 docsroot = config.get('docsroot', None) # docsroot like './data/skins/evolution' if docsroot is None: return None return docsroot.split('/')[-1] @staticmethod def get_encoding_name(): """ Retrieve encoding name, used for package/reseller names :return: """ enhanced_skin_config = os.path.join(DA_DIR, "data/skins/enhanced/lang/en/lf_standard.html") default_encoding = 'utf8' current_skin = PanelPlugin._get_da_skin_name() if current_skin == 'enhanced': # For enchanced skin we read encoding from its config # :LANG_ENCODING=iso-8859-1 see LU-99 for more info skin_config = loadconfig(enhanced_skin_config) # Option in file is 'LANG_ENCODING', but key is lowercase return skin_config.get('lang_encoding', default_encoding) return default_encoding def get_unsupported_cl_features(self) -> tuple[Feature, ...]: return ( Feature.RUBY_SELECTOR, ) @staticmethod def get_apache_ports_list() -> List[int]: """ Retrieves active httpd's ports from httpd's config :return: list of apache's ports """ # cat /etc/apache2/conf/httpd.conf | grep Listen _httpd_ports_list = [] try: lines = get_file_lines(PanelPlugin.HTTPD_CONFIG_FILE) except (OSError, IOError): return None lines = [line.strip() for line in lines] for line in grep('Listen', match_any_position=False, multiple_search=True, data_from_file=lines): # line examples: # Listen 0.0.0.0:80 # Listen [::]:80 try: value = int(line.split(' ')[1]) if value not in _httpd_ports_list: _httpd_ports_list.append(value) except (IndexError, ValueError): pass if not _httpd_ports_list: _httpd_ports_list.append(80) return _httpd_ports_list @staticmethod def _get_active_web_server_params() -> Tuple[str, str]: """ Determines active web server from options.conf, directive 'webserver' :return: tuple (active_web_server_name, apache_active_module_name) active_web_server_name: 'apache', 'nginx', 'nginx_apache', 'litespeed', 'openlitespeed', etc apache_active_module_name: 'prefork', 'event', 'worker' (None, None) if DA options.conf read/parse error """ web_server_name = None apache_active_module_name = None try: # cat /usr/local/directadmin/custombuild/options.conf | grep webserver # webserver=apache # webserver can be: apache, nginx, nginx_apache, litespeed, openlitespeed. options_lines = get_file_lines(DA_OPT_PATH) grep_result_list = list(grep('^apache_mpm|^webserver', fixed_string=False, match_any_position=False, multiple_search=True, data_from_file=options_lines)) # grep_result_list example: ['webserver=apache\n', 'apache_mpm=auto\n'] for line in grep_result_list: line_parts = line.strip().split('=') if line_parts[0] == 'webserver': web_server_name = line_parts[1] if line_parts[0] == 'apache_mpm': apache_active_module_name = line_parts[1] # modules are 'prefork', 'event', 'worker'. 'auto' == 'worker' if apache_active_module_name == 'auto': apache_active_module_name = 'worker' except (OSError, IOError, IndexError): pass return web_server_name, apache_active_module_name def _get_max_request_workers_for_module(self, apache_module_name: str) -> Tuple[int, str]: """ Determine MaxRequestWorkers directive value for specified apache module. Reads config file /etc/httpd/conf/extra/httpd-mpm.conf :param apache_module_name: Current apache's module name: 'prefork', 'event', 'worker' :return: tuple (max_req_num, message) max_req_num - Maximum request apache workers number or 0 if error message - OK/Error message """ try: return find_module_param_in_config(self.HTTPD_MPM_CONFIG, apache_module_name, 'MaxRequestWorkers') except (OSError, IOError, IndexError, ValueError): return 0, format_exc() def get_apache_max_request_workers(self) -> Tuple[int, str]: """ Get current maximum request apache workers from httpd's config :return: tuple (max_req_num, message) max_req_num - Maximum request apache workers number or 0 if error message - OK/Error message """ web_server_name, apache_active_module_name = self._get_active_web_server_params() if web_server_name is None or apache_active_module_name is None: return 0, f"There was error during read/parse {DA_OPT_PATH}. Apache collector will not work" if web_server_name != "apache": return 0, f"DA is configured for web server '{web_server_name}'; but 'apache' is needed. " \ "Apache collector will not work" return self._get_max_request_workers_for_module(apache_active_module_name) def _get_httpd_status_uri(self) -> str: """ Determine apache mod_status URI from /etc/httpd/conf/extra/httpd-info.conf config :return Apache mod_status URI or None if error/not found """ location_uri = None try: # # grep -B 2 'SetHandler server-status' /etc/httpd/conf/extra/httpd-info.conf # # <Location /server-status> # SetHandler server-status info_lines = get_file_lines(self.HTTPD_INFO_CONFIG) location_directive = '<Location' location_line = None for line in info_lines: line = line.strip() if line.startswith(location_directive): # Location directive found, save it location_line = line continue if line.startswith('SetHandler server-status') and location_line: # server-status found, Extract URI from Location directive start tag location_uri = location_line.replace(location_directive, '').replace('>', '').strip() break except (OSError, IOError): pass return location_uri def get_apache_connections_number(self): """ Retrieves Apache's connections number (from apache's mod_status) :return: tuple (conn_num, message) conn_num - current connections number, 0 if error message - OK/Trace """ web_server_name, _ = self._get_active_web_server_params() if web_server_name is None: return 0, f"There was error during read/parse {DA_OPT_PATH}. Apache collector will not work" if web_server_name != "apache": return 0, f"DA is configured for web server '{web_server_name}'; but 'apache' is needed. " \ "Apache collector will not work" try: # curl localhost/server-status?auto | grep "Total Accesses" # Total Accesses: 25 location_uri = self._get_httpd_status_uri() if location_uri is None: return 0, "Can't found mod_status URI in configs" url = f'http://127.0.0.1{location_uri}?auto' response = requests.get(url, timeout=5) if response.status_code != 200: return 0, f"GET {url} response code is {response.status_code}" s_response = response.content.decode('utf-8') s_response_list = s_response.split('\n') out_list = list(grep("Total Accesses", data_from_file=s_response_list)) # out_list example: ['Total Accesses: 200'] s_total_accesses = out_list[0].split(':')[1].strip() return int(s_total_accesses), 'OK' except Exception: return 0, format_exc() @staticmethod def get_installed_php_versions(): """ Returns installed alt-php(s) on the server compiled phpXY via custombuild and alt-phpXY has different paths also user could choose version via PHP selector which was not compiled with custombuild (will be absent in DA configs) """ return _get_installed_alt_php_versions() + _get_compiled_custombuild_versions() def get_server_ip(self): ip_list_file = '/usr/local/directadmin/data/admin/ip.list' if not os.path.exists(ip_list_file): return '' with open(ip_list_file, encoding='utf-8') as f: ips = f.readlines() if not ips: return '' return ips[0].strip() @staticmethod def get_user_emails_list(username: str, domain: str): user_conf = f'/usr/local/directadmin/data/users/{username}/user.conf' if not os.path.exists(user_conf): return '' user_conf = load_fast(user_conf) return user_conf.get('email', '') @staticmethod def panel_login_link(username): generated_login = subprocess.run(['/usr/local/directadmin/directadmin', '--create-login-url', f'user={username}'], capture_output=True, text=True, check=False).stdout # http://server-206-252-237-2.da.direct:2222/api/login/url?key=0SrJm1CNAIh34w4Fk8Kp4ohypUFp_pMm if len(generated_login) == 0: return '' parsed = urlparse(generated_login) return f'{parsed.scheme}://{parsed.netloc}/' @staticmethod def panel_awp_link(username): link = PanelPlugin.panel_login_link(username).rstrip("/") if len(link) == 0: return '' return f'{link}/evo/user/plugins/awp#/' def suspended_users_list(self): all_users = cpusers() suspended_users = [] for user in all_users: user_conf_file = os.path.join(DA_USERS_PATH, user, USER_CONF) if not os.path.exists(user_conf_file): continue user_config = load_fast(user_conf_file) if user_config.get('suspended') == 'yes': suspended_users.append(user) return suspended_users