You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

223 lines
8.8 KiB

#!/usr/bin/python3
import logging
import mimetypes
import os
import re
import socket
import ssl
import subprocess
import sys
from socket import AF_INET, SHUT_RDWR, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET
from urllib.parse import urlparse
import yaml
class gMNd:
def __init__(self, options={}):
self.allow_dir_list = options.get('allow_dir_list', False)
self.base_path = options.get('base_path', './content')
self.cgi_registry = options.get('cgi_registry', None)
self.config_file = options.get('config_file', None)
self.listen_addr = options.get('listen_addr', '127.0.0.1')
self.listen_port = options.get('listen_port', 1965)
self.logg_level = options.get('logg_level', logging.INFO)
self.server_cert = options.get('server_cert', './certs/cert.pem')
self.server_key = options.get('server_key', './certs/cert.key')
logging.basicConfig(stream=sys.stderr, level=self.logg_level)
if self.config_file:
if os.path.isfile(self.config_file):
self.read_config()
logging.basicConfig(stream=sys.stderr, level=self.logg_level)
else:
logging.warning("Config file supplied, but it is not a file")
if not (os.path.isfile(self.server_cert)
and os.path.isfile(self.server_key)):
logging.error("ERROR: " + str(self.server_cert) + " and/or " +
str(self.server_key) +
" does not exist, and TLS is required for gemini.")
sys.exit(1)
self.bindsocket = socket.socket()
self.bindsocket.bind((self.listen_addr, self.listen_port))
self.bindsocket.listen(5)
def read_config(self):
with open(self.config_file) as configfile:
try:
config_dict = yaml.load(configfile, Loader=yaml.FullLoader)
except AttributeError:
config_dict = yaml.safe_load(configfile)
if 'allow_dir_list' in config_dict:
self.allow_dir_list = config_dict['allow_dir_list']
if 'base_path' in config_dict:
self.base_path = config_dict['base_path']
if 'cgi_registry' in config_dict:
self.cgi_registry = config_dict['cgi_registry']
if 'listen_addr' in config_dict:
self.listen_addr = config_dict['listen_addr']
if 'listen_port' in config_dict:
self.listen_port = config_dict['listen_port']
if 'logg_level' in config_dict:
if config_dict['logg_level'] == "CRITICAL":
self.logg_level = logging.CRITICAL
elif config_dict['logg_level'] == "DEBUG":
self.logg_level = logging.DEBUG
elif config_dict['logg_level'] == "ERROR":
self.logg_level = logging.ERROR
elif config_dict['logg_level'] == "INFO":
self.logg_level = logging.INFO
elif config_dict['logg_level'] == "NOTSET":
self.logg_level = logging.NOTSET
elif config_dict['logg_level'] == "WARNING":
self.logg_level = logging.WARNING
if 'server_cert' in config_dict:
self.server_cert = config_dict['server_cert']
if 'server_key' in config_dict:
self.server_key = config_dict['server_key']
def run(self):
while True:
logging.debug("Waiting for client")
newsocket, fromaddr = self.bindsocket.accept()
logging.debug("Client connected: {}:{}".format(
fromaddr[0], fromaddr[1]))
conn = ssl.wrap_socket(newsocket,
server_side=True,
certfile=self.server_cert,
keyfile=self.server_key)
logging.debug("SSL established. Peer: {}".format(
conn.getpeercert()))
try:
request = conn.recv()
logging.debug("Full request: {}".format(request))
url = urlparse(request)
scheme = url.scheme.decode()
netloc = url.netloc.decode()
path = url.path.decode().rstrip()
logging.debug("Scheme: {}".format(scheme))
logging.debug("Netloc: {}".format(netloc))
logging.debug("Path: {}".format(path))
header = get_header()
body = b""
cgi = False
try:
for key, val in self.cgi_registry.items():
if re.match(key, path):
cgi = True
env = self.get_env(url, fromaddr[0])
script = val
except:
script = None
env = None
if cgi:
body = run_cgi(script, env)[0]
elif os.path.isfile(self.base_path + path):
if not path.endswith(".gmi"):
header = get_header(
'20',
mimetypes.guess_type(self.base_path +
path)[0].encode())
cfile = open(self.base_path + path)
body = cfile.read().encode()
cfile.close()
elif os.path.isfile(self.base_path + path.rstrip('/') +
'/index.gmi'):
cfile = open(self.base_path + path.rstrip('/') +
'/index.gmi')
body = cfile.read().encode()
cfile.close()
logging.debug("Body: {}".format(body))
elif os.path.isdir(self.base_path +
path) and self.allow_dir_list:
body = self.get_dir_list(path)
else:
header = get_header('40', b"File not found")
conn.write(header + body)
finally:
logging.debug("Closing connection")
conn.shutdown(socket.SHUT_RDWR)
conn.close()
def get_dir_list(self, directory):
contents = b"#Contents:\r\n"
dirs = []
files = []
for mfile in os.listdir(self.base_path + directory):
if os.path.isdir(os.path.join(self.base_path + directory, mfile)):
dirs.append(os.path.join(directory, mfile))
elif os.path.isfile(os.path.join(self.base_path + directory,
mfile)):
files.append(os.path.join(directory, mfile))
dirs = sorted(dirs)
files = sorted(files)
for entry in dirs:
contents = contents + b"=> " + entry.encode() + b"\r\n"
for entry in files:
contents = contents + b"=> " + entry.encode() + b"\r\n"
return contents
def get_env(self, url, remote_addr):
path = url.path.decode().rstrip()
query = url.query.decode().rstrip()
env = {}
env['GATEWAY_INTERFACE'] = 'CGI/1.1'
env['GEMINI_DOCUMENT_ROOT'] = str(self.base_path)
env['GEMINI_URL'] = str(url.geturl().decode().rstrip())
env['GEMINI_URL_PATH'] = str(path)
env['PATH_INFO'] = str(path)
env['PATH_TRANSLATED'] = str(self.base_path + path)
env['QUERY_STRING'] = str(query)
env['REMOTE_ADDR'] = str(remote_addr)
try:
env['REMOTE_HOST'] = str(
socket.getfqdn(socket.gethostbyaddr(remote_addr)[0]))
except:
env['REMOTE_HOST'] = str(remote_addr)
env['SERVER_NAME'] = str(socket.getfqdn())
env['SERVER_PORT'] = str(self.listen_port)
env['SERVER_PROTOCOL'] = 'GEMINI'
env['SERVER_SOFTWARE'] = 'gMNd 0.0.1'
return env
def run_cgi(script, menv):
"""Run a command on system and capture result"""
process = subprocess.Popen(script,
env=menv,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
result = process.communicate()
return result
def get_header(status='20', meta=b"text/gemini"):
metadict = {}
metadict['10'] = meta
metadict['20'] = meta
metadict['30'] = meta
metadict['40'] = meta
metadict['50'] = meta
metadict['60'] = b"Client certificate required"
separator = b" "
terminator = b"\r\n"
header = bytes(status.encode()) + separator + metadict[status] + terminator
return header
if __name__ == "__main__":
options = {}
if (sys.argv[1] == "-f" or sys.argv[1] == "--file") and sys.argv[2]:
options['config_file'] = sys.argv[2]
server = gMNd(options)
server.run()