����JFIFXX�����    $.' ",#(7),01444'9=82<.342  2!!22222222222222222222222222222222222222222222222222����"��4�� ���,�PG"Z_�4�˷����kjز�Z�,F+��_z�,�© �����zh6�٨�ic�fu���#ډb���_�N�?��wQ���5-�~�I���8����TK<5o�Iv-�����k�_U_�����~b�M��d����Ӝ�U�Hh��?]��E�w��Q���k�{��_}qFW7HTՑ��Y��F�?_�'ϔ��_�Ջt��=||I ��6�έ"�����D���/[�k�9���Y�8ds|\���Ҿp6�Ҵ���]��.����6�z<�v��@]�i%��$j��~�g��J>��no����pM[me�i$[����s�o�ᘨ�˸ nɜG-�ĨU�ycP�3.DB�li�;��hj���x7Z^�N�h������N3u{�:j�x�힞��#M&��jL P@_���� P��&��o8������9�����@Sz6�t7#O�ߋ �s}Yf�T���lmr����Z)'N��k�۞p����w\�Tȯ?�8`�O��i{wﭹW�[�r�� ��Q4F�׊���3m&L�=��h3����z~��#�\�l :�F,j@�� ʱ�wQT����8�"kJO���6�֚l����}���R�>ډK���]��y����&����p�}b��;N�1�m�r$�|��7�>e�@B�TM*-iH��g�D�)� E�m�|�ؘbҗ�a��Ҿ����t4���o���G��*oCN�rP���Q��@z,|?W[0�����:�n,jWiE��W��$~/�hp\��?��{(�0���+�Y8rΟ�+����>S-S����VN;�}�s?.����� w�9��˟<���Mq4�Wv'��{)0�1mB��V����W[�����8�/<� �%���wT^�5���b��)iM� pg�N�&ݝ��VO~�q���u���9� ����!��J27����$O-���! �:�%H��� ـ����y�ΠM=t{!S�� oK8������t<����è:a������[�����ա�H���~��w��Qz`�po�^ ����Q��n� �,uu�C�$ ^���,������8�#��:�6��e�|~���!�3�3.�\0��q��o�4`.|� ����y�Q�`~;�d�ׯ,��O�Zw�������`73�v�܋�<���Ȏ�� ـ4k��5�K�a�u�=9Yd��$>x�A�&�� j0� ���vF��� Y�|�y��� ~�6�@c��1vOp�Ig����4��l�OD���L����� R���c���j�_�uX6��3?nk��Wy�f;^*B� ��@�~a�`��Eu������+���6�L��.ü>��}y���}_�O�6�͐�:�YrG�X��kG�����l^w���~㒶sy��Iu�!� W ��X��N�7BV��O��!X�2����wvG�R�f�T#�����t�/?���%8�^�W�aT��G�cL�M���I��(J����1~�8�?aT ���]����AS�E��(��*E}� 2��#I/�׍qz��^t�̔���b�Yz4x���t�){ OH��+(E��A&�N�������XT��o��"�XC��'���)}�J�z�p� ��~5�}�^����+�6����w��c��Q�|Lp�d�H��}�(�.|����k��c4^�"�����Z?ȕ ��a<�L�!039C� �Eu�C�F�Ew�ç ;�n?�*o���B�8�bʝ���'#Rqf���M}7����]����s2tcS{�\icTx;�\��7K���P���ʇ Z O-��~��c>"��?�������P��E��O�8��@�8��G��Q�g�a�Վ���󁶠�䧘��_%#r�>�1�z�a��eb��qcPѵ��n���#L��� =��׀t� L�7�`��V���A{�C:�g���e@�w1 Xp3�c3�ġ����p��M"'-�@n4���fG��B3�DJ�8[Jo�ߐ���gK)ƛ��$���� ���8�3�����+���� �����6�ʻ���� ���S�kI�*KZlT _`���?��K����QK�d����B`�s}�>���`��*�>��,*@J�d�oF*����弝��O}�k��s��]��y�ߘ��c1G�V���<=�7��7����6�q�PT��tXԀ�!9*4�4Tހ3XΛex�46���Y��D ����� �BdemDa����\�_l,��G�/���֌7���Y�](�xTt^%�GE�����4�}bT���ڹ�����;Y)���B�Q��u��>J/J �⮶.�XԄ��j�ݳ�+E��d ��r�5�_D�1 ��o�� �B�x�΢�#���<��W�����8���R6�@g�M�.��� dr�D��>(otU��@x=��~v���2� ӣ�d�oBd��3�eO�6�㣷�����ݜ6��6Y��Qz`��S��{���\P�~z m5{J/L��1������<�e�ͅPu�b�]�ϔ���'������f�b� Zpw��c`"��i���BD@:)ִ�:�]��hv�E�w���T�l��P���"Ju�}��وV J��G6��. J/�Qgl߭�e�����@�z�Zev2u�)]կ�����7x���s�M�-<ɯ�c��r�v�����@��$�ޮ}lk���a���'����>x��O\�ZFu>�����ck#��&:��`�$�ai�>2Δ����l���oF[h��lE�ܺ�Πk:)���`�� $[6�����9�����kOw�\|���8}������ބ:��񶐕��I�A1/�=�2[�,�!��.}gN#�u����b��� ~��݊��}34q����d�E��Lc��$��"�[q�U�硬g^��%B �z���r�pJ�ru%v\h1Y�ne`ǥ:g���pQM~�^�Xi� ��`S�:V29.�P���V�?B�k�� AEvw%�_�9C�Q����wKekPؠ�\�;Io d�{ ߞo�c1eP����\� `����E=���@K<�Y���eڼ�J���w����{av�F�'�M�@/J��+9p���|]�����Iw &`��8���&M�hg��[�{��Xj��%��Ӓ�$��(����ʹN���<>�I���RY���K2�NPlL�ɀ)��&e����B+ь����( � �JTx���_?EZ� }@ 6�U���뙢ط�z��dWI�n` D����噥�[��uV��"�G&Ú����2g�}&m��?ċ�"����Om#��������� ��{�ON��"S�X��Ne��ysQ���@Fn��Vg���dX�~nj�]J�<�K]:��FW��b�������62�=��5f����JKw��bf�X�55��~J �%^����:�-�QIE��P��v�nZum� z � ~ə ���� ���ة����;�f��\v���g�8�1��f24;�V���ǔ�)����9���1\��c��v�/'Ƞ�w�������$�4�R-��t���� e�6�/�ġ �̕Ecy�J���u�B���<�W�ַ~�w[B1L۲�-JS΂�{���΃������A��20�c#��@ 0!1@AP"#2Q`$3V�%45a6�FRUq��� ����^7ׅ,$n�������+��F�`��2X'��0vM��p�L=������5��8������u�p~���.�`r�����\���O��,ư�0oS ��_�M�����l���4�kv\JSd���x���SW�<��Ae�IX����������$I���w�:S���y���›R��9�Q[���,�5�;�@]�%���u�@ *ro�lbI �� ��+���%m:�͇ZV�����u�̉����θau<�fc�.����{�4Ա� �Q����*�Sm��8\ujqs]{kN���)qO�y�_*dJ�b�7���yQqI&9�ԌK!�M}�R�;������S�T���1���i[U�ɵz�]��U)V�S6���3$K{�ߊ<�(� E]Զ[ǼENg�����'�\?#)Dkf��J���o��v���'�%ƞ�&K�u�!��b�35LX�Ϸ��63$K�a�;�9>,R��W��3�3� d�JeTYE.Mϧ��-�o�j3+y��y^�c�������VO�9NV\nd�1 ��!͕_)a�v;����թ�M�lWR1��)El��P;��yوÏ�u 3�k�5Pr6<�⒲l�!˞*��u־�n�!�l:����UNW ��%��Chx8vL'��X�@��*��)���̮��ˍ��� ���D-M�+J�U�kvK����+�x8��cY������?�Ԡ��~3mo��|�u@[XeY�C�\Kp�x8�oC�C�&����N�~3-H���� ��MX�s�u<`���~"WL��$8ξ��3���a�)|:@�m�\���^�`�@ҷ)�5p+��6���p�%i)P M���ngc�����#0Aruz���RL+xSS?���ʮ}()#�t��mˇ!��0}}y����<�e� �-ή�Ԩ��X������ MF���ԙ~l L.3���}�V뽺�v�����멬��Nl�)�2����^�Iq��a��M��qG��T�����c3#������3U�Ǎ���}��לS�|qa��ڃ�+���-��2�f����/��bz��ڐ�� �ݼ[2�ç����k�X�2�* �Z�d���J�G����M*9W���s{��w���T��x��y,�in�O�v��]���n����P�$�JB@=4�OTI�n��e�22a\����q�d���%�$��(���:���: /*�K[PR�fr\nڙdN���F�n�$�4�[�� U�zƶ����� �mʋ���,�ao�u 3�z� �x��Kn����\[��VFmbE;�_U��&V�Gg�]L�۪&#n%�$ɯ�dG���D�TI=�%+AB�Ru#��b4�1�»x�cs�YzڙJG��f��Il��d�eF'T� iA��T���uC�$����Y��H?����[!G`}���ͪ� �纤Hv\������j�Ex�K���!���OiƸ�Yj�+u-<���'q����uN�*�r\��+�]���<�wOZ.fp�ێ��,-*)V?j-kÊ#�`�r��dV����(�ݽBk�����G�ƛk�QmUڗe��Z���f}|����8�8��a���i��3'J�����~G_�^���d�8w������ R�`(�~�.��u���l�s+g�bv���W���lGc}��u���afE~1�Ue������Z�0�8�=e�� f@/�jqEKQQ�J��oN��J���W5~M>$6�Lt�;$ʳ{���^��6�{����v6���ķܰg�V�cnn �~z�x�«�,2�u�?cE+Ș�H؎�%�Za�)���X>uW�Tz�Nyo����s���FQƤ��$��*�&�LLXL)�1�" L��eO��ɟ�9=���:t��Z���c��Ž���Y?�ӭV�wv�~,Y��r�ۗ�|�y��GaF�����C�����.�+� ���v1���fήJ�����]�S��T��B��n5sW}y�$��~z�'�c ��8 ��� ,! �p��VN�S��N�N�q��y8z˱�A��4��*��'������2n<�s���^ǧ˭P�Jޮɏ�U�G�L�J�*#��<�V��t7�8����TĜ>��i}K%,���)[��z�21z ?�N�i�n1?T�I�R#��m-�����������������1����lA�`��fT5+��ܐ�c�q՝��ʐ��,���3�f2U�եmab��#ŠdQ�y>\��)�SLY����w#��.���ʑ�f��� ,"+�w�~�N�'�c�O�3F�������N<���)j��&��,-� �љ���֊�_�zS���TǦ����w�>��?�������n��U仆�V���e�����0���$�C�d���rP �m�׈e�Xm�Vu� �L��.�bֹ��� �[Դaզ���*��\y�8�Է:�Ez\�0�Kq�C b��̘��cө���Q��=0Y��s�N��S.���3.���O�o:���#���v7�[#߫ ��5�܎�L���Er4���9n��COWlG�^��0k�%<���ZB���aB_���������'=��{i�v�l�$�uC���mƎҝ{�c㱼�y]���W�i ��ߧc��m�H� m�"�"�����;Y�ߝ�Z�Ǔ�����:S#��|}�y�,/k�Ld� TA�(�AI$+I3��;Y*���Z��}|��ӧO��d�v��..#:n��f>�>���ȶI�TX��� 8��y����"d�R�|�)0���=���n4��6ⲑ�+��r<�O�܂~zh�z����7ܓ�HH�Ga롏���nCo�>������a ���~]���R���̲c?�6(�q�;5%� |�uj�~z8R=X��I�V=�|{v�Gj\gc��q����z�؋%M�ߍ����1y��#��@f^���^�>N�����#x#۹��6�Y~�?�dfPO��{��P�4��V��u1E1J �*|���%���JN��`eWu�zk M6���q t[�� ��g�G���v��WIG��u_ft����5�j�"�Y�:T��ɐ���*�;� e5���4����q$C��2d�}���� _S�L#m�Yp��O�.�C�;��c����Hi#֩%+) �Ӎ��ƲV���SYź��g |���tj��3�8���r|���V��1#;.SQ�A[���S������#���`n�+���$��$I �P\[�@�s��(�ED�z���P��])8�G#��0B��[ى��X�II�q<��9�~[Z멜�Z�⊔IWU&A>�P~�#��dp<�?����7���c��'~���5 ��+$���lx@�M�dm��n<=e�dyX��?{�|Aef ,|n3�<~z�ƃ�uۧ�����P��Y,�ӥQ�*g�#먙R�\���;T��i,��[9Qi歉����c>]9�� ��"�c��P�� �Md?٥��If�ت�u��k��/����F��9�c*9��Ǎ:�ØF���z�n*�@|I�ށ9����N3{'��[�'ͬ�Ҳ4��#}��!�V� Fu��,�,mTIk���v C�7v���B�6k�T9��1�*l� '~��ƞF��lU��'�M ����][ΩũJ_�{�i�I�n��$���L�� j��O�dx�����kza۪��#�E��Cl����x˘�o�����V���ɞ�ljr��)�/,�߬h�L��#��^��L�ф�,íMƁe�̩�NB�L�����iL����q�}��(��q��6IçJ$�W�E$��:������=#����(�K�B����zђ <��K(�N�۫K�w��^O{!����)�H���>x�������lx�?>Պ�+�>�W���,Ly!_�D���Ō�l���Q�!�[ �S����J��1��Ɛ�Y}��b,+�Lo�x�ɓ)����=�y�oh�@�꥟/��I��ѭ=��P�y9��� �ۍYӘ�e+�p�Jnϱ?V\SO%�(�t� ���=?MR�[Ș�����d�/ ��n�l��B�7j� ��!�;ӥ�/�[-���A�>�dN�sLj ��,ɪv��=1c�.SQ�O3�U���ƀ�ܽ�E����������̻��9G�ϷD�7(�}��Ävӌ\�y�_0[w ���<΍>����a_��[0+�L��F.�޺��f�>oN�T����q;���y\��bՃ��y�jH�<|q-eɏ�_?_9+P���Hp$�����[ux�K w�Mw��N�ی'$Y2�=��q���KB��P��~������Yul:�[<����F1�2�O���5=d����]Y�sw:���Ϯ���E��j,_Q��X��z`H1,#II ��d�wr��P˂@�ZJV����y$�\y�{}��^~���[:N����ߌ�U�������O��d�����ؾe��${p>G��3c���Ė�lʌ�� ת��[��`ϱ�-W����dg�I��ig2��� ��}s ��ؤ(%#sS@���~���3�X�nRG�~\jc3�v��ӍL��M[JB�T��s3}��j�Nʖ��W����;7��ç?=X�F=-�=����q�ߚ���#���='�c��7���ڑW�I(O+=:uxq�������������e2�zi+�kuG�R��������0�&e�n���iT^J����~\jy���p'dtG��s����O��3����9* �b#Ɋ�� p������[Bws�T�>d4�ۧs���nv�n���U���_�~,�v����ƜJ1��s�� �QIz��)�(lv8M���U=�;����56��G���s#�K���MP�=��LvyGd��}�VwWBF�'�à �?MH�U�g2�� ����!�p�7Q��j��ڴ����=��j�u��� Jn�A s���uM������e��Ɔ�Ҕ�!)'��8Ϣ�ٔ��ޝ(��Vp���צ֖d=�IC�J�Ǡ{q������kԭ�߸���i��@K����u�|�p=..�*+����x�����z[Aqġ#s2a�Ɗ���RR�)*HRsi�~�a &f��M��P����-K�L@��Z��Xy�'x�{}��Zm+���:�)�) IJ�-i�u���� ���ܒH��'�L(7�y�GӜq���� j��� 6ߌg1�g�o���,kر���tY�?W,���p���e���f�OQS��!K�۟cҒA�|ս�j�>��=⬒��˧L[�� �߿2JaB~R��u�:��Q�] �0H~���]�7��Ƽ�I���(}��cq '�ήET���q�?f�ab���ӥvr� �)o��-Q��_'����ᴎo��K������;��V���o��%���~OK ����*��b�f:���-ťIR��`B�5!RB@���ï�� �u �̯e\�_U�_������� g�ES��3�������QT��a����x����U<~�c?�*�#]�MW,[8O�a�x��]�1bC|踤�P��lw5V%�)�{t�<��d��5���0i�XSU��m:��Z�┵�i�"��1�^B�-��P�hJ��&)O��*�D��c�W��vM��)����}���P��ܗ-q����\mmζZ-l@�}��a��E�6��F�@��&Sg@���ݚ�M����� ȹ 4����#p�\H����dYDo�H���"��\��..R�B�H�z_�/5˘����6��KhJR��P�mƶi�m���3�,#c�co��q�a)*Pt����R�m�k�7x�D�E�\Y�閣_X�<���~�)���c[[�BP����6�Yq���S��0����%_����;��Àv�~�| VS؇ ��'O0��F0��\���U�-�d@�����7�SJ*z��3n��y��P����O���������m�~�P�3|Y��ʉr#�C�<�G~�.,! ���bqx���h~0=��!ǫ�jy����l�O,�[B��~��|9��ٱ����Xly�#�i�B��g%�S��������tˋ���e���ې��\[d�t)��.+u�|1 ������#�~Oj����hS�%��i.�~X���I�H�m��0n���c�1uE�q��cF�RF�o���7� �O�ꮧ� ���ۛ{��ʛi5�rw?׌#Qn�TW��~?y$��m\�\o����%W� ?=>S�N@�� �Ʈ���R����N�)�r"C�:��:����� �����#��qb��Y�. �6[��2K����2u�Ǧ�HYR��Q�MV��� �G�$��Q+.>�����nNH��q�^��� ����q��mM��V��D�+�-�#*�U�̒ ���p욳��u:�������IB���m���PV@O���r[b= �� ��1U�E��_Nm�yKbN�O���U�}�the�`�|6֮P>�\2�P�V���I�D�i�P�O;�9�r�mAHG�W�S]��J*�_�G��+kP�2����Ka�Z���H�'K�x�W�MZ%�O�YD�Rc+o��?�q��Ghm��d�S�oh�\�D�|:W������UA�Qc yT�q������~^�H��/��#p�CZ���T�I�1�ӏT����4��"�ČZ�����}��`w�#�*,ʹ�� ��0�i��課�Om�*�da��^gJ݅{���l�e9uF#T�ֲ��̲�ٞC"�q���ߍ ոޑ�o#�XZTp����@ o�8��(jd��xw�]�,f���`~�|,s��^����f�1���t��|��m�򸄭/ctr��5s��7�9Q�4�H1꠲BB@l9@���C�����+�wp�xu�£Yc�9��?`@#�o�mH�s2��)�=��2�.�l����jg�9$�Y�S�%*L������R�Y������7Z���,*=�䷘$�������arm�o�ϰ���UW.|�r�uf����IGw�t����Zwo��~5 ��YյhO+=8fF�)�W�7�L9lM�̘·Y���֘YLf�큹�pRF���99.A �"wz��=E\Z���'a� 2��Ǚ�#;�'}�G���*��l��^"q��+2FQ� hj��kŦ��${���ޮ-�T�٭cf�|�3#~�RJ����t��$b�(R��(����r���dx� >U b�&9,>���%E\� Ά�e�$��'�q't��*�א���ެ�b��-|d���SB�O�O��$�R+�H�)�܎�K��1m`;�J�2�Y~9��O�g8=vqD`K[�F)k�[���1m޼c��n���]s�k�z$@��)!I �x՝"v��9=�ZA=`Ɠi �:�E��)`7��vI��}d�YI�_ �o�:ob���o ���3Q��&D&�2=�� �Ά��;>�h����y.*ⅥS������Ӭ�+q&����j|UƧ����}���J0��WW< ۋS�)jQR�j���Ư��rN)�Gű�4Ѷ(�S)Ǣ�8��i��W52���No˓� ۍ%�5brOn�L�;�n��\G����=�^U�dI���8$�&���h��'���+�(������cȁ߫k�l��S^���cƗjԌE�ꭔ��gF���Ȓ��@���}O���*;e�v�WV���YJ\�]X'5��ղ�k�F��b 6R�o՜m��i N�i����>J����?��lPm�U��}>_Z&�KK��q�r��I�D�Չ~�q�3fL�:S�e>���E���-G���{L�6p�e,8��������QI��h��a�Xa��U�A'���ʂ���s�+טIjP�-��y�8ۈZ?J$��W�P� ��R�s�]��|�l(�ԓ��sƊi��o(��S0��Y� 8�T97.�����WiL��c�~�dxc�E|�2!�X�K�Ƙਫ਼�$((�6�~|d9u+�qd�^3�89��Y�6L�.I�����?���iI�q���9�)O/뚅����O���X��X�V��ZF[�یgQ�L��K1���RҖr@v�#��X�l��F���Нy�S�8�7�kF!A��sM���^rkp�jP�DyS$N���q��nxҍ!U�f�!eh�i�2�m���`�Y�I�9r�6� �TF���C}/�y�^���Η���5d�'��9A-��J��>{�_l+�`��A���[�'��յ�ϛ#w:݅�%��X�}�&�PSt�Q�"�-��\縵�/����$Ɨh�Xb�*�y��BS����;W�ջ_mc�����vt?2}1�;qS�d�d~u:2k5�2�R�~�z+|HE!)�Ǟl��7`��0�<�,�2*���Hl-��x�^����'_TV�gZA�'j� ^�2Ϊ��N7t�����?w�� �x1��f��Iz�C-Ȗ��K�^q�;���-W�DvT�7��8�Z�������� hK�(P:��Q- �8�n�Z���܃e貾�<�1�YT<�,�����"�6{/ �?�͟��|1�:�#g��W�>$����d��J��d�B��=��jf[��%rE^��il:��B���x���Sּ�1հ��,�=��*�7 fcG��#q� �eh?��2�7�����,�!7x��6�n�LC�4x��},Geǝ�tC.��vS �F�43��zz\��;QYC,6����~;RYS/6���|2���5���v��T��i����������mlv��������&� �nRh^ejR�LG�f���? �ۉҬܦƩ��|��Ȱ����>3����!v��i�ʯ�>�v��オ�X3e���_1z�Kȗ\<������!�8���V��]��?b�k41�Re��T�q��mz��TiOʦ�Z��Xq���L������q"+���2ۨ��8}�&N7XU7Ap�d�X��~�׿��&4e�o�F��� �H����O���č�c�� 懴�6���͉��+)��v;j��ݷ�� �UV�� i��� j���Y9GdÒJ1��詞�����V?h��l����l�cGs�ځ�������y�Ac�����\V3�? �� ܙg�>qH�S,�E�W�[�㺨�uch�⍸�O�}���a��>�q�6�n6����N6�q������N ! 1AQaq�0@����"2BRb�#Pr���3C`��Scst���$4D���%Td�� ?���N����a��3��m���C���w��������xA�m�q�m���m������$����4n淿t'��C"w��zU=D�\R+w�p+Y�T�&�պ@��ƃ��3ޯ?�Aﶂ��aŘ���@-�����Q�=���9D��ռ�ѻ@��M�V��P��܅�G5�f�Y<�u=,EC)�<�Fy'�"�&�չ�X~f��l�KԆV��?�� �W�N����=(� �;���{�r����ٌ�Y���h{�١������jW����P���Tc�����X�K�r��}���w�R��%��?���E��m�� �Y�q|����\lEE4���r���}�lsI�Y������f�$�=�d�yO����p�����yBj8jU�o�/�S��?�U��*������ˍ�0������u�q�m [�?f����a�� )Q�>����6#������� ?����0UQ����,IX���(6ڵ[�DI�MNލ�c&���υ�j\��X�R|,4��� j������T�hA�e��^���d���b<����n�� �즇�=!���3�^�`j�h�ȓr��jẕ�c�,ٞX����-����a�ﶔ���#�$��]w�O��Ӫ�1y%��L�Y<�wg#�ǝ�̗`�x�xa�t�w��»1���o7o5��>�m뭛C���Uƃߜ}�C���y1Xνm�F8�jI���]����H���ۺиE@I�i;r�8ӭ����V�F�Շ| ��&?�3|x�B�MuS�Ge�=Ӕ�#BE5G�����Y!z��_e��q�р/W>|-�Ci߇�t�1ޯќd�R3�u��g�=0 5��[?�#͏��q�cf���H��{ ?u�=?�?ǯ���}Z��z���hmΔ�BFTW�����<�q�(v� ��!��z���iW]*�J�V�z��gX֧A�q�&��/w���u�gYӘa���; �i=����g:��?2�dž6�ى�k�4�>�Pxs����}������G�9��3 ���)gG�R<>r h�$��'nc�h�P��Bj��J�ҧH� -��N1���N��?��~��}-q!=��_2hc�M��l�vY%UE�@|�v����M2�.Y[|y�"Eï��K�ZF,�ɯ?,q�?v�M 80jx�"�;�9vk�����+ ֧�� �ȺU��?�%�vcV��mA�6��Qg^M����A}�3�nl� QRN�l8�kkn�'�����(��M�7m9و�q���%ޟ���*h$Zk"��$�9��: �?U8�Sl��,,|ɒ��xH(ѷ����Gn�/Q�4�P��G�%��Ա8�N��!� �&�7�;���eKM7�4��9R/%����l�c>�x;������>��C�:�����t��h?aKX�bhe�ᜋ^�$�Iհ �hr7%F$�E��Fd���t��5���+�(M6�t����Ü�UU|zW�=a�Ts�Tg������dqP�Q����b'�m���1{|Y����X�N��b �P~��F^F:����k6�"�j!�� �I�r�`��1&�-$�Bevk:y���#yw��I0��x��=D�4��tU���P�ZH��ڠ底taP��6����b>�xa����Q�#� WeF��ŮNj�p�J* mQ�N����*I�-*�ȩ�F�g�3 �5��V�ʊ�ɮ�a��5F���O@{���NX��?����H�]3��1�Ri_u��������ѕ�� ����0��� F��~��:60�p�͈�S��qX#a�5>���`�o&+�<2�D����: �������ڝ�$�nP���*)�N�|y�Ej�F�5ټ�e���ihy�Z �>���k�bH�a�v��h�-#���!�Po=@k̆IEN��@��}Ll?j�O������߭�ʞ���Q|A07x���wt!xf���I2?Z��<ץ�T���cU�j��]��陎Ltl �}5�ϓ��$�,��O�mˊ�;�@O��jE��j(�ا,��LX���LO���Ц�90�O �.����a��nA���7������j4 ��W��_ٓ���zW�jcB������y՗+EM�)d���N�g6�y1_x��p�$Lv:��9�"z��p���ʙ$��^��JԼ*�ϭ����o���=x�Lj�6�J��u82�A�H�3$�ٕ@�=Vv�]�'�qEz�;I˼��)��=��ɯ���x �/�W(V���p�����$ �m�������u�����񶤑Oqˎ�T����r��㠚x�sr�GC��byp�G��1ߠ�w e�8�$⿄����/�M{*}��W�]˷.�CK\�ުx���/$�WPw���r� |i���&�}�{�X� �>��$-��l���?-z���g����lΆ���(F���h�vS*���b���߲ڡn,|)mrH[���a�3�ר�[1��3o_�U�3�TC�$��(�=�)0�kgP���� ��u�^=��4 �WYCҸ:��vQ�ר�X�à��tk�m,�t*��^�,�}D*� �"(�I��9R����>`�`��[~Q]�#af��i6l��8���6�:,s�s�N6�j"�A4���IuQ��6E,�GnH��zS�HO�uk�5$�I�4��ؤ�Q9�@��C����wp�BGv[]�u�Ov���0I4���\��y�����Q�Ѹ��~>Z��8�T��a��q�ޣ;z��a���/��S��I:�ܫ_�|������>=Z����8:�S��U�I�J��"IY���8%b8���H��:�QO�6�;7�I�S��J��ҌAά3��>c���E+&jf$eC+�z�;��V����� �r���ʺ������my�e���aQ�f&��6�ND��.:��NT�vm�<- u���ǝ\MvZY�N�NT��-A�>jr!S��n�O 1�3�Ns�%�3D@���`������ܟ 1�^c<���� �a�ɽ�̲�Xë#�w�|y�cW�=�9I*H8�p�^(4���՗�k��arOcW�tO�\�ƍR��8����'�K���I�Q�����?5�>[�}��yU�ײ -h��=��% q�ThG�2�)���"ו3]�!kB��*p�FDl�A���,�eEi�H�f�Ps�����5�H:�Փ~�H�0Dت�D�I����h�F3�������c��2���E��9�H��5�zԑ�ʚ�i�X�=:m�xg�hd(�v����׊�9iS��O��d@0ڽ���:�p�5�h-��t�&���X�q�ӕ,��ie�|���7A�2���O%P��E��htj��Y1��w�Ѓ!����  ���� ࢽ��My�7�\�a�@�ţ�J �4�Ȼ�F�@o�̒?4�wx��)��]�P��~�����u�����5�����7X ��9��^ܩ�U;Iꭆ 5 �������eK2�7(�{|��Y׎ �V��\"���Z�1� Z�����}��(�Ǝ"�1S���_�vE30>���p;� ΝD��%x�W�?W?v����o�^V�i�d��r[��/&>�~`�9Wh��y�;���R��� ;;ɮT��?����r$�g1�K����A��C��c��K��l:�'��3 c�ﳯ*"t8�~l��)���m��+U,z��`(�>yJ�?����h>��]��v��ЍG*�{`��;y]��I�T� ;c��NU�fo¾h���/$���|NS���1�S�"�H��V���T���4��uhǜ�]�v;���5�͠x��'C\�SBpl���h}�N����� A�Bx���%��ޭ�l��/����T��w�ʽ]D�=����K���ž�r㻠l4�S�O?=�k �M:� ��c�C�a�#ha���)�ѐxc�s���gP�iG��{+���x���Q���I= �� z��ԫ+ �8"�k�ñ�j=|����c ��y��CF��/��*9ж�h{ �?4�o� ��k�m�Q�N�x��;�Y��4膚�a�w?�6�>e]�����Q�r�:����g�,i"�����ԩA�*M�<�G��b�if��l^M��5� �Ҩ�{����6J��ZJ�����P�*�����Y���ݛu�_4�9�I8�7���������,^ToR���m4�H��?�N�S�ѕw��/S��甍�@�9H�S�T��t�ƻ���ʒU��*{Xs�@����f�����֒Li�K{H�w^���������Ϥm�tq���s� ���ք��f:��o~s��g�r��ט� �S�ѱC�e]�x���a��) ���(b-$(�j>�7q�B?ӕ�F��hV25r[7 Y� }L�R��}����*sg+��x�r�2�U=�*'WS��ZDW]�WǞ�<��叓���{�$�9Ou4��y�90-�1�'*D`�c�^o?(�9��u���ݐ��'PI&� f�Jݮ�������:wS����jfP1F:X �H�9dԯ���˝[�_54 �}*;@�ܨ�� ð�yn�T���?�ןd�#���4rG�ͨ��H�1�|-#���Mr�S3��G�3�����)�.᧏3v�z֑��r����$G"�`j �1t��x0<Ɔ�Wh6�y�6��,œ�Ga��gA����y��b��)��h�D��ß�_�m��ü �gG;��e�v��ݝ�nQ� ��C����-�*��o���y�a��M��I�>�<���]obD��"�:���G�A��-\%LT�8���c�)��+y76���o�Q�#*{�(F�⽕�y����=���rW�\p���۩�c���A���^e6��K������ʐ�cVf5$�'->���ՉN"���F�"�UQ@�f��Gb~��#�&�M=��8�ט�JNu9��D��[̤�s�o�~������ G��9T�tW^g5y$b��Y'��س�Ǵ�=��U-2 #�MC�t(�i� �lj�@Q 5�̣i�*�O����s�x�K�f��}\��M{E�V�{�υ��Ƈ�����);�H����I��fe�Lȣr�2��>��W�I�Ȃ6������i��k�� �5�YOxȺ����>��Y�f5'��|��H+��98pj�n�.O�y�������jY��~��i�w'������l�;�s�2��Y��:'lg�ꥴ)o#'Sa�a�K��Z� �m��}�`169�n���"���x��I ��*+� }F<��cГ���F�P�������ֹ*�PqX�x۩��,� ��N�� �4<-����%����:��7����W���u�`����� $�?�I��&����o��o��`v�>��P��"��l���4��5'�Z�gE���8���?��[�X�7(��.Q�-��*���ތL@̲����v��.5���[��=�t\+�CNܛ��,g�SQnH����}*F�G16���&:�t��4ُ"A��̣��$�b �|����#rs��a�����T�� ]�<�j��BS�('$�ɻ� �wP;�/�n��?�ݜ��x�F��yUn�~mL*-�������Xf�wd^�a�}��f�,=t�׵i�.2/wpN�Ep8�OР���•��R�FJ� 55TZ��T �ɭ�<��]��/�0�r�@�f��V��V����Nz�G��^���7hZi����k��3�,kN�e|�vg�1{9]_i��X5y7� 8e]�U����'�-2,���e"����]ot�I��Y_��n�(JҼ��1�O ]bXc���Nu�No��pS���Q_���_�?i�~�x h5d'�(qw52] ��'ޤ�q��o1�R!���`ywy�A4u���h<קy���\[~�4�\ X�Wt/� 6�����n�F�a8��f���z �3$�t(���q��q�x��^�XWeN'p<-v�!�{�(>ӽDP7��ո0�y)�e$ٕv�Ih'Q�EA�m*�H��RI��=:��� ���4牢) �%_iN�ݧ�l]� �Nt���G��H�L��� ɱ�g<���1V�,�J~�ٹ�"K��Q�� 9�HS�9�?@��k����r�;we݁�]I�!{ �@�G�[�"��`���J:�n]�{�cA�E����V��ʆ���#��U9�6����j�#Y�m\��q�e4h�B�7��C�������d<�?J����1g:ٳ���=Y���D�p�ц� ׈ǔ��1�]26؜oS�'��9�V�FVu�P�h�9�xc�oq�X��p�o�5��Ա5$�9W�V(�[Ak�aY錎qf;�'�[�|���b�6�Ck��)��#a#a˙��8���=äh�4��2��C��4tm^ �n'c���]GQ$[Wҿ��i���vN�{Fu ��1�gx��1┷���N�m��{j-,��x�� Ūm�ЧS�[�s���Gna���䑴�� x�p 8<������97�Q���ϴ�v�aϚG��Rt�Һ׈�f^\r��WH�JU�7Z���y)�vg=����n��4�_)y��D'y�6�]�c�5̪�\� �PF�k����&�c;��cq�$~T�7j ���nç]�<�g ":�to�t}�159�<�/�8������m�b�K#g'I'.W�����6��I/��>v��\�MN��g���m�A�yQL�4u�Lj�j9��#44�t��l^�}L����n��R��!��t��±]��r��h6ٍ>�yҏ�N��fU�� ���� Fm@�8}�/u��jb9������he:A�y�ծw��GpΧh�5����l}�3p468��)U��d��c����;Us/�֔�YX�1�O2��uq�s��`hwg�r~�{ R��mhN��؎*q 42�*th��>�#���E����#��Hv�O����q�}�����6�e��\�,Wk�#���X��b>��p}�դ��3���T5��†��6��[��@�P�y*n��|'f�֧>�lư΂�̺����SU�'*�q�p�_S�����M�� '��c�6�����m�� ySʨ;M��r���Ƌ�m�Kxo,���Gm�P��A�G�:��i��w�9�}M(�^�V��$ǒ�ѽ�9���|���� �a����J�SQ�a���r�B;����}���ٻ֢�2�%U���c�#�g���N�a�ݕ�'�v�[�OY'��3L�3�;,p�]@�S��{ls��X�'���c�jw�k'a�.��}�}&�� �dP�*�bK=ɍ!����;3n�gΊU�ߴmt�'*{,=SzfD� A��ko~�G�aoq�_mi}#�m�������P�Xhύ����mxǍ�΂���巿zf��Q���c���|kc�����?���W��Y�$���_Lv����l߶��c���`?����l�j�ݲˏ!V��6����U�Ђ(A���4y)H���p�Z_�x��>���e��R��$�/�`^'3qˏ�-&Q�=?��CFVR �D�fV�9��{�8g�������n�h�(P"��6�[�D���< E�����~0<@�`�G�6����Hг�cc�� �c�K.5��D��d�B���`?�XQ��2��ٿyqo&+�1^� DW�0�ꊩ���G�#��Q�nL3��c���������/��x ��1�1[y�x�პCW��C�c�UĨ80�m�e�4.{�m��u���I=��f�����0QRls9���f���������9���~f�����Ǩ��a�"@�8���ȁ�Q����#c�ic������G��$���G���r/$W�(��W���V�"��m�7�[m�A�m����bo��D� j����۳� l���^�k�h׽����� ��#� iXn�v��eT�k�a�^Y�4�BN��ĕ��0 !01@Q"2AaPq3BR������?���@4�Q�����T3,���㺠�W�[=JK�Ϟ���2�r^7��vc�:�9 �E�ߴ�w�S#d���Ix��u��:��Hp��9E!�� V 2;73|F��9Y���*ʬ�F��D����u&���y؟��^EA��A��(ɩ���^��GV:ݜDy�`��Jr29ܾ�㝉��[���E;Fzx��YG��U�e�Y�C���� ����v-tx����I�sם�Ę�q��Eb�+P\ :>�i�C'�;�����k|z�رn�y]�#ǿb��Q��������w�����(�r|ӹs��[�D��2v-%��@;�8<a���[\o[ϧw��I!��*0�krs)�[�J9^��ʜ��p1)� "��/_>��o��<1����A�E�y^�C��`�x1'ܣn�p��s`l���fQ��):�l����b>�Me�jH^?�kl3(�z:���1ŠK&?Q�~�{�ٺ�h�y���/�[��V�|6��}�KbX����mn[-��7�5q�94�������dm���c^���h� X��5��<�eޘ>G���-�}�دB�ޟ� ��|�rt�M��V+�]�c?�-#ڛ��^ǂ}���Lkr���O��u�>�-D�ry� D?:ޞ�U��ǜ�7�V��?瓮�"�#���r��չģVR;�n���/_� ؉v�ݶe5d�b9��/O��009�G���5n�W����JpA�*�r9�>�1��.[t���s�F���nQ� V 77R�]�ɫ8����_0<՜�IF�u(v��4��F�k�3��E)��N:��yڮe��P�`�1}�$WS��J�SQ�N�j�ٺ��޵�#l���ј(�5=��5�lǏmoW�v-�1����v,W�mn��߀$x�<����v�j(����c]��@#��1������Ǔ���o'��u+����;G�#�޸��v-lη��/(`i⣍Pm^���ԯ̾9Z��F��������n��1��� ��]�[��)�'������:�֪�W��FC����� �B9،!?���]��V��A�Վ�M��b�w��G F>_DȬ0¤�#�QR�[V��kz���m�w�"��9ZG�7'[��=�Q����j8R?�zf�\a�=��O�U����*oB�A�|G���2�54 �p��.w7� �� ��&������ξxGHp� B%��$g�����t�Џ򤵍z���HN�u�Я�-�'4��0��;_��3 !01"@AQa2Pq#3BR������?��ʩca��en��^��8���<�u#��m*08r��y�N"�<�Ѳ0��@\�p��� �����Kv�D��J8�Fҽ� �f�Y��-m�ybX�NP����}�!*8t(�OqѢ��Q�wW�K��ZD��Δ^e��!� ��B�K��p~�����e*l}z#9ң�k���q#�Ft�o��S�R����-�w�!�S���Ӥß|M�l޶V��!eˈ�8Y���c�ЮM2��tk���� ������J�fS����Ö*i/2�����n]�k�\���|4yX�8��U�P.���Ы[���l��@"�t�<������5�lF���vU�����W��W��;�b�cД^6[#7@vU�xgZv��F�6��Q,K�v��� �+Ъ��n��Ǣ��Ft���8��0��c�@�!�Zq s�v�t�;#](B��-�nῃ~���3g������5�J�%���O������n�kB�ĺ�.r��+���#�N$?�q�/�s�6��p��a����a��J/��M�8��6�ܰ"�*������ɗud"\w���aT(����[��F��U՛����RT�b���n�*��6���O��SJ�.�ij<�v�MT��R\c��5l�sZB>F��<7�;EA��{��E���Ö��1U/�#��d1�a�n.1ě����0�ʾR�h��|�R��Ao�3�m3 ��%�� ���28Q� ��y��φ���H�To�7�lW>����#i`�q���c����a��� �m,B�-j����݋�'mR1Ήt�>��V��p���s�0IbI�C.���1R�ea�����]H�6����������4B>��o��](��$B���m�����a�!=��?�B� K�Ǿ+�Ծ"�n���K��*��+��[T#�{E�J�S����Q�����s�5�:�U�\wĐ�f�3����܆&�)����I���Ԇw��E T�lrTf6Q|R�h:��[K�� �z��c֧�G�C��%\��_�a�84��HcO�bi��ؖV��7H �)*ģK~Xhչ0��4?�0��� �E<���}3���#���u�?�� ��|g�S�6ꊤ�|�I#Hڛ� �ա��w�X��9��7���Ŀ%�SL��y6č��|�F�a 8���b��$�sק�h���b9RAu7�˨p�Č�_\*w��묦��F ����4D~�f����|(�"m���NK��i�S�>�$d7SlA��/�²����SL��|6N�}���S�˯���g��]6��; �#�.��<���q'Q�1|KQ$�����񛩶"�$r�b:���N8�w@��8$�� �AjfG|~�9F ���Y��ʺ��Bwؒ������M:I岎�G��`s�YV5����6��A �b:�W���G�q%l�����F��H���7�������Fsv7��k�� 403WebShell
403Webshell
Server IP : 213.165.242.4  /  Your IP : 216.73.216.128
Web Server : Apache
System : Linux amsngx344.inmotionhosting.com 4.18.0-553.40.1.lve.el8.x86_64 #1 SMP Wed Feb 12 18:54:57 UTC 2025 x86_64
User : aquafi9 ( 1305)
PHP Version : 7.4.33
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : ON  |  Pkexec : ON
Directory :  /opt/maint/bin/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /opt/maint/bin/audit_resellers.py
#!/opt/maint/venv/bin/python3
"""
Reseller audit script.  Does the following:
1) Makes sure that all resellers are owned by 'inmotion' or 'hubhost'
2) Resets reseller ACL limits and IP pools
3) Checks for orphaned accounts (accounts that have a non-existent owner)
"""
from collections import defaultdict
import configparser
import argparse
import logging
import platform
import shutil
import sys
import time
import pwd
from pathlib import Path
import yaml
import rads
from cpapis import whmapi1, CpAPIError


HOST = platform.node().split('.')[0]
RESELLER = 'hubhost' if rads.IMH_CLASS == 'hub' else 'inmotion'

PKGLIMIT_MAP = {
    'pkglimit_level_one': '1-Site cPanel',
    'pkglimit_level_two': '2-Site cPanel',
    'pkglimit_level_three': '5-Site cPanel',
    'pkglimit_level_four': '10-Site cPanel',
    'pkglimit_level_five': '15-Site cPanel',
}


def parse_args() -> tuple[int, bool]:
    """Parse sys.argv"""
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        '--loglevel',
        '-l',
        default='INFO',
        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
    )
    parser.add_argument(
        '--noop',
        '--dry-run',
        '-n',
        dest='noop',
        action='store_true',
        help="Make no changes",
    )
    args = parser.parse_args()
    loglevel = getattr(logging, args.loglevel)
    return loglevel, args.noop


def get_dips() -> dict[str, set[str]]:
    """Get a mapping of ipaddr -> resellers from /var/cpanel/dips"""
    dips = defaultdict(set)
    try:
        for res_path in Path('/var/cpanel/dips').iterdir():
            try:
                res_ips = set(res_path.read_text('ascii').split())
            except OSError:
                continue
            for ipaddr in res_ips:
                dips[ipaddr].add(res_path.name)
    except FileNotFoundError:
        pass
    return dict(dips)


def check_double_ip_delegations(resellers: set[str], noop: bool):
    """Check for IPs which are assigned to more than one reseller"""
    double_delegations = {
        ipaddr: resellers
        for ipaddr, resellers in get_dips().items()
        if len(resellers) > 1
    }
    if double_delegations:
        auto_fix_double_dips(resellers, double_delegations, noop)
    if not double_delegations:
        return
    logging.warning("Double-delegated IP addresses detected - sending ticket")
    logging.debug('double delegations: %r', double_delegations)
    if noop:
        return
    body = (
        "The following IP addresses were detected as being delegated to "
        "more than one reseller and must be corrected:\n"
    )
    for ip_addr, res in double_delegations.items():
        body = f"{body}\n{ip_addr}: {', '.join(res)}"
    try:
        rads.make_ticket(
            dest="str@imhadmin.net",
            subject="Reseller IP delegation conflict",
            body=body,
        )
    except rads.TicketError as exc:
        logging.error("Could not make ticket - %s", exc)


def auto_fix_double_dips(
    resellers: set[str], double_delegations: dict[str, set[str]], noop: bool
):
    """Attempt to automatically fix IP double-delegations by checking if the IP
    is actually in use, and removing it from resellers which aren't using it"""
    user_ips: dict[str, str] = yaml.load(
        Path('/etc/userips').read_text('ascii'), rads.DumbYamlLoader
    )
    user_resellers: dict[str, str] = yaml.load(
        Path('/etc/trueuserowners').read_text('ascii'), rads.DumbYamlLoader
    )
    user_resellers = {
        k: k if k in resellers else v for k, v in user_resellers.items()
    }
    for ipaddr, res in double_delegations.copy().items():
        if res.intersection(rads.OUR_RESELLERS):
            # if there's a conflict involving one of our resellers, don't try
            # to auto-fix it
            continue
        # collect resellers actually using the IP
        using = list(
            {user_resellers[k] for k, v in user_ips.items() if v == ipaddr}
        )
        if len(using) > 1:
            continue  # legit conflict. don't auto-fix
        if len(using) == 0:
            # No one is using this IP. Take it away from all but one reseller.
            # If this takes away any reseller's last IP, the next run of this
            # cron should fix it.
            for remove in list(res)[1:]:
                remove_dip(ipaddr, remove, double_delegations, noop)
        elif using[0] in res:
            # else one reseller is using it but it's delegated to multiple
            for remove in list(res):
                if remove != using[0]:
                    remove_dip(ipaddr, remove, double_delegations, noop)


def remove_dip(
    ipaddr: str,
    reseller: str,
    double_delegations: dict[str, set[str]],
    noop: bool,
) -> None:
    """Remove an IP from a reseller's pool to fix a double delegation"""
    # make sure it wasn't their main. the calling function already checked that
    # the reseller didn't have it assigned
    main_ip = Path('/var/cpanel/mainips', reseller).read_text('ascii').strip()
    if main_ip == ipaddr:
        return
    logging.warning("removing %s from %s's IP pool", ipaddr, reseller)
    pool = whmapi1.getresellerips(reseller)['ip']
    try:
        pool.remove(ipaddr)
    except ValueError:
        # but the previous lookup had it?
        logging.error("Could not remove %s from %s's IP pool", ipaddr, reseller)
        return
    if not noop:
        try:
            whmapi1.setresellerips(reseller, pool, delegate=True)
        except CpAPIError as exc:
            logging.error(
                "Could not remove %s from %s's IP pool: %s",
                ipaddr,
                reseller,
                exc,
            )
            return
    double_delegations[ipaddr].remove(reseller)
    if len(double_delegations[ipaddr]) < 2:
        double_delegations.pop(ipaddr)


class CpanelConf(configparser.ConfigParser):
    """Handles reading /var/cpanel/users and /var/cpanel/packages files"""

    def __init__(self, path: Path):
        super().__init__(allow_no_value=True, interpolation=None, strict=False)
        try:
            self.read_string(f"[config]\n{path.read_text('utf-8')}")
        except Exception as exc:
            logging.error('%s - %s: %s', path, type(exc).__name__, exc)
            raise

    @classmethod
    def user_conf(cls, user: str):
        """Read /var/cpanel/users/{user}"""
        try:
            return cls(Path('/var/cpanel/users', user))
        except Exception as exc:
            logging.warning("%s user conf: %s", user, exc)
            raise

    @classmethod
    def pkg_conf(cls, pkg: str):
        """Read /var/cpanel/packages/{pkg}"""
        return cls(Path('/var/cpanel/packages', pkg))

    @property
    def res_limits(self) -> dict[str, str]:
        """Read imh custom reseller limits from a cPanel package
        (use only with pkg_conf)"""
        imh_keys = (
            'account_limit',
            'bandwidth_limit',
            'diskspace_limit',
            'enable_account_limit',
            'enable_resource_limits',
            'enable_package_limit_numbers',
            'enable_overselling',
            'enable_overselling_bandwidth',
            'enable_overselling_diskspace',
        )
        return {
            x: self.get('config', f'imh_{x}', fallback='') for x in imh_keys
        }

    @property
    def pkg_limits(self) -> dict[str, str]:
        """Read imh custom reseller package limits from a cPanel package
        (use only with pkg_conf)"""

        return {
            val: self.get('config', f'imh_{key}', fallback='')
            for key, val in PKGLIMIT_MAP.items()
        }


def get_main_ips() -> set[str]:
    """Collect IPs from /var/cpanel/mainip and /var/cpanel/mainips/root"""
    with open('/var/cpanel/mainip', encoding='ascii') as ip_file:
        ips = set(ip_file.read().split())
    try:
        with open('/var/cpanel/mainips/root', encoding='ascii') as ip_file:
            ips.update(ip_file.read().split())
    except FileNotFoundError:
        pass
    return ips


def get_new_ip() -> str:
    """Get an IP which is not already in use"""
    with open('/etc/ipaddrpool', encoding='ascii') as pool:
        # not assigned as dedicated, but may be in a reseller pool
        unassigned = pool.read().split()
    for ip_addr in unassigned:
        if not assigned_to_res(ip_addr):
            return ip_addr
    return ''


def assigned_to_res(ip_addr):
    """Determine if an IP is already delegated to a reseller"""
    for entry in Path('/var/cpanel/dips').iterdir():
        with entry.open('r', encoding='ascii') as dips:
            if ip_addr in dips.read().split():
                return True
    return False


def non_res_checks(noop: bool):
    """Reseller-owner checks on non-reseller, non-ngx servers"""
    for path in Path('/var/cpanel/users').iterdir():
        user = path.name
        if user == 'root':
            logging.warning('%s exists. Skipping.', path)
            continue
        if user in rads.OUR_RESELLERS:
            try:
                whmapi1.set_owner(user, 'root')
            except CpAPIError as exc:
                logging.error(
                    "Error changing owner of %s to root: %s", user, exc
                )
            continue
        try:
            user_conf = CpanelConf.user_conf(user)
        except Exception:
            continue
        try:
            owner = user_conf.get('config', 'owner')
        except configparser.NoOptionError:
            logging.warning(
                '%s is missing OWNER and may not be a valid CPanel user file',
                path,
            )
            continue
        if owner != RESELLER:
            set_owner(user, owner, RESELLER, noop)


def get_resellers() -> set[str]:
    """Read resellers from /var/cpanel/resellers"""
    resellers = set()
    with open('/var/cpanel/resellers', encoding='utf-8') as res_file:
        for line in res_file:
            if res := line.split(':', maxsplit=1)[0]:
                resellers.add(res)
    return resellers


def setup_logging(noop: bool, loglevel: int):
    """Setup the root logger"""
    if noop:
        logfmt = '%(asctime)s %(levelname)s NOOP %(message)s'
    else:
        logfmt = '%(asctime)s %(levelname)s %(message)s'
    rads.setup_logging(
        path=None, loglevel=loglevel, fmt=logfmt, print_out=sys.stdout
    )


def main():
    """Cron main"""
    loglevel, noop = parse_args()
    setup_logging(noop, loglevel)
    if rads.IMH_ROLE != 'shared':
        logging.critical("rads.IMH_CLASS=%r", rads.IMH_ROLE)
        sys.exit(1)
    if rads.IMH_CLASS == 'ngx':
        check_ngx_resellers(noop)
    # Get all resellers defined in /var/cpanel/resellers
    resellers = get_resellers()
    all_resellers: set[str] = resellers | set(rads.OUR_RESELLERS)
    if rads.IMH_CLASS in ('reseller', 'ngx'):
        # main server IPs which should not be an a reseller ip delegation
        # These come from /var/cpanel/mainip and /var/cpanel/mainips/root
        main_ips = get_main_ips()
        for reseller in all_resellers:
            # 1. Set the ACL to match the package name
            # 2. Reset their reseller resource limits
            # 3. Make sure the owner is set to inmotion (or root, for ours)
            # 4. if running on a res server, run setup_dips to
            #    setup /var/cpanel/dips
            res_checks(reseller, main_ips, noop)
    else:
        non_res_checks(noop)
    if rads.IMH_CLASS == 'reseller':
        orphan_storage = defaultdict(list)
        term_fails = defaultdict(list)
        for user in rads.all_cpusers():
            logging.debug("checking user %s", user)
            if user in all_resellers or user == rads.SECURE_USER:
                continue
            check_orphans(user, main_ips, orphan_storage, term_fails, noop)
        for reseller, orphans in orphan_storage.items():
            orphans_notify(reseller, orphans, noop)
        for reseller, orphans in term_fails.items():
            term_fail_notice(reseller, orphans, noop)
    cleanup_unknown_cpusers(noop)
    if rads.IMH_CLASS == 'ngx':
        check_ngx_owners(noop)
    cleanup_delegations(all_resellers, noop)
    check_double_ip_delegations(resellers, noop)


def check_ngx_owners(noop: bool):
    """Set owner to inmotion for non-reseller, non-child accounts"""
    resellers = get_resellers()
    user_owners = rads.all_cpusers(owners=True)
    for user, owner in user_owners.items():
        if user in rads.OUR_RESELLERS:
            continue
        # for resellers, ownership is fixed in res_checks()
        if user not in resellers:
            # if owner doesn't exist or is root/tier2s
            if owner in ('root', 'tier2s') or owner not in user_owners:
                set_owner(user, owner, RESELLER, noop)


def check_ngx_reseller_ips(noop: bool, user: str):
    imh_ip_file = Path('/var/cpanel/mainips/inmotion')
    user_ip_file = Path('/var/cpanel/mainips') / user

    if imh_ip_file.exists():
        if not user_ip_file.exists():
            logging.info("Copying Shared IPs to %s", user)
            if not noop:
                shutil.copy(imh_ip_file, user_ip_file)

def check_ngx_reseller(noop: bool, user: str):
    """Ensure ngx user on reseller plans have reseller permissions"""
    resellers = get_resellers()
    try:
        plan = CpanelConf.user_conf(user).get('config', 'plan', fallback=None)
    except Exception:
        return

    if not plan or not Path('/var/cpanel/acllists', plan).exists():
        return

    if user not in rads.OUR_RESELLERS:
        check_ngx_reseller_ips(noop, user)

    if user not in resellers:
        # res_checks() will handle setting the ACL
        logging.info("Adding reseller permissions to %s (plan=%s)", user, plan)
        if noop:
            return
        try:
            whmapi1.setupreseller(user)
        except CpAPIError as exc:
            logging.error(
                "Error adding reseller permissions to %s: %s", user, exc
            )


def check_ngx_resellers(noop: bool):
    """Ensure ngx users on reseller plans have reseller permissions"""
    for user in rads.all_cpusers():
        check_ngx_reseller(noop, user)


def cleanup_unknown_cpusers(noop: bool):
    """Delete files in /var/cpanel/users for non-users"""
    for entry in Path("/var/cpanel/users").iterdir():
        user = entry.name
        try:
            pwd.getpwnam(user)
        except KeyError:
            logging.warning("Removing erroneous file at %s", entry)
            if not noop:
                entry.unlink()


def cleanup_delegations(all_resellers: set[str], noop: bool):
    """Remove /var/cpanel/dips (ip delegation) files for deleted resellers"""
    for entry in Path('/var/cpanel/dips').iterdir():
        user: str = entry.name
        if user not in all_resellers:
            logging.debug('%s is not a reseller. Removing %s', user, entry)
            if not noop:
                entry.unlink()


def check_orphans(
    user: str,
    main_ips: set[str],
    orphan_storage: defaultdict[list],
    term_fails: defaultdict[list],
    noop: bool,
):
    """Find orphaned accounts (accounts that have no existing owner)"""
    try:
        user_conf = CpanelConf.user_conf(user)
    except Exception:
        return
    owner = user_conf.get('config', 'owner', fallback=None)
    if not owner:
        return
    ip_address = user_conf.get('config', 'ip', fallback=None)
    if (
        not Path('/var/cpanel/users', owner).exists()
        or owner in rads.OUR_RESELLERS
    ):
        # this is an orphaned account
        try:
            susp_time = Path('/var/cpanel/suspended', user).stat().st_mtime
        except FileNotFoundError:
            # the orphaned account is not suspended
            orphan_storage[owner].append(user)
            return
        # If the orphan is suspended for more than 14 days, terminate it
        if time.time() - susp_time > 14 * 86400:
            logging.info("Terminating suspended orphan user %s", user)
            if noop:
                return
            try:
                whmapi1.removeacct(user, keepdns=False)
            except CpAPIError as exc:
                logging.warning("Failed to terminate user %s: %s", user, exc)
                term_fails[owner].append(user)
        else:
            logging.debug(
                "Orphaned user %s has not been suspended long "
                "enough for auto-terminate",
                user,
            )
        return
    # This is a non-orphaned, child account.
    # While we're here, make sure the user's IP is correct.
    if not ip_address or ip_address in main_ips:
        # Assign the user their owner's IP
        set_child_owner_ip(user, owner, noop)


def orphans_notify(reseller: str, orphans: list[str], noop: bool) -> None:
    """Notify for unsuspended orphan accounts"""
    logging.warning(
        '%s orphaned accounts exist under the reseller %s. Sending STR.',
        len(orphans),
        reseller,
    )
    logging.debug('Orphans under %s: %r', reseller, orphans)
    if noop:
        return
    str_body = f"""
The following orphan accounts have been located under owner {reseller}:

    {' '.join(orphans)}

They appear to have an owner that does not exist, or is a reseller missing
reseller privileges. If the orphan's owner exists in PowerPanel, please set
their owner to 'inmotion' or 'hubhost' as appropriate. If the orphan's owner is
a reseller, add reseller privileges. If the orphan account does not exist,
please suspend them on the server with the command

"for orphan in {' '.join(orphans)}; do suspend_user $orphan -r orphan; done"

Thank you,
    {HOST}"""
    try:
        rads.make_ticket(
            dest="str@imhadmin.net",
            subject=f"Orphan accounts on {HOST} with owner {reseller}",
            body=str_body,
        )
    except rads.TicketError as exc:
        logging.error("Could not make ticket - %s", exc)


def term_fail_notice(reseller: str, orphans: list[str], noop: bool) -> None:
    """Separate notification for orphans that failed to auto-term, because
    suspending them again won't fix the problem"""
    logging.warning(
        "%s orphaned accounts failed to auto-terminate under the reseller %s. "
        "Sending STR.",
        len(orphans),
        reseller,
    )
    logging.debug("terms failed for %r", orphans)
    if noop:
        return
    str_body = f"""
The following orphan accounts were found under owner {reseller} and were
suspended long enough to auto-terminate, but auto-termination failed:

    {' '.join(orphans)}

Please investigate and if appropriate, run removeacct on the orphan accounts.

Thank you,
    {HOST}"""
    try:
        rads.make_ticket(
            dest="str@imhadmin.net",
            subject=f"Failed to auto-term orphans on {HOST} with"
            f" owner {reseller}",
            body=str_body,
        )
    except rads.TicketError as exc:
        logging.error("Could not make ticket - %s", exc)


def set_child_owner_ip(user: str, owner: str, noop: bool) -> None:
    """Assign the user their owner's IP"""
    try:
        owner_conf = CpanelConf.user_conf(owner)
    except Exception:
        owner_ipaddr = None
    else:
        owner_ipaddr = owner_conf.get('config', 'ip')
    if not owner_ipaddr:
        logging.error(
            "User %s has shared IP, but couldn't determine the IP of "
            "the owner %s to assign it to the child account",
            user,
            owner,
        )
        return
    logging.warning(
        "User %s has shared IP. Changing to owner %s's IP of %s",
        user,
        owner,
        owner_ipaddr,
    )
    if noop:
        return
    try:
        whmapi1.setsiteip(user, owner_ipaddr)
    except CpAPIError as exc:
        logging.error(
            "Error changing IP of %s to %s: %s", user, owner_ipaddr, exc
        )


def set_owner(user: str, old: str, new: str, noop: bool):
    """Change user owner and log"""
    logging.info("Changing ownership of %s from %s to %s", user, old, new)
    if noop:
        return
    try:
        whmapi1.set_owner(user, new)
    except CpAPIError as exc:
        logging.error(
            "Error changing ownership of %s to %s: %s", user, new, exc
        )


def res_checks(reseller: str, main_ips: set[str], noop: bool):
    """All reseller and IP checks for res servers"""
    try:
        user_conf = CpanelConf.user_conf(reseller)
    except Exception:
        return
    if Path('/var/cpanel/suspended', reseller).exists():
        return
    if reseller not in rads.OUR_RESELLERS:
        owner_needed = RESELLER
        # 1) Reset the reseller ACL to match the package name
        if pkg := set_reseller_acl(reseller, user_conf, noop):
            try:
                package_conf = CpanelConf.pkg_conf(pkg)
            except Exception:
                return
            # 2) Reset the reseller's resource limits
            set_reseller_resource_limits(reseller, package_conf, noop)
        else:
            logging.warning(
                "Removing reseller permissions for %s - their plan does not "
                "correspond to a reseller ACL",
                reseller,
            )
            if not noop:
                try:
                    whmapi1('unsetupreseller', {'user': reseller}, check=True)
                except CpAPIError as exc:
                    logging.error(
                        "Could not remove reseller perms from %s: %s",
                        reseller,
                        exc,
                    )

    else:
        owner_needed = 'root'
    # 3) Make sure the reseller itself is owned by the correct user
    owner = user_conf.get('config', 'owner', fallback=None)
    if owner != owner_needed:
        set_owner(reseller, owner, owner_needed, noop)
    if rads.IMH_CLASS == 'reseller' and reseller not in rads.OUR_RESELLERS:
        setup_dips(reseller, user_conf, main_ips, noop)


def setup_dips(
    user: str, user_conf: CpanelConf, main_ips: set[str], noop: bool
):
    """Create a dedicated IP pool for resellers that don't have one"""
    # This is necessary to prevent resellers from having access to assign all
    # IPs on a server
    ipaddr = user_conf.get('config', 'ip', fallback='')
    if not ipaddr or ipaddr in main_ips:
        # Assign the user a new IP ###
        if ipaddr := get_new_ip():
            logging.info("Assigning reseller %s its own IP %s", user, ipaddr)
            if not noop:
                try:
                    whmapi1.setsiteip(user, ipaddr)
                except CpAPIError as exc:
                    logging.error(
                        "Error changing IP of %s to %s: %s", user, ipaddr, exc
                    )
        else:
            logging.error("Could not find an unused IP to assign to %s", user)
            return
    set_reseller_mainip(user, ipaddr, noop)
    # check if user has a dedicated ip pool
    if Path(f'/var/cpanel/dips/{user}').exists():
        current = set(whmapi1.getresellerips(user)['ip'])
        pool = {x for x in current if x not in main_ips}
        pool.add(ipaddr)
    # whmapi1 getresellerips returns all free ips if no delegation exists
    else:
        current = None
        pool = {ipaddr}
    if current != pool:
        logging.info(
            "Changing IP delegation for %s from %r to %r", user, current, pool
        )
        if noop:
            return
        try:
            whmapi1.setresellerips(user, pool, delegate=True)
        except CpAPIError as exc:
            logging.error(
                "Error changing IP delegation for %s to %r: %s", user, pool, exc
            )


def set_reseller_acl(
    user: str, user_conf: CpanelConf, noop: bool
) -> str | None:
    """Reset the reseller ACL to match the package name"""
    pkg = user_conf.get('config', 'plan', fallback=None)
    if not pkg or not Path('/var/cpanel/acllists', pkg).exists():
        # This means the reseller is set to a plan that likely isn't configured
        # on the server. If this is the case, strip their ACL (just to be safe)
        pkg = None
    logging.debug("Setting reseller %s to ACL %r", user, pkg)
    if noop:
        return pkg
    try:
        whmapi1.set_acllist(user, pkg)
    except CpAPIError as exc:
        logging.error("Error setting %s to ACL %s: %s", user, pkg, exc)
    return pkg


def set_reseller_mainip(user: str, ipaddr: str, noop: bool):
    """Call setresellermainip if needed"""
    try:
        current = Path('/var/cpanel/mainips', user).read_text('ascii').strip()
    except OSError:
        current = None
    if current == ipaddr:
        return
    logging.info('Setting main IP for %s to %s', user, ipaddr)
    if noop:
        return
    try:
        whmapi1(
            'setresellermainip', args={'user': user, 'ip': ipaddr}, check=True
        )
    except CpAPIError as exc:
        logging.error(
            "Could not set main IP for %s to %s: %s", user, ipaddr, exc
        )


def set_reseller_resource_limits(
    user: str, package_conf: CpanelConf, noop: bool
) -> None:
    """Reset the reseller's resource limits"""
    limit_kwargs = package_conf.res_limits.copy()
    if limit_kwargs['enable_package_limit_numbers'] == '1':
        logging.info("Enabling package limits for %s", user)
        args = {'user': user, 'enable_package_limit_numbers': '1'}
        if not noop:
            try:
                whmapi1('setresellerlimits', args=args, check=True)
            except CpAPIError as exc:
                logging.error(
                    "Error enabling package limits for %s: %s", user, exc
                )

        for pkgname, pkglimit in package_conf.pkg_limits.items():
            if pkglimit != '0':
                logging.info(
                    "Setting package limit for reseller %s, package %s to %s",
                    user,
                    pkgname,
                    pkglimit,
                )
            if noop:
                continue
            try:
                whmapi1(
                    'setresellerpackagelimit',
                    args={
                        'allowed': '1',
                        'user': user,
                        'number': pkglimit,
                        'package': pkgname,
                    },
                    check=True,
                )
            except CpAPIError as exc:
                logging.error(
                    "Error setting pkg limits for reseller %s, pkg %s: %s",
                    user,
                    pkgname,
                    exc,
                )

    if (
        limit_kwargs['enable_resource_limits'] == '1'
        or limit_kwargs['enable_account_limit'] == '1'
    ):
        logging.info("Setting reseller limits for %s to %r", user, limit_kwargs)
        limit_kwargs['user'] = user
        if noop:
            return
        try:
            whmapi1('setresellerlimits', args=limit_kwargs, check=True)
        except CpAPIError as exc:
            logging.error("Error setting reseller limits for %s: %s", user, exc)


if __name__ == '__main__':
    main()

Youez - 2016 - github.com/yon3zu
LinuXploit