First commit

This commit is contained in:
Fierelier 2022-02-14 22:43:12 +01:00
parent 6fef4d3fc6
commit cdcc03d0bf
23 changed files with 1987 additions and 0 deletions

90
fsockets.py Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env python3
import sys
import os
p = os.path.join
pUp = os.path.dirname
s = False
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
s = os.path.realpath(sys.executable)
else:
s = os.path.realpath(__file__)
sp = pUp(s)
# script start
import socket
import threading
import queue
# IMPORTANT! Obtain locks in this order, if you need multiple at once:
# - clientDataLock
# - clientsLock
# - serverThreadsLock
# - fileLock
modulePath = p(sp,"modules")
mainQueue = queue.Queue()
clientsLock = threading.Lock()
clientID = 0
clients = {}
clientDataLock = threading.Lock()
clientData = {}
serverThreadsLock = threading.Lock()
serverThreads = []
fileLock = threading.Lock()
def runCode(str, lcs = False, description = "loose-code"):
if lcs == False: lcs = {}
code = compile(str,description,"exec")
exec(code,globals(),lcs)
return lcs
def runScript(sf, lcs = False):
if lcs == False: lcs = {}
code = False
with fileLock:
with open(sf,"r",encoding="utf-8") as script:
code = script.read()
runCode(code,lcs,sf)
return lcs
def readModuleFile(path):
with open(path,"r",encoding="utf-8") as modulesFile:
for line in modulesFile:
line = line.split("#",1)[0].strip(" \t\r\n")
if line == "": continue
modType = line.rsplit(".",1)[-1].lower()
line = line.replace("\\","/")
if modType == "mods":
print(">> " +line+ " <<")
else:
print("> " +line+ " ...")
line = line.replace("/",os.path.sep)
if line.startswith("." +os.path.sep):
line = pUp(path) + line[1:]
else:
line = p(modulePath,line)
if modType == "py":
runScript(line,locals())
if modType == "mods":
readModuleFile(line)
def main():
if os.path.isfile(p(modulePath,"main.mods")):
print("Loading modules...")
print(">> main.mods <<")
readModuleFile(p(modulePath,"main.mods"))
print("OK.\n")
with serverThreadsLock:
for server in servers:
makeServer(*server)
print("Serving!\n")
main()

6
index/index.html Normal file
View File

@ -0,0 +1,6 @@
<html>
<head></head>
<body>
<h1>Hello world!</h1>
</body>
</html>

View File

@ -0,0 +1,2 @@
env["handler"] = handle404 # Always pretend as if the file wasn't found
env["htaccessPropagate"] = False # Do not read .fhtpyaccess files in sub-folders

View File

@ -0,0 +1,6 @@
<html>
<head></head>
<body>
<h1>You shouldn't be able to access this page.</h1>
</body>
</html>

33
index/test/index.pyp Normal file
View File

@ -0,0 +1,33 @@
global json
import json
response = '''<!DOCTYPE html>
<html>
<head>
<style>
body {
background-color: #000000;
color: #ffffff;
font-size: 12px;
}
code {
background-color: #222222;
display: block;
}
</style>
</head>
<body>'''
for key in env:
value = str(env[key])
if value == "": value = " "
response += "<b>" +html.escape(key)+ ":</b><br>\n<code>" +html.escape(value)+ "</code><br>\n"
response += "</html>"
simpleResponse(
env["self"].connection,"200 OK",
{
"Content-Type": "text/html; charset=UTF-8",
},(response).encode("utf-8")
)

95
modules/clients.py Normal file
View File

@ -0,0 +1,95 @@
global clientThreadIn
class clientThreadIn(threading.Thread):
def __init__(self,cID,connection,address):
threading.Thread.__init__(self)
self.cID = cID
self.connection = connection
self.address = address
def run(self):
try:
clientLoopIn(self)
except Exception as e:
handleException(e)
with clientDataLock:
with clientsLock:
closeClient(self.cID,0)
global clientThreadOut
class clientThreadOut(threading.Thread):
def __init__(self,cID,connection,address):
threading.Thread.__init__(self)
self.cID = cID
self.connection = connection
self.address = address
def run(self):
try:
clientLoopOut(self)
except Exception as e:
handleException(e)
with clientDataLock:
with clientsLock:
closeClient(self.cID,1)
global closeClient
def closeClient(cID,threadType = None):
try: # Close connection
clients[cID][0].close()
except:
pass
try: # Set reference of connection to false, to denote the client is to not be served
clients[cID][0] = False
except:
pass
try: # Set reference of the thread to false, to denote that it is closed
if threadType != None:
clients[cID][1 + threadType] = False
except:
pass
try: # Get rid of leftover data to free memory
if clients[cID] == [False,False,False]:
del clients[cID]
del clientData[cID]
except:
pass
global setClientData
def setClientData(cID,key,data):
clientData[cID][key] = data
global getClientData
def getClientData(cID,key):
if not key in clientData[cID]: return None
return clientData[cID][key]
def main():
def onConnectionEvent(event,eEnv,connection,address):
with clientDataLock:
with clientsLock:
global clientID
clientID += 1
cID = str(clientID)
threadIn = clientThreadIn(cID,connection,address)
threadOut = False
if enableOutThread:
threadOut = clientThreadOut(cID,connection,address)
clients[cID] = [connection,threadIn,threadOut]
clientData[cID] = {"address":address}
threadIn.start()
if enableOutThread:
threadOut.start()
if clientDebug:
print("---")
print("Clients: " +str(len(clients)))
print("Threads: " +str(threading.active_count()))
return True
addEventHandler("onConnection",onConnectionEvent)
main()

11
modules/connlimit.py Normal file
View File

@ -0,0 +1,11 @@
def main():
def onConnectionEvent(event,eEnv,connection,address):
count = 0
with clientDataLock:
for cID in clientData:
if getClientData(cID,"address") == address:
count += 1
if count >= maxConnections: return False
return True
addEventHandler("onConnection",onConnectionEvent)
main()

30
modules/events.py Normal file
View File

@ -0,0 +1,30 @@
global eventHandlers
eventHandlers = {}
global addEventHandler
def addEventHandler(event,handler):
if not event in eventHandlers: eventHandlers[event] = []
try:
eventHandlers[event].remove(handler)
except:
pass
eventHandlers[event].append(handler)
global removeEventHandler
def removeEventHandler(event,handler):
if not event in eventHandlers: return
try:
eventHandlers[event].remove(handler)
except:
pass
if len(eventHandlers[event]) == 0:
del eventHandlers[event]
global triggerEvent
def triggerEvent(event,*args,eEnv=False,**kwargs):
if not eEnv: eEnv = {}
if not event in eventHandlers: return
for func in eventHandlers[event]:
result = func(event,eEnv,*args,**kwargs)
if result == False: return False
return True

19
modules/exceptions.py Normal file
View File

@ -0,0 +1,19 @@
global traceback
import traceback
global excConnectionClosed
class excConnectionClosed(Exception): pass
global handleException
def handleException(e):
try:
if printExceptions:
print(traceback.format_exc())
except:
try:
print(e)
except:
try:
print("Printing exception failed!")
except:
pass

15
modules/helpers.py Normal file
View File

@ -0,0 +1,15 @@
global time
import time
global recv
def recv(conn,l):
start = time.process_time()
timeo = conn.gettimeout()
bytes = b""
while l > 0:
b = conn.recv(l)
if b == b"": raise ConnectionResetError
if time.process_time() - start > timeo: raise TimeoutError
bytes += b
l -= len(b)
return bytes

8
modules/http/404.py Normal file
View File

@ -0,0 +1,8 @@
global handle404
def handle404(env):
newPath = "/" + pathToURL(env["pathFixed"])
rawArgs = env["protocolHeaderList"][1].split("?",1)
if len(rawArgs) > 1:
newPath += "?" +rawArgs[-1]
notFound(env["self"].connection,newPath)

View File

@ -0,0 +1,55 @@
global handleBinary
def handleBinary(env):
filePath = env["fPath"]
fileExt = env["fileExt"]
connection = env["self"].connection
length = 0
with fileLock:
length = os.path.getsize(filePath)
rangeDefined = False
rangeStart = 0
rangeEnd = None
if "range" in env["headerList"]:
rangeDefined = True
rangeStart,rangeEnd = getRange(env["headerList"]["range"])
rangeStart,rangeEnd = convertRanges(rangeStart,rangeEnd,length)
if rangeStart == None:
raise # tell the client the request is invalid
mimetype = "application/octet-stream"
if fileExt in mimetypesBinary:
mimetype = mimetypesBinary[fileExt]
if not rangeDefined:
simpleResponse(connection,"200 OK",{
"Content-Length": str(length),
"Content-Type": mimetype,
"Accept-Ranges": "bytes"
})
else:
simpleResponse(connection,"206 Partial Content",{
"Content-Range": "bytes " +str(rangeStart)+ "-" +str(rangeEnd - 1)+ "/" +str(length),
"Content-Length": str(rangeEnd - rangeStart),
"Content-Type": mimetype,
"Accept-Ranges": "bytes"
})
print(rangeStart,rangeEnd)
cByte = rangeStart
while cByte < rangeEnd:
bytes = b""
rSize = readBufferSize
if cByte + rSize > rangeEnd:
rSize = rangeEnd - cByte
with fileLock:
with open(filePath,"rb") as file:
file.seek(cByte)
bytes = file.read(rSize)
connection.sendall(bytes)
cByte += rSize
fileHandlers[".*"] = handleBinary
for t in mimetypesBinary:
fileHandlers[t] = handleBinary

View File

@ -0,0 +1,18 @@
def main():
def handleHTTP(event,eenv,env):
env["htaccessPropagate"] = True
paths = [indexPath] + env["lPath"].split(os.path.sep)[:-1]
pathl = []
for pathbit in paths:
pathl.append(pathbit)
path = p(*pathl,".fhtpyaccess")
if not os.path.isfile(path): continue
handlePYP(env,path)
if env["htaccessPropagate"] == False:
return True
addEventHandler("handleHTTP",handleHTTP)
main()
fileHandlers["fhtpyaccess"] = handle404
fileHandlers["htaccess"] = handle404

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
global handlePYP
def handlePYP(env,cpath = False,getlock = True):
code = False
if not cpath: cpath = env["fPath"]
if getlock:
with fileLock:
with open(cpath,encoding="utf-8") as cfile:
code = cfile.read()
else:
with open(cpath,encoding="utf-8") as cfile:
code = cfile.read()
env = runCode(code,{"env":env},cpath)["env"]
fileHandlers["pyp"] = handlePYP
global indexFiles
indexFiles = ["index.pyp"] + indexFiles

View File

@ -0,0 +1,19 @@
global handleText
def handleText(env):
data = b""
with fileLock:
with open(env["fPath"],"rb") as textFile:
data = textFile.read()
simpleResponse(
env["self"].connection,"200 OK",
{
"Content-Type": mimetypesText[env["fileExt"]]+ "; charset=UTF-8",
"Accept-Ranges": "bytes"
},data
)
for t in mimetypesText:
fileHandlers[t] = handleText
indexFiles.append("index.html")

211
modules/http/helpers.py Normal file
View File

@ -0,0 +1,211 @@
global time
import time
global urllib
import urllib.parse
global html
import html
global getHeaderFromConnection
def getHeaderFromConnection(connection):
start = time.process_time()
timeo = connection.gettimeout()
l = 0
nl = 0
header = ""
while True:
b = connection.recv(1)
if b == b"": raise ConnectionResetError
if time.process_time() - start > timeo: raise TimeoutError
l += 1
if l > maxHeaderLength:
connection.sendall("""\
HTTP 1.1 413 Payload Too Large\r
\r
""".encode("ascii"))
raise excConnectionClosed
bd = None
try:
bd = b.decode("ascii")
except:
connection.sendall("""\
HTTP 1.1 400 Bad Request\r
\r
""".encode("ASCII"))
raise excConnectionClosed
if bd == "\n":
nl += 1
if nl == 2:
return header
else:
if bd != "\r":
nl = 0
header += bd
global parseHeader
def parseHeader(headers):
headers = headers.replace("\r","").split("\n")
del headers[-1]
for i in range(len(headers)):
headers[i] = headers[i].strip(" \t")
mainHeader = headers.pop(0).split(" ")
index = 0
length = len(mainHeader)
while index < length:
val = mainHeader[index]
val = val.strip(" \t")
if val == "":
del mainHeader[index]
length -= 1
continue
index += 1
mainHeader[0] = mainHeader[0].lower()
mainHeader[-1] = mainHeader[-1].lower()
headerList = {}
for header in headers:
header = header.split(":",1)
if len(header) != 2: continue
headerKey = header[0].strip(" \t").lower()
headerValue = header[1].strip(" \t")
if headerKey in headerList:
headers[headerKey] += ", " +headerValue
else:
headerList[headerKey] = headerValue
return mainHeader,headerList
global parseHeaderPath
def parseHeaderPath(path):
path = path.split("?",1)
if len(path) < 2: path.append("")
args = {}
for arg in path[1].split("&"):
arg = arg.split("=",1)
if len(arg) < 2: arg.append("")
args[urllib.parse.unquote(arg[0]).lower()] = urllib.parse.unquote(arg[1])
return urllib.parse.unquote(path[0]),args
global fixUserPath
def fixUserPath(path):
path = path.replace("\\","/") # Replace backslash with forward slash
path = path.lstrip("/")
npath = ""
for pathbit in path.split("/"):
pathbit = pathbit.strip(" \t\r\n") # Remove spaces, tabs, line return, and new line
if pathbit in [".",".."]: # Remove . and ..
continue
npath += pathbit + "/"
npath = npath[:-1]
while "//" in npath: npath = npath.replace("//","/") # Remove double slashes
return npath
global simpleResponse
def simpleResponse(connection,status,headers = None,content = None,autolength = True):
if headers == None:
headers = {}
if not "Accept-Ranges" in headers:
headers["Accept-Ranges"] = "none"
if content != None and autolength == True:
headers["Content-Length"] = str(len(content))
response = 'HTTP/1.1 ' +status+ '\r\n'
for header in headers:
response += header + ": " +headers[header] + "\r\n"
response += "\r\n"
connection.sendall(response.encode("ascii"))
if content != None:
connection.sendall(content)
global refer
def refer(connection,path):
simpleResponse(
connection,"302 Found",
{
"Content-Type": "text/html; charset=ASCII",
"Location": path
},('''\
<html>
<head></head>
<body>
Referring you to <a href="''' +html.escape(path)+ '''">''' +html.escape(path)+ '''</a>...
</body>
</html>''').encode("ascii")
)
global notFound
def notFound(connection,path):
simpleResponse(
connection,"404 Not Found",
{
"Content-Type": "text/html; charset=ASCII"
},('''\
<html>
<head></head>
<body>
Not found: <a href="''' +html.escape(path)+ '''">''' +html.escape(path)+ '''</a>
</body>
</html>''').encode("ascii")
)
global pathToURL
def pathToURL(path):
path = path.split("/")
length = len(path)
index = 0
while index < length:
path[index] = urllib.parse.quote(path[index])
index += 1
path = "/".join(path)
return path
# Can return the following:
# positive integer, None: Send entire file content starting at arg 1
# negative integer, None: Send entire file content starting at file end + arg 1
# positive integer, positive integer: Send entire file, from arg 1 to arg 2, not including arg 2
global getRange
def getRange(range):
try:
range = range.split("=",1)
if range[0].strip("\t ") != "bytes": return None,None
range = range[1].split(",")[0].split("-")
range[0] = range[0].strip("\t ")
range[1] = range[1].strip("\t ")
if range[0] == "":
return 0 - int(range[1]),None
if range[1] == "":
return int(range[0]),None
return int(range[0]),int(range[1]) + 1
except:
return 0,None
global convertRanges
def convertRanges(rangeStart,rangeEnd,length):
# Convert given ranges into complete ranges
if rangeStart < 0:
rangeStart = length - rangeStart
rangeEnd = length
else:
if rangeEnd == None:
rangeEnd = length
# Check if the ranges make sense
if rangeStart < 0:
return None,None
if rangeEnd > length:
return None,None
if rangeStart > rangeEnd:
return None,None
# OK
return rangeStart,rangeEnd

11
modules/http/main.mods Normal file
View File

@ -0,0 +1,11 @@
./settings.py # Settings
./helpers.py # Helper functions
./main.py # Main loop and functions
./404.py # 404 page
# File handlers:
./file-handlers/mimetypes.py # List of file endings and the mimetypes they belong to
./file-handlers/binary.py # Images, video, audio, executables, etc...
./file-handlers/text.py # HTML, XML, TXT, etc...
./file-handlers/pyp.py # pyp, fhttpy's script format
./file-handlers/htaccess.py # .fhtpyaccess - can be used to override handlers on an entire folder

85
modules/http/main.py Normal file
View File

@ -0,0 +1,85 @@
global email
import email.utils
global fileHandlers
fileHandlers = {}
global indexFiles
indexFiles = []
global pathHandlers
pathHandlers = {}
global clientLoopIn
def clientLoopIn(self):
env = {}
env["self"] = self
env["requestTime"] = time.time()
env["header"] = getHeaderFromConnection(self.connection)
env["protocolHeaderList"],env["headerList"] = parseHeader(env["header"])
env["cmd"] = env["protocolHeaderList"][0]
env["path"],env["args"] = parseHeaderPath(env["protocolHeaderList"][1])
env["pathFixed"] = fixUserPath(env["path"])
env["lPath"] = env["pathFixed"].replace("/",os.path.sep)
env["fPath"] = p(indexPath,env["lPath"])
env["fileExt"] = "."
if not env["pathFixed"] == "" and not os.path.isfile(env["fPath"]) and os.path.isdir(env["fPath"]) and env["pathFixed"][-1] != "/": env["pathFixed"] += "/" # This is dirty, since it possibly circumvents .fhtpyaccess (You can see if a folder exists or not by probing)
if "/" + env["pathFixed"] != env["path"]:
newPath = "/" + pathToURL(env["pathFixed"])
rawArgs = env["protocolHeaderList"][1].split("?",1)
if len(rawArgs) > 1:
newPath += "?" +rawArgs[-1]
refer(self.connection,newPath)
return
if env["pathFixed"] in pathHandlers:
pathHandlers[env["pathFixed"]](env)
return
if not os.path.isfile(env["fPath"]):
if not os.path.isdir(env["fPath"]):
handle404(env)
return
found = False
for file in indexFiles:
if os.path.isfile(p(env["fPath"],file)):
found = file
break
if found == False:
env["fileExt"] = ".d"
env["lPath"] = p(env["lPath"],".")
env["fPath"] = p(indexPath,env["lPath"])
else:
env["lPath"] = p(env["lPath"],found)
env["fPath"] = p(indexPath,env["lPath"])
lPathSplit = env["lPath"].rsplit(os.path.sep,1)[-1].rsplit(".",1)
if len(lPathSplit) > 1:
env["fileExt"] = lPathSplit[-1].lower()
else:
lPathSplit = env["lPath"].rsplit(os.path.sep,1)[-1].rsplit(".",1)
if len(lPathSplit) > 1:
env["fileExt"] = lPathSplit[-1].lower()
env["fPathDir"] = pUp(env["fPath"])
env["requestTimeFormatted"] = email.utils.formatdate(int(env["requestTime"])).replace("-0000","GMT")
env["handler"] = False
if env["fileExt"] in fileHandlers:
env["handler"] = fileHandlers[env["fileExt"]]
elif ".*" in fileHandlers:
env["handler"] = fileHandlers[".*"]
if triggerEvent("handleHTTP",env) == False: return
if env["handler"]:
env["handler"](env)
else:
handle404(env)
return

6
modules/http/settings.py Normal file
View File

@ -0,0 +1,6 @@
global maxHeaderLength
maxHeaderLength = 4096
global indexPath
indexPath = p(sp,"index")
global readBufferSize
readBufferSize = 32768

8
modules/main.mods Normal file
View File

@ -0,0 +1,8 @@
settings.py # User settings
helpers.py # Helper functions
events.py # Event/event handler implementation
exceptions.py # Handle exceptions, close connections
servers.py # Create sockets, optionally with SSL/TLS
connlimit.py # Optional: Limit the amount of connections made by one IP
clients.py # Create and remove client sessions and connections
http/main.mods # HTTP server

45
modules/servers.py Normal file
View File

@ -0,0 +1,45 @@
global ssl
import ssl
global serverThread
class serverThread(threading.Thread):
def __init__(self,socket):
threading.Thread.__init__(self)
self.socket = socket
def run(self):
connection = False
address = False
while True:
try:
connection,address = self.socket.accept()
except:
continue
try:
connection.settimeout(timeout)
if not triggerEvent("onConnection",connection,address): raise excConnectionClosed
except Exception as e:
handleException(e)
try:
connection.close()
except:
pass
global makeServer
def makeServer(host,port,https):
print("Opening " +str(host)+ ":" +str(port)+ " (" +str(https)+ ") ...")
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serverSocket.bind((host,port))
serverSocket.settimeout(5)
if https:
serverSocket = ssl.wrap_socket(
serverSocket,
server_side = True,
certfile = https,
ssl_version = ssl.PROTOCOL_TLS
)
serverSocket.listen(65535)
thread = serverThread(serverSocket)
serverThreads.append(thread)
thread.start()

17
modules/settings.py Normal file
View File

@ -0,0 +1,17 @@
global servers
servers = [
# Host Port SSL Certificate
("127.0.0.1", 80, False),
# ("127.0.0.1", 443, "localhost.pem")
]
global timeout
timeout = 15 # Seconds until the connection should be timed out
global maxConnections
maxConnections = 50 # Maximum connections per IP, needs connlimit.py to be activated
global enableOutThread
enableOutThread = False # Use a seperate thread for data output?
global printExceptions
printExceptions = False # Print exceptions as they happen, enable if you're developing
global clientDebug
clientDebug = False # Print how many clients and threads there are