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 Side-by-Side Diffs Ignore Whitespace Patch

Changes to main.py.

     7      7   from get import GET_mixin
     8      8   from post import POST_mixin
     9      9   from put import PUT_mixin
    10     10   from fcm import FCM_mixin
    11     11   from local import Local_Server_mixin
    12     12   from chore.authen import Authen_mixin, Authen_Server_mixin
    13     13   import chore
    14         -import udp
           14  +import udp, telnet
    15     15   
    16     16   # Seconds between search for timeouts
    17     17   TIME_GRANULARITY = 5
    18     18   
    19     19   # Web interface handling
    20     20   class App_Handler(chore.handlers.Chore_Handler, GET_mixin,
    21     21   	POST_mixin, PUT_mixin, Authen_mixin):
................................................................................
    67     67   	c.ints.add( (f,) )
    68     68       for f in ("pollslop",):
    69     69   	c.floats.add( (f,) )
    70     70   
    71     71       # Messaging key from Google for Firebase Cloud Messaging
    72     72       c.onearg.add( ("fcmkey",) )
    73     73   
    74         -    # Multiple of these, each with a list of args
    75         -    c.args.add( ("sms",) )
    76         -    c.mults.add( ("sms",) )
           74  +    # Lists of SMS peers, also a blacklist
           75  +    c.noarg.add( ("sms",) )
           76  +    c.args.add( ("sms", "peers") )
           77  +    c.mults.add( ("sms", "peers") )
           78  +    c.args.add( ("sms", "blacklist") )
           79  +    c.mults.add( ("sms", "blacklist") )
    77     80   
    78     81       # Multiple of these, with sub-config
    79     82       c.onearg.add( ("user",) )
    80     83       c.mults.add( ("user",) )
    81     84   
    82     85       # Sub-config of "user", XMPP account(s)
    83     86       c.args.add( ("user", "account") )
................................................................................
   140    143   
   141    144   	# Callbacks when time expires
   142    145   	# List of tuples; (expires-tm, call-fn, call-arg)
   143    146   	self.timeouts = []
   144    147   
   145    148   	# SMS source filters, if any
   146    149   	self.smsok = []
   147         -	for tup in cfg.get("sms", ()):
   148         -	    for a in tup:
   149         -		self.smsok.append(chore.net.parseaddr(a))
          150  +	self.smsbad = []
          151  +	if "sms" in cfg:
          152  +	    for tup in cfg["sms"][1].get("peers"):
          153  +		for a in tup:
          154  +		    self.smsok.append(chore.net.parseaddr(a))
          155  +	    for tup in cfg["sms"][1].get("blacklist"):
          156  +		for a in tup:
          157  +		    self.smsbad.append(a)
   150    158   
   151    159   	# Static/local authentication config?  Can't mix with
   152    160   	#  an authentication server.
   153    161   	if any(("password" in acct[1]) for acct in cfg["user"]):
   154    162   	    self.authentication.append(WebXMPP.auth_local)
   155    163   	else:
   156    164   	    # Account server to get logged in & get cookie
................................................................................
   198    206   	# Mint a new, active user
   199    207   	u = User(self, uname, cfg["account"])
   200    208   	self.users[user] = u
   201    209   	u.start()
   202    210   	return True
   203    211   
   204    212       # In addition to Chore's service for HTTP(s), we also
   205         -    #  can have a UDP protocol module
          213  +    #  can have UDP and telnet protocol modules
   206    214       def serve_proto(self, proto, cfg):
   207         -	server = udp.UDP(cfg, self)
          215  +	if proto == "udp":
          216  +	    server = udp.UDP(cfg, self)
          217  +	elif proto == "telnet":
          218  +	    server = telnet.TelnetD(cfg, self)
   208    219   	return server
   209    220   
   210    221   if __name__ == "__main__":
   211    222       if len(sys.argv) != 2:
   212    223           sys.stderr.write("Usage is: %s <config-file>\n" %
   213    224               (sys.argv[0],))
   214    225           sys.exit(1)

Changes to post.py.

    23     23   	if not self.path_match("sms.json"):
    24     24   	    return False,None
    25     25   
    26     26   	# Verify that it's a legit source
    27     27   	sys.stderr.write("POST SMS: %s\n" % (buf,))
    28     28   	approot = self.server.approot
    29     29   	if approot.smsok:
    30         -	    sys.stderr.write(" check IP %s\n" % (self.client_address,))
    31     30   	    addr = self.client_address[0]
    32     31   	    if not chore.net.ok_ip(approot.smsok, addr):
    33     32   		# Just close'em off
    34     33   		sys.stderr.write("Unknown SMS peer: %r\n" % (addr,))
    35     34   		self.conn.close()
    36     35   		sys.exit(0)
    37     36   
    38     37   	# Decode message payload
    39     38   	msg = json.loads(buf)
    40     39   	sys.stderr.write(" JSON result %s\n" % (msg,))
    41     40   
           41  +	# Pre-filter
           42  +	for a in approot.smsbad:
           43  +	    if a in msg.get("from", ""):
           44  +		sys.stderr.write("Blacklist SMS from %s\n" %
           45  +		    (msg["from"],))
           46  +		return True,self.send_result("", "text/html")
           47  +
    42     48   	# Hand it off
    43     49   	acct_sms.incoming(msg)
    44     50   
    45     51   	# If we error, they just re-deliver more times, so always
    46     52   	#  accept it, even if we have to log an issue with handling it.
    47     53   	return True,self.send_result("", "text/html")
    48     54   

Added telnet.py.

            1  +#
            2  +# telnet.py
            3  +#	An interface via telnet
            4  +#
            5  +import sys, threading, socket
            6  +import pdb
            7  +
            8  +# Pre-tabulation of telnet accounts
            9  +accounts = {}
           10  +
           11  +# Log some debug
           12  +def log(s):
           13  +    sys.stderr.write("%s\n" % (s,))
           14  +
           15  +# Keep binary cruft out of account strings
           16  +def cleanup(s):
           17  +    res = ""
           18  +    for c in s:
           19  +	ci = ord(c)
           20  +	if (ci < 32) or (ci > 126):
           21  +	    res += ' '
           22  +	    continue
           23  +	res += c
           24  +    return res
           25  +
           26  +# This sits on the "new message" queue, and pushes the new messages
           27  +#  out to the telnet user when it gets told about new stuff
           28  +# We use our own thread, because our writes out to our cient can
           29  +#  be arbitrarily slow.
           30  +class Watcher(object):
           31  +    def __init__(self, user, telnet):
           32  +	self.user = user
           33  +	self.done = False
           34  +	self.telnet = telnet
           35  +
           36  +    # Endless processing loop
           37  +    def run(self):
           38  +	sema = threading.Semaphore(0)
           39  +
           40  +	while True:
           41  +	    # Show any new messages
           42  +	    self.telnet.updates()
           43  +
           44  +	    # Wait for new ones
           45  +	    self.user.await(999999, sema, None, None)
           46  +	    sema.acquire()
           47  +	    if self.done:
           48  +		log(" session done")
           49  +		sys.exit(0)
           50  +
           51  +	    log("telnet notify %s" % (self.user.name,))
           52  +
           53  +# One of these per user session.  It waits for a new notification,
           54  +#  and displays it to the telnet user
           55  +
           56  +class Telnet(object):
           57  +    def __init__(self, sock, approot):
           58  +	self.approot = approot
           59  +	self.user = None
           60  +	self.sock = sock
           61  +	self.gen = -1
           62  +	self.curwho = ""
           63  +
           64  +    # Show any new messages
           65  +    def updates(self):
           66  +	newgen = gen = self.gen
           67  +	for tup in self.user.msgs:
           68  +	    if tup[0] <= gen:
           69  +		continue
           70  +
           71  +	    # Direction (rx/tx) indication
           72  +	    s = ">" if tup[1] else "<"
           73  +
           74  +	    # Who's on the other side?  Ellide if it's the same as the
           75  +	    #  last display
           76  +	    if tup[2] != self.curwho:
           77  +		self.writeln(s + ("%s:" % (tup[2],)))
           78  +		self.curwho = tup[2]
           79  +		s = ""
           80  +	    s += " "
           81  +
           82  +	    # Message body
           83  +	    s += cleanup(tup[3])
           84  +
           85  +	    # Display to telnet client
           86  +	    self.writeln(s)
           87  +
           88  +	    # Update to latest generation seen
           89  +	    newgen = max(newgen, tup[0])
           90  +
           91  +	# Update input generation
           92  +	self.gen = newgen
           93  +
           94  +    # Switch self.curwho, or display why you can't
           95  +    # Return True if we switched to somebody, False on any sort of
           96  +    #  error (unknown, ambig)
           97  +    def set_dest(self, who):
           98  +	matches = []
           99  +	for name in self.user.roster:
          100  +	    if who in name:
          101  +		matches.append(name)
          102  +
          103  +	# Unknown recipient
          104  +	if not matches:
          105  +	    self.writeln("No destination matches.")
          106  +	    return False
          107  +
          108  +	# Ambig, so list resolutions.  Note ">" by itself lists all
          109  +	#  recipients in your roster.
          110  +	if len(matches) > 1:
          111  +	    self.writeln("Matches:")
          112  +	    for name in matches:
          113  +		self.writeln(" %s" % (name,))
          114  +	    return False
          115  +
          116  +	# Here's the new dest
          117  +	self.writeln("Dest set to: %s" % (matches[0],))
          118  +	self.curwho = matches[0]
          119  +	return True
          120  +
          121  +    # Do the login dance
          122  +    def login(self):
          123  +	global accounts
          124  +
          125  +	# Get username
          126  +	self.writeln("User name:")
          127  +	l = self.readln()
          128  +	uname = cleanup(l)
          129  +	if uname not in accounts:
          130  +	    self.sock.close()
          131  +	    sys.exit(0)
          132  +
          133  +	# and password
          134  +	self.writeln("Password:")
          135  +	l = self.readln()
          136  +	l = cleanup(l)
          137  +	if l != accounts[uname]:
          138  +	    self.writeln("Bad login.")
          139  +	    self.sock.flush()
          140  +	    self.sock.close()
          141  +	    sys.exit(0)
          142  +
          143  +	# Now we know who we are
          144  +	self.user = self.approot.users[uname]
          145  +
          146  +	# Clear screen since password is on it
          147  +	self.writeln("%s[H%s[J" % (chr(27), chr(27)))
          148  +
          149  +    # Clean up and exit
          150  +    def done(self):
          151  +	self.sock.close()
          152  +	self.waiter.done = True
          153  +	sys.exit(0)
          154  +
          155  +    # Hook for commands
          156  +    def cmds(self, args):
          157  +	# What do they want?
          158  +	op = args[0] if args else None
          159  +
          160  +	# Buh-bye
          161  +	if op in ("quit", "q"):
          162  +	    self.done()
          163  +
          164  +	# List online users
          165  +	if op in ("roster", "r"):
          166  +	    user = self.user
          167  +	    for name in user.roster:
          168  +		st = user.status.get(name)
          169  +		if st is None:
          170  +		    continue
          171  +		if st in ("available", "chat"):
          172  +		    c = '+'
          173  +		elif st in ("away",):
          174  +		    c = '-'
          175  +		elif st in ("xa",):
          176  +		    c = '.'
          177  +		elif st in ("dnd",):
          178  +		    c = '*'
          179  +		else:
          180  +		    continue
          181  +		self.writeln("%s %s" % (c, name))
          182  +	    return
          183  +
          184  +	# Help
          185  +	self.writeln("Commands are:")
          186  +	self.writeln(" quit")
          187  +	self.writeln(" roster")
          188  +
          189  +    # I/O to socket
          190  +    def writeln(self, s):
          191  +	try:
          192  +	    self.sock.send(s + "\r\n")
          193  +	except:
          194  +	    self.done()
          195  +    def readln(self):
          196  +	res = ""
          197  +	while True:
          198  +	    try:
          199  +		c = self.sock.recv(1)
          200  +	    except:
          201  +		self.done()
          202  +	    if not c:
          203  +		self.done()
          204  +	    # Line ends are \r\n, so ignore \r and wait for \n
          205  +	    if c == '\r':
          206  +		continue
          207  +	    if c == '\n':
          208  +		return res
          209  +	    res += c
          210  +
          211  +    # New telnet connection; get auth, verify, then go into
          212  +    #  message mode
          213  +    def run(self):
          214  +	# This won't return if they fail us
          215  +	self.login()
          216  +	user = self.user
          217  +
          218  +	# Our own thread reads their typing, this one pushes
          219  +	#  out received messages
          220  +	self.waiter = w = Watcher(user, self)
          221  +	t = threading.Thread(target=w.run)
          222  +	t.start()
          223  +
          224  +	# Endless input loop
          225  +	while True:
          226  +	    l = cleanup(self.readln()).strip()
          227  +	    if not l:
          228  +		self.writeln("Current recipient: %s" %
          229  +		    (self,curwho or "(none)",))
          230  +		continue
          231  +
          232  +	    # Session commands
          233  +	    if l.startswith("::"):
          234  +		self.cmds(l[2:].split())
          235  +		continue
          236  +
          237  +	    # Destination selection
          238  +	    if l[0] == '>':
          239  +		# You can do ">dest msg..." or just switch dest's
          240  +		if ' ' in l[0]:
          241  +		    idx = l.index(' ')
          242  +		    who = l[0][:idx]
          243  +		    rest = l[idx:].strip()
          244  +		else:
          245  +		    who = l
          246  +		    rest = None
          247  +
          248  +		# Decode dest, and don't send if it didn't simply set a dest,
          249  +		#  or if there is no message on this line
          250  +		if (not self.set_dest(who[1:])) or (not rest):
          251  +		    continue
          252  +
          253  +		# Send this
          254  +		l = rest
          255  +
          256  +	    # We have a message body to send
          257  +
          258  +	    # Our account to use for this dest (our SMS, etc.)
          259  +	    assert self.curwho
          260  +	    acct = user.roster[self.curwho]
          261  +
          262  +	    # Send via our account to them
          263  +	    acct.send(self.curwho, l)
          264  +
          265  +# Telnet Daemon
          266  +#
          267  +# Accepts telnet connections, and spins up a separate thread
          268  +#  to talk to the user on that connection
          269  +class TelnetD(object):
          270  +    def __init__(self, cfg, approot):
          271  +	self.approot = approot
          272  +
          273  +	# Proto port, addr with default of localhost only (so, ssh
          274  +	#  in to your host, *then* use this insecure proto)
          275  +	self.port = port = cfg["port"]
          276  +	addr = cfg.get("addr", "127.0.0.1")
          277  +	s = self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
          278  +	s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
          279  +	s.bind( (addr, port) )
          280  +	s.listen(4)
          281  +
          282  +    # This is the telnet port service loop.  We wait for incoming
          283  +    #  telnet connections, and run Telnet instances for them
          284  +    def run(self):
          285  +	global accounts
          286  +
          287  +	# Pull in telnet accounts
          288  +	for uname,d in self.approot.config["user"]:
          289  +	    for tup,d2 in d["account"]:
          290  +		if tup[0] == "telnet":
          291  +		    accounts[uname] = tup[1]
          292  +
          293  +	while True:
          294  +
          295  +	    # Next request
          296  +	    conn,addr = self.sock.accept()
          297  +	    log("telnet from %r" % (addr,))
          298  +	    tn = Telnet(conn, self.approot)
          299  +	    t = threading.Thread(target=tn.run)
          300  +	    t.start()

Changes to user.py.

    94     94   			#  waiting to get them in our history.
    95     95   			for tup2 in cfg.get("sms", ()):
    96     96   			    # tup2 = (Name, phone#)
    97     97   			    phnum = acct_sms.normalize(tup2[1])
    98     98   			    a.register(tup2[0], tup2[1])
    99     99   			    self.roster["sms:" + tup2[0]] = a
   100    100   
   101         -		    # UDP PONG server password
   102         -		    elif typ == "pong":
   103         -			# Resolved by udp.py
          101  +		    # UDP PONG server password, telnet server
          102  +		    elif typ in ("pong", "telnet"):
          103  +			# Resolved by server init
   104    104   			pass
   105    105   
   106    106   		    # Checked in config during startup?
   107    107   		    else:
   108    108   			raise Exception, "Bad account: %r" % (tup,)
   109    109   
   110    110       # After the system sees us without a user long enough, it figures the