webXMPP

Check-in [dd233913a4]
Login

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

Overview
Comment:Two features: SMS blacklist Adjust config now that we have two SMS config params (acceptable submitters for REST, and blacklist pattern) And yes, blacklisting of numbers, just a simple string match Also a telnet interface, so there's now CLI-friendly messaging.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:dd233913a45aa3bd1b68c2548a06b778fb64880a90dfd9f8cd0fe356965efa8f
User & Date: ajv-899-334-8894@vsta.org 2018-01-18 00:31:27
Context
2018-03-12
14:59
Long cookie life check-in: ba6eb42033 user: ajv-899-334-8894@vsta.org tags: master, trunk
2018-01-18
00:31
Two features: SMS blacklist Adjust config now that we have two SMS config params (acceptable submitters for REST, and blacklist pattern) And yes, blacklisting of numbers, just a simple string match Also a telnet interface, so there's now CLI-friendly messaging. check-in: dd233913a4 user: ajv-899-334-8894@vsta.org tags: master, trunk
00:30
Tidy a comment check-in: 29dee4f7c7 user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to main.py.

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
..
67
68
69
70
71
72
73
74

75
76


77
78
79
80
81
82
83
...
140
141
142
143
144
145
146


147
148
149



150
151
152
153
154
155
156
...
198
199
200
201
202
203
204
205
206

207


208
209
210
211
212
213
214
from get import GET_mixin
from post import POST_mixin
from put import PUT_mixin
from fcm import FCM_mixin
from local import Local_Server_mixin
from chore.authen import Authen_mixin, Authen_Server_mixin
import chore
import udp

# Seconds between search for timeouts
TIME_GRANULARITY = 5

# Web interface handling
class App_Handler(chore.handlers.Chore_Handler, GET_mixin,
	POST_mixin, PUT_mixin, Authen_mixin):
................................................................................
	c.ints.add( (f,) )
    for f in ("pollslop",):
	c.floats.add( (f,) )

    # Messaging key from Google for Firebase Cloud Messaging
    c.onearg.add( ("fcmkey",) )

    # Multiple of these, each with a list of args

    c.args.add( ("sms",) )
    c.mults.add( ("sms",) )



    # Multiple of these, with sub-config
    c.onearg.add( ("user",) )
    c.mults.add( ("user",) )

    # Sub-config of "user", XMPP account(s)
    c.args.add( ("user", "account") )
................................................................................

	# Callbacks when time expires
	# List of tuples; (expires-tm, call-fn, call-arg)
	self.timeouts = []

	# SMS source filters, if any
	self.smsok = []


	for tup in cfg.get("sms", ()):
	    for a in tup:
		self.smsok.append(chore.net.parseaddr(a))




	# Static/local authentication config?  Can't mix with
	#  an authentication server.
	if any(("password" in acct[1]) for acct in cfg["user"]):
	    self.authentication.append(WebXMPP.auth_local)
	else:
	    # Account server to get logged in & get cookie
................................................................................
	# Mint a new, active user
	u = User(self, uname, cfg["account"])
	self.users[user] = u
	u.start()
	return True

    # In addition to Chore's service for HTTP(s), we also
    #  can have a UDP protocol module
    def serve_proto(self, proto, cfg):

	server = udp.UDP(cfg, self)


	return server

if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.stderr.write("Usage is: %s <config-file>\n" %
            (sys.argv[0],))
        sys.exit(1)







|







 







|
>
|
|
>
>







 







>
>
|
|
|
>
>
>







 







|

>
|
>
>







7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
..
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
...
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
...
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
from get import GET_mixin
from post import POST_mixin
from put import PUT_mixin
from fcm import FCM_mixin
from local import Local_Server_mixin
from chore.authen import Authen_mixin, Authen_Server_mixin
import chore
import udp, telnet

# Seconds between search for timeouts
TIME_GRANULARITY = 5

# Web interface handling
class App_Handler(chore.handlers.Chore_Handler, GET_mixin,
	POST_mixin, PUT_mixin, Authen_mixin):
................................................................................
	c.ints.add( (f,) )
    for f in ("pollslop",):
	c.floats.add( (f,) )

    # Messaging key from Google for Firebase Cloud Messaging
    c.onearg.add( ("fcmkey",) )

    # Lists of SMS peers, also a blacklist
    c.noarg.add( ("sms",) )
    c.args.add( ("sms", "peers") )
    c.mults.add( ("sms", "peers") )
    c.args.add( ("sms", "blacklist") )
    c.mults.add( ("sms", "blacklist") )

    # Multiple of these, with sub-config
    c.onearg.add( ("user",) )
    c.mults.add( ("user",) )

    # Sub-config of "user", XMPP account(s)
    c.args.add( ("user", "account") )
................................................................................

	# Callbacks when time expires
	# List of tuples; (expires-tm, call-fn, call-arg)
	self.timeouts = []

	# SMS source filters, if any
	self.smsok = []
	self.smsbad = []
	if "sms" in cfg:
	    for tup in cfg["sms"][1].get("peers"):
		for a in tup:
		    self.smsok.append(chore.net.parseaddr(a))
	    for tup in cfg["sms"][1].get("blacklist"):
		for a in tup:
		    self.smsbad.append(a)

	# Static/local authentication config?  Can't mix with
	#  an authentication server.
	if any(("password" in acct[1]) for acct in cfg["user"]):
	    self.authentication.append(WebXMPP.auth_local)
	else:
	    # Account server to get logged in & get cookie
................................................................................
	# Mint a new, active user
	u = User(self, uname, cfg["account"])
	self.users[user] = u
	u.start()
	return True

    # In addition to Chore's service for HTTP(s), we also
    #  can have UDP and telnet protocol modules
    def serve_proto(self, proto, cfg):
	if proto == "udp":
	    server = udp.UDP(cfg, self)
	elif proto == "telnet":
	    server = telnet.TelnetD(cfg, self)
	return server

if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.stderr.write("Usage is: %s <config-file>\n" %
            (sys.argv[0],))
        sys.exit(1)

Changes to post.py.

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
	if not self.path_match("sms.json"):
	    return False,None

	# Verify that it's a legit source
	sys.stderr.write("POST SMS: %s\n" % (buf,))
	approot = self.server.approot
	if approot.smsok:
	    sys.stderr.write(" check IP %s\n" % (self.client_address,))
	    addr = self.client_address[0]
	    if not chore.net.ok_ip(approot.smsok, addr):
		# Just close'em off
		sys.stderr.write("Unknown SMS peer: %r\n" % (addr,))
		self.conn.close()
		sys.exit(0)

	# Decode message payload
	msg = json.loads(buf)
	sys.stderr.write(" JSON result %s\n" % (msg,))








	# Hand it off
	acct_sms.incoming(msg)

	# If we error, they just re-deliver more times, so always
	#  accept it, even if we have to log an issue with handling it.
	return True,self.send_result("", "text/html")








<











>
>
>
>
>
>
>







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
	if not self.path_match("sms.json"):
	    return False,None

	# Verify that it's a legit source
	sys.stderr.write("POST SMS: %s\n" % (buf,))
	approot = self.server.approot
	if approot.smsok:

	    addr = self.client_address[0]
	    if not chore.net.ok_ip(approot.smsok, addr):
		# Just close'em off
		sys.stderr.write("Unknown SMS peer: %r\n" % (addr,))
		self.conn.close()
		sys.exit(0)

	# Decode message payload
	msg = json.loads(buf)
	sys.stderr.write(" JSON result %s\n" % (msg,))

	# Pre-filter
	for a in approot.smsbad:
	    if a in msg.get("from", ""):
		sys.stderr.write("Blacklist SMS from %s\n" %
		    (msg["from"],))
		return True,self.send_result("", "text/html")

	# Hand it off
	acct_sms.incoming(msg)

	# If we error, they just re-deliver more times, so always
	#  accept it, even if we have to log an issue with handling it.
	return True,self.send_result("", "text/html")

Added telnet.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
#
# telnet.py
#	An interface via telnet
#
import sys, threading, socket
import pdb

# Pre-tabulation of telnet accounts
accounts = {}

# Log some debug
def log(s):
    sys.stderr.write("%s\n" % (s,))

# Keep binary cruft out of account strings
def cleanup(s):
    res = ""
    for c in s:
	ci = ord(c)
	if (ci < 32) or (ci > 126):
	    res += ' '
	    continue
	res += c
    return res

# This sits on the "new message" queue, and pushes the new messages
#  out to the telnet user when it gets told about new stuff
# We use our own thread, because our writes out to our cient can
#  be arbitrarily slow.
class Watcher(object):
    def __init__(self, user, telnet):
	self.user = user
	self.done = False
	self.telnet = telnet

    # Endless processing loop
    def run(self):
	sema = threading.Semaphore(0)

	while True:
	    # Show any new messages
	    self.telnet.updates()

	    # Wait for new ones
	    self.user.await(999999, sema, None, None)
	    sema.acquire()
	    if self.done:
		log(" session done")
		sys.exit(0)

	    log("telnet notify %s" % (self.user.name,))

# One of these per user session.  It waits for a new notification,
#  and displays it to the telnet user

class Telnet(object):
    def __init__(self, sock, approot):
	self.approot = approot
	self.user = None
	self.sock = sock
	self.gen = -1
	self.curwho = ""

    # Show any new messages
    def updates(self):
	newgen = gen = self.gen
	for tup in self.user.msgs:
	    if tup[0] <= gen:
		continue

	    # Direction (rx/tx) indication
	    s = ">" if tup[1] else "<"

	    # Who's on the other side?  Ellide if it's the same as the
	    #  last display
	    if tup[2] != self.curwho:
		self.writeln(s + ("%s:" % (tup[2],)))
		self.curwho = tup[2]
		s = ""
	    s += " "

	    # Message body
	    s += cleanup(tup[3])

	    # Display to telnet client
	    self.writeln(s)

	    # Update to latest generation seen
	    newgen = max(newgen, tup[0])

	# Update input generation
	self.gen = newgen

    # Switch self.curwho, or display why you can't
    # Return True if we switched to somebody, False on any sort of
    #  error (unknown, ambig)
    def set_dest(self, who):
	matches = []
	for name in self.user.roster:
	    if who in name:
		matches.append(name)

	# Unknown recipient
	if not matches:
	    self.writeln("No destination matches.")
	    return False

	# Ambig, so list resolutions.  Note ">" by itself lists all
	#  recipients in your roster.
	if len(matches) > 1:
	    self.writeln("Matches:")
	    for name in matches:
		self.writeln(" %s" % (name,))
	    return False

	# Here's the new dest
	self.writeln("Dest set to: %s" % (matches[0],))
	self.curwho = matches[0]
	return True

    # Do the login dance
    def login(self):
	global accounts

	# Get username
	self.writeln("User name:")
	l = self.readln()
	uname = cleanup(l)
	if uname not in accounts:
	    self.sock.close()
	    sys.exit(0)

	# and password
	self.writeln("Password:")
	l = self.readln()
	l = cleanup(l)
	if l != accounts[uname]:
	    self.writeln("Bad login.")
	    self.sock.flush()
	    self.sock.close()
	    sys.exit(0)

	# Now we know who we are
	self.user = self.approot.users[uname]

	# Clear screen since password is on it
	self.writeln("%s[H%s[J" % (chr(27), chr(27)))

    # Clean up and exit
    def done(self):
	self.sock.close()
	self.waiter.done = True
	sys.exit(0)

    # Hook for commands
    def cmds(self, args):
	# What do they want?
	op = args[0] if args else None

	# Buh-bye
	if op in ("quit", "q"):
	    self.done()

	# List online users
	if op in ("roster", "r"):
	    user = self.user
	    for name in user.roster:
		st = user.status.get(name)
		if st is None:
		    continue
		if st in ("available", "chat"):
		    c = '+'
		elif st in ("away",):
		    c = '-'
		elif st in ("xa",):
		    c = '.'
		elif st in ("dnd",):
		    c = '*'
		else:
		    continue
		self.writeln("%s %s" % (c, name))
	    return

	# Help
	self.writeln("Commands are:")
	self.writeln(" quit")
	self.writeln(" roster")

    # I/O to socket
    def writeln(self, s):
	try:
	    self.sock.send(s + "\r\n")
	except:
	    self.done()
    def readln(self):
	res = ""
	while True:
	    try:
		c = self.sock.recv(1)
	    except:
		self.done()
	    if not c:
		self.done()
	    # Line ends are \r\n, so ignore \r and wait for \n
	    if c == '\r':
		continue
	    if c == '\n':
		return res
	    res += c

    # New telnet connection; get auth, verify, then go into
    #  message mode
    def run(self):
	# This won't return if they fail us
	self.login()
	user = self.user

	# Our own thread reads their typing, this one pushes
	#  out received messages
	self.waiter = w = Watcher(user, self)
	t = threading.Thread(target=w.run)
	t.start()

	# Endless input loop
	while True:
	    l = cleanup(self.readln()).strip()
	    if not l:
		self.writeln("Current recipient: %s" %
		    (self,curwho or "(none)",))
		continue

	    # Session commands
	    if l.startswith("::"):
		self.cmds(l[2:].split())
		continue

	    # Destination selection
	    if l[0] == '>':
		# You can do ">dest msg..." or just switch dest's
		if ' ' in l[0]:
		    idx = l.index(' ')
		    who = l[0][:idx]
		    rest = l[idx:].strip()
		else:
		    who = l
		    rest = None

		# Decode dest, and don't send if it didn't simply set a dest,
		#  or if there is no message on this line
		if (not self.set_dest(who[1:])) or (not rest):
		    continue

		# Send this
		l = rest

	    # We have a message body to send

	    # Our account to use for this dest (our SMS, etc.)
	    assert self.curwho
	    acct = user.roster[self.curwho]

	    # Send via our account to them
	    acct.send(self.curwho, l)

# Telnet Daemon
#
# Accepts telnet connections, and spins up a separate thread
#  to talk to the user on that connection
class TelnetD(object):
    def __init__(self, cfg, approot):
	self.approot = approot

	# Proto port, addr with default of localhost only (so, ssh
	#  in to your host, *then* use this insecure proto)
	self.port = port = cfg["port"]
	addr = cfg.get("addr", "127.0.0.1")
	s = self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
	s.bind( (addr, port) )
	s.listen(4)

    # This is the telnet port service loop.  We wait for incoming
    #  telnet connections, and run Telnet instances for them
    def run(self):
	global accounts

	# Pull in telnet accounts
	for uname,d in self.approot.config["user"]:
	    for tup,d2 in d["account"]:
		if tup[0] == "telnet":
		    accounts[uname] = tup[1]

	while True:

	    # Next request
	    conn,addr = self.sock.accept()
	    log("telnet from %r" % (addr,))
	    tn = Telnet(conn, self.approot)
	    t = threading.Thread(target=tn.run)
	    t.start()

Changes to user.py.

94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
			#  waiting to get them in our history.
			for tup2 in cfg.get("sms", ()):
			    # tup2 = (Name, phone#)
			    phnum = acct_sms.normalize(tup2[1])
			    a.register(tup2[0], tup2[1])
			    self.roster["sms:" + tup2[0]] = a

		    # UDP PONG server password
		    elif typ == "pong":
			# Resolved by udp.py
			pass

		    # Checked in config during startup?
		    else:
			raise Exception, "Bad account: %r" % (tup,)

    # After the system sees us without a user long enough, it figures the







|
|
|







94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
			#  waiting to get them in our history.
			for tup2 in cfg.get("sms", ()):
			    # tup2 = (Name, phone#)
			    phnum = acct_sms.normalize(tup2[1])
			    a.register(tup2[0], tup2[1])
			    self.roster["sms:" + tup2[0]] = a

		    # UDP PONG server password, telnet server
		    elif typ in ("pong", "telnet"):
			# Resolved by server init
			pass

		    # Checked in config during startup?
		    else:
			raise Exception, "Bad account: %r" % (tup,)

    # After the system sees us without a user long enough, it figures the