This is an old revision of the document!
In this part we will be doing a little refactoring, then adding support for ignoring IAC codes and reading strings from the user.
We will be sending some IAC codes to the client to set up the terminal, but anything the client sends back we are just ignoring.
The first thing we're going to do is create a new python file and call it “do_bbs.py” then we're going to remove the “getChar” and “sendString” functions from bbs.py and create new ones in do_bbs.py.
So, from now on, bbs.py will handle incoming connections, when one is made and a thread is created for that node it will call do_bbs, which will dictate what happens on that thread.
Inside do_bbs.py we want to create our sendString function and a new getCharRaw function:
def sendString(conn, text): conn.sendall(text.encode()) def getCharRaw(conn): c = conn.recv(1) if c == b'': print("Connection dropped") raise RuntimeError("Socket connection dropped") return c
The send string function is copied directly from our old version, and the getCharRaw function will be used by our new getChar function. getCharRaw simply receives a character from the socket, checks for disconnection and returns that character.
Next we have our new getChar function:
def getChar(conn): while True: c = getCharRaw(conn) while c[0] == 255: c = getCharRaw(conn) if c[0] == 251 or c[0] == 252 or c[0] == 253 or c[0] == 254: c = getCharRaw(conn) elif c[0] == 250: c = getCharRaw(conn) while c[0] != 240: c = getCharRaw(conn) c = getCharRaw(conn) if c[0] != '\n' && c[0] != '\0': break return str(c, 'utf-8')
This getChar function will strip out any IAC codes, and the newline character (or NULL). IAC codes usually come in the form of:
IAC [DO|DONT|WILL|WONT] COMMAND
Where: IAC = 255 DO = 253 DONT = 254 WILL = 251 WONT = 252
So, as you can see from our getChar function, firstly we check if we get an IAC code.
Secondly we get another character and check if it's another IAC code, if it is we should pass it along, if not, check if it's a DO, DONT, WILL or WONT code, if it is, get another character which is the command, discard it and loop.
If the second character is not DO, DONT, WILL or WONT, it could be 250 which indicates the start of sub negotiation, in this case, we continue getting characters until we get 240 which indicates the end of sub negotiation.
Finally, we check if the character is a new line and if it is we start the whole process again.
We discard newlines because the telnet specification indicates that newlines consist of the CARRIAGE RETURN character followed by either NEWLINE or NULL. It's easier to discard this so later we can detect enters by just checking for a CARRIAGE RETURN character.
Next we want to be able to read strings. This will be how we get things like the username and password.
def getString(conn, max): result = "" index = 0 while index < max: c = getChar(conn) if (c[0] == '\b' or c[0] == 127) and index > 0: result = result[:-1] index -= 1 sendString(conn, "\x1b[D \x1b[D") continue elif c[0] == '\b' or c[0] == 127: continue if c[0] == '\r': return result result += c[0] sendString(conn, c) index += 1 return result
This function reads the characters coming in using our getChar function, and appends them to the string. If it encounters a backspace character and the string has some characters in it it will chop off the last string and update the display. If the user presses enter (notice we are just checking for carriage return as explained before) it returns the string.
We also check to see if a string has reached the maximum length specified. You will notice some ANSI codes used when we detect a backspace, this moves the cursor back one, prints a space and then moves the character back one again effectively erasing the character from the screen.
Finally, we want our new do_bbs function which is going to get called when a user connects. For this we want to firstly send the IAC commands to tell the client that we will Suppress Go Ahead, and that we will be echoing characters to the screen and the client should not. Then we're going to ask for a username, and quit. Handling logins will come in part three.
def do_bbs(conn, node): iac_echo = bytes([255, 251, 1]) iac_sga = bytes([255, 251, 3]) conn.sendall(iac_echo) conn.sendall(iac_sga) sendString(conn, "Welcome to my BBS on Node %s\r\n" % node) sendString(conn, "Enter your username, or type NEW if a new user.\r\n") sendString(conn, "Login: ") result = getString(conn, 16) sendString(conn, "You entered %s\r\n" % result)
Now that we've finished what we are going to do on do_bbs.py, we need to call the do_bbs function from our bbs.py file.
Firstly, in bbs.py we need to import the function so at the top of your file (after the import lines) put:
from do_bbs import do_bbs
Then call do_bbs from run_thread.
do_bbs.py:
def sendString(conn, text): conn.sendall(text.encode()) def getString(conn, max): result = "" index = 0 while index < max: c = getChar(conn) if (c[0] == '\b' or c[0] == 127) and index > 0: result = result[:-1] index -= 1 sendString(conn, "\x1b[D \x1b[D") continue elif c[0] == '\b' or c[0] == 127: continue if c[0] == '\r': return result result += c[0] sendString(conn, c) index += 1 return result def getCharRaw(conn): c = conn.recv(1) if c == b'': print("Connection dropped") raise RuntimeError("Socket connection dropped") return c def getChar(conn): while True: c = getCharRaw(conn) while c[0] == 255: c = getCharRaw(conn) if c[0] == 251 or c[0] == 252 or c[0] == 253 or c[0] == 254: c = getCharRaw(conn) elif c[0] == 250: c = getCharRaw(conn) while c[0] != 240: c = getCharRaw(conn) c = getCharRaw(conn) if c[0] != '\n': break return str(c, 'utf-8') def do_bbs(conn, node): iac_echo = bytes([255, 251, 1]) iac_sga = bytes([255, 251, 3]) conn.sendall(iac_echo) conn.sendall(iac_sga) sendString(conn, "Welcome to my BBS on Node %s\r\n" % node) sendString(conn, "Enter your username, or type NEW if a new user.\r\n") sendString(conn, "Login: ") result = getString(conn, 16) sendString(conn, "You entered %s\r\n" % result)
bbs.py:
import configparser import sys import socket import threading from do_bbs import do_bbs nodes = [] class bbsServer(): def __init__(self, port, host='0.0.0.0', nodemax=5): self.port = port self.host = host self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.nodemax = nodemax for index in range(nodemax): nodes.append(False) try: self.server.bind((self.host, self.port)) except socket.error: print("Couldn't bind %s" % (socket.error)) sys.exit() self.server.listen(10) def run_thread(self, conn, addr, node): global nodes try: do_bbs(conn, node + 1) except RuntimeError: print("Node %s hung up..." % (node + 1)) print("Node %s offline." % (node + 1)) nodes[node] = False conn.close() sys.exit() def run(self): print("Starting BBS on port %s" % (self.port)) while True: conn, addr = self.server.accept() for index in range(self.nodemax): if nodes[index] == False: nodes[index] = True threading.Thread(target=self.run_thread, args=(conn, addr, index)).start() break else: conn.sendall("BUSY\r\n") conn.close() if __name__ == "__main__": if len(sys.argv) < 2: print("Usage python bbs.py config.ini") exit(1) config = configparser.ConfigParser() config.read(sys.argv[1]) server = bbsServer(config.getint("Main", "Port")) server.run()