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 Unified Diffs Show Whitespace Changes Patch

Changes to notified.py.

41
42
43
44
45
46
47

48
49
50
51
52
53
54
..
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
...
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151




152
153
154
155
156
157
158
159
160
...
195
196
197
198
199
200
201




202











#  that it's an XMPP message from Joe.  At detail 0, you
#  would only see that there were new events, but no other
#  detail.
#
import sys, json, time, os
import notify2
import pong


# For DBus sniffing
import dbus, dbus.exceptions, dbus.mainloop.glib
import threading
from gi.repository import GLib

# Are LEDs available for notification indication?
................................................................................
def log(s):
    sys.stderr.write(s)
    sys.stderr.write('\n')

# Quick/easy way to turn dict into an ob with those k/v as attrs
class DictOb(object):
    def __init__(self, d):
        for k,v in d.iteritems():
            setattr(self, str(k), v)

# Configuration, a DictOb
cfg = None

# DBus monitoring.  We need to know if the device is on/active, since
#  we only want to blink the LED if it's off (and only until it
................................................................................
    set_led("rgb_start", "0")
    blinking = True
def unblink():
    global blinking
    set_led("rgb_start", "0")
    blinking = False

# Endless execution, notification client
def run(ipa, port):
    global cfg

    # Get our configuration
    f = open(os.getenv("HOME") + "/.config/notify.json", "r")
    d = json.loads(f.read())
    f.close()
    cfg = DictOb(d)





    # Get a wrapper for our pong network connection
    conn = pong.PONG(cfg.server, cfg.port, cfg.user, cfg.password)

    # Prep LEDs for use if possible
    setup_leds()

    # Watch DBus to turn LED's off when screen unlocks
    setup_dbus()

................................................................................
        if resp["serial"] == serial:
            continue

        # New messages
        notify(resp)

if __name__ == "__main__":




    run()


















>







 







|







 







|
|








>
>
>
>

|







 







>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
..
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
...
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
...
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
#  that it's an XMPP message from Joe.  At detail 0, you
#  would only see that there were new events, but no other
#  detail.
#
import sys, json, time, os
import notify2
import pong
import pdb

# For DBus sniffing
import dbus, dbus.exceptions, dbus.mainloop.glib
import threading
from gi.repository import GLib

# Are LEDs available for notification indication?
................................................................................
def log(s):
    sys.stderr.write(s)
    sys.stderr.write('\n')

# Quick/easy way to turn dict into an ob with those k/v as attrs
class DictOb(object):
    def __init__(self, d):
        for k,v in d.items():
            setattr(self, str(k), v)

# Configuration, a DictOb
cfg = None

# DBus monitoring.  We need to know if the device is on/active, since
#  we only want to blink the LED if it's off (and only until it
................................................................................
    set_led("rgb_start", "0")
    blinking = True
def unblink():
    global blinking
    set_led("rgb_start", "0")
    blinking = False

# Pull config out of JSON save format
def load_cfg():
    global cfg

    # Get our configuration
    f = open(os.getenv("HOME") + "/.config/notify.json", "r")
    d = json.loads(f.read())
    f.close()
    cfg = DictOb(d)

# Endless execution, notification client
def run(ipa, port):
    global cfg

    # Get a wrapper for our pong network connection
    conn = pong.Client(cfg.server, cfg.port, cfg.user, cfg.password)

    # Prep LEDs for use if possible
    setup_leds()

    # Watch DBus to turn LED's off when screen unlocks
    setup_dbus()

................................................................................
        if resp["serial"] == serial:
            continue

        # New messages
        notify(resp)

if __name__ == "__main__":
    load_cfg()

    # No arg, just be a service daemon
    if len(sys.argv) == 1:
        run()

    # Debug mode; get a connection to our test server
    conn = pong.Client(cfg.server, cfg.port, cfg.user, cfg.password)

    # Get params, display, exit
    if sys.argv[1] == "params":
        params = conn.pingpong(conn.msg("params", "get"))
        print(params)

    sys.stderr.write("Unknown operation %s\n" % (sys.argv[1],))
    sys.exit(1)

Changes to pong.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
#
# pong.py
#	UDP notifications infrastructure
# vim: expandtab
#
# Ugh, Ubuntu Phone seems to have pretty much gone to Python 3, so
#  here we are.  This file is written to load under Python 2 or 3.
#
# The protocol uses a JSON-encoded string carried over UDP.
# The message structure is documented below, but at a high
#  level the protocol advances in three stages.  First,
#  the client sends a request to the notification server.
#  Among other items, it indicates the last event serial
#  seen.
# The server immediately responds to the client's port and
#  address.  If its response indicates new events, then
#  the exchange is over.  The client will presumably post
#  notifications and eventually start a new protocol exchange.
# If the server returns an indication that the client has
#  already seen the latest event, it will specify a
#  timeout.  If a new event occurs before that timeout, the
#  server will send the event.  Otherwise, at the timeout
#  interval, the server will send a message specifying
#  (still) no new messages.
# In both cases of the previous paragraph, the exchange is
#  considered finished.  A new exchange from the beginning
#  must be undertaken to receive subsequent notifications.
#
# The relative transience of an exchange, and the lack of
#  any ongoing network connection is intentional.  The client
#  might change its own address, or it might be behind a NAT
#  device which changes its address.  The most that can be
#  disrupted by such a change is a single span of exchanges
#  within the bounds of one timeout period.
# Thus, each exchange starts with the client creating a
#  fresh socket so that no long-term network state is carried.
#
# The initial version of this protocol implements authentication
#  of its operations, but not privacy (i.e., crypto).  Because
#  all the messages are JSON, it's easy to imagine further fields
#  which would configure crypto.  This is TBD.
# The initial version's authentication uses SHA-256 along with
#  a nonce to protect packets from spoofing, modification, or
#  replay.
#
# Each packet thus has a nonce (generated by the system's crypto-
#  quality random number generator) and a SHA-256 derived
#  signature reflecting the shared secret.
#
import socket, sys, json, time, os
from hashlib import sha256
from Crypto.Cipher import AES

# How long to wait for initial/immediate response
# (This is the starting timeout, which backs off with each
#  retry.)
WAITRESP = 1

# How long to hold off network activity after seeing a
#  network failure
WAITNET = 60

# Max sized message
# (You can turn this up, but you really need to start thinking
#  about REST and TCP.)
MAXMSG = 1024

# Hook for logging
def log(s):
    sys.stderr.write(s)
    sys.stderr.write('\n')

# XOR a byte range into one half its size
def fold(buf):
    assert (len(buf) & 1) == 0
    l = len(buf)/2
    return bytes((c1 ^ c2)
        for c1,c2 in zip(buf[:l], buf[l:]))

# Display bytes in hex and back
def tohex(buf):
    return ''.join(("%02x" % (c,)) for c in buf)
def fromhex(buf):
    res = bytearray()
    while buf:
        res.append(buf[:2].decode("hex"))
        buf = buf[2:]
    return res

# SHA256 rep of password string
def pwhash(pw):
    sh = sha256()
    sh.update(cfg.password.encode("utf8"))
    return tohex(sh.digest())

# Views of the packet being sent
class Packet(object):
    def __init__(self, inner, outer, buf):
        self.inner = inner
        self.outer = outer
        self.buf = buf
        # When appropriate, this is the (host,port) tuple
        #  of a recvfrom()
        self.who = None

class PONG(object):
    def __init__(self):
        # Defend against replays
        self.used_nonces = set()

    # SHA256 adapted signature
    def calchash(self, buf, passwd, nonce):
        sh = sha256()
        sh.update(buf)
        sh.update(nonce)
        sh.update(passwd)
        return fold(fold(sh.digest()))

    # Encode and sign this message
    #
    # We're using 64 bits of nonce, and 64 bits of folded SHA256
    # @d may have its contents modified.
    def wrap_msg(self, uname, d):

        # String rep of actual operation
        inner = json.dumps(d)

        # 64 bits of nonce
        nonce = os.urandom(8)

        # And collapse 256 bits of sha256 to 64 bits too
        # (We hash the content, nonce, and shared secret.)
        passwd = self.get_password(uname)
        if passwd is None:
            return None
        sig = self.calchash(inner, passwd, nonce)

        # Outer signature of inner
        outer = {"sig": tohex(sig), "nonce": tohex(nonce)}

        # Encrypt?
        if "crypto" in d:
            # Move the flag to the outer JSON
            assert d["crypto"] == "AES"
            outer["crypto"] = d["crypto"]
            del d["crypto"]

            # Ask our instance for the hashed version of the
            #  PSK with this uname
            hashedpw = self.get_hashed_password(uname)

            # Pad to block boundary with spaces (JSON's OK with
            #  white space)
            li = len(inner)
            bs = AES.block_size
            resid = li % bs
            if resid:
                inner += (" " * (bs - resid))

            # Encrypt the inner
            a = AES.new(hashedpw, AES.MODE_CFB, nonce)
            inner = hex(a.encrypt(inner))

        # Signed and possibly encrypted inner content
        outer["inner"] = inner

        # As a JSON string
        buf = json.dumps(outer)
        assert len(buf) < MAXMSG
        return Packet(d, outer, buf)

    # Decode JSON of message, return Packet or None
    def unwrap_msg(self, buf):
        try:
            # Decode
            outer = json.loads(buf)
            inner = outer["inner"]
            if inner["pakseq"] != pakseq:
                # Answers to this op?
                log(" mismatch pakseq")
                return None
            nonce = fromhex(outer["nonce"])

            # Protect nonce value
            if nonce in self.used_nonces:
                log(" dup nonce")
                return None

            # Crypto?
            uname = outer["user"]
            passwd = self.get_password(uname)
            if passwd is None:
                log("No password for " + uname)
                return None
            if "crypto" in outer:
                if outer["crypto"] != "AES":
                    log(" unknown crypto " + outer["crypto"])
                    return None
                hashedpw = self.get_hashed_password(uname)
                a = AES.new(hashedpw, AES.MODE_CFB, fromhex(nonce))
                inner = a.decrypt(fromhex(inner))

            # Verify sig
            sig = self.calchash(inner, passwd, outer["nonce"])
            if sig != outer["sig"]:
                log(" bad sig")
                return None

            # Convert inner
            inner = json.loads(inner)

        except:
            log(" malformed")
            return None

        # Don't use this nonce again
        self.used_nonces.add(nonce)

        # Here's something signed by our peer
        log(" good pak")
        return Packet(inner, outer, buf)

class Server(PONG):
    def __init__(self, port):
        self.sock = sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.bind(('', port))

    # Build response message to request in @inpak
    # Like PONG.Client.ping(), this routine also modifies the
    #  dict @resp if it's provided.
    def pong(self, inpak, subop, resp=None):
        if resp is None:
            resp = {}
        inner = inpak.inner
        resp["op"] = inner["op"]
        resp["subop"] = subop
        resp["pakseq"] = inner["pakseq"]

        # Sign/encrypt this response
        return self.wrap_msg(inner["user"], resp)

    # Wait for the next (legit) message
    def next_msg(self):
        while True:
            buf,who = self.sock.recvfrom(MAXMSG)
            log("Msg rx %s %s: %s" % (who, time.asctime(), buf))
            pak = self.decode(buf)
            if pak is not None:
                pak.who = who
                return pak

    # Subclass hooks to look up user account
    def get_password(self, uname):
        assert False, "Not Implemented"
        return None
    def get_hashed_password(self, uname):
        assert False, "Not Implemented"
        return None

    # Craft reply to this request
    def reply(self, inpak, outpak):
        try:
            self.sock.sendto(outpak.buf, inpak.who)
        except:
            log(" reply failed")

class Client(PONG):
    def __init__(self, server, port, uname, password):
        self.server = server
        self.port = port
        self.uname = uname
        self.password = password
        self.pakseq = 0

        # Used as crypto key (calculated on first use)
        self.hashedpw = None

    # Password access
    def get_password(self, uname):
        assert uname == self.uname
        return self.password
    def get_hashed_password(self, uname):
        assert uname == self.uname
        if self.hashedpw is None:
            self.hashedpw = pwhash(self.password)
        return self.hashedpw

    # Build request, signed, all that stuff
    # Return JSON encoding of it.
    # Note this routine scribbles on the dict @req if it is
    #  passed in.
    def ping(self, op, subop, req=None):
        # Start with any params they specified
        if req is None:
            req = {}

        # Plug in some standard fields
        assert hasattr(self, "uname"), "TBD: server->client request?"
        req["user"] = self.uname
        req["op"] = op
        req["subop"] = subop
        req["pakseq"] = self.pakseq
        self.pakseq += 1

        # Sign and maybe encrypt
        return self.wrap_msg(self.uname, req)

    # Receive a message, with timeout
    # The message is verified for correct sig
    def recv_msg(self, sock, pakseq, timeout):
        targtm = time.time() + timeout
        while True:
            # No more time
            tmo = targtm - time.time()
            if tmo <= 0:
                log(" out of time")
                return None

            # Receive, with timeout
            sock.settimeout(tmo)
            try:
                buf,who = sock.recvfrom(MAXMSG)
            except:
                # Timeout
                log(" timeout")
                return None
            log("pak %s from %s\n" % (buf, who))

            # Valid?
            pak = self.unwrap_msg(buf)
            if pak is not None:
                pak.who = who
                return pak

    # Ping-Pong message exchange.  We said something to them,
    #  and we expect to hear something back.
    # JSON in each direction.
    def pingpong(self, pak):

        # A new socket each time.  This is an easy way to make
        #  sure we bind to the latest IP address, for instance.
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

        # Adaptive timeout
        tmo = WAITRESP
        pakseq = pak.inner["pakseq"]
        while True:
            log("sending %r" % (req,))
            try:
                sock.sendto(pak.buf, (ipa, port))
            except:
                log(" send failed")
                # With a network outage, hang back for a bit
                time.sleep(WAITNET)
                return Non

            # Get back a response, or None for timeout
            resp = self.recv_msg(sock, pakseq, tmo)
            if resp is not None:
                sock.close()
                return resp

            # Didn't reach our server
            log(" timeout")
            tmo *= 2
            if tmo > WAITNET:
                # Backed off to failure
                sock.close()
                return None

    # Ping...Pong message behavior.
    # This is long polling with UDP.  We send a request, and we expect to
    #  hear back from them when they have something, or a null closing
    #  message at the timeout.
    def ping_pong(self, pak, timeout):

        # Request
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        log("sending %r" % (req,))
        try:
            sock.sendto(buf, (ipa, port))
        except:
            log(" send failed")
            # With a network outage, hang back for a bit
            time.sleep(WAITNET)
            return Non

        # Get back a response, or None for timeout
        resp = self.recv_msg(sock, pak.inner["pakseq"], timeout)
        sock.close()

        # None (timeout) or a response
        return resp
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

1








































































































































































































































































































































































































chore/pong.py







































































































































































































































































































































































































Changes to udp.py.

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60






61
62
63
64
65
66
67
68
69

70
71
72
73
74
75
76
77
78
79
80
..
92
93
94
95
96
97
98
99
100
101
102
103
104

	# Already calculated
	res = hashed.get(uname)
	if res is not None:
	    return res

	# Unknown user?
	pw = hashed.get(uname)
	if pw is None:
	    return None

	# Calculate hash
	hashedpw = pong.pwhash(pw)
	# Cache
	hashed[uname] = hashedpw
	# Return answer
	return hashedpw

class UDP(object):
    def __init__(self, cfg, approot):
	global TIMEOUT

	self.approot = approot
	TIMEOUT = approot.config["poll1"]
	port = self.port = cfg["port"]
        self.pong = Serve(port)







    # Operational parameters
    def handle_params(self, pak):
	inner = pak.inner
	subop = inner["subop"]

	# Tell them about the operation parameters
	if subop == "get":
	    resp = pong.msg("params", "got", {

		"timeout": TIMEOUT,
	    })
	    XXX
	    self.pong.reply(pak, resp)
	    return

	log(" param unknown subop: " + subop)

    # Dispatch the latest request
    def handle(self, pak):
	inner = pak.inner
................................................................................

	log(" unknown op: " + op)

    # This is the UDP port service loop.  It runs in its own
    #  thread, interacting with the rest of the server
    #  via self.approot
    def run(self):
	pdb.set_trace()
	while True:

	    # Next request
            pak = self.pong.next_msg()
            self.handle(pak)







|












|




|
>
>
>
>
>
>








|
>
|
<
<
|







 







<



|

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77


78
79
80
81
82
83
84
85
..
97
98
99
100
101
102
103

104
105
106
107
108

	# Already calculated
	res = hashed.get(uname)
	if res is not None:
	    return res

	# Unknown user?
	pw = accounts.get(uname)
	if pw is None:
	    return None

	# Calculate hash
	hashedpw = pong.pwhash(pw)
	# Cache
	hashed[uname] = hashedpw
	# Return answer
	return hashedpw

class UDP(object):
    def __init__(self, cfg, approot):
	global TIMEOUT, accounts

	self.approot = approot
	TIMEOUT = approot.config["poll1"]
	port = self.port = cfg["port"]
        self.conn = Serve(port)

	# Cache any UDP accounts
	for uname,d in approot.config["user"]:
	    if "password" not in d:
		continue
	    accounts[uname] = d["password"]

    # Operational parameters
    def handle_params(self, pak):
	inner = pak.inner
	subop = inner["subop"]

	# Tell them about the operation parameters
	if subop == "get":
	    uname = pak.outer["user"]
	    resp = self.conn.msg(uname, inner["pakseq"],
		"params", "got", { "timeout": TIMEOUT, })


	    self.conn.reply(pak, resp)
	    return

	log(" param unknown subop: " + subop)

    # Dispatch the latest request
    def handle(self, pak):
	inner = pak.inner
................................................................................

	log(" unknown op: " + op)

    # This is the UDP port service loop.  It runs in its own
    #  thread, interacting with the rest of the server
    #  via self.approot
    def run(self):

	while True:

	    # Next request
            pak = self.conn.next_msg()
            self.handle(pak)