diff --git a/Dockerfile b/Dockerfile index ac1f557..149e53e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,8 @@ RUN pip install pyyaml WORKDIR /app COPY ./gmnd/__init__.py . COPY ./content content +COPY ./cgi-bin cgi-bin COPY config.yml . COPY --from=build-stage /app/certs certs EXPOSE 1965 -CMD ["python", "__init__.py", "--file", "config.yml" ] +CMD ["python", "__init__.py", "--file", "./config.yml" ] diff --git a/README.md b/README.md index a180c32..272d434 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # gMNd -gMNd is my gemini server, which is written in python. Documentation will primarily be supplied via [gemini://mic.ke/gmnd/docs](gemini://mic.ke/gmnd/docs), but if you are not yet able to access content via gemini, here is a quick start guide for your viewing pleasure: +gMNd is my gemini server, which is written in python. It has support for serving static files, or run cgi-scripts. Documentation will primarily be supplied via [gemini://mic.ke/gmnd/docs](gemini://mic.ke/gmnd/docs), but if you are not yet able to access content via gemini, here is a quick start guide for your viewing pleasure: -Currently it only serves static files. You can build and run it from the supplied Dockerfile if you so whish: +You can build and run it from the supplied Dockerfile if you so whish: ``` docker build -t gmnd:latest . ``` @@ -9,11 +9,11 @@ By just running it, it will create self signed certs and serve example content f ``` docker run -p 1965:1965 gmnd ``` -A slightly more interesting thing it can do is serve your own content, in this example from /tmp/content on your host machine: +A slightly more interesting thing it can do is serve your own content, in this example from /tmp/content on your host machine and cgi-scripts from /tmp/cgi-bin: ``` -docker run --mount type=bind,source="/tmp/content,target=/app/content" -p 1965:1965 gmnd +docker run --mount type=bind,source="/tmp/content,target=/app/content" --mount type=bind,source="/tmp/cgi-bin,target=/app/cgi-bin" -p 1965:1965 gmnd ``` -Or even supply your own certificates from the outside, in this example in /usr/local/certs: +Or even supply your own certificates from the outside, in this example in /usr/local/certs with static content from /tmp/content: ``` docker run --mount type=bind,source="/tmp/content,target=/app/content" --mount type=bind,source="/usr/local/certs,target=/app/certs" -p 1965:1965 gmnd ``` diff --git a/config.yml b/config.yml index b24981f..c2b8f6b 100644 --- a/config.yml +++ b/config.yml @@ -2,3 +2,6 @@ allow_dir_list: true listen_addr: '0.0.0.0' logg_level: 'DEBUG' +#cgi_registry: +# /.*: +# - ./cgi-bin/envtest.sh diff --git a/gmnd/__init__.py b/gmnd/__init__.py index 2c2a2e9..42d6565 100644 --- a/gmnd/__init__.py +++ b/gmnd/__init__.py @@ -1,10 +1,12 @@ import logging import mimetypes import os +import re import socket import ssl import sys from socket import AF_INET, SHUT_RDWR, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET +import subprocess from urllib.parse import urlparse import yaml @@ -39,6 +41,8 @@ class gMNd: 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: @@ -67,6 +71,7 @@ class gMNd: 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, @@ -86,7 +91,23 @@ class gMNd: header = get_header() body = b"" - if os.path.isfile(self.base_path + path): + + + 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: + cgi = False + 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', @@ -133,6 +154,41 @@ class gMNd: 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' + + 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 = {}