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: | dd233913a45aa3bd1b68c2548a06b778 |
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
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