├── templateBot.py ├── sampleBot.py ├── README.md ├── RMUDBot.py ├── MUDBot.py └── telnetlib_mod.py /templateBot.py: -------------------------------------------------------------------------------- 1 | """Шаблон бота для мада «Серый Камень Гаргата». 2 | 3 | В этом коде выполняется подключение к миру «Серый Камень Гаргата» и 4 | переназначен обработчик входящих от сервера строк (обрабочик пустой). 5 | """ 6 | from RMUDBot import RMUDBot 7 | 8 | class TemplateBot(RMUDBot): 9 | def lineProcessing(self, line, ansi): 10 | pass 11 | 12 | if __name__ == "__main__": 13 | bot = TemplateBot("ИМЯ ПЕРСОНАЖА", "ПАРОЛЬ") 14 | if bot.connect(verbose = True): 15 | while bot.connected: 16 | command = input() 17 | bot.send(command) -------------------------------------------------------------------------------- /sampleBot.py: -------------------------------------------------------------------------------- 1 | """Пример бота для мада «Серый Камень Гаргата». 2 | Бот сохраняет все сообщения, которые говорят персонажу. В ответ на "Привет" танцует. 3 | """ 4 | from RMUDBot import RMUDBot 5 | import re 6 | import queue 7 | 8 | class SampleBot(RMUDBot): 9 | def lineProcessing(self, line, ansi): 10 | regex = re.compile("^(.+) сказала* Вам: \"(.+)\"$") 11 | result = regex.match(line) 12 | if result: 13 | messages.put(line) 14 | if result.group(2).lower() == "привет": 15 | self.send("танцевать") 16 | self.send("сказать " + result.group(1) + " И тебе привет!") 17 | 18 | messages = queue.Queue() 19 | 20 | if __name__ == "__main__": 21 | bot = SampleBot("ИМЯ ПЕРСОНАЖА", "ПАРОЛЬ") 22 | if bot.connect(verbose = True): 23 | while True: 24 | command = input() 25 | bot.send(command) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Простой и эффективный бот для MUD, написанный на Python. Легко адаптируется под любой MUD/мад с поддержкой UTF-8. Может работать в любой среде с поддержкой Python 3 (утюги, пылесосы, виртуальные машины и пр.), т.е. везде, на любом хостинге, не требуя никаких ресурсов. 2 | 3 | ## MUDBot 4 | Основной класс работы с мадами. Внутри есть базовое описание всех методов. 5 | 6 | ## RMUDBot 7 | Простой подкласс MUDBot для работы с мадом «Серый Камень Гаргата» (RMUD). Внутри есть базовое описание. Для справки — изменив всего 3 строки, вы можете сделать готовый подкласс для мада «Былины». 8 | 9 | ## templateBot 10 | Шаблон бота (базовый код) для «Серый Камень Гаргата» состоит всего из 10 строк (без комментариев и пустых строк). Он понятный и простой. В нем активирован verbose-режим, т.е. в консоли вы видите все данные от сервера и можете вводить команды. 11 | 12 | ## sampleBot 13 | Простой пример бота для мада «Серый Камень Гаргата». Вам нужно лишь заменить ИМЯ и ПАРОЛЬ от вашего персонажа. Бот сохраняет все сообщения, которые говорят персонажу. В ответ на "Привет" танцует. 14 | 15 | MUDBot использует слегка модифицированную (для обработки команды IAC GA) штатную библиотеку telnetlib из Python. 16 | -------------------------------------------------------------------------------- /RMUDBot.py: -------------------------------------------------------------------------------- 1 | """RMUDBot — подкласс MUDBot для работы с мадом «Серый Камень Гаргата». 2 | Вам необходимо его наследовать в своей программе. 3 | 4 | RMUDBot(name, password) 5 | Конструктор с данными подключения к серверу мира «Серый Камень Гаргата». 6 | 7 | loginProcessing(line) 8 | Переопределенный метод класса MUDBot. 9 | В нем содержится логика входа персонажа в игровой мир «Серый Камень Гаргата». 10 | """ 11 | from MUDBot import MUDBot 12 | 13 | class RMUDBot(MUDBot): 14 | def __init__(self, name, password) -> None: 15 | if len(name) == 0 or len(password) == 0: 16 | raise ValueError("RMUDBot(name, password): Имя и пароль не могут быть пустыми.") 17 | super().__init__("rmud.org", 3041) 18 | self.name = name 19 | self.password = password 20 | 21 | def loginProcessing(self, line): 22 | str = MUDBot.stripANSI(line).strip() 23 | 24 | if str.startswith("Make a choice:"): 25 | self.send("1") 26 | return 27 | 28 | if str.startswith("Введите имя Вашего персонажа или \"новый\" для создания нового:"): 29 | self.send(self.name) 30 | return 31 | 32 | if str.startswith("Пароль:"): 33 | self.send(self.password) 34 | return 35 | 36 | if str.startswith("*** НАЖМИТЕ ВВОД:"): 37 | self.send("") 38 | return 39 | 40 | if str.startswith("1) Войти в игру."): 41 | self.send("1") 42 | return 43 | 44 | if str.startswith("Имя персонажа может содержать только русские буквы."): 45 | self.disconnect() 46 | return 47 | 48 | if str.startswith("Персонажа с таким именем не существует."): 49 | self.disconnect() 50 | return 51 | 52 | if str.startswith("Неверный пароль."): 53 | self.disconnect() 54 | return -------------------------------------------------------------------------------- /MUDBot.py: -------------------------------------------------------------------------------- 1 | """MUDBot — основной класс работы с мадами. В основном он предназначен для создания подклассов под конкретные 2 | мады. Класс использует модифированную библиотеку telnetlib. 3 | 4 | MUDBot(host, port) 5 | Конструктор. 6 | 7 | connect() -> bool 8 | Подключение к серверу. 9 | 10 | disconnect() 11 | Принудительное отключение от сервера. 12 | 13 | send(msg) 14 | Отправка команды в мад. Можно отправлять строку или массив строк. 15 | 16 | stripANSI(text) -> str 17 | Статический метод класс, удаляет цветовую ANSI-разметку в тексте. 18 | 19 | receiveThread() 20 | Внутренний метод обработки входящих данных от сервера. 21 | Не предназначен для использования вне класса и переопределения. 22 | 23 | loginProcessing(line) 24 | Метод подлежащий переопределению (callback) в подклассе. 25 | В нем должна содержаться логика входа персонажа в игровой мир. 26 | Переопределенный метод выполняется в потоке receiveThread, 27 | а не в основном потоке вашей программы — учитывайте это. 28 | 29 | line — строка от сервера в оригинальном виде. 30 | 31 | lineProcessing(line, ansi) 32 | Метод подлежащий переопределению (callback) в последнем подклассе. 33 | В нем должна содержаться вся логика работы бота. 34 | Переопределенный метод выполняется в потоке receiveThread, 35 | а не в основном потоке вашей программы — учитывайте это. 36 | 37 | line — строка от сервера без цветовой ANSI-разметки. 38 | ansi — строка от сервера в оригинальном виде. 39 | """ 40 | import telnetlib_mod 41 | import threading 42 | import re 43 | import select 44 | import time 45 | 46 | class MUDBot: 47 | def __init__(self, host:str, port:int) -> None: 48 | self.host = host 49 | self.port = port 50 | self.socketLock = threading.Lock() 51 | self.connected = False 52 | self.telnet = None 53 | #self.lineProcessing = False 54 | self.connectionID = 1 55 | 56 | def connect(self, verbose = False) -> bool: 57 | self.verbose = verbose 58 | 59 | if self.connected or self.telnet: 60 | return True 61 | 62 | try: 63 | self.telnet = telnetlib_mod.Telnet(self.host, self.port) 64 | except: 65 | return False 66 | 67 | self.connected = True 68 | 69 | t = threading.Thread(target=self.receiveThread, args=[self.connectionID]) 70 | t.start() 71 | 72 | return True 73 | 74 | def disconnect(self, threadSafe = True): 75 | if threadSafe: 76 | self.socketLock.acquire() 77 | 78 | try: 79 | del self.telnet 80 | except: 81 | pass 82 | 83 | self.telnet = None 84 | self.connected = False 85 | self.connectionID += 1 86 | if threadSafe: 87 | self.socketLock.release() 88 | 89 | def send(self, msg, append = "\n", threadSafe = True) -> bool: 90 | if not self.connected or not self.telnet: 91 | return False 92 | 93 | if self.telnet.eof: 94 | self.disconnect(threadSafe = threadSafe) 95 | return False 96 | 97 | strToSend = "" 98 | 99 | if not isinstance(append, str): 100 | raise TypeError("send(msg): Отправлять можно только строку или массив строк.") 101 | 102 | if isinstance(msg, list): 103 | for item in list: 104 | if not isinstance(item, str): 105 | raise TypeError("send(msg): Отправлять можно только строки или массив строк.") 106 | strToSend = append.join(msg) + append 107 | elif isinstance(msg, str): 108 | strToSend = msg + append 109 | else: 110 | raise TypeError("send(msg): Отправлять можно только строки или массив строк.") 111 | 112 | if len(strToSend) == 0: 113 | return True 114 | 115 | if threadSafe: 116 | self.socketLock.acquire() 117 | 118 | try: 119 | self.telnet.write(strToSend.encode()) 120 | except: 121 | self.disconnect(threadSafe = False) 122 | 123 | if threadSafe: 124 | self.socketLock.release() 125 | 126 | return self.connected 127 | 128 | def receiveThread(self, connectionID): 129 | remainder = b"" 130 | unFinishedLine = "" 131 | 132 | while True: 133 | if connectionID != self.connectionID: 134 | return 135 | if not self.connected or not self.telnet: 136 | return 137 | 138 | try: 139 | r, w, e = select.select([self.telnet.sock], [], []) 140 | except: 141 | if connectionID == self.connectionID and self.connected: 142 | self.disconnect() 143 | return 144 | 145 | if r: 146 | self.socketLock.acquire() 147 | try: 148 | buff = self.telnet.read_very_eager() 149 | except: 150 | self.disconnect(threadSafe = False) 151 | self.socketLock.release() 152 | 153 | if connectionID != self.connectionID: 154 | return 155 | 156 | if len(buff) == 0: 157 | time.sleep(0.01) 158 | continue 159 | 160 | if len(remainder) > 0: 161 | buff = remainder + buff 162 | remainder = b"" 163 | 164 | str = None 165 | 166 | try: 167 | str = buff.decode("utf8") 168 | str = str.replace("\r", "") 169 | except: 170 | slices = [] 171 | if len(buff) > 1: 172 | slices.append(-1) 173 | if len(buff) > 2: 174 | slices.append(-2) 175 | if len(buff) > 3: 176 | slices.append(-3) 177 | 178 | for x in slices: 179 | newbuff = buff[:x] 180 | newremainder = buff[x:] 181 | try: 182 | str = newbuff.decode("utf8") 183 | str = str.replace("\r", "") 184 | remainder = newremainder 185 | break 186 | except: 187 | pass 188 | 189 | if str == None: 190 | if (len(buff) > 3): 191 | print("receiveThread: Ошибка в данных от сервера. Разрываем соединение.") 192 | self.disconnect() 193 | return 194 | remainder = buff 195 | else: 196 | if self.verbose: 197 | print(str, end="") 198 | 199 | str = unFinishedLine + str 200 | lines = str.split("\n") 201 | 202 | if (len(lines) == 1): 203 | if len(lines[0]) > 1000: 204 | unFinishedLine = "" 205 | self.loginProcessing(lines[0]) 206 | self.lineProcessing(MUDBot.stripANSI(lines[0]), lines[0]) 207 | else: 208 | unFinishedLine = lines[0] 209 | else: 210 | unFinishedLine = lines[-1] 211 | lines = lines[:-1] 212 | for line in lines: 213 | self.loginProcessing(line) 214 | self.lineProcessing(MUDBot.stripANSI(line), line) 215 | 216 | """Переопределяется подклассом""" 217 | def loginProcessing(self, line): 218 | pass 219 | 220 | """Переопределяется подклассом""" 221 | def lineProcessing(self, line, ansi): 222 | pass 223 | 224 | @staticmethod 225 | def stripANSI(text:str) -> str: 226 | ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') 227 | return ansi_escape.sub('', text) 228 | -------------------------------------------------------------------------------- /telnetlib_mod.py: -------------------------------------------------------------------------------- 1 | r"""TELNET client class. 2 | 3 | Based on RFC 854: TELNET Protocol Specification, by J. Postel and 4 | J. Reynolds 5 | 6 | Example: 7 | 8 | >>> from telnetlib import Telnet 9 | >>> tn = Telnet('www.python.org', 79) # connect to finger port 10 | >>> tn.write(b'guido\r\n') 11 | >>> print(tn.read_all()) 12 | Login Name TTY Idle When Where 13 | guido Guido van Rossum pts/2 snag.cnri.reston.. 14 | 15 | >>> 16 | 17 | Note that read_all() won't read until eof -- it just reads some data 18 | -- but it guarantees to read at least one byte unless EOF is hit. 19 | 20 | It is possible to pass a Telnet object to a selector in order to wait until 21 | more data is available. Note that in this case, read_eager() may return b'' 22 | even if there was data on the socket, because the protocol negotiation may have 23 | eaten the data. This is why EOFError is needed in some cases to distinguish 24 | between "no data" and "connection closed" (since the socket also appears ready 25 | for reading when it is closed). 26 | 27 | To do: 28 | - option negotiation 29 | - timeout should be intrinsic to the connection object instead of an 30 | option on one of the read calls only 31 | 32 | """ 33 | 34 | 35 | # Imported modules 36 | import sys 37 | import socket 38 | import selectors 39 | from time import monotonic as _time 40 | 41 | __all__ = ["Telnet"] 42 | 43 | # Tunable parameters 44 | DEBUGLEVEL = 0 45 | 46 | # Telnet protocol defaults 47 | TELNET_PORT = 23 48 | 49 | # Telnet protocol characters (don't change) 50 | IAC = bytes([255]) # "Interpret As Command" 51 | DONT = bytes([254]) 52 | DO = bytes([253]) 53 | WONT = bytes([252]) 54 | WILL = bytes([251]) 55 | theNULL = bytes([0]) 56 | 57 | SE = bytes([240]) # Subnegotiation End 58 | NOP = bytes([241]) # No Operation 59 | DM = bytes([242]) # Data Mark 60 | BRK = bytes([243]) # Break 61 | IP = bytes([244]) # Interrupt process 62 | AO = bytes([245]) # Abort output 63 | AYT = bytes([246]) # Are You There 64 | EC = bytes([247]) # Erase Character 65 | EL = bytes([248]) # Erase Line 66 | GA = bytes([249]) # Go Ahead 67 | SB = bytes([250]) # Subnegotiation Begin 68 | 69 | 70 | # Telnet protocol options code (don't change) 71 | # These ones all come from arpa/telnet.h 72 | BINARY = bytes([0]) # 8-bit data path 73 | ECHO = bytes([1]) # echo 74 | RCP = bytes([2]) # prepare to reconnect 75 | SGA = bytes([3]) # suppress go ahead 76 | NAMS = bytes([4]) # approximate message size 77 | STATUS = bytes([5]) # give status 78 | TM = bytes([6]) # timing mark 79 | RCTE = bytes([7]) # remote controlled transmission and echo 80 | NAOL = bytes([8]) # negotiate about output line width 81 | NAOP = bytes([9]) # negotiate about output page size 82 | NAOCRD = bytes([10]) # negotiate about CR disposition 83 | NAOHTS = bytes([11]) # negotiate about horizontal tabstops 84 | NAOHTD = bytes([12]) # negotiate about horizontal tab disposition 85 | NAOFFD = bytes([13]) # negotiate about formfeed disposition 86 | NAOVTS = bytes([14]) # negotiate about vertical tab stops 87 | NAOVTD = bytes([15]) # negotiate about vertical tab disposition 88 | NAOLFD = bytes([16]) # negotiate about output LF disposition 89 | XASCII = bytes([17]) # extended ascii character set 90 | LOGOUT = bytes([18]) # force logout 91 | BM = bytes([19]) # byte macro 92 | DET = bytes([20]) # data entry terminal 93 | SUPDUP = bytes([21]) # supdup protocol 94 | SUPDUPOUTPUT = bytes([22]) # supdup output 95 | SNDLOC = bytes([23]) # send location 96 | TTYPE = bytes([24]) # terminal type 97 | EOR = bytes([25]) # end or record 98 | TUID = bytes([26]) # TACACS user identification 99 | OUTMRK = bytes([27]) # output marking 100 | TTYLOC = bytes([28]) # terminal location number 101 | VT3270REGIME = bytes([29]) # 3270 regime 102 | X3PAD = bytes([30]) # X.3 PAD 103 | NAWS = bytes([31]) # window size 104 | TSPEED = bytes([32]) # terminal speed 105 | LFLOW = bytes([33]) # remote flow control 106 | LINEMODE = bytes([34]) # Linemode option 107 | XDISPLOC = bytes([35]) # X Display Location 108 | OLD_ENVIRON = bytes([36]) # Old - Environment variables 109 | AUTHENTICATION = bytes([37]) # Authenticate 110 | ENCRYPT = bytes([38]) # Encryption option 111 | NEW_ENVIRON = bytes([39]) # New - Environment variables 112 | # the following ones come from 113 | # http://www.iana.org/assignments/telnet-options 114 | # Unfortunately, that document does not assign identifiers 115 | # to all of them, so we are making them up 116 | TN3270E = bytes([40]) # TN3270E 117 | XAUTH = bytes([41]) # XAUTH 118 | CHARSET = bytes([42]) # CHARSET 119 | RSP = bytes([43]) # Telnet Remote Serial Port 120 | COM_PORT_OPTION = bytes([44]) # Com Port Control Option 121 | SUPPRESS_LOCAL_ECHO = bytes([45]) # Telnet Suppress Local Echo 122 | TLS = bytes([46]) # Telnet Start TLS 123 | KERMIT = bytes([47]) # KERMIT 124 | SEND_URL = bytes([48]) # SEND-URL 125 | FORWARD_X = bytes([49]) # FORWARD_X 126 | PRAGMA_LOGON = bytes([138]) # TELOPT PRAGMA LOGON 127 | SSPI_LOGON = bytes([139]) # TELOPT SSPI LOGON 128 | PRAGMA_HEARTBEAT = bytes([140]) # TELOPT PRAGMA HEARTBEAT 129 | EXOPL = bytes([255]) # Extended-Options-List 130 | NOOPT = bytes([0]) 131 | 132 | 133 | # poll/select have the advantage of not requiring any extra file descriptor, 134 | # contrarily to epoll/kqueue (also, they require a single syscall). 135 | if hasattr(selectors, 'PollSelector'): 136 | _TelnetSelector = selectors.PollSelector 137 | else: 138 | _TelnetSelector = selectors.SelectSelector 139 | 140 | 141 | class Telnet: 142 | 143 | """Telnet interface class. 144 | 145 | An instance of this class represents a connection to a telnet 146 | server. The instance is initially not connected; the open() 147 | method must be used to establish a connection. Alternatively, the 148 | host name and optional port number can be passed to the 149 | constructor, too. 150 | 151 | Don't try to reopen an already connected instance. 152 | 153 | This class has many read_*() methods. Note that some of them 154 | raise EOFError when the end of the connection is read, because 155 | they can return an empty string for other reasons. See the 156 | individual doc strings. 157 | 158 | read_until(expected, [timeout]) 159 | Read until the expected string has been seen, or a timeout is 160 | hit (default is no timeout); may block. 161 | 162 | read_all() 163 | Read all data until EOF; may block. 164 | 165 | read_some() 166 | Read at least one byte or EOF; may block. 167 | 168 | read_very_eager() 169 | Read all data available already queued or on the socket, 170 | without blocking. 171 | 172 | read_eager() 173 | Read either data already queued or some data available on the 174 | socket, without blocking. 175 | 176 | read_lazy() 177 | Read all data in the raw queue (processing it first), without 178 | doing any socket I/O. 179 | 180 | read_very_lazy() 181 | Reads all data in the cooked queue, without doing any socket 182 | I/O. 183 | 184 | read_sb_data() 185 | Reads available data between SB ... SE sequence. Don't block. 186 | 187 | set_option_negotiation_callback(callback) 188 | Each time a telnet option is read on the input flow, this callback 189 | (if set) is called with the following parameters : 190 | callback(telnet socket, command, option) 191 | option will be chr(0) when there is no option. 192 | No other action is done afterwards by telnetlib. 193 | 194 | """ 195 | sock = None # for __del__() 196 | 197 | def __init__(self, host=None, port=0, 198 | timeout=socket._GLOBAL_DEFAULT_TIMEOUT): 199 | """Constructor. 200 | 201 | When called without arguments, create an unconnected instance. 202 | With a hostname argument, it connects the instance; port number 203 | and timeout are optional. 204 | """ 205 | self.debuglevel = DEBUGLEVEL 206 | self.host = host 207 | self.port = port 208 | self.timeout = timeout 209 | self.sock = None 210 | self.rawq = b'' 211 | self.irawq = 0 212 | self.cookedq = b'' 213 | self.eof = 0 214 | self.iacseq = b'' # Buffer for IAC sequence. 215 | self.sb = 0 # flag for SB and SE sequence. 216 | self.sbdataq = b'' 217 | self.option_callback = None 218 | if host is not None: 219 | self.open(host, port, timeout) 220 | 221 | def open(self, host, port=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): 222 | """Connect to a host. 223 | 224 | The optional second argument is the port number, which 225 | defaults to the standard telnet port (23). 226 | 227 | Don't try to reopen an already connected instance. 228 | """ 229 | self.eof = 0 230 | if not port: 231 | port = TELNET_PORT 232 | self.host = host 233 | self.port = port 234 | self.timeout = timeout 235 | sys.audit("telnetlib.Telnet.open", self, host, port) 236 | self.sock = socket.create_connection((host, port), timeout) 237 | 238 | def __del__(self): 239 | """Destructor -- close the connection.""" 240 | self.close() 241 | 242 | def msg(self, msg, *args): 243 | """Print a debug message, when the debug level is > 0. 244 | 245 | If extra arguments are present, they are substituted in the 246 | message using the standard string formatting operator. 247 | 248 | """ 249 | if self.debuglevel > 0: 250 | print('Telnet(%s,%s):' % (self.host, self.port), end=' ') 251 | if args: 252 | print(msg % args) 253 | else: 254 | print(msg) 255 | 256 | def set_debuglevel(self, debuglevel): 257 | """Set the debug level. 258 | 259 | The higher it is, the more debug output you get (on sys.stdout). 260 | 261 | """ 262 | self.debuglevel = debuglevel 263 | 264 | def close(self): 265 | """Close the connection.""" 266 | sock = self.sock 267 | self.sock = None 268 | self.eof = True 269 | self.iacseq = b'' 270 | self.sb = 0 271 | if sock: 272 | sock.close() 273 | 274 | def get_socket(self): 275 | """Return the socket object used internally.""" 276 | return self.sock 277 | 278 | def fileno(self): 279 | """Return the fileno() of the socket object used internally.""" 280 | return self.sock.fileno() 281 | 282 | def write(self, buffer): 283 | """Write a string to the socket, doubling any IAC characters. 284 | 285 | Can block if the connection is blocked. May raise 286 | OSError if the connection is closed. 287 | 288 | """ 289 | if IAC in buffer: 290 | buffer = buffer.replace(IAC, IAC+IAC) 291 | sys.audit("telnetlib.Telnet.write", self, buffer) 292 | self.msg("send %r", buffer) 293 | self.sock.sendall(buffer) 294 | 295 | def read_until(self, match, timeout=None): 296 | """Read until a given string is encountered or until timeout. 297 | 298 | When no match is found, return whatever is available instead, 299 | possibly the empty string. Raise EOFError if the connection 300 | is closed and no cooked data is available. 301 | 302 | """ 303 | n = len(match) 304 | self.process_rawq() 305 | i = self.cookedq.find(match) 306 | if i >= 0: 307 | i = i+n 308 | buf = self.cookedq[:i] 309 | self.cookedq = self.cookedq[i:] 310 | return buf 311 | if timeout is not None: 312 | deadline = _time() + timeout 313 | with _TelnetSelector() as selector: 314 | selector.register(self, selectors.EVENT_READ) 315 | while not self.eof: 316 | if selector.select(timeout): 317 | i = max(0, len(self.cookedq)-n) 318 | self.fill_rawq() 319 | self.process_rawq() 320 | i = self.cookedq.find(match, i) 321 | if i >= 0: 322 | i = i+n 323 | buf = self.cookedq[:i] 324 | self.cookedq = self.cookedq[i:] 325 | return buf 326 | if timeout is not None: 327 | timeout = deadline - _time() 328 | if timeout < 0: 329 | break 330 | return self.read_very_lazy() 331 | 332 | def read_all(self): 333 | """Read all data until EOF; block until connection closed.""" 334 | self.process_rawq() 335 | while not self.eof: 336 | self.fill_rawq() 337 | self.process_rawq() 338 | buf = self.cookedq 339 | self.cookedq = b'' 340 | return buf 341 | 342 | def read_some(self): 343 | """Read at least one byte of cooked data unless EOF is hit. 344 | 345 | Return b'' if EOF is hit. Block if no data is immediately 346 | available. 347 | 348 | """ 349 | self.process_rawq() 350 | while not self.cookedq and not self.eof: 351 | self.fill_rawq() 352 | self.process_rawq() 353 | buf = self.cookedq 354 | self.cookedq = b'' 355 | return buf 356 | 357 | def read_very_eager(self): 358 | """Read everything that's possible without blocking in I/O (eager). 359 | 360 | Raise EOFError if connection closed and no cooked data 361 | available. Return b'' if no cooked data available otherwise. 362 | Don't block unless in the midst of an IAC sequence. 363 | 364 | """ 365 | self.process_rawq() 366 | while not self.eof and self.sock_avail(): 367 | self.fill_rawq() 368 | self.process_rawq() 369 | return self.read_very_lazy() 370 | 371 | def read_eager(self): 372 | """Read readily available data. 373 | 374 | Raise EOFError if connection closed and no cooked data 375 | available. Return b'' if no cooked data available otherwise. 376 | Don't block unless in the midst of an IAC sequence. 377 | 378 | """ 379 | self.process_rawq() 380 | while not self.cookedq and not self.eof and self.sock_avail(): 381 | self.fill_rawq() 382 | self.process_rawq() 383 | return self.read_very_lazy() 384 | 385 | def read_lazy(self): 386 | """Process and return data that's already in the queues (lazy). 387 | 388 | Raise EOFError if connection closed and no data available. 389 | Return b'' if no cooked data available otherwise. Don't block 390 | unless in the midst of an IAC sequence. 391 | 392 | """ 393 | self.process_rawq() 394 | return self.read_very_lazy() 395 | 396 | def read_very_lazy(self): 397 | """Return any data available in the cooked queue (very lazy). 398 | 399 | Raise EOFError if connection closed and no data available. 400 | Return b'' if no cooked data available otherwise. Don't block. 401 | 402 | """ 403 | buf = self.cookedq 404 | self.cookedq = b'' 405 | if not buf and self.eof and not self.rawq: 406 | raise EOFError('telnet connection closed') 407 | return buf 408 | 409 | def read_sb_data(self): 410 | """Return any data available in the SB ... SE queue. 411 | 412 | Return b'' if no SB ... SE available. Should only be called 413 | after seeing a SB or SE command. When a new SB command is 414 | found, old unread SB data will be discarded. Don't block. 415 | 416 | """ 417 | buf = self.sbdataq 418 | self.sbdataq = b'' 419 | return buf 420 | 421 | def set_option_negotiation_callback(self, callback): 422 | """Provide a callback function called after each receipt of a telnet option.""" 423 | self.option_callback = callback 424 | 425 | def process_rawq(self): 426 | """Transfer from raw queue to cooked queue. 427 | 428 | Set self.eof when connection is closed. Don't block unless in 429 | the midst of an IAC sequence. 430 | 431 | """ 432 | buf = [b'', b''] 433 | 434 | try: 435 | while self.rawq: 436 | c = self.rawq_getchar() 437 | if not self.iacseq: 438 | if c == theNULL: 439 | continue 440 | if c == b"\021": 441 | continue 442 | if c != IAC: 443 | buf[self.sb] = buf[self.sb] + c 444 | continue 445 | else: 446 | self.iacseq += c 447 | elif len(self.iacseq) == 1: 448 | # 'IAC: IAC CMD [OPTION only for WILL/WONT/DO/DONT]' 449 | if c in (DO, DONT, WILL, WONT): 450 | self.iacseq += c 451 | continue 452 | elif c == GA: 453 | buf[0] = buf[0] + b'\n' 454 | self.iacseq = b'' 455 | continue 456 | #print("GA") 457 | #self.cookedq = self.cookedq + b'GA\n' 458 | #continue""" 459 | 460 | self.iacseq = b'' 461 | if c == IAC: 462 | buf[self.sb] = buf[self.sb] + c 463 | else: 464 | if c == SB: # SB ... SE start. 465 | self.sb = 1 466 | self.sbdataq = b'' 467 | elif c == SE: 468 | self.sb = 0 469 | self.sbdataq = self.sbdataq + buf[1] 470 | buf[1] = b'' 471 | if self.option_callback: 472 | # Callback is supposed to look into 473 | # the sbdataq 474 | self.option_callback(self.sock, c, NOOPT) 475 | else: 476 | # We can't offer automatic processing of 477 | # suboptions. Alas, we should not get any 478 | # unless we did a WILL/DO before. 479 | self.msg('IAC %d not recognized' % ord(c)) 480 | elif len(self.iacseq) == 2: 481 | cmd = self.iacseq[1:2] 482 | self.iacseq = b'' 483 | opt = c 484 | if cmd in (DO, DONT): 485 | self.msg('IAC %s %d', 486 | cmd == DO and 'DO' or 'DONT', ord(opt)) 487 | if self.option_callback: 488 | self.option_callback(self.sock, cmd, opt) 489 | else: 490 | self.sock.sendall(IAC + WONT + opt) 491 | elif cmd in (WILL, WONT): 492 | self.msg('IAC %s %d', 493 | cmd == WILL and 'WILL' or 'WONT', ord(opt)) 494 | if self.option_callback: 495 | self.option_callback(self.sock, cmd, opt) 496 | else: 497 | self.sock.sendall(IAC + DONT + opt) 498 | except EOFError: # raised by self.rawq_getchar() 499 | self.iacseq = b'' # Reset on EOF 500 | self.sb = 0 501 | self.cookedq = self.cookedq + buf[0] 502 | self.sbdataq = self.sbdataq + buf[1] 503 | 504 | def rawq_getchar(self): 505 | """Get next char from raw queue. 506 | 507 | Block if no data is immediately available. Raise EOFError 508 | when connection is closed. 509 | 510 | """ 511 | if not self.rawq: 512 | self.fill_rawq() 513 | if self.eof: 514 | raise EOFError 515 | c = self.rawq[self.irawq:self.irawq+1] 516 | self.irawq = self.irawq + 1 517 | if self.irawq >= len(self.rawq): 518 | self.rawq = b'' 519 | self.irawq = 0 520 | return c 521 | 522 | def fill_rawq(self): 523 | """Fill raw queue from exactly one recv() system call. 524 | 525 | Block if no data is immediately available. Set self.eof when 526 | connection is closed. 527 | 528 | """ 529 | if self.irawq >= len(self.rawq): 530 | self.rawq = b'' 531 | self.irawq = 0 532 | # The buffer size should be fairly small so as to avoid quadratic 533 | # behavior in process_rawq() above 534 | buf = self.sock.recv(50) 535 | self.msg("recv %r", buf) 536 | self.eof = (not buf) 537 | self.rawq = self.rawq + buf 538 | 539 | def sock_avail(self): 540 | """Test whether data is available on the socket.""" 541 | with _TelnetSelector() as selector: 542 | selector.register(self, selectors.EVENT_READ) 543 | return bool(selector.select(0)) 544 | 545 | def interact(self): 546 | """Interaction function, emulates a very dumb telnet client.""" 547 | if sys.platform == "win32": 548 | self.mt_interact() 549 | return 550 | with _TelnetSelector() as selector: 551 | selector.register(self, selectors.EVENT_READ) 552 | selector.register(sys.stdin, selectors.EVENT_READ) 553 | 554 | while True: 555 | for key, events in selector.select(): 556 | if key.fileobj is self: 557 | try: 558 | text = self.read_eager() 559 | except EOFError: 560 | print('*** Connection closed by remote host ***') 561 | return 562 | if text: 563 | sys.stdout.write(text.decode('ascii')) 564 | sys.stdout.flush() 565 | elif key.fileobj is sys.stdin: 566 | line = sys.stdin.readline().encode('ascii') 567 | if not line: 568 | return 569 | self.write(line) 570 | 571 | def mt_interact(self): 572 | """Multithreaded version of interact().""" 573 | import _thread 574 | _thread.start_new_thread(self.listener, ()) 575 | while 1: 576 | line = sys.stdin.readline() 577 | if not line: 578 | break 579 | self.write(line.encode('ascii')) 580 | 581 | def listener(self): 582 | """Helper for mt_interact() -- this executes in the other thread.""" 583 | while 1: 584 | try: 585 | data = self.read_eager() 586 | except EOFError: 587 | print('*** Connection closed by remote host ***') 588 | return 589 | if data: 590 | sys.stdout.write(data.decode('ascii')) 591 | else: 592 | sys.stdout.flush() 593 | 594 | def expect(self, list, timeout=None): 595 | """Read until one from a list of a regular expressions matches. 596 | 597 | The first argument is a list of regular expressions, either 598 | compiled (re.Pattern instances) or uncompiled (strings). 599 | The optional second argument is a timeout, in seconds; default 600 | is no timeout. 601 | 602 | Return a tuple of three items: the index in the list of the 603 | first regular expression that matches; the re.Match object 604 | returned; and the text read up till and including the match. 605 | 606 | If EOF is read and no text was read, raise EOFError. 607 | Otherwise, when nothing matches, return (-1, None, text) where 608 | text is the text received so far (may be the empty string if a 609 | timeout happened). 610 | 611 | If a regular expression ends with a greedy match (e.g. '.*') 612 | or if more than one expression can match the same input, the 613 | results are undeterministic, and may depend on the I/O timing. 614 | 615 | """ 616 | re = None 617 | list = list[:] 618 | indices = range(len(list)) 619 | for i in indices: 620 | if not hasattr(list[i], "search"): 621 | if not re: import re 622 | list[i] = re.compile(list[i]) 623 | if timeout is not None: 624 | deadline = _time() + timeout 625 | with _TelnetSelector() as selector: 626 | selector.register(self, selectors.EVENT_READ) 627 | while not self.eof: 628 | self.process_rawq() 629 | for i in indices: 630 | m = list[i].search(self.cookedq) 631 | if m: 632 | e = m.end() 633 | text = self.cookedq[:e] 634 | self.cookedq = self.cookedq[e:] 635 | return (i, m, text) 636 | if timeout is not None: 637 | ready = selector.select(timeout) 638 | timeout = deadline - _time() 639 | if not ready: 640 | if timeout < 0: 641 | break 642 | else: 643 | continue 644 | self.fill_rawq() 645 | text = self.read_very_lazy() 646 | if not text and self.eof: 647 | raise EOFError 648 | return (-1, None, text) 649 | 650 | def __enter__(self): 651 | return self 652 | 653 | def __exit__(self, type, value, traceback): 654 | self.close() 655 | 656 | 657 | def test(): 658 | """Test program for telnetlib. 659 | 660 | Usage: python telnetlib.py [-d] ... [host [port]] 661 | 662 | Default host is localhost; default port is 23. 663 | 664 | """ 665 | debuglevel = 0 666 | while sys.argv[1:] and sys.argv[1] == '-d': 667 | debuglevel = debuglevel+1 668 | del sys.argv[1] 669 | host = 'localhost' 670 | if sys.argv[1:]: 671 | host = sys.argv[1] 672 | port = 0 673 | if sys.argv[2:]: 674 | portstr = sys.argv[2] 675 | try: 676 | port = int(portstr) 677 | except ValueError: 678 | port = socket.getservbyname(portstr, 'tcp') 679 | with Telnet() as tn: 680 | tn.set_debuglevel(debuglevel) 681 | tn.open(host, port, timeout=0.5) 682 | tn.interact() 683 | 684 | if __name__ == '__main__': 685 | test() 686 | --------------------------------------------------------------------------------