webXMPP

Check-in [40e22b5fdb]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Bringup
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:40e22b5fdb6f66a86add15207dd67b51375937e4f2c00f1cc086a0419b07c371
User & Date: vandyswa@gmail.com 2017-06-26 23:34:11
Context
2017-06-27
16:01
Cleanly exit check-in: c644b96a10 user: vandyswa@gmail.com tags: master, trunk
2017-06-26
23:34
Bringup check-in: 40e22b5fdb user: vandyswa@gmail.com tags: master, trunk
2017-06-25
15:55
Continue workup for UDP based notification handling. To make testing easier, support directly configured accounts in place of an account server. check-in: c0c170c411 user: vandyswa@gmail.com tags: master, trunk
Changes
Hide Diffs Side-by-Side Diffs Ignore Whitespace Patch

Changes to notified.py.

    41     41   #  that it's an XMPP message from Joe.  At detail 0, you
    42     42   #  would only see that there were new events, but no other
    43     43   #  detail.
    44     44   #
    45     45   import sys, json, time, os
    46     46   import notify2
    47     47   import pong
           48  +import pdb
    48     49   
    49     50   # For DBus sniffing
    50     51   import dbus, dbus.exceptions, dbus.mainloop.glib
    51     52   import threading
    52     53   from gi.repository import GLib
    53     54   
    54     55   # Are LEDs available for notification indication?
................................................................................
    62     63   def log(s):
    63     64       sys.stderr.write(s)
    64     65       sys.stderr.write('\n')
    65     66   
    66     67   # Quick/easy way to turn dict into an ob with those k/v as attrs
    67     68   class DictOb(object):
    68     69       def __init__(self, d):
    69         -        for k,v in d.iteritems():
           70  +        for k,v in d.items():
    70     71               setattr(self, str(k), v)
    71     72   
    72     73   # Configuration, a DictOb
    73     74   cfg = None
    74     75   
    75     76   # DBus monitoring.  We need to know if the device is on/active, since
    76     77   #  we only want to blink the LED if it's off (and only until it
................................................................................
   135    136       set_led("rgb_start", "0")
   136    137       blinking = True
   137    138   def unblink():
   138    139       global blinking
   139    140       set_led("rgb_start", "0")
   140    141       blinking = False
   141    142   
   142         -# Endless execution, notification client
   143         -def run(ipa, port):
          143  +# Pull config out of JSON save format
          144  +def load_cfg():
   144    145       global cfg
   145    146   
   146    147       # Get our configuration
   147    148       f = open(os.getenv("HOME") + "/.config/notify.json", "r")
   148    149       d = json.loads(f.read())
   149    150       f.close()
   150    151       cfg = DictOb(d)
          152  +
          153  +# Endless execution, notification client
          154  +def run(ipa, port):
          155  +    global cfg
   151    156   
   152    157       # Get a wrapper for our pong network connection
   153         -    conn = pong.PONG(cfg.server, cfg.port, cfg.user, cfg.password)
          158  +    conn = pong.Client(cfg.server, cfg.port, cfg.user, cfg.password)
   154    159   
   155    160       # Prep LEDs for use if possible
   156    161       setup_leds()
   157    162   
   158    163       # Watch DBus to turn LED's off when screen unlocks
   159    164       setup_dbus()
   160    165   
................................................................................
   195    200           if resp["serial"] == serial:
   196    201               continue
   197    202   
   198    203           # New messages
   199    204           notify(resp)
   200    205   
   201    206   if __name__ == "__main__":
   202         -    run()
          207  +    load_cfg()
          208  +
          209  +    # No arg, just be a service daemon
          210  +    if len(sys.argv) == 1:
          211  +        run()
          212  +
          213  +    # Debug mode; get a connection to our test server
          214  +    conn = pong.Client(cfg.server, cfg.port, cfg.user, cfg.password)
          215  +
          216  +    # Get params, display, exit
          217  +    if sys.argv[1] == "params":
          218  +        params = conn.pingpong(conn.msg("params", "get"))
          219  +        print(params)
          220  +
          221  +    sys.stderr.write("Unknown operation %s\n" % (sys.argv[1],))
          222  +    sys.exit(1)

Changes to pong.py.

     1         -#
     2         -# pong.py
     3         -#	UDP notifications infrastructure
     4         -# vim: expandtab
     5         -#
     6         -# Ugh, Ubuntu Phone seems to have pretty much gone to Python 3, so
     7         -#  here we are.  This file is written to load under Python 2 or 3.
     8         -#
     9         -# The protocol uses a JSON-encoded string carried over UDP.
    10         -# The message structure is documented below, but at a high
    11         -#  level the protocol advances in three stages.  First,
    12         -#  the client sends a request to the notification server.
    13         -#  Among other items, it indicates the last event serial
    14         -#  seen.
    15         -# The server immediately responds to the client's port and
    16         -#  address.  If its response indicates new events, then
    17         -#  the exchange is over.  The client will presumably post
    18         -#  notifications and eventually start a new protocol exchange.
    19         -# If the server returns an indication that the client has
    20         -#  already seen the latest event, it will specify a
    21         -#  timeout.  If a new event occurs before that timeout, the
    22         -#  server will send the event.  Otherwise, at the timeout
    23         -#  interval, the server will send a message specifying
    24         -#  (still) no new messages.
    25         -# In both cases of the previous paragraph, the exchange is
    26         -#  considered finished.  A new exchange from the beginning
    27         -#  must be undertaken to receive subsequent notifications.
    28         -#
    29         -# The relative transience of an exchange, and the lack of
    30         -#  any ongoing network connection is intentional.  The client
    31         -#  might change its own address, or it might be behind a NAT
    32         -#  device which changes its address.  The most that can be
    33         -#  disrupted by such a change is a single span of exchanges
    34         -#  within the bounds of one timeout period.
    35         -# Thus, each exchange starts with the client creating a
    36         -#  fresh socket so that no long-term network state is carried.
    37         -#
    38         -# The initial version of this protocol implements authentication
    39         -#  of its operations, but not privacy (i.e., crypto).  Because
    40         -#  all the messages are JSON, it's easy to imagine further fields
    41         -#  which would configure crypto.  This is TBD.
    42         -# The initial version's authentication uses SHA-256 along with
    43         -#  a nonce to protect packets from spoofing, modification, or
    44         -#  replay.
    45         -#
    46         -# Each packet thus has a nonce (generated by the system's crypto-
    47         -#  quality random number generator) and a SHA-256 derived
    48         -#  signature reflecting the shared secret.
    49         -#
    50         -import socket, sys, json, time, os
    51         -from hashlib import sha256
    52         -from Crypto.Cipher import AES
    53         -
    54         -# How long to wait for initial/immediate response
    55         -# (This is the starting timeout, which backs off with each
    56         -#  retry.)
    57         -WAITRESP = 1
    58         -
    59         -# How long to hold off network activity after seeing a
    60         -#  network failure
    61         -WAITNET = 60
    62         -
    63         -# Max sized message
    64         -# (You can turn this up, but you really need to start thinking
    65         -#  about REST and TCP.)
    66         -MAXMSG = 1024
    67         -
    68         -# Hook for logging
    69         -def log(s):
    70         -    sys.stderr.write(s)
    71         -    sys.stderr.write('\n')
    72         -
    73         -# XOR a byte range into one half its size
    74         -def fold(buf):
    75         -    assert (len(buf) & 1) == 0
    76         -    l = len(buf)/2
    77         -    return bytes((c1 ^ c2)
    78         -        for c1,c2 in zip(buf[:l], buf[l:]))
    79         -
    80         -# Display bytes in hex and back
    81         -def tohex(buf):
    82         -    return ''.join(("%02x" % (c,)) for c in buf)
    83         -def fromhex(buf):
    84         -    res = bytearray()
    85         -    while buf:
    86         -        res.append(buf[:2].decode("hex"))
    87         -        buf = buf[2:]
    88         -    return res
    89         -
    90         -# SHA256 rep of password string
    91         -def pwhash(pw):
    92         -    sh = sha256()
    93         -    sh.update(cfg.password.encode("utf8"))
    94         -    return tohex(sh.digest())
    95         -
    96         -# Views of the packet being sent
    97         -class Packet(object):
    98         -    def __init__(self, inner, outer, buf):
    99         -        self.inner = inner
   100         -        self.outer = outer
   101         -        self.buf = buf
   102         -        # When appropriate, this is the (host,port) tuple
   103         -        #  of a recvfrom()
   104         -        self.who = None
   105         -
   106         -class PONG(object):
   107         -    def __init__(self):
   108         -        # Defend against replays
   109         -        self.used_nonces = set()
   110         -
   111         -    # SHA256 adapted signature
   112         -    def calchash(self, buf, passwd, nonce):
   113         -        sh = sha256()
   114         -        sh.update(buf)
   115         -        sh.update(nonce)
   116         -        sh.update(passwd)
   117         -        return fold(fold(sh.digest()))
   118         -
   119         -    # Encode and sign this message
   120         -    #
   121         -    # We're using 64 bits of nonce, and 64 bits of folded SHA256
   122         -    # @d may have its contents modified.
   123         -    def wrap_msg(self, uname, d):
   124         -
   125         -        # String rep of actual operation
   126         -        inner = json.dumps(d)
   127         -
   128         -        # 64 bits of nonce
   129         -        nonce = os.urandom(8)
   130         -
   131         -        # And collapse 256 bits of sha256 to 64 bits too
   132         -        # (We hash the content, nonce, and shared secret.)
   133         -        passwd = self.get_password(uname)
   134         -        if passwd is None:
   135         -            return None
   136         -        sig = self.calchash(inner, passwd, nonce)
   137         -
   138         -        # Outer signature of inner
   139         -        outer = {"sig": tohex(sig), "nonce": tohex(nonce)}
   140         -
   141         -        # Encrypt?
   142         -        if "crypto" in d:
   143         -            # Move the flag to the outer JSON
   144         -            assert d["crypto"] == "AES"
   145         -            outer["crypto"] = d["crypto"]
   146         -            del d["crypto"]
   147         -
   148         -            # Ask our instance for the hashed version of the
   149         -            #  PSK with this uname
   150         -            hashedpw = self.get_hashed_password(uname)
   151         -
   152         -            # Pad to block boundary with spaces (JSON's OK with
   153         -            #  white space)
   154         -            li = len(inner)
   155         -            bs = AES.block_size
   156         -            resid = li % bs
   157         -            if resid:
   158         -                inner += (" " * (bs - resid))
   159         -
   160         -            # Encrypt the inner
   161         -            a = AES.new(hashedpw, AES.MODE_CFB, nonce)
   162         -            inner = hex(a.encrypt(inner))
   163         -
   164         -        # Signed and possibly encrypted inner content
   165         -        outer["inner"] = inner
   166         -
   167         -        # As a JSON string
   168         -        buf = json.dumps(outer)
   169         -        assert len(buf) < MAXMSG
   170         -        return Packet(d, outer, buf)
   171         -
   172         -    # Decode JSON of message, return Packet or None
   173         -    def unwrap_msg(self, buf):
   174         -        try:
   175         -            # Decode
   176         -            outer = json.loads(buf)
   177         -            inner = outer["inner"]
   178         -            if inner["pakseq"] != pakseq:
   179         -                # Answers to this op?
   180         -                log(" mismatch pakseq")
   181         -                return None
   182         -            nonce = fromhex(outer["nonce"])
   183         -
   184         -            # Protect nonce value
   185         -            if nonce in self.used_nonces:
   186         -                log(" dup nonce")
   187         -                return None
   188         -
   189         -            # Crypto?
   190         -            uname = outer["user"]
   191         -            passwd = self.get_password(uname)
   192         -            if passwd is None:
   193         -                log("No password for " + uname)
   194         -                return None
   195         -            if "crypto" in outer:
   196         -                if outer["crypto"] != "AES":
   197         -                    log(" unknown crypto " + outer["crypto"])
   198         -                    return None
   199         -                hashedpw = self.get_hashed_password(uname)
   200         -                a = AES.new(hashedpw, AES.MODE_CFB, fromhex(nonce))
   201         -                inner = a.decrypt(fromhex(inner))
   202         -
   203         -            # Verify sig
   204         -            sig = self.calchash(inner, passwd, outer["nonce"])
   205         -            if sig != outer["sig"]:
   206         -                log(" bad sig")
   207         -                return None
   208         -
   209         -            # Convert inner
   210         -            inner = json.loads(inner)
   211         -
   212         -        except:
   213         -            log(" malformed")
   214         -            return None
   215         -
   216         -        # Don't use this nonce again
   217         -        self.used_nonces.add(nonce)
   218         -
   219         -        # Here's something signed by our peer
   220         -        log(" good pak")
   221         -        return Packet(inner, outer, buf)
   222         -
   223         -class Server(PONG):
   224         -    def __init__(self, port):
   225         -        self.sock = sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
   226         -        sock.bind(('', port))
   227         -
   228         -    # Build response message to request in @inpak
   229         -    # Like PONG.Client.ping(), this routine also modifies the
   230         -    #  dict @resp if it's provided.
   231         -    def pong(self, inpak, subop, resp=None):
   232         -        if resp is None:
   233         -            resp = {}
   234         -        inner = inpak.inner
   235         -        resp["op"] = inner["op"]
   236         -        resp["subop"] = subop
   237         -        resp["pakseq"] = inner["pakseq"]
   238         -
   239         -        # Sign/encrypt this response
   240         -        return self.wrap_msg(inner["user"], resp)
   241         -
   242         -    # Wait for the next (legit) message
   243         -    def next_msg(self):
   244         -        while True:
   245         -            buf,who = self.sock.recvfrom(MAXMSG)
   246         -            log("Msg rx %s %s: %s" % (who, time.asctime(), buf))
   247         -            pak = self.decode(buf)
   248         -            if pak is not None:
   249         -                pak.who = who
   250         -                return pak
   251         -
   252         -    # Subclass hooks to look up user account
   253         -    def get_password(self, uname):
   254         -        assert False, "Not Implemented"
   255         -        return None
   256         -    def get_hashed_password(self, uname):
   257         -        assert False, "Not Implemented"
   258         -        return None
   259         -
   260         -    # Craft reply to this request
   261         -    def reply(self, inpak, outpak):
   262         -        try:
   263         -            self.sock.sendto(outpak.buf, inpak.who)
   264         -        except:
   265         -            log(" reply failed")
   266         -
   267         -class Client(PONG):
   268         -    def __init__(self, server, port, uname, password):
   269         -        self.server = server
   270         -        self.port = port
   271         -        self.uname = uname
   272         -        self.password = password
   273         -        self.pakseq = 0
   274         -
   275         -        # Used as crypto key (calculated on first use)
   276         -        self.hashedpw = None
   277         -
   278         -    # Password access
   279         -    def get_password(self, uname):
   280         -        assert uname == self.uname
   281         -        return self.password
   282         -    def get_hashed_password(self, uname):
   283         -        assert uname == self.uname
   284         -        if self.hashedpw is None:
   285         -            self.hashedpw = pwhash(self.password)
   286         -        return self.hashedpw
   287         -
   288         -    # Build request, signed, all that stuff
   289         -    # Return JSON encoding of it.
   290         -    # Note this routine scribbles on the dict @req if it is
   291         -    #  passed in.
   292         -    def ping(self, op, subop, req=None):
   293         -        # Start with any params they specified
   294         -        if req is None:
   295         -            req = {}
   296         -
   297         -        # Plug in some standard fields
   298         -        assert hasattr(self, "uname"), "TBD: server->client request?"
   299         -        req["user"] = self.uname
   300         -        req["op"] = op
   301         -        req["subop"] = subop
   302         -        req["pakseq"] = self.pakseq
   303         -        self.pakseq += 1
   304         -
   305         -        # Sign and maybe encrypt
   306         -        return self.wrap_msg(self.uname, req)
   307         -
   308         -    # Receive a message, with timeout
   309         -    # The message is verified for correct sig
   310         -    def recv_msg(self, sock, pakseq, timeout):
   311         -        targtm = time.time() + timeout
   312         -        while True:
   313         -            # No more time
   314         -            tmo = targtm - time.time()
   315         -            if tmo <= 0:
   316         -                log(" out of time")
   317         -                return None
   318         -
   319         -            # Receive, with timeout
   320         -            sock.settimeout(tmo)
   321         -            try:
   322         -                buf,who = sock.recvfrom(MAXMSG)
   323         -            except:
   324         -                # Timeout
   325         -                log(" timeout")
   326         -                return None
   327         -            log("pak %s from %s\n" % (buf, who))
   328         -
   329         -            # Valid?
   330         -            pak = self.unwrap_msg(buf)
   331         -            if pak is not None:
   332         -                pak.who = who
   333         -                return pak
   334         -
   335         -    # Ping-Pong message exchange.  We said something to them,
   336         -    #  and we expect to hear something back.
   337         -    # JSON in each direction.
   338         -    def pingpong(self, pak):
   339         -
   340         -        # A new socket each time.  This is an easy way to make
   341         -        #  sure we bind to the latest IP address, for instance.
   342         -        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
   343         -
   344         -        # Adaptive timeout
   345         -        tmo = WAITRESP
   346         -        pakseq = pak.inner["pakseq"]
   347         -        while True:
   348         -            log("sending %r" % (req,))
   349         -            try:
   350         -                sock.sendto(pak.buf, (ipa, port))
   351         -            except:
   352         -                log(" send failed")
   353         -                # With a network outage, hang back for a bit
   354         -                time.sleep(WAITNET)
   355         -                return Non
   356         -
   357         -            # Get back a response, or None for timeout
   358         -            resp = self.recv_msg(sock, pakseq, tmo)
   359         -            if resp is not None:
   360         -                sock.close()
   361         -                return resp
   362         -
   363         -            # Didn't reach our server
   364         -            log(" timeout")
   365         -            tmo *= 2
   366         -            if tmo > WAITNET:
   367         -                # Backed off to failure
   368         -                sock.close()
   369         -                return None
   370         -
   371         -    # Ping...Pong message behavior.
   372         -    # This is long polling with UDP.  We send a request, and we expect to
   373         -    #  hear back from them when they have something, or a null closing
   374         -    #  message at the timeout.
   375         -    def ping_pong(self, pak, timeout):
   376         -
   377         -        # Request
   378         -        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
   379         -        log("sending %r" % (req,))
   380         -        try:
   381         -            sock.sendto(buf, (ipa, port))
   382         -        except:
   383         -            log(" send failed")
   384         -            # With a network outage, hang back for a bit
   385         -            time.sleep(WAITNET)
   386         -            return Non
   387         -
   388         -        # Get back a response, or None for timeout
   389         -        resp = self.recv_msg(sock, pak.inner["pakseq"], timeout)
   390         -        sock.close()
   391         -
   392         -        # None (timeout) or a response
   393         -        return resp
            1  +chore/pong.py

Changes to udp.py.

    35     35   
    36     36   	# Already calculated
    37     37   	res = hashed.get(uname)
    38     38   	if res is not None:
    39     39   	    return res
    40     40   
    41     41   	# Unknown user?
    42         -	pw = hashed.get(uname)
           42  +	pw = accounts.get(uname)
    43     43   	if pw is None:
    44     44   	    return None
    45     45   
    46     46   	# Calculate hash
    47     47   	hashedpw = pong.pwhash(pw)
    48     48   	# Cache
    49     49   	hashed[uname] = hashedpw
    50     50   	# Return answer
    51     51   	return hashedpw
    52     52   
    53     53   class UDP(object):
    54     54       def __init__(self, cfg, approot):
    55         -	global TIMEOUT
           55  +	global TIMEOUT, accounts
    56     56   
    57     57   	self.approot = approot
    58     58   	TIMEOUT = approot.config["poll1"]
    59     59   	port = self.port = cfg["port"]
    60         -        self.pong = Serve(port)
           60  +        self.conn = Serve(port)
           61  +
           62  +	# Cache any UDP accounts
           63  +	for uname,d in approot.config["user"]:
           64  +	    if "password" not in d:
           65  +		continue
           66  +	    accounts[uname] = d["password"]
    61     67   
    62     68       # Operational parameters
    63     69       def handle_params(self, pak):
    64     70   	inner = pak.inner
    65     71   	subop = inner["subop"]
    66     72   
    67     73   	# Tell them about the operation parameters
    68     74   	if subop == "get":
    69         -	    resp = pong.msg("params", "got", {
    70         -		"timeout": TIMEOUT,
    71         -	    })
    72         -	    XXX
    73         -	    self.pong.reply(pak, resp)
           75  +	    uname = pak.outer["user"]
           76  +	    resp = self.conn.msg(uname, inner["pakseq"],
           77  +		"params", "got", { "timeout": TIMEOUT, })
           78  +	    self.conn.reply(pak, resp)
    74     79   	    return
    75     80   
    76     81   	log(" param unknown subop: " + subop)
    77     82   
    78     83       # Dispatch the latest request
    79     84       def handle(self, pak):
    80     85   	inner = pak.inner
................................................................................
    92     97   
    93     98   	log(" unknown op: " + op)
    94     99   
    95    100       # This is the UDP port service loop.  It runs in its own
    96    101       #  thread, interacting with the rest of the server
    97    102       #  via self.approot
    98    103       def run(self):
    99         -	pdb.set_trace()
   100    104   	while True:
   101    105   
   102    106   	    # Next request
   103         -            pak = self.pong.next_msg()
          107  +            pak = self.conn.next_msg()
   104    108               self.handle(pak)