2021-07-29 20:43:26 -05:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2021-07-20 12:50:52 -05:00
|
|
|
from socketserver import TCPServer
|
|
|
|
from socketserver import BaseRequestHandler
|
2021-07-19 15:11:31 -05:00
|
|
|
import subprocess
|
2021-07-20 03:43:12 -05:00
|
|
|
import argparse
|
2021-07-18 23:24:08 -05:00
|
|
|
import os
|
2021-07-20 02:55:32 -05:00
|
|
|
import time
|
2021-07-20 03:18:48 -05:00
|
|
|
import pwd
|
|
|
|
import grp
|
2021-07-18 23:24:08 -05:00
|
|
|
|
|
|
|
class GopherError(BaseException): pass
|
|
|
|
class RequestError(GopherError): pass
|
|
|
|
|
2021-07-19 16:17:45 -05:00
|
|
|
class GopherLine:
|
|
|
|
def __init__(self, line):
|
|
|
|
self.item = "i"
|
|
|
|
self.text = ""
|
|
|
|
self.location = "null"
|
|
|
|
self.host = "null.host"
|
|
|
|
self.port = "0"
|
|
|
|
self.line = line
|
|
|
|
self.parse()
|
2021-07-20 12:50:52 -05:00
|
|
|
|
2021-07-19 16:17:45 -05:00
|
|
|
def parse(self):
|
|
|
|
split = self.line.rstrip().split('\t')
|
|
|
|
location = False
|
|
|
|
if len(split) == 1:
|
|
|
|
self.text = split[0]
|
|
|
|
return
|
|
|
|
|
|
|
|
try: self.item = split[0][0]
|
|
|
|
except IndexError: pass
|
|
|
|
|
|
|
|
try: self.text = split[0][1:]
|
|
|
|
except IndexError: pass
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.location = split[1]
|
|
|
|
location = True
|
|
|
|
except IndexError: pass
|
|
|
|
|
|
|
|
try: self.host = split[2]
|
|
|
|
except IndexError:
|
2021-07-29 22:59:21 -05:00
|
|
|
if location == True: self.host = ghost
|
2021-07-19 16:17:45 -05:00
|
|
|
|
|
|
|
try: self.port = split[3]
|
|
|
|
except IndexError:
|
2021-07-19 17:15:39 -05:00
|
|
|
if location == True: self.port = str(port)
|
2021-07-20 02:44:48 -05:00
|
|
|
|
2021-07-19 16:17:45 -05:00
|
|
|
def render(self):
|
|
|
|
return "{}{}\t{}\t{}\t{}".format(
|
|
|
|
self.item,
|
|
|
|
self.text,
|
|
|
|
self.location,
|
|
|
|
self.host,
|
|
|
|
self.port
|
|
|
|
)
|
|
|
|
|
2021-07-20 02:55:32 -05:00
|
|
|
class Logger:
|
|
|
|
def __init__(self, file=None):
|
|
|
|
if file != None: self.fd = open(file, "a+")
|
|
|
|
else: self.fd = False
|
2021-07-20 12:50:52 -05:00
|
|
|
|
2021-07-20 02:59:01 -05:00
|
|
|
def time(self):
|
2021-07-20 02:55:32 -05:00
|
|
|
return int(time.time())
|
2021-07-20 12:50:52 -05:00
|
|
|
|
2021-07-20 02:55:32 -05:00
|
|
|
def write(self, msg):
|
|
|
|
if self.fd: self.fd.write("{}\n")
|
2021-07-20 12:50:52 -05:00
|
|
|
|
2021-07-20 02:55:32 -05:00
|
|
|
def log(self, msg):
|
|
|
|
ct = self.time()
|
|
|
|
self.write("[{}] {}".format(str(ct), msg))
|
|
|
|
print("[{}] {}".format(str(ct), msg))
|
2021-07-20 12:50:52 -05:00
|
|
|
|
2021-07-20 02:55:32 -05:00
|
|
|
def warn(self, msg):
|
|
|
|
ct = self.time()
|
|
|
|
self.write("! [{}] {}".format(str(ct), msg))
|
|
|
|
print("! [{}] {}".format(str(ct), msg))
|
2021-07-20 12:50:52 -05:00
|
|
|
|
2021-07-20 02:55:32 -05:00
|
|
|
def error(self, msg):
|
|
|
|
ct = self.time()
|
|
|
|
self.write("!! [{}] {}".format(str(ct), msg))
|
|
|
|
print("!! [{}] {}".format(str(ct), msg))
|
|
|
|
self.close()
|
|
|
|
raise SystemExit
|
2021-07-20 12:50:52 -05:00
|
|
|
|
2021-07-20 02:55:32 -05:00
|
|
|
def close(self):
|
2021-07-20 03:02:47 -05:00
|
|
|
if self.fd: self.fd.close()
|
2021-07-20 02:55:32 -05:00
|
|
|
|
2021-07-20 02:40:48 -05:00
|
|
|
class GopherServerLogic:
|
2021-07-20 02:35:29 -05:00
|
|
|
def recieveRequest(self):
|
|
|
|
data = b""
|
|
|
|
while data[-2:] != b"\r\n":
|
|
|
|
data += self.request.recv(1)
|
|
|
|
decoded = data[:-2].decode("utf-8")
|
|
|
|
return decoded
|
|
|
|
|
|
|
|
def requestParser(self, request):
|
|
|
|
if ".." in request: raise RequestError
|
|
|
|
try:
|
|
|
|
if request[0] == "/": request = request[1:]
|
|
|
|
except IndexError: pass
|
|
|
|
request = request.replace("?", "\t").split("\t")
|
|
|
|
try: return (request[0], request[1])
|
|
|
|
except IndexError: return (request[0], "")
|
|
|
|
|
|
|
|
def returnRelative(self, file):
|
|
|
|
gph = False
|
|
|
|
if file == "":
|
|
|
|
ret = "gophermap"
|
|
|
|
gph = True
|
|
|
|
elif os.path.isdir(file):
|
2021-07-23 20:12:36 -05:00
|
|
|
if file[-1] != "/": ret = file + "/gophermap"
|
|
|
|
else: ret = file + "gophermap"
|
2021-07-20 02:35:29 -05:00
|
|
|
gph = True
|
2021-07-23 20:12:36 -05:00
|
|
|
elif os.path.isfile(file):
|
|
|
|
ret = file
|
2021-07-20 02:35:29 -05:00
|
|
|
else: raise RequestError("unreachable state")
|
|
|
|
return (ret, gph)
|
|
|
|
|
2021-07-20 12:50:52 -05:00
|
|
|
def fileSendable(self, file):
|
|
|
|
return os.access(file, os.F_OK|os.R_OK)
|
2021-07-20 02:35:29 -05:00
|
|
|
|
|
|
|
def fileCGI(self, file):
|
2021-07-23 20:12:36 -05:00
|
|
|
if exempt != None:
|
|
|
|
for i in exempt:
|
|
|
|
if i in file: return False
|
|
|
|
if nocgi:
|
|
|
|
return False
|
2021-07-20 02:35:29 -05:00
|
|
|
return os.access(file, os.F_OK|os.R_OK|os.X_OK)
|
|
|
|
|
|
|
|
def notFound(self):
|
2021-07-20 12:50:52 -05:00
|
|
|
self.request.sendall(
|
|
|
|
b"3error: file not found!\r\n.\r\n")
|
2021-07-20 02:35:29 -05:00
|
|
|
|
|
|
|
def invalid(self):
|
2021-07-20 12:50:52 -05:00
|
|
|
self.request.sendall(
|
|
|
|
b"3error: selector contains '..'!\r\n.\r\n")
|
2021-07-20 02:35:29 -05:00
|
|
|
|
|
|
|
def fileToFileArray(self, file):
|
|
|
|
fd = open(file, "r")
|
|
|
|
ret = [i.rstrip() for i in fd.readlines()]
|
|
|
|
fd.close()
|
|
|
|
return ret
|
|
|
|
|
2021-07-20 02:44:48 -05:00
|
|
|
def sendFileArray(self, fileArray):
|
2021-07-20 02:35:29 -05:00
|
|
|
[self.request.sendall(
|
|
|
|
(i.rstrip() + "\r\n").encode("utf-8")
|
2021-07-20 02:44:48 -05:00
|
|
|
) for i in fileArray]
|
2021-07-20 02:35:29 -05:00
|
|
|
self.request.sendall(b".\r\n")
|
|
|
|
|
|
|
|
def cgi(self, file, query):
|
|
|
|
env = {}
|
|
|
|
env["QUERY_STRING"] = query
|
|
|
|
env["SCRIPT_NAME"] = "/" + file
|
|
|
|
env["REMOTE_ADDR"] = self.client_address[0]
|
|
|
|
|
|
|
|
proc = subprocess.Popen(
|
|
|
|
[os.getcwd() + "/" + file],
|
|
|
|
stdout = subprocess.PIPE,
|
|
|
|
env = env
|
|
|
|
)
|
|
|
|
|
|
|
|
try: out, err = proc.communicate(timeout=10)
|
|
|
|
except TimeoutExpired:
|
|
|
|
proc.kill()
|
|
|
|
out, err = proc.communicate()
|
|
|
|
|
2021-07-20 12:50:52 -05:00
|
|
|
return out.decode("utf-8")\
|
|
|
|
.replace("\r", "")\
|
|
|
|
.split("\n")
|
2021-07-20 02:35:29 -05:00
|
|
|
|
|
|
|
def gopherRenderer(self, fileArray):
|
|
|
|
gopherlines = []
|
|
|
|
for i in fileArray:
|
|
|
|
gopherlines.append(GopherLine(i))
|
|
|
|
returned = []
|
|
|
|
for i in gopherlines:
|
|
|
|
returned.append(i.render())
|
|
|
|
return returned
|
|
|
|
|
|
|
|
def serveFile(self, file, query, gph):
|
2021-07-23 20:12:36 -05:00
|
|
|
if self.fileCGI(file):
|
|
|
|
fa = self.cgi(file, query)
|
2021-07-20 12:50:52 -05:00
|
|
|
elif self.fileSendable(file):
|
|
|
|
fa = self.fileToFileArray(file)
|
2021-07-20 02:35:29 -05:00
|
|
|
else:
|
2021-07-20 02:59:01 -05:00
|
|
|
log.log("selector not found")
|
2021-07-20 02:35:29 -05:00
|
|
|
self.notFound()
|
|
|
|
return
|
|
|
|
if gph: fa = self.gopherRenderer(fa)
|
|
|
|
self.sendFileArray(fa)
|
|
|
|
|
2021-07-20 12:50:52 -05:00
|
|
|
class GopherHandler(BaseRequestHandler,
|
|
|
|
GopherServerLogic):
|
2021-07-18 23:24:08 -05:00
|
|
|
def handle(self):
|
2021-07-20 12:50:52 -05:00
|
|
|
log.log("request from {}".format(
|
|
|
|
self.client_address[0]))
|
2021-07-20 02:35:29 -05:00
|
|
|
decoded = self.recieveRequest()
|
2021-07-20 02:44:48 -05:00
|
|
|
try: parsed = self.requestParser(decoded)
|
2021-07-19 15:11:31 -05:00
|
|
|
except RequestError:
|
2021-07-20 02:59:01 -05:00
|
|
|
log.log("request was invalid")
|
2021-07-20 02:35:29 -05:00
|
|
|
self.invalid()
|
2021-07-19 15:11:31 -05:00
|
|
|
return
|
|
|
|
|
2021-07-20 02:35:29 -05:00
|
|
|
try: file = self.returnRelative(parsed[0])
|
2021-07-18 23:24:08 -05:00
|
|
|
except RequestError:
|
2021-07-20 02:59:01 -05:00
|
|
|
log.log("selector not found")
|
2021-07-20 02:35:29 -05:00
|
|
|
self.notFound()
|
2021-07-18 23:24:08 -05:00
|
|
|
return
|
2021-07-20 02:59:01 -05:00
|
|
|
log.log("serving {}".format(file[0]))
|
2021-07-20 02:35:29 -05:00
|
|
|
self.serveFile(file[0], parsed[1], file[1])
|
2021-07-18 23:24:08 -05:00
|
|
|
|
2021-07-20 02:59:59 -05:00
|
|
|
log = Logger()
|
|
|
|
|
2021-07-20 12:39:53 -05:00
|
|
|
def parseArgs():
|
2021-07-20 03:43:12 -05:00
|
|
|
parse = argparse.ArgumentParser()
|
2021-07-20 12:50:52 -05:00
|
|
|
parse.add_argument("-u", "--user", default="nobody",
|
|
|
|
help="user to change to on startup")
|
|
|
|
parse.add_argument("-g", "--group", default="nobody",
|
|
|
|
help="group to change to on startup")
|
|
|
|
parse.add_argument("-s", "--host", default="localhost",
|
|
|
|
help="host to host on")
|
2021-07-29 22:59:21 -05:00
|
|
|
parse.add_argument("-gs", "--gopherhost", default=None,
|
|
|
|
help="host defaulted to in links on gophermaps")
|
2021-07-20 12:50:52 -05:00
|
|
|
parse.add_argument("-p", "--port", default=70, type=int,
|
|
|
|
help="port to host on")
|
|
|
|
parse.add_argument("-d", "--dir", default="/var/gopher",
|
|
|
|
help="directory to host from")
|
2021-07-23 20:12:36 -05:00
|
|
|
parse.add_argument("-ndx", "--direxc",
|
2021-07-29 21:00:41 -05:00
|
|
|
action="append", nargs="*",
|
2021-07-23 20:12:36 -05:00
|
|
|
help="make directory-wide exception for file execution")
|
2021-07-29 20:46:15 -05:00
|
|
|
parse.add_argument("-nx", "--nocgi", action="store_true",
|
|
|
|
help="disable execution of cgi scripts")
|
2021-07-20 12:39:53 -05:00
|
|
|
return parse.parse_args()
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
args = parseArgs()
|
2021-07-20 03:43:12 -05:00
|
|
|
log.log("arguments parsed")
|
|
|
|
|
|
|
|
switchgrp = grp.getgrnam(args.group).gr_gid
|
|
|
|
switchusr = pwd.getpwnam(args.user).pw_uid
|
|
|
|
host = args.host
|
2021-07-29 22:59:21 -05:00
|
|
|
ghost = args.gopherhost
|
2021-07-20 03:43:12 -05:00
|
|
|
port = args.port
|
|
|
|
location = args.dir
|
2021-07-23 20:12:36 -05:00
|
|
|
exempt = args.direxc
|
|
|
|
nocgi = args.nocgi
|
2021-07-20 03:43:12 -05:00
|
|
|
log.log("arguments good")
|
2021-07-29 22:59:21 -05:00
|
|
|
if ghost == None: ghost = host
|
2021-07-20 03:31:02 -05:00
|
|
|
try:
|
2021-07-20 12:50:52 -05:00
|
|
|
with TCPServer((host, port), GopherHandler) as server:
|
2021-07-20 03:31:02 -05:00
|
|
|
os.setgid(switchgrp)
|
|
|
|
os.seteuid(switchusr)
|
|
|
|
log.log("switch user successful")
|
|
|
|
os.chdir(location)
|
|
|
|
log.log("change directory successful")
|
|
|
|
server.serve_forever()
|
2021-07-20 12:50:52 -05:00
|
|
|
except PermissionError: log.error("invalid permissions")
|
|
|
|
except OSError: log.error("address already in use")
|