Initial Commit

This commit is contained in:
jaycee 2025-07-19 17:33:10 -06:00
parent 72469e2172
commit b6b67576c1
16 changed files with 538 additions and 1 deletions

2
.kdev4/bbs.kdev4 Normal file
View File

@ -0,0 +1,2 @@
[Project]
VersionControlSupport=kdevgit

View File

@ -1,3 +1,3 @@
# Pyscii-BBS # Pyscii-BBS
A modern implementation of an ASCII bbs in Python with secure data storage options. An actual ascii-based BBS

68
auth.py Normal file
View File

@ -0,0 +1,68 @@
'''
auth.py - an authentication library for adding users and checking them against a flat-file database
part of Py-BBS.
'''
import sys, json, configparser, comms, hashlib, random, string, menus
config = configparser.ConfigParser()
config.read(sys.argv[1])
dbFile = config.get("Auth","userDb")
def login(conn, username, password, node):
with open(dbFile) as userdb:
users = json.loads(userdb.read())
for user in users['users']:
if user['name'] == username.replace('\x00',''):
if user['id'] == 0:
comms.sendString(conn, "\r\nThis account is not usable. This event has been logged.\r\n")
conn.close()
return
password = hashlib.sha256(''.join(password.replace('\x00','')).join(user['salt']).encode('utf-8')).hexdigest()
if user['password'] == password:
menus.printMenu(conn, 0, node, user['id'])
else:
comms.sendString(conn, "\r\nInvalid Username or Password!\r\n")
def setProfile(conn):
comms.sendString(conn,"\r\nDo you want to set a Description?(Y/N) ")
if comms.getString(conn,2).replace("\x00",'')== "Y":
comms.sendString(conn,"\r\nWhat do you want to set as your Description?(max 150 char) ")
description = comms.getString(conn, 150)
else:
description = "You Exist!"
return description
def setPassword(conn, salt):
comms.sendString(conn, "\r\nWhat do you want your password to be?(max 32 char) ")
comms.sendString(conn, "\r\nThis will be stored salted and hashed")
password = comms.getString(conn, 32).replace('\x00','')
password = hashlib.sha256(''.join(password).join(salt).encode('utf-8')).hexdigest()
return password
def create(conn):
salt = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
comms.sendString(conn, "\r\nWelcome to SB17's BBS!\r\nPowered by Pyscii-BBS!")
comms.sendString(conn, "\r\nWhat is your name? (max 16 char) ")
name = comms.getString(conn, 16)
comms.sendString(conn, "\r\nYou want your name to be " + name + "?(Y/N) ")
if comms.getChar(conn) == "N":
comms.sendString(conn, "\r\nRestarting... Press Enter to continue...")
comms.getChar(conn)
password = setPassword(conn, salt)
description = setProfile(conn)
with open(dbFile) as userdb:
users = json.load(userdb)
user = {}
user["id"] = users["users"][-1]["id"] + 1
user["name"] = name.replace('\x00','').replace('\u0000','')
user["password"] = password
user["salt"] = salt
user["description"] = description
users["users"].append(user)
with open(dbFile,'w') as output:
json.dump(users, output, indent=4)
comms.sendString(conn,"\r\nYour profile has been created" + name + "! Reconnect to login!")

3
bbs.kdev4 Normal file
View File

@ -0,0 +1,3 @@
[Project]
Manager=KDevGenericManager
Name=bbs

83
bbs.py Normal file
View File

@ -0,0 +1,83 @@
'''
BBS.py
A BBS that was shamelessly based entirely on the blog post found: https://www.fsxnet.nz/tutorials/bbs/python-bbs/start
'''
import configparser
import sys
import socket
import threading
from comms import comms
nodes = []
class bbsServer():
def __init__(self, port, host='0.0.0.0', nodemax=1):
self.port = port
self.host = host
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# try:
config = configparser.ConfigParser()
config.read("config.ini")
self.nodemax = config.getint("Main","MaxConnections")
# except:
# 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:
comms(conn, addr, 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()

88
comms.py Normal file
View File

@ -0,0 +1,88 @@
import auth, sys
import configparser
config = configparser.ConfigParser()
config.read(sys.argv[1])
def sendString(conn, text):
conn.sendall(text.encode('ascii'))
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, 'ascii')
def comms(conn, addr, node):
"""
comms - Initializes the communication with the user, authorizes them, and passes them to the menus
"""
iac_echo = bytes([255, 251, 1])
iac_sga = bytes([255, 251, 3])
conn.sendall(iac_echo)
conn.sendall(iac_sga)
# Splash Screen
try:
with open("pages/splash.asc") as splash:
for line in splash.readlines():
line = line.replace("%node%", str(node))
sendString(conn, line + "\r")
getString(conn, 1)
except:
sendString(conn, config.get("Main","DefaultSplash").replace("%node%", str(node)))
# Login Screen
sendString(conn, "\r\nEnter your username, or type NEW if a new user.")
sendString(conn, "\r\nLogin: ")
username = getString(conn, 16)
if username.replace('\x00','') == "NEW":
auth.create(conn)
else:
sendString(conn, "\r\nPassword: ")
password = getString(conn, 16)
auth.login(conn,username,password, node)

27
config.ini Normal file
View File

@ -0,0 +1,27 @@
[Main]
Port = 2043
DefaultSplash = "Welcome to the SB17 BBS on %node%!"
MaxConnections = 5
Menus = 2
[Auth]
userDb = users.json
[Menu0]
Title = "Main Menu"
Page = menus/main.asc
Groups = 0
options = 3
Option0 = Profile
Option0Key = P
Option1 = Edit Profile
Option1Key = E
Option2 = Logout
Option2Key = X
[Menu1]
Title = "Admin Menu"
Groups = 99
options = 1
Option0 = Back
Option0Key = B

44
docs/Configuration.md Normal file
View File

@ -0,0 +1,44 @@
# Configuration
This file contains the information necessary to configure Pyscii-BBS. The default configuration provided will run, but you will likely want to change many of these values.
## String Replacements
Except where noted, these can be used in either the asc page files, or the config strings.
* %node%
- Replaced with the node number of the connected party.
### Messages
* %subject%
- The message's subject
* %content%
- The message's content
* %date%
- The date the message was posted
## Config.ini Key-Value Pairs
* [Main] - Main Configuration Information.
- Port = The integer value of the port to bind Pyscii BBS to
- DefaultSplash = The string that will greet users if no splash.asc page exists in the "pages" location
* [Auth] - Configuration information related to Authentication and Authorization
- jsonFile = The string of the json file that holds the user database. This database is expected to follow the user.schema
* [Menu#] - Configuration for a menu. Starts with 0
- Title = The title of the menu, a string.
- Groups = A list of the groups that can see this menu
- options = an integer number of the count of options
- Option# = The option description to display to the user
- Option#Key = The key to press to use the menu option
## Pages
The pages for the BBS are stored in the "pages" directory.
* splash.asc
- Replaces the "DefaultSplash" Config Key/Value pair.
- Is shown to the users before login
* menu#.asc
- The default display screen for a given menu.
- If this does not exist, the menu will be displayed from the configuration in the config.ini file

37
docs/Roadmap.md Normal file
View File

@ -0,0 +1,37 @@
# Pyscii-BBS Roadmap
This file is the guiding roadmap for the Pyscii-BBS project
## Project Mission
Pyscii-BBS is supposed to be a telnet-capable data-secure BBS solution. It should be extensible, well-documented, and easy to implement.
## Project Stages
Right now the BBS is a barely functional bbs server. It requests Username and Password from the connecting system, and potentially returns an error or the user's description.
The plan is to write the project in the following stages:
Remember these rules for working on the following features:
* Everything must be extensible. There should be configuration options either in the form of asc page files or configuration key/values
* Data must be stored in a secure way. Just because the protocol is insecure, doesn't mean we should be.
* Anywhere data is stored, it must be in a file with a specific and valid JSON schema
* Document everything. Every configuration key/value, every asc page file purpose, every JSON schema.
### Pre-Alpha
#### Complete
- BBS Server accepts multiple connections up to a configurable max ✓
- BBS Server sends data to the client and gracefully terminates the connections ✓
- BBS Server accepts responses from the client ✓
- BBS Server can create users with passwords and profile information ✓
- BBS Server can authenticate users ✓
- BBS Server can show a menu to the user after logging in ✓
- BBS Server menu allows user to modify their own profile ✓
- BBS Server can display pre-coded messages in a messages space ✓
#### ToDo
- BBS Server can accept messages from users
- BBS Server can display user submitted messages to message boards
- BBS Server can show only certain users a different menu option (RBAC implementation)
At this point we have the bare minimum of a functioning BBS. The user flow at this stage is they connect, log in, and are presented with a menu that will let them edit their profile or visit a message board. The message board will be a single flow of messages and the user will have to select what messages to read. No unread/read list exists. It is a simple text-based telnet message board with an auth system.
### Alpha

35
docs/user.schema.json Normal file
View File

@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bbs.sb17.space/user.schema.json",
"title": "User",
"description": "A Pyscii-BBS user object",
"type": "object",
"properties": {
"id" : {
"description": "The unique ID of the user entity",
"type": "integer"
},
"name": {
"description": "The unique username of the user entity",
"type": "string"
},
"description": {
"description": "Description of the user Entity. Like a Profile String",
"type": "string"
},
"password": {
"description": "The user entity's password hash.",
"type": "string",
},
"salt":{
"description": "The user entity's salt. This will be generated by the application.",
"type": "string"
}
},
"required": [
"id",
"name",
"password",
"salt"
]
}

57
functions.py Normal file
View File

@ -0,0 +1,57 @@
import configparser
import json
import sys
import comms
import menus
import auth
config = configparser.ConfigParser()
config.read(sys.argv[1])
userDb = config.get("Auth","userDb")
def printProfile(conn, userId, node, lastMenu):
comms.sendString(conn, "\u001B[2JProfile:\r\n")
with open(userDb) as userdb:
users = json.loads(userdb.read())
for user in users['users']:
if user['id'] == userId:
name = "Name: " + user["name"]
description = "Description: " + user['description']
profile = name + "\r\n" + description + "\r\n"
comms.sendString(conn, profile)
comms.sendString(conn, "Press Enter to go back to the Menu... ")
comms.getChar(conn)
menus.printMenu(conn, lastMenu, node, userId)
def editProfile(conn, userID, node, lastMenu):
comms.sendString(conn, "\u001B[2JProfile:\r\n")
with open(userDb) as userdb:
users = json.loads(userdb.read())
for user in users['users']:
if user['id'] == userID:
name = "Name: " + user["name"]
description = "Description: " + user['description']
profile = name + "\r\n" + description + "\r\n"
comms.sendString(conn, profile)
comms.sendString(conn, "What would you like to edit?\r\n")
comms.sendString(conn, "D: Description\tP:Password\r\n? ")
response = comms.getString(conn,2)
if response.strip('\x00') == "D":
description = auth.setProfile(conn)
user['description'] = description
with open(userDb,'w') as output:
json.dump(users, output, indent=4)
if response.strip('\x00') == "P":
password = auth.setPassword(conn, user['salt'])
user['password'] = password
with open(userDb,'w') as output:
json.dump(users, output, indent=4)
menus.printMenu(conn, lastMenu, node, userID)
def pubMessages(conn, userID, node, lastMenu):
comms.sendString(conn, "There are no messages!\r\nPress any key to go back... ")
comms.getChar(conn)
menus.printMenu(conn, lastMenu, node, userID)

51
menus.py Normal file
View File

@ -0,0 +1,51 @@
import configparser, sys, comms, functions
config = configparser.ConfigParser()
config.read(sys.argv[1])
menuCount = config.getint("Main","Menus")
def printMenu(conn, menu, node, user):
comms.sendString(conn, "\u001B[2J\r")
try:
with open("pages/menu"+str(menu)+".asc") as menuAscii:
for line in menuAscii.readlines():
line = line.replace("%node%", str(node))
comms.sendString(conn, line + "\r")
response = comms.getString(conn, 2)
except:
dispMenu = "Menu"+str(menu)
options = []
for i in range(0, config.getint(dispMenu, "Options")):
option = (config.get(dispMenu, "Option"+str(i)), config.get(dispMenu, "Option"+str(i)+"Key"))
options.append(option)
comms.sendString(conn,"Menu: \r\n")
for option in options:
comms.sendString(conn, option[0]+" : "+option[1]+"\r\n")
comms.sendString(conn, "What would you like to do? ")
response = comms.getString(conn, 2)
parseResponse(conn, menu, node, user, response)
def parseResponse(conn, menu, node, user, response):
# Add Menu functions and results to this function
# Menu 0 Functions
if menu == 0:
comms.sendString(conn, "\u001B[2J\r")
if response.strip('\x00') == "X":
comms.sendString(conn, "Goodbye!")
if response.strip('\x00') == "P":
functions.printProfile(conn, user, node, menu)
if response.strip('\x00') == "E":
functions.editProfile(conn, user, node, menu)
if response.strip('\x00') == "M":
functions.pubMessages(conn, user, node, menu)
# Menu 1 Functions
if menu == 1:
if response.strip('\x00') == "B":
printMenu(conn, 0, node, user)

7
pages/menu0.asc Normal file
View File

@ -0,0 +1,7 @@
======================================*========================================
| MAIN MENU |
| P: Profile M: Messages |
| E: Edit Profile X: Logout |
| |
===============================================================================
What Do you want to do?

7
pages/splash.asc Normal file
View File

@ -0,0 +1,7 @@
____ ____ _ _____ ____ ____ ____
/ ___|| __ )/ |___ | | __ )| __ ) ___|
\___ \| _ \| | / / | _ \| _ \___ \
___) | |_) | | / / | |_) | |_) |_) |
|____/|____/|_|/_/ |____/|____/____/
You are connected on node: %node%
Press Enter to Continue...

10
test.py Normal file
View File

@ -0,0 +1,10 @@
import json
jstring = '{"users":[{"name": "J4YC33","description": "J4YC33\'s Just this person..."}]}'
with open('users.json') as userdb:
users = json.loads(userdb.read())
for user in users['users']:
if user['name'] == "J4YC33":
print(user['description'])

18
users.json Normal file
View File

@ -0,0 +1,18 @@
{
"users": [
{
"id": 0,
"name": "Default",
"password": "Unusable",
"description": "This account is not to be used",
"salt": "This account cannot be accessed"
},
{
"id": 1,
"name": "J4YC33",
"password": "d32190c6322e27087c934b7bd9ff642bbe71d5782f2242028c3c15298c7aeac6",
"salt": "SI13YMJl8kWLBhNN",
"description": "J4YC33 IS SPARTACUS!"
}
]
}