Adding static directory serving
This commit is contained in:
parent
16c2140d2b
commit
8db0ae5217
105
jetforce.py
105
jetforce.py
|
@ -2,6 +2,8 @@
|
|||
import argparse
|
||||
import asyncio
|
||||
import datetime
|
||||
import mimetypes
|
||||
import os
|
||||
import ssl
|
||||
import sys
|
||||
from typing import Any, Callable, Dict, Iterator, Optional, Union
|
||||
|
@ -14,14 +16,14 @@ __copyright__ = "(c) 2019 Michael Lazar"
|
|||
|
||||
|
||||
ABOUT = fr"""
|
||||
You are now running...
|
||||
You are now riding on...
|
||||
_________ _____________
|
||||
______ /______ /___ __/_______________________
|
||||
___ _ /_ _ \ __/_ /_ _ __ \_ ___/ ___/ _ \
|
||||
/ /_/ / / __/ /_ _ __/ / /_/ / / / /__ / __/
|
||||
\____/ \___/\__/ /_/ \____//_/ \___/ \___/
|
||||
|
||||
An Experimental Gemini Protocol Server, v{__version__}
|
||||
An Experimental Gemini Server, v{__version__}
|
||||
https://github.com/michael-lazar/jetforce
|
||||
"""
|
||||
|
||||
|
@ -38,7 +40,7 @@ STATUS_TOO_MANY_REQUESTS = 9
|
|||
|
||||
class EchoApp:
|
||||
"""
|
||||
A demonstration of a simple echo server in Gemini.
|
||||
A simple application that echos back the requested path.
|
||||
"""
|
||||
|
||||
def __init__(self, environ: dict, send_status: Callable) -> None:
|
||||
|
@ -51,6 +53,76 @@ class EchoApp:
|
|||
yield f"Received path: {path}".encode()
|
||||
|
||||
|
||||
class StaticDirectoryApp:
|
||||
"""
|
||||
Serve a static directory over Gemini.
|
||||
|
||||
If a directory contains a hidden file with the name ".gemini", that file
|
||||
will be returned when the directory path is requested. Otherwise, a
|
||||
directory listing will be auto-generated.
|
||||
"""
|
||||
|
||||
root: str = "/var/gemini"
|
||||
|
||||
def __init__(self, environ: dict, send_status: Callable) -> None:
|
||||
self.environ = environ
|
||||
self.send_status = send_status
|
||||
|
||||
self.mimetypes = mimetypes.MimeTypes()
|
||||
|
||||
def __iter__(self) -> Iterator[bytes]:
|
||||
filename = self.environ["PATH"]
|
||||
filename = filename.lstrip("/")
|
||||
|
||||
abs_filename = os.path.abspath(os.path.join(self.root, filename))
|
||||
if not abs_filename.startswith(self.root):
|
||||
# Guard against breaking out of the directory
|
||||
self.send_status(STATUS_NOT_FOUND, "Not Found")
|
||||
|
||||
elif os.path.isfile(abs_filename):
|
||||
mimetype = self.guess_mimetype(abs_filename)
|
||||
yield from self.load_file(abs_filename, mimetype)
|
||||
|
||||
elif os.path.isdir(abs_filename):
|
||||
gemini_map_file = os.path.join(abs_filename, ".gemini")
|
||||
if os.path.exists(gemini_map_file):
|
||||
yield from self.load_file(gemini_map_file, "text/gemini")
|
||||
else:
|
||||
yield from self.list_directory(abs_filename)
|
||||
|
||||
else:
|
||||
self.send_status(STATUS_NOT_FOUND, "Not Found")
|
||||
|
||||
def load_file(self, abs_filename: str, mimetype: str):
|
||||
self.send_status(STATUS_SUCCESS, mimetype)
|
||||
with open(abs_filename, "rb") as fp:
|
||||
data = fp.read(1024)
|
||||
while data:
|
||||
yield data
|
||||
data = fp.read(1024)
|
||||
|
||||
def list_directory(self, abs_folder: str):
|
||||
self.send_status(STATUS_SUCCESS, "text/gemini")
|
||||
|
||||
for filename in os.listdir(abs_folder):
|
||||
if filename == ".gemini" or filename.startswith("~"):
|
||||
continue
|
||||
abs_filename = os.path.join(abs_folder, filename)
|
||||
if os.path.isdir(abs_filename):
|
||||
# The directory end slash is necessary for relative paths to work
|
||||
filename += "/"
|
||||
yield f"=>{filename}\r\n".encode()
|
||||
|
||||
def guess_mimetype(self, filename: str):
|
||||
mime, encoding = self.mimetypes.guess_type(filename)
|
||||
if encoding:
|
||||
return f"{mime}; charset={encoding}"
|
||||
elif mime:
|
||||
return mime
|
||||
else:
|
||||
return "text/plain"
|
||||
|
||||
|
||||
class GeminiRequestHandler:
|
||||
"""
|
||||
Handle a single Gemini Protocol TCP request.
|
||||
|
@ -103,14 +175,14 @@ class GeminiRequestHandler:
|
|||
environ = self.build_environ()
|
||||
app = self.app(environ, self.write_status)
|
||||
for data in app:
|
||||
self.write_body(data)
|
||||
await self.write_body(data)
|
||||
|
||||
except Exception as e:
|
||||
self.write_status(STATUS_SERVER_ERROR, str(e))
|
||||
raise
|
||||
|
||||
finally:
|
||||
self.flush_status()
|
||||
await self.flush_status()
|
||||
self.log_request()
|
||||
await writer.drain()
|
||||
|
||||
|
@ -121,6 +193,8 @@ class GeminiRequestHandler:
|
|||
return {
|
||||
"SERVER_HOST": self.server.host,
|
||||
"SERVER_PORT": self.server.port,
|
||||
"CLIENT_IP": self.client_ip,
|
||||
"CLIENT_PORT": self.client_port,
|
||||
"PATH": self.path,
|
||||
}
|
||||
|
||||
|
@ -153,15 +227,16 @@ class GeminiRequestHandler:
|
|||
self.mimetype = mimetype
|
||||
self.response_buffer = f"{status}\t{mimetype}\r\n"
|
||||
|
||||
def write_body(self, data: bytes) -> None:
|
||||
async def write_body(self, data: bytes) -> None:
|
||||
"""
|
||||
Write bytes to the gemini response body.
|
||||
"""
|
||||
self.flush_status()
|
||||
await self.flush_status()
|
||||
self.response_size += len(data)
|
||||
self.writer.write(data)
|
||||
await self.writer.drain()
|
||||
|
||||
def flush_status(self) -> None:
|
||||
async def flush_status(self) -> None:
|
||||
"""
|
||||
Flush the status line from the internal buffer to the socket stream.
|
||||
"""
|
||||
|
@ -169,6 +244,7 @@ class GeminiRequestHandler:
|
|||
data = self.response_buffer.encode()
|
||||
self.response_size += len(data)
|
||||
self.writer.write(data)
|
||||
await self.writer.drain()
|
||||
self.response_buffer = None
|
||||
|
||||
def log_request(self) -> None:
|
||||
|
@ -199,6 +275,7 @@ class GeminiServer:
|
|||
ssl_context: Union[tuple, ssl.SSLContext],
|
||||
app: Callable,
|
||||
) -> None:
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app = app
|
||||
|
@ -218,7 +295,7 @@ class GeminiServer:
|
|||
)
|
||||
|
||||
socket_info = server.sockets[0].getsockname()
|
||||
self.log_message(f"listening on {socket_info[0]}:{socket_info[1]}")
|
||||
self.log_message(f"Listening on {socket_info[0]}:{socket_info[1]}")
|
||||
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
@ -250,6 +327,12 @@ def run_server():
|
|||
)
|
||||
parser.add_argument("--host", help="Server host", default="127.0.0.1")
|
||||
parser.add_argument("--port", help="Server port", type=int, default=1965)
|
||||
parser.add_argument(
|
||||
"--dir",
|
||||
help="Local directory to serve files from",
|
||||
type=str,
|
||||
default=StaticDirectoryApp.root,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tls-certfile",
|
||||
help="TLS certificate file",
|
||||
|
@ -264,11 +347,13 @@ def run_server():
|
|||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
StaticDirectoryApp.root = args.dir
|
||||
|
||||
server = GeminiServer(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
ssl_context=(args.tls_certfile, args.tls_keyfile),
|
||||
app=EchoApp,
|
||||
app=StaticDirectoryApp,
|
||||
)
|
||||
asyncio.run(server.run())
|
||||
|
||||
|
|
Loading…
Reference in New Issue