Index: main.py ================================================================== --- main.py +++ main.py @@ -9,11 +9,11 @@ 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 +import udp, telnet # Seconds between search for timeouts TIME_GRANULARITY = 5 # Web interface handling @@ -69,13 +69,16 @@ 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",) ) + # 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",) ) @@ -142,13 +145,18 @@ # 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)) + 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) @@ -200,13 +208,16 @@ 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 + # can have UDP and telnet protocol modules def serve_proto(self, proto, cfg): - server = udp.UDP(cfg, self) + 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 \n" % Index: post.py ================================================================== --- post.py +++ post.py @@ -25,11 +25,10 @@ # 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() @@ -37,10 +36,17 @@ # 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. ADDED telnet.py Index: telnet.py ================================================================== --- telnet.py +++ telnet.py @@ -0,0 +1,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() Index: user.py ================================================================== --- user.py +++ user.py @@ -96,13 +96,13 @@ # 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 + # 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,)