fsxNet Wiki

BBS Development & Resources

User Tools

Site Tools


tutorials:python_bbs:part_two

Python BBS - Part Two

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.

Refactoring

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.

do_bbs.py

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.

IAC Handling

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.

Reading Strings

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.

Do BBS!

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)

Tying it Together

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.

Entire Source Files

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()
tutorials/python_bbs/part_two.txt · Last modified: 2017/03/10 23:26 by apam