Initial Commit
This commit is contained in:
parent
72469e2172
commit
b6b67576c1
2
.kdev4/bbs.kdev4
Normal file
2
.kdev4/bbs.kdev4
Normal file
@ -0,0 +1,2 @@
|
||||
[Project]
|
||||
VersionControlSupport=kdevgit
|
@ -1,3 +1,3 @@
|
||||
# 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
68
auth.py
Normal 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!")
|
83
bbs.py
Normal file
83
bbs.py
Normal 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
88
comms.py
Normal 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
27
config.ini
Normal 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
44
docs/Configuration.md
Normal 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
37
docs/Roadmap.md
Normal 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
35
docs/user.schema.json
Normal 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
57
functions.py
Normal 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
51
menus.py
Normal 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
7
pages/menu0.asc
Normal 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
7
pages/splash.asc
Normal file
@ -0,0 +1,7 @@
|
||||
____ ____ _ _____ ____ ____ ____
|
||||
/ ___|| __ )/ |___ | | __ )| __ ) ___|
|
||||
\___ \| _ \| | / / | _ \| _ \___ \
|
||||
___) | |_) | | / / | |_) | |_) |_) |
|
||||
|____/|____/|_|/_/ |____/|____/____/
|
||||
You are connected on node: %node%
|
||||
Press Enter to Continue...
|
10
test.py
Normal file
10
test.py
Normal 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
18
users.json
Normal 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!"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user