First attempt at CGI support.

This commit is contained in:
Michael Lazar 2019-08-26 23:41:10 -04:00
parent 65c8a6e47c
commit 27c2ee12ae
3 changed files with 84 additions and 18 deletions

9
examples/cgi_debug.cgi Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env python
"""
This is a demo CGI script that prints a bunch of debug information about
the request as an HTML document. Place it in the cgi-bin/ directory of your
server.
"""
import cgi
cgi.test()

View File

@ -7,7 +7,7 @@ import jetforce
def echo(environ, send_status): def echo(environ, send_status):
url = environ["URL"] url = environ["GEMINI_URL"]
send_status(jetforce.Status.SUCCESS, "text/gemini") send_status(jetforce.Status.SUCCESS, "text/gemini")
yield f"Received path: {url}".encode() yield f"Received path: {url}".encode()

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3.7 #!/usr/bin/env python3.7
import argparse import argparse
import asyncio import asyncio
import codecs
import dataclasses import dataclasses
import datetime import datetime
import mimetypes import mimetypes
@ -83,15 +84,16 @@ class Request:
def __init__(self, environ: dict): def __init__(self, environ: dict):
self.environ = environ self.environ = environ
url = urllib.parse.urlparse(environ["URL"]) self.url = environ["GEMINI_URL"]
self.scheme = url.scheme
self.hostname = url.hostname url_parts = urllib.parse.urlparse(self.url)
self.port = url.port self.scheme = url_parts.scheme
self.path = url.path self.hostname = url_parts.hostname
self.params = url.params self.port = url_parts.port
self.query = url.query self.path = url_parts.path
self.fragment = url.fragment self.params = url_parts.params
self.url = url.geturl() self.query = url_parts.query
self.fragment = url_parts.fragment
@dataclasses.dataclass @dataclasses.dataclass
@ -198,11 +200,18 @@ class StaticDirectoryApplication(JetforceApplication):
directory listing will be auto-generated. directory listing will be auto-generated.
""" """
def __init__(self, directory: str = "/var/gemini", index_file: str = "index.gmi"): def __init__(
self,
root_directory: str = "/var/gemini",
index_file: str = "index.gmi",
cgi_directory: str = "cgi-bin/",
):
super().__init__() super().__init__()
self.routes.append((RoutePattern(), self.serve_static_file)) self.routes.append((RoutePattern(), self.serve_static_file))
self.root = pathlib.Path(directory).resolve(strict=True) self.root = pathlib.Path(root_directory).resolve(strict=True)
self.cgi_directory = cgi_directory.strip("/") + "/"
self.index_file = index_file self.index_file = index_file
self.mimetypes = mimetypes.MimeTypes() self.mimetypes = mimetypes.MimeTypes()
self.mimetypes.add_type("text/gemini", ".gmi") self.mimetypes.add_type("text/gemini", ".gmi")
@ -220,6 +229,9 @@ class StaticDirectoryApplication(JetforceApplication):
filesystem_path = self.root / filename filesystem_path = self.root / filename
if filesystem_path.is_file(): if filesystem_path.is_file():
if str(filename).startswith(self.cgi_directory):
if os.access(filesystem_path, os.X_OK):
return self.run_cgi_script(filesystem_path, request.environ)
mimetype = self.guess_mimetype(filesystem_path.name) mimetype = self.guess_mimetype(filesystem_path.name)
generator = self.load_file(filesystem_path) generator = self.load_file(filesystem_path)
return Response(Status.SUCCESS, mimetype, generator) return Response(Status.SUCCESS, mimetype, generator)
@ -233,6 +245,36 @@ class StaticDirectoryApplication(JetforceApplication):
else: else:
return Response(Status.NOT_FOUND, "Not Found") return Response(Status.NOT_FOUND, "Not Found")
def run_cgi_script(self, filesystem_path: pathlib.Path, environ: dict):
script_name = str(filesystem_path)
cgi_env = environ.copy()
cgi_env["GATEWAY_INTERFACE"] = "GCI/1.1"
cgi_env["SCRIPT_NAME"] = script_name
out = subprocess.Popen(
[script_name],
stdout=subprocess.PIPE,
env=cgi_env,
bufsize=1,
universal_newlines=True,
errors="surrogateescape",
)
# Try to get the mimetype from the CGI response's content-type.
# Discard all other response headers for now.
mimetype = "text/plain"
for line in out.stdout:
header = line.lower().strip()
if header.startswith("content-type:"):
mimetype = header.split(":", maxsplit=1)[1]
elif not header:
# A empty line signals the end of the headers
break
# Re-encode the rest of the body as bytes
body = codecs.iterencode(out.stdout, encoding="utf-8", errors="surrogateescape")
return Response(Status.SUCCESS, mimetype, body)
def load_file(self, filesystem_path: pathlib.Path): def load_file(self, filesystem_path: pathlib.Path):
with filesystem_path.open("rb") as fp: with filesystem_path.open("rb") as fp:
data = fp.read(1024) data = fp.read(1024)
@ -325,13 +367,23 @@ class GeminiRequestHandler:
def build_environ(self) -> typing.Dict[str, typing.Any]: def build_environ(self) -> typing.Dict[str, typing.Any]:
""" """
Construct a dictionary that will be passed to the application handler. Construct a dictionary that will be passed to the application handler.
Variable names conform to the CGI spec defined in RFC 3875.
""" """
url_parts = urllib.parse.urlparse(self.url)
return { return {
"SERVER_HOST": self.server.host, "GEMINI_URL": self.url,
"SERVER_PORT": self.server.port,
"REMOTE_ADDR": self.remote_addr,
"HOSTNAME": self.server.hostname, "HOSTNAME": self.server.hostname,
"URL": self.url, "PATH_INFO": url_parts.path,
"QUERY_STRING": url_parts.query,
"SERVER_NAME": self.server.hostname,
"SERVER_PORT": str(self.server.port),
"SERVER_PROTOCOL": "GEMINI",
"SERVER_SOFTWARE": f"jetforce/{__version__}",
"REMOTE_ADDR": self.remote_addr,
"REMOTE_HOST": self.remote_addr,
"REMOTE_USER": "",
"REQUEST_METHOD": "GET",
} }
async def parse_header(self) -> None: async def parse_header(self) -> None:
@ -522,14 +574,19 @@ def run_server() -> None:
""" """
parser = command_line_parser() parser = command_line_parser()
parser.add_argument( parser.add_argument(
"--dir", help="Path on the filesystem to serve", default="/var/gemini" "--root-dir", help="Root path on the filesystem to serve", default="/var/gemini"
)
parser.add_argument(
"--cgi-dir",
help="CGI script folder, relative to the root path",
default="cgi-bin/",
) )
parser.add_argument( parser.add_argument(
"--index-file", help="The gemini directory index file", default="index.gmi" "--index-file", help="The gemini directory index file", default="index.gmi"
) )
args = parser.parse_args() args = parser.parse_args()
app = StaticDirectoryApplication(args.dir, args.index_file) app = StaticDirectoryApplication(args.root_dir, args.index_file, args.cgi_dir)
server = GeminiServer( server = GeminiServer(
host=args.host, host=args.host,
port=args.port, port=args.port,