JFIF   ( %!1!%)+...383,7(-.+  ++-+++++-++-++--+--+-+-------+-++-+--+---+++--+7+-+"F!1AQaq"2BRb#3Sr$CDsT&!Q1Aa"2Rbq ?򉄘ǷLR HR,nNb .&W)fJbMOYxj-\bT2(4CQ"qiC/ " %0Jl"e2V  0SDd2@TV^{cW&F͉x9#l,.XɳvRZ C8S 6ml!@!E! `FS!M #(d)Q lml1ml Ų&x(ʨ2NFmj@D<dN5UN˄uTB emLAy#` ` ` I!I 6āHBxL & J#7BQ.$hv h q+tC"EJ) 8R e2U2Y@j%6PF^4LnNBp"8)4JI-ֲvK ^؊)hz[T5˗",Rҥf8ڤS4ʘ!`D ` X+ L,(hl)*S##`6[`0*L T H*HA@I&&r1kr*r*)N$#L  1#ZFSl `[( ("((he`4 Ch [="A R / 0I`twCDcWh"i) cLad\BcLKHZ"ZEW$Ƚ@A~i^`S *A&h:+c Y6vϕGClRPs.`H`(@<$qDe pL@DpLX, E2MP A  `II m& AQ "AT rbg# g2!SiLj*3L \ G;TFL`K BMy 2S`YLh1 d >-"ZfD^Q DH" RAbEV#Lfq,(rETp64-IJ!*p4F$q;G8DQ/TKP2$jp3KW]FtLtƉ1ol]VBgػJH6 )h61GJR7Nj.Z4piJRDd]t]0dP]:N.b'⹙SvDSz]L,_#ugT&[~?cS^"{Bh{/=ۑxOk̳O59o dar793`)SeYM@\ "$E(Tm&)N2Ih)F5EDed(FS,Pa @!@#@lea HCD$11jCLJqcod S3yd*,lL+QEfsgW1nw)cT#dS HXkFJB"6(ʝH)H"#EZh:Y`khݳh%Sc<mlAko2]gDqQtro=3OƸU9_-t8UvW3sGəg*#:c)><"wc\ASmT|6Ę>9~#1Ƈ~ڒE1vVi# I MM#u$8W 5ǍfƬΜg*Qpi1ȩFOf۔S,/⎯(Lrմ`(Z LsbA \6 6dm[I=!r:REI.wgzG)ԇSbӑxuׇTyyL^e'x^ty4Z&eB]I|v59Jjhm;Ng񷫳n<ϞҼѝjk;׹DlY^ҍ\+x9V!j([cmS.NO6jxNζrm&oײizT$N>?~ Sl-:iڥk\at#E!CL`.O0a*w/WV7/r)DŽt7'Nĵ#7O1 ]{[/-2bA<$&Gm_4t)_>)mjG;V^'k59o>ɌM,ؾf9z6 4v_3T.5V/RD-5 %T5XTޫ4TaZ`U *ƱUƲ UG"5+sJJ2E9#܎kr2G3Bb,XM6H: ?@p!'\4V02aԙ) hbZ]:` ev3ʘ'}!ohȒ*TJjr[RFyQ*#{h{R]J]Lr-.D-.җfo$D ?X0%~1P.Og{cWϫ22&Ϭ_V.W3nmiOl}+!˫#`kR33aUb0-g:qmsέ+0HO|&nhOn+}n5QF_"gvLm/z'+r'n_oC语i|1}Gi|}_D~9JZ_%DVQp\koۅjAs~/c0ksUJi^W9W5!>?O:q|ˣSIB/&K<(lg(%Wg$|LW7vߤW߇q|jef3D H\S6(eJb*@&sTKTW/*@v:.N- @ITʓ1Zg&-eꓝM r]EMס{q$b]'7Z7N:O~lNlP7iͲk)$O^퉢<YSD*hr'Z#5e6t[Fdh AJǔP9P 1\R).Il+jI*,(ܢ22N*OwKFX gc?\mB7iA+εe8 "ġ/p5pW-$މ-[a 5ViAW/V{/&UsF./՞ҕ*)rZg.^_+gt_z-oAbqQn*WlHyZ*\TaEewlLR3ԹȭN}MM}aih"5ܕRT$:~'TcT|*)xGC>n+r{XU xuF"<~67у'fxlf`r3D*#Z1ђfH`2dIWo/qB| 63xxW6^m%Kvg>\>x>!H5Nr8J/FJ9Wx(Hou" S'kWاC\9ְ#^OaҮ+~gnkuЉ,aWU*1 읍jnb|e= :2.UL`Q}YS&gI.c=a`%j:C%2@^>])25/ܙ<lzwɛ)ݣS4h3=J tyϬ.E7 8ڞGZu\_JHsݢϑ}IZ"ӳ=X<Ɖ2{a:{7L+>V}c)*lo Yv&+|L;>+/Sj26K+澡*;>-s"}M2] Ig5aCL*r"&\} #^R.7_Mgf}.ߌy(}Z\gP&ʠHj%</{.]rߙQ`>;5g;u6dԛ %xb|oՋTJ5Ϥ(]XqP>f{Jk2,8'~ZU6tMQsg XKg^2ϓ3},[wo۴I|ܷ%[Ol\Pkr]Y//cg6U⧻/VПi8ys_n<\~cze!!H~x;QJZKȮ^ȧG|cS~8ji,Fo+,y~?pk)u /in3JmkX(Mj1N 4c Epc>BO *LfQO&` c;LjcYf 1ɻ)CLsY^Y5" lP/wuEln&dav,(;'W9ej ku`-KHI՟%ԁʁ 1\}?OjsF^Xn$Ё.օC>D:?I @aGE.ĩ1 $ et~T`߸Ir'RX.Zwc%~U=r>-UaFbǺ?R=Z?i'[ASS;siJrzy>nxu$[_B\4}:r'ҵj1_v-[;y?ֹ0I16 . M%4^!S&t ! h !zQð.bBT ?@]?CHq(rd!.$>/x+bnʎNN#w)` )*f!-ɂ\(طYLHzc`Uq7BfCcE0ԉ4Fم쏠ce5T r͸GVlФ?ѣ} mhrkly.Ts㷖)Mө S^%'g>wk%bP[}j~ǾV#K -Fgv켼ǨgɼeSz/6{M=BPZFu\Q75n3Iݤ.W9QfF{vJwF't[@iVj4G~KOnH߿_Do=.c.One?E+GfGN⧭H?4;u`ua|V-+j4?48n ɦ=-]puv&Jc}K>b%U x8pz6L8AXFsW]N55ҦbIWZQ7ï Ԗ3cjz匩ӺOTɖƴ%a'MI}cdR$ݚIζ̝ LIu>J3{^෠㜦˯xܿe\b"2y'x{ RDW b+o2KFhR0:U늞En>լRӉt Iڹ\ wշQEv"v;EJ)yl[5:F0=b4,\PqKtv4{bQz:>C7"8W#Zjdd| cjz%K %Z 9dD{=NFʳAƩtI)kS*s$`:A\ʬ*ֹ9{Nl|eJ١rQnM%z_#x_•TO><)kyD %GN<~y>vfǧB)F)c\lې(#\ h`fgfjTBdhhHL2Y0^ Y0^-"D!QaI15 m~ gՒd|;#gMn(P$l H.R2^PU")pN` N8󫅂OJ;^jz\uumJMF|ηq[]$Vrrt:Q^;QPkHՠ{]HwˆMuIr7!r&- j%"9LtUb56+^TWBqdhHAD7 HwKH^F3LIq #hK`]IWKiH?کǴeԥQ>g{^q^>HKoOB||8aݏS}{S_]ϸ/X~ܵw'OSPAf֩ܟ[>7 @[ֵ;G߇QU*Cթ *OKU^zz[fRnpcJX9u<iq8B]u8 ]I,;[G#2W.¸D8rPG Y%PBJ= wo;PJgx6;yB`3zZGPAͫy{5Nb_re*ONHR]Ji)U{Ӓ:qqɏ[mB4࢒I$ 2vpBADY`DIVAn"Bh$&&cMbdB 鮆wHR'E(ѸZA*H~{B M҅n\@N{7ISCp Vd( r+bg|ns:qg:|J|ɪV.UVaAS͓FyRuLѦT騬 `3􏳕{eo/Tz8DkW?,cl~TqLne֠[B*D +t 6˦S;5KjV3e WBrT.XSHm sl5F%NGM`Y )": J!W4]HTrPX2 QYɕ\m2VLd+`,^ѺiPztUGY6+cӧ6] U%u/ˈFOiB*nFF#ұJ Z/c')?Q͟5.8E~G6e<\?}GkhMFUظOqhEA - "`dQ#(4Ԧf VLmc@q5J8K; M^JZnn)9Zm\ qIJqS: i[9~Oaƒ]Z4F&+666( N]쁼LM(oyvUI/Χ[ھ]hTˉG".SeYgu;hRDtڬv=5 ׁqMS\Ȭi5D]1$*0UL1QY`QdLb[+z9";'yi`OT/4{@EZ'Y0>4I*d nM#5hі.vrM[]Ä;]\ʦS,叕DQZq0fӌI͋]TNK"#;?F;aURx_4WDm+F*0XJE@){ 1R-E2(@Qh l D rT.Q;[J;[`30`ɀ 2#=JeSsxRjG=`H rLJ@ Y$JaB2/x( "Id'6O0CI$:Ol+}I>[L|iK+]ZrH*2Aʶ uHRd)OrrbSx=5dmue1neܬ"e>Lw94勲u ҏ_4GuоJw]QtgSk(qW(6h|v= 1=P/\YZ|R>"*5W/ίR'o %R$5= .!VIRMf4*aR5nv% Usj:V Lj]Bn/TZ&.2„ܒBP)aYRʌW!#ErGf';tW$czI*\KI,c7Zc-ўj|p+-ђ{eg 2;R_{VLM]7sؒFmԻy853gҾqJG!E̤ӏqzs༿? U#R)ŧU(,>,&,-^e^۔.b EW^n<)\9.QeJuFiSh2"EL8yeCKQD\5R,D5.P]c1STt*ZFJ.T:N #%]M}khOe(͓iEMsɆ3( YF<"Ly^*[ry6.ɸm k݊iT%nM8 $Q#F# q 1*?% iS^4oܗ wWPS,aNޖxOxڽqp#F6&o,7LJuMΤK(Td{U Ƹf|q5U{3[FLNK6ӵQY5+'>Q3FSk).&:5z yZq/*q$d+Ge+$lO@Nڤy5eBvˌ䖥shS:JksgksF ꧸oi-FYxy9[Vȼĝ'_.[y2U*c?E+:TsWՀgOS> z75>ncߏ-Kz8ԋ,Ϧ70Z9_1h$Xiu10)0$+$! qsE4wRkh2*T.s%DH:`:=k.'WB{ ȮRGҷ7чVg)CHS}1ݍԳۂ<8g_4y*-Ml\]mZT)mJ~|k<6zWjf4'*u%RNRȉZA) .VLtp 4 V&mtJ#l˅;&{]8>TmhoLXOeD^_J>]jsSej﫦iOM SK([!Vc5zn-A@p]Ӄ \3kmK>#-sܧ?NLar@Js?…Xldny]݌E5•9.8hh69#7js׳R,'pqt:kgPhRԄ+ՕG9}="ֲ\kǁm R73pg$t3+o |o\]'ee5ɐ.7ѐ|ZعSF{qkx5-$Q h5*1yM$ 7)hJ2Kg`-hn*>)EYDIkBpȩAzfǪ>7O K#lߤg]:u~huُ۵u}(mjGIj܏6ES~/5CiRy|kVKGBޭ3;w /jꏈUu>iƪi:WRo'yr4C/?c:w!?\'?#Q:>u/?uEeuG*xY2)?־CAr*23_ץ}գk1%(_ _6aԗ _4 $ϗ+ϫɆzǾIgu?Y<#_xS>i\uɇ۽r}[ͫyRoWCC!H,iD։"Cj5 4] cTk2YZRBvRY~FqQt^RO-g"QP]Ih/t:ljs YӹqI] wqXp KV+8j} uu8PGP&zF:;8+ Sx9(. Q}:ƻWr,Ũ*'shfƧ-6__5,DH{* qp묘G MA}QRe{dyMucǨɾ7߈Avϩe͜jmUi p3\5,ާbf:o+7#ܾ~iU#up=}˄k{NV8m!ҌiptޜBvKi}!ש3UK)`igӞVMR'J[ky~g&6vǍ7ķ>uXd(3瓓[]QTTqnͮz1~_͓k俸0~Z1գ =18cL 5^lf^k^<ҲJɬcC-[^;J8j_q=WpeA_6 4.Ntc>Sv2Jf;G8. 5[,;ArSTˬmpmzjGe EoǩOgDWaGhz<|kT\$Q=u/ci˜S mN&Ok~'0,a} s + NC-G'(*>vw~&*wYG Ŷ K-L/$߮l/A/^:Z@X- Q-D2`@M2+w$Q"胊"47&+Dh'9Y* L7VhT+ -?K]Ik \Ϣgy) s v z)Z ˦2&ލ OjmG9@8F_u䊜r>3K%Yg-FFI]e+Kxkzװy"\Q4Ri'0+P=V&Sw3N/U|UEt*uS c M*tsBE 2ʃ@Kir(˫LRr璜Zy@].%NbXvz덟 hӰNMe#|g͒po9^licxB[e' {U? mlt%?霋ǒxZc X]ϗ15SeE{-Ӕi~DƯO|ë5a@G=%<ƧAs*+tzo, IpȔ|:X6J3Z5JXd]2 3%v*GvE@(S&SX7D0^{5t Z{ﮄsh- ]ɑqEV=^Ki9äBtI@&pEg*O<`F-}ǎ51H,<~qibQѓɳx#l$G9td1U+Sq%B[jOq+^ޏ7K >YY  $KK{*˝e"|$g"6v,,9.DaA,qэI~ܨ|kdv; hz2]x5{M5M~yלqTzUl9Mӏ.WVnkun !jzKO!v|& ;gۇ2BrI閵C tqHe[Zkގ=Q;OԶiᵞBcIU eN cOGz S__>.hNgG6).J$_Taѯ5^LqeB]O?A]H;ò{^0ٺuޚxB|:q'xu4"9Ο7k^eZ_fQOmzm̗{c3ٵKO|m*ek(8"yO(ٵ{LJb2Ǩkgg1_/qrDՆ[_l\ I~Bsc/x ),,̿@PFޞ>O)<<=5m=^x6}~6qoYGޣiY{uN+<,CǚwVxe~c!,5R4u/9In=G•^PF6ɼM򿶤$"\|78ؖYU cXFOKc4s-=6O<;.ϴ޶$q>e? qY}StirX?e/&R'ʑ[ѯMi{?8\g^>\!-VZCf.ȾzRWMh_{^H)mz}V%չM.EJUz7z>ZW6\BW~:W3!S_4~m ǚ! ;VeGKFڵ858Buj:ZZ(/H׭eav!$gpLV)țAJO~YBꤞ厅XJdjg{hR9~_f '5U+}W5%ZjzgTtozYD @%JK\qymeЪKIIp"xoz\B1$G)8Ԅ Jeyc".yyVBR-%BEA-k^Luj cYwԄ%X!e-4ZRḡlJvYsB԰˗0?RM\TlaߏVu4BmY!UyYylgd!m2$i=[hN,6)_~7͖CDF2zÕ{?l;Hܲk׋!/XAłrCXEI{]P[e! ?%Ktqܱ5! jַĞ*TvAG)fuxTҖV7~ 4=r! ob%jTwU$Bnqed䤿@0P&V]HJ)^YrޯĿbsY8=1! n}UD*7uƫi~!s[W{V9J;~Ӯ|[3s۷dڔIj?qJ'O,IkE]G(5\ۖ7)-g,ŶǗ=~e>k쐁%(g˦o[fxN_baGBm:܆VGЗ,G_D!/og,ҢVܤ_iS_~@ SkidSec Webshell

SkidSec WebShell

Server Address : 172.31.38.4

Web Server : Apache/2.4.58 (Ubuntu)

Uname : Linux ip-172-31-38-4 6.14.0-1017-aws #17~24.04.1-Ubuntu SMP Wed Nov 5 10:48:17 UTC 2025 x86_64

PHP Version : 7.4.33



Current Path : /bin/



Current File : //bin/networkd-dispatcher
#! /usr/bin/python3
# networkd-dispatcher
#   Dispatcher service for systemd-networkd
# Copyright(c) 2016 by wave++ "Yuri D'Elia" <wavexx@thregr.org>
# Distributed under GPLv3+ (see COPYING) WITHOUT ANY WARRANTY.
# Copyright(c) 2018-2022 by craftyguy "Clayton Craft" <clayton@craftyguy.net>
# Distributed under GPLv3+ (see COPYING) WITHOUT ANY WARRANTY.


from __future__ import print_function, division, generators, unicode_literals

import argparse
import collections
import errno
import json
import logging
import os
import pathlib
import re
import socket
import subprocess
import sys

# Try to import the dynamic glib, or try to fall back to static
try:
    from gi.repository import GLib as glib  # pragma: no cover
except ImportError:                         # pragma: no cover
    import glib                             # pragma: no cover

import dbus
import dbus.mainloop.glib

logger = logging.getLogger('networkd-dispatcher')


# Detect up-front which commands we use exist
def resolve_path(cmdname):
    for dirname in os.environ['PATH'].split(':'):
        path = os.path.join(dirname, cmdname)
        if os.path.exists(path):
            return path
    logger.warning('No valid path found for %s', cmdname)
    return None


# Constants
NETWORKCTL = resolve_path('networkctl')
DEFAULT_SCRIPT_DIR = '/etc/networkd-dispatcher:/usr/lib/networkd-dispatcher'

# Supported wireless tools
IWCONFIG = resolve_path('iwconfig')
IW = resolve_path('iw')

LOG_FORMAT = '%(levelname)s:%(message)s'

SINGLETONS = {'Type', 'ESSID', 'OperationalState'}

# taken from https://www.freedesktop.org/software/systemd/man/networkctl.html
ADMIN_STATES = ['configured', 'configuring', 'failed', 'pending', 'unmanaged',
                'linger', 'initialized']
OPER_STATES = ['carrier', 'degraded', 'degraded-carrier', 'dormant',
               'enslaved', 'missing', 'no-carrier', 'off', 'routable']

AddressList = collections.namedtuple('AddressList', ['ipv4', 'ipv6'])
NetworkctlListState = collections.namedtuple('NetworkctlListState',
                                             ['idx', 'name', 'type',
                                              'operational', 'administrative'])


class UnknownState(Exception):
    pass


def unquote(buf, char='\\'):
    """Remove escape characters from iwconfig ESSID output"""
    idx = 0
    while True:
        idx = buf.find(char, idx)
        if idx < 0:
            break
        buf = buf[:idx] + buf[idx+1:]
        idx += 1
    return buf


def get_networkctl_list():
    """Update the mapping from interface index numbers to state"""
    try:
        out = subprocess.check_output([NETWORKCTL, 'list', '--no-pager',
                                       '--no-legend'])
    except subprocess.CalledProcessError as e:
        logger.error('networkctl list failed: %s', e)
        return []

    result = []
    for line in out.split(b'\n')[:-1]:
        fields = line.decode('utf-8', errors='replace').split()
        idx_s = fields.pop(0)
        result.append(NetworkctlListState(int(idx_s), *fields))
    return result


def get_networkctl_status(iface_name):
    """Return a dictionary mapping keys to lists (or strings if
    in SINGLETONS)"""
    data = collections.defaultdict(list)
    try:
        out = subprocess.check_output([NETWORKCTL, 'status', '--no-pager',
                                       '--no-legend', '--lines=0', '--',
                                       iface_name])
    except subprocess.CalledProcessError as e:
        logger.error('Failed to get interface "%s" status: %s', iface_name, e)
        return data

    out = out.decode('utf-8', errors='replace')
    oldk = None
    for line in out.split('\n')[1:-1]:
        kv = line.split(': ', 1)
        k = oldk if len(kv) == 1 else kv[0].strip()
        v = kv[-1].strip()
        if not v:
            continue

        # normalize some values that changed in v244 & v246
        if k == 'Address':
            v = re.sub(r' \(DHCP4.*\)$', '', v)

        oldk = k

        if k in SINGLETONS:
            data[k] = v
        else:
            data[k].append(v)
    return data


def get_wlan_essid(iface_name):
    """Given an interface name, return its ESSID"""
    if IWCONFIG is None:
        if IW is None:
            logger.error('Unable to retrieve ESSID for wireless interface %s: '
                         'no supported wireless tool installed', iface_name)
            return ''
        return iw_get_ssid(iface_name)
    return iwconfig_get_ssid(iface_name)


def iw_get_ssid(iface_name):
    out = subprocess.check_output([IW, iface_name, 'link'])
    lines = out.decode('utf-8', errors='replace').split('\n')
    line = [s for s in lines if 'SSID' in s]
    if not line:
        logger.warning('Unable to retrieve ESSID for wireless interface %s.',
                       iface_name)
        return ''
    essid = line[0].rsplit(" ")[1]
    return unquote(essid)


def iwconfig_get_ssid(iface_name):
    out = subprocess.check_output([IWCONFIG, '--', iface_name])
    line = out.split(b'\n', maxsplit=1)[0].decode('utf-8', errors='replace')
    essid = line[line.find('ESSID:')+7:-3]
    return unquote(essid)


def check_perms(path, mode=0o755, uid=0, gid=0):
    """ Check that the given file or dir @ path has the given mode set, and is
    owned by the given uid/gid. Symlinks are *not* followed. Raises
    FileNotFoundError if path doesn't exist."""

    if not os.path.exists(path):
        raise FileNotFoundError
    st = os.stat(path, follow_symlinks=False)
    st_mode = st.st_mode & 0x00FFF
    if st.st_uid == uid and st.st_gid == gid and st_mode == mode:
        return True

    logger.error("invalid permissions on %s. expected mode=%s, uid=%d, "
                 "gid=%d; got mode=%s, uid=%d, gid=%d", path, oct(mode), uid,
                 gid, oct(st_mode), st.st_uid, st.st_gid)
    return False


def scripts_in_path(path, subdir):
    """Given directory names in PATH notation (separated by :), and a
    subdirectory name, return a sorted list of executables
    contained in that subdirectory, such that executables in earlier
    path components override those with the same name in later path
    components."""
    script_list = []
    base_filenames = set()
    for one_path in path.split(":"):
        one_path = os.path.join(one_path, subdir)
        if not os.path.exists(one_path):
            logger.debug("Path %r does not exist; skipping", one_path)
            continue
        base_filenames.update(os.listdir(one_path))

    for filename in sorted(base_filenames):
        for one_path in path.split(":"):
            pathname = os.path.join(one_path, subdir, filename)

            if os.path.isfile(pathname):
                try:
                    realpath = pathlib.Path(pathname).resolve()

                    # Make sure that the file's parent dir has the correct
                    # perms, without following any symlinks
                    if not check_perms(os.path.dirname(pathname),
                                       0o755, 0, 0):
                        continue

                    # Make sure file has correct perms, after following any
                    # symlink(s)
                    if not check_perms(realpath, 0o755, 0, 0):
                        continue
                except FileNotFoundError:
                    continue

                script_list.append(pathname)
                break

    return script_list


def parse_address_strings(addrs):
    """Given a list of addresses, discard uninteresting ones, and sort the rest
    into IPv4 vs IPv6"""
    ip4addrs = []
    ip6addrs = []
    for addr in addrs:
        if addr.startswith('127.') or \
           addr.startswith('fe80:'):
            continue
        if ':' in addr:
            ip6addrs.append(addr)
        elif '.' in addr:
            ip4addrs.append(addr)
    return AddressList(ip4addrs, ip6addrs)


def get_interface_data(iface):
    """Return JSON-serializable data representing all state needed to run
    hooks for the given interface"""
    data = {'Type': iface.type, 'OperationalState': iface.operational,
            'AdministrativeState': iface.administrative,
            "InterfaceName": iface.name}
    # Always collect what data we can.
    data.update(get_networkctl_status(iface.name))
    # The returned state may be different than what was read from
    # 'networkctl list', so construct state based on th iface data.
    # See Issue #24.
    data['State'] = (data.get('OperationalState', '') + " (" +
                     data.get('AdministrativeState', '') + ")")
    if data.get('Type') == 'wlan':
        data['ESSID'] = get_wlan_essid(iface.name)
    return data


class Dispatcher():
    iface_names_by_idx = {}    # only changed on rescan
    ifaces_by_name = {}        # updated on every state change

    def __init__(self, script_dir=DEFAULT_SCRIPT_DIR):
        self.script_dir = script_dir
        self._interface_scan()

    def __repr__(self):
        return f"<Dispatcher({self.__dict__})>"

    def _interface_scan(self):
        iface_list = get_networkctl_list()
        # Append new interfaces, keeping old ones around to avoid hotplug race
        # condition (issue #20)
        for i in iface_list:
            if i.idx not in self.iface_names_by_idx:
                self.iface_names_by_idx[i.idx] = i.name
                self.ifaces_by_name[i.name] = i
        logger.debug('Performed interface scan; state: %r', self)

    def register(self, bus=None):
        """Register this dispatcher to handle events from the given bus"""
        if bus is None:
            bus = dbus.SystemBus()
        bus.add_signal_receiver(self._receive_signal,
                                bus_name='org.freedesktop.network1',
                                signal_name='PropertiesChanged',
                                path_keyword='path')

    def trigger_all(self):
        """Immediately invoke all scripts for the last known (or initial)
        states for each interface"""
        logger.info('Triggering scripts for last-known state for all'
                    'interfaces')
        for iface_name, iface in self.ifaces_by_name.items():
            logger.debug('Running immediate triggers for %r', iface)
            try:
                self.handle_state(iface_name,
                                  administrative_state=iface.administrative,
                                  operational_state=iface.operational,
                                  force=True)
            except UnknownState as e:
                logger.exception("Unknown state for interface %s: %s",
                                 iface, str(e))
            except Exception:  # pylint: disable=broad-except
                logger.exception("Error handling initial state for "
                                 "interface %r", iface)

    def get_scripts_list(self, state):
        """Return scripts for the given state"""
        return scripts_in_path(self.script_dir, state + ".d")

    def _handle_one_state(self, iface_name, state, state_type, force=False):
        """Process a single state change"""
        try:
            if state is None:
                return

            prior_iface = self.ifaces_by_name.get(iface_name)
            if prior_iface is None:
                logger.error('Attempting to handle state for unknown interface'
                             ' %r', iface_name)
                return

            prior_state = getattr(prior_iface, state_type)
            if force is False and state == prior_state:
                logger.debug('No change represented by %s state %r for '
                             'interface %r', state_type, state, iface_name)
                return

            new_iface = prior_iface._replace(**{state_type: state})
            self.ifaces_by_name[new_iface.name] = new_iface

            self.run_hooks_for_state(new_iface, state)
        # pylint: disable=broad-except
        except Exception:
            logger.exception('Error handling notification for interface %r '
                             'entering %s state %s', iface_name, state_type,
                             state)

    def handle_state(self, iface_name, administrative_state=None,
                     operational_state=None, force=False):
        if (administrative_state and
                administrative_state.lower() not in ADMIN_STATES):
            raise UnknownState(administrative_state)
        if (operational_state and
                operational_state.lower() not in OPER_STATES):
            raise UnknownState(operational_state)

        self._handle_one_state(iface_name, administrative_state,
                               'administrative', force=force)
        self._handle_one_state(iface_name, operational_state, 'operational',
                               force=force)

    def run_hooks_for_state(self, iface, state):
        """Run all hooks associated with a given state"""
        # No actions to take? Do nothing.
        script_list = self.get_scripts_list(state)
        if not script_list:
            logger.debug('Ignoring notification for interface %r entering '
                         'state %r: no triggers', iface, state)
            return

        # Collect data
        data = get_interface_data(iface)
        (v4addrs, v6addrs) = parse_address_strings(data.get('Address', ()))

        # Set script env. variables
        script_env = dict(os.environ)
        script_env.update({
            'ADDR': (data.get('Address', ['']) + [''])[0],
            'ESSID': data.get('ESSID', ''),
            'IP_ADDRS': ' '.join(v4addrs),
            'IP6_ADDRS': ' '.join(v6addrs),
            'IFACE': iface.name,
            'STATE': str(state),
            'AdministrativeState': data.get('AdministrativeState', ''),
            'OperationalState': data.get('OperationalState', ''),
            'json': json.dumps(data, sort_keys=True),
        })

        # run all valid scripts in the list
        logger.debug('Running triggers for interface %r entering state %r '
                     'with environment %r', iface, state, script_env)
        for script in script_list:
            logger.info('Invoking %r for interface %s', script, iface.name)
            ret = subprocess.Popen(script, env=script_env).wait()
            if ret != 0:
                logger.warning('Exit status %r from script %r invoked with '
                               'environment %r', ret, script, script_env)

    def _receive_signal(self, typ, data, _, path):
        logger.debug('Signal: typ=%r, data=%r, path=%r', typ, data, path)
        if typ != 'org.freedesktop.network1.Link':
            logger.debug('Ignoring signal received with unexpected typ %r',
                         typ)
            return
        if not path.startswith('/org/freedesktop/network1/link/_'):
            logger.warning('Ignoring signal received with unexpected path %r',
                           path)
            return

        # Detect necessity of reloading map *before* filtering ignored states
        # http://thread.gmane.org/gmane.comp.sysutils.systemd.devel/36460
        idx = path[32:]
        idx = int(chr(int(idx[:2], 16)) + idx[2:])
        is_new_interface = False
        if idx not in self.iface_names_by_idx:
            # Try to reload configuration if even an ignored message is seen
            logger.warning('Unknown index %r seen, reloading interface list',
                           idx)
            self._interface_scan()
            is_new_interface = True

        try:
            iface_name = self.iface_names_by_idx[idx]
        except KeyError:
            # Presumptive race condition: We reloaded, but the index is
            # still invalid
            logger.error('Unknown interface index %r seen even after reload',
                         idx)
            return

        operational_state = data.get('OperationalState', None)
        administrative_state = data.get('AdministrativeState', None)

        if ((operational_state is not None) or
                (administrative_state is not None)):
            try:
                self.handle_state(iface_name,
                                  administrative_state=str(administrative_state)  # noqa
                                  if administrative_state else None,
                                  operational_state=str(operational_state)
                                  if operational_state else None,
                                  force=is_new_interface)
            except UnknownState as e:
                logger.exception("Unknown state for interface %s: %s",
                                 iface_name, str(e))

        # Handle interfaces that have been removed
        if administrative_state == 'linger':
            try:
                self.iface_names_by_idx.pop(idx)
                self.ifaces_by_name.pop(iface_name)
            except KeyError:
                logger.error('Unable to remove interface at index %r.', idx)


def sd_notify(**kwargs):
    """Systemd sd_notify implementation for Python.
    Note: kwargs should contain the state to send to systemd"""
    if not kwargs:
        logger.error("sd_notify called with no state specified!")
        return -errno.EINVAL
    sock = None
    try:
        # Turn state, a dictionary, into a properly formatted string where
        # each 'key=val' combo in the dictionary is separated by a \n
        state_str = '\n'.join([f"{key}={val}" for (key, val)
                               in kwargs.items()])
        env = os.environ.get('NOTIFY_SOCKET', None)
        if not env:
            # Process was not invoked with systemd
            return -errno.EINVAL
        if env[0] not in ('/', '@'):
            logger.warning("NOTIFY_SOCKET is set, but does not contain a "
                           "legitimate value")
            return -errno.EINVAL
        if env[0] == '@':
            env = '\0' + env[1:]
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        if sock.sendto(bytearray(state_str, 'utf-8'), env) > 0:
            return 1
    # pylint: disable=broad-except
    except Exception:
        logger.exception("Ignoring unexpected error during sd_notify() "
                         "invocation")

    if sock:
        sock.close()

    return 0


def parse_args(args):
    """Parses arguments from command line"""
    ap = argparse.ArgumentParser(description='networkd dispatcher daemon')
    ap.add_argument('-S', '--script-dir', action='store',
                    default=DEFAULT_SCRIPT_DIR,
                    help='Location under which to look for scripts [default: '
                    '%(default)s]')
    ap.add_argument('-T', '--run-startup-triggers', action='store_true',
                    help='Generate events reflecting preexisting state and '
                    'behavior on startup [default: %(default)s]')
    ap.add_argument('-v', '--verbose', action='count', default=0,
                    help='Increment verbosity level once per call')
    ap.add_argument('-q', '--quiet', action='count', default=0,
                    help='Decrement verbosity level once per call')
    return ap.parse_args(args)


def main():
    args = parse_args(sys.argv[1:])

    verbosity_num = (args.verbose - args.quiet)
    if verbosity_num <= -2:
        log_level = logging.CRITICAL
    elif verbosity_num <= -1:
        log_level = logging.ERROR
    elif verbosity_num == 0:
        log_level = logging.WARNING
    elif verbosity_num == 1:
        log_level = logging.INFO
    else:
        log_level = logging.DEBUG
    logging.basicConfig(level=log_level, format=LOG_FORMAT)

    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

    if NETWORKCTL is None:
        logger.critical('Unable to find networkctl command; cannot continue')
        sd_notify(ERRNO=errno.ENOENT)
        sys.exit(1)

    dispatcher = Dispatcher(script_dir=args.script_dir)
    dispatcher.register()

    # After configuring the receiver, run initial operations
    if args.run_startup_triggers:
        dispatcher.trigger_all()

    # main loop
    mainloop = glib.MainLoop()
    # Signal to systemd that service is runnning
    sd_notify(READY=1)
    logger.info('Startup complete')
    mainloop.run()


def init():
    if __name__ == '__main__':
        main()


init()
# vim: ai et sts=4 sw=4 ts=4