#!/usr/bin/env python3 from socketserver import TCPServer from socketserver import BaseRequestHandler import subprocess import argparse import os import time import pwd import grp class GopherError(BaseException): pass class RequestError(GopherError): pass 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() 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: if location == True: self.host = host try: self.port = split[3] except IndexError: if location == True: self.port = str(port) def render(self): return "{}{}\t{}\t{}\t{}".format( self.item, self.text, self.location, self.host, self.port ) class Logger: def __init__(self, file=None): if file != None: self.fd = open(file, "a+") else: self.fd = False def time(self): return int(time.time()) def write(self, msg): if self.fd: self.fd.write("{}\n") def log(self, msg): ct = self.time() self.write("[{}] {}".format(str(ct), msg)) print("[{}] {}".format(str(ct), msg)) def warn(self, msg): ct = self.time() self.write("! [{}] {}".format(str(ct), msg)) print("! [{}] {}".format(str(ct), msg)) def error(self, msg): ct = self.time() self.write("!! [{}] {}".format(str(ct), msg)) print("!! [{}] {}".format(str(ct), msg)) self.close() raise SystemExit def close(self): if self.fd: self.fd.close() class GopherServerLogic: 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): if file[-1] != "/": ret = file + "/gophermap" else: ret = file + "gophermap" gph = True elif os.path.isfile(file): ret = file else: raise RequestError("unreachable state") return (ret, gph) def fileSendable(self, file): return os.access(file, os.F_OK|os.R_OK) def fileCGI(self, file): if exempt != None: for i in exempt: if i in file: return False if nocgi: return False return os.access(file, os.F_OK|os.R_OK|os.X_OK) def notFound(self): self.request.sendall( b"3error: file not found!\r\n.\r\n") def invalid(self): self.request.sendall( b"3error: selector contains '..'!\r\n.\r\n") def fileToFileArray(self, file): fd = open(file, "r") ret = [i.rstrip() for i in fd.readlines()] fd.close() return ret def sendFileArray(self, fileArray): [self.request.sendall( (i.rstrip() + "\r\n").encode("utf-8") ) for i in fileArray] 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() return out.decode("utf-8")\ .replace("\r", "")\ .split("\n") 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): if self.fileCGI(file): fa = self.cgi(file, query) elif self.fileSendable(file): fa = self.fileToFileArray(file) else: log.log("selector not found") self.notFound() return if gph: fa = self.gopherRenderer(fa) self.sendFileArray(fa) class GopherHandler(BaseRequestHandler, GopherServerLogic): def handle(self): log.log("request from {}".format( self.client_address[0])) decoded = self.recieveRequest() try: parsed = self.requestParser(decoded) except RequestError: log.log("request was invalid") self.invalid() return try: file = self.returnRelative(parsed[0]) except RequestError: log.log("selector not found") self.notFound() return log.log("serving {}".format(file[0])) self.serveFile(file[0], parsed[1], file[1]) log = Logger() def parseArgs(): parse = argparse.ArgumentParser() 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") 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") parse.add_argument("-ndx", "--direxc", action="extend", nargs="*", help="make directory-wide exception for file execution") parse.add_argument("-nx", "--nocgi", action="store_true") return parse.parse_args() if __name__ == "__main__": args = parseArgs() log.log("arguments parsed") switchgrp = grp.getgrnam(args.group).gr_gid switchusr = pwd.getpwnam(args.user).pw_uid host = args.host port = args.port location = args.dir exempt = args.direxc nocgi = args.nocgi log.log("arguments good") try: with TCPServer((host, port), GopherHandler) as server: os.setgid(switchgrp) os.seteuid(switchusr) log.log("switch user successful") os.chdir(location) log.log("change directory successful") server.serve_forever() except PermissionError: log.error("invalid permissions") except OSError: log.error("address already in use")