Significant refactoring, moving examples to a separate directory
This commit is contained in:
parent
deeab25604
commit
dd76b04d76
|
@ -0,0 +1,27 @@
|
||||||
|
"""
|
||||||
|
A simple Gemini server that echos back the request to the client.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import jetforce
|
||||||
|
|
||||||
|
|
||||||
|
def echo(environ: dict, send_status: typing.Callable) -> typing.Iterator[bytes]:
|
||||||
|
url = environ["URL"]
|
||||||
|
send_status(jetforce.STATUS_SUCCESS, "text/gemini")
|
||||||
|
yield f"Received path: {url}".encode()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = jetforce.build_argument_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
server = jetforce.GeminiServer(
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
certfile=args.certfile,
|
||||||
|
keyfile=args.keyfile,
|
||||||
|
hostname=args.hostname,
|
||||||
|
app=echo,
|
||||||
|
)
|
||||||
|
asyncio.run(server.run())
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""
|
||||||
|
A guestbook application that accepts user messages using the INPUT response type
|
||||||
|
and stores messages in a simple SQLite database file.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sqlite3
|
||||||
|
import typing
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import jetforce
|
||||||
|
|
||||||
|
|
||||||
|
class GuestbookApplication(jetforce.BaseApplication):
|
||||||
|
|
||||||
|
db_file = "guestbook.sql"
|
||||||
|
|
||||||
|
def connect_db(self) -> typing.Tuple[sqlite3.Connection, sqlite3.Cursor]:
|
||||||
|
db = sqlite3.connect(self.db_file, detect_types=sqlite3.PARSE_DECLTYPES)
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS guestbook (
|
||||||
|
id int PRIMARY KEY,
|
||||||
|
message text,
|
||||||
|
created timestamp,
|
||||||
|
ip_address text);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return db, cursor
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self, environ: dict, send_status: typing.Callable
|
||||||
|
) -> typing.Iterator[bytes]:
|
||||||
|
url = environ["URL"]
|
||||||
|
url_parts = urllib.parse.urlparse(url)
|
||||||
|
|
||||||
|
error_message = self.block_proxy_requests(url, environ["HOSTNAME"])
|
||||||
|
if error_message:
|
||||||
|
return send_status(jetforce.STATUS_PROXY_REQUEST_REFUSED, error_message)
|
||||||
|
|
||||||
|
if url_parts.path in ("", "/"):
|
||||||
|
send_status(jetforce.STATUS_SUCCESS, "text/gemini")
|
||||||
|
yield from self.list_messages()
|
||||||
|
elif url_parts.path == "/submit":
|
||||||
|
if url_parts.query:
|
||||||
|
self.save_message(url_parts.query, environ["REMOTE_ADDR"])
|
||||||
|
return send_status(jetforce.STATUS_REDIRECT_TEMPORARY, "/")
|
||||||
|
else:
|
||||||
|
return send_status(
|
||||||
|
jetforce.STATUS_INPUT, "Enter your message (max 256 characters)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return send_status(jetforce.STATUS_NOT_FOUND, "Invalid address")
|
||||||
|
|
||||||
|
def save_message(self, message: str, ip_address: str) -> None:
|
||||||
|
message = message[:256]
|
||||||
|
created = datetime.utcnow()
|
||||||
|
|
||||||
|
db, cursor = self.connect_db()
|
||||||
|
sql = "INSERT INTO guestbook(message, created, ip_address) VALUES (?, ?, ?)"
|
||||||
|
cursor.execute(sql, (message, created, ip_address))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
def list_messages(self) -> typing.Iterator[bytes]:
|
||||||
|
yield "Guestbook\n=>/submit Leave a Message\n".encode()
|
||||||
|
|
||||||
|
db, cursor = self.connect_db()
|
||||||
|
cursor.execute("SELECT created, message FROM guestbook ORDER BY created DESC")
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
yield f"\n[{row[0]:%Y-%m-%d %I:%M %p}]\n{row[1]}\n".encode()
|
||||||
|
|
||||||
|
yield "\n...\n".encode()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = jetforce.build_argument_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
app = GuestbookApplication()
|
||||||
|
server = jetforce.GeminiServer(
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
certfile=args.certfile,
|
||||||
|
keyfile=args.keyfile,
|
||||||
|
hostname=args.hostname,
|
||||||
|
app=app,
|
||||||
|
)
|
||||||
|
asyncio.run(server.run())
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""
|
||||||
|
This is an example of setting up a Gemini server to proxy requests to other
|
||||||
|
protocols. This application will accept HTTP URLs, download and render them
|
||||||
|
locally using the `w3m` tool, and render the output to the client as plain text.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import subprocess
|
||||||
|
import typing
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import jetforce
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPProxyApplication(jetforce.BaseApplication):
|
||||||
|
|
||||||
|
command = [b"w3m", b"-dump"]
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self, environ: dict, send_status: typing.Callable
|
||||||
|
) -> typing.Iterator[bytes]:
|
||||||
|
url = environ["URL"]
|
||||||
|
url_parts = urllib.parse.urlparse(url)
|
||||||
|
if url_parts.scheme not in ("http", "https"):
|
||||||
|
return send_status(jetforce.STATUS_NOT_FOUND, "Invalid Resource")
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = self.command + [url.encode()]
|
||||||
|
out = subprocess.run(command, stdout=subprocess.PIPE)
|
||||||
|
out.check_returncode()
|
||||||
|
except Exception:
|
||||||
|
send_status(jetforce.STATUS_CGI_ERROR, "Failed to load URL")
|
||||||
|
else:
|
||||||
|
send_status(jetforce.STATUS_SUCCESS, "text/plain")
|
||||||
|
yield out.stdout
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = jetforce.build_argument_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
app = HTTPProxyApplication()
|
||||||
|
server = jetforce.GeminiServer(
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
certfile=args.certfile,
|
||||||
|
keyfile=args.keyfile,
|
||||||
|
hostname=args.hostname,
|
||||||
|
app=app,
|
||||||
|
)
|
||||||
|
asyncio.run(server.run())
|
156
jetforce.py
156
jetforce.py
|
@ -66,22 +66,37 @@ STATUS_FUTURE_CERTIFICATE_REJECTED = 64
|
||||||
STATUS_EXPIRED_CERTIFICATE_REJECTED = 65
|
STATUS_EXPIRED_CERTIFICATE_REJECTED = 65
|
||||||
|
|
||||||
|
|
||||||
class EchoApp:
|
class BaseApplication:
|
||||||
"""
|
"""
|
||||||
A simple application that echos back the requested path.
|
Base Jetforce application class, analogous to a WSGI app.
|
||||||
|
|
||||||
|
At a minimum you must implement the ``__call__()`` method, which will
|
||||||
|
perform the following tasks:
|
||||||
|
|
||||||
|
1. Send the response header by calling ``send_status()``.
|
||||||
|
2. Optionally, yield the response body as bytes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, environ: dict, send_status: typing.Callable) -> None:
|
def __call__(
|
||||||
self.environ = environ
|
self, environ: dict, send_status: typing.Callable
|
||||||
self.send_status = send_status
|
) -> typing.Iterator[bytes]:
|
||||||
|
raise NotImplemented
|
||||||
|
|
||||||
def __iter__(self) -> typing.Iterator[bytes]:
|
@staticmethod
|
||||||
self.send_status(STATUS_SUCCESS, "text/plain")
|
def block_proxy_requests(url: str, hostname: str) -> typing.Optional[str]:
|
||||||
url = self.environ["RAW_URL"]
|
"""
|
||||||
yield f"Received path: {url}".encode()
|
Optional method that may be used to restrict request URLs that do not
|
||||||
|
match your current host and the gemini:// scheme. This may be used if
|
||||||
|
you don't want to worry about proxying traffic to other servers.
|
||||||
|
"""
|
||||||
|
url_parts = urllib.parse.urlparse(url)
|
||||||
|
if url_parts.scheme and url_parts.scheme != "gemini":
|
||||||
|
return 'URL scheme must be "gemini://"'
|
||||||
|
if url_parts.hostname and url_parts.hostname != hostname:
|
||||||
|
return f'URL hostname must be "{hostname}"'
|
||||||
|
|
||||||
|
|
||||||
class StaticDirectoryApp:
|
class StaticDirectoryApplication(BaseApplication):
|
||||||
"""
|
"""
|
||||||
Serve a static directory over Gemini.
|
Serve a static directory over Gemini.
|
||||||
|
|
||||||
|
@ -90,50 +105,46 @@ class StaticDirectoryApp:
|
||||||
directory listing will be auto-generated.
|
directory listing will be auto-generated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, root: str, environ: dict, send_status: typing.Callable) -> None:
|
def __init__(self, directory="/var/gemini"):
|
||||||
self.root = pathlib.Path(root).resolve(strict=True)
|
self.root = pathlib.Path(directory).resolve(strict=True)
|
||||||
self.environ = environ
|
|
||||||
self.send_status = send_status
|
|
||||||
self.mimetypes = mimetypes.MimeTypes()
|
self.mimetypes = mimetypes.MimeTypes()
|
||||||
|
|
||||||
@classmethod
|
def __call__(
|
||||||
def serve_directory(cls, root: str) -> typing.Callable:
|
self, environ: dict, send_status: typing.Callable
|
||||||
"""
|
) -> typing.Iterator[bytes]:
|
||||||
Return an app that points to the given root directory on the file system.
|
url = environ["URL"]
|
||||||
"""
|
url_parts = urllib.parse.urlparse(url)
|
||||||
|
url_path = pathlib.Path(url_parts.path.strip("/"))
|
||||||
|
|
||||||
def build_class(environ: dict, send_status: typing.Callable):
|
error_message = self.block_proxy_requests(url, environ["HOSTNAME"])
|
||||||
return cls(root, environ, send_status)
|
if error_message:
|
||||||
|
return send_status(STATUS_PROXY_REQUEST_REFUSED, error_message)
|
||||||
return build_class
|
|
||||||
|
|
||||||
def __iter__(self) -> typing.Iterator[bytes]:
|
|
||||||
url_path = pathlib.Path(self.environ["URL"].path.strip("/"))
|
|
||||||
|
|
||||||
filename = pathlib.Path(os.path.normpath(str(url_path)))
|
filename = pathlib.Path(os.path.normpath(str(url_path)))
|
||||||
if filename.is_absolute() or str(filename.name).startswith(".."):
|
if filename.is_absolute() or str(filename.name).startswith(".."):
|
||||||
# Guard against breaking out of the directory
|
# Guard against breaking out of the directory
|
||||||
self.send_status(STATUS_NOT_FOUND, "Not Found")
|
return send_status(STATUS_NOT_FOUND, "Not Found")
|
||||||
return
|
|
||||||
else:
|
|
||||||
filesystem_path = self.root / filename
|
filesystem_path = self.root / filename
|
||||||
|
|
||||||
if filesystem_path.is_file():
|
if filesystem_path.is_file():
|
||||||
mimetype = self.guess_mimetype(filesystem_path.name)
|
mimetype = self.guess_mimetype(filesystem_path.name)
|
||||||
yield from self.load_file(filesystem_path, mimetype)
|
send_status(STATUS_SUCCESS, mimetype)
|
||||||
|
yield from self.load_file(filesystem_path)
|
||||||
|
|
||||||
elif filesystem_path.is_dir():
|
elif filesystem_path.is_dir():
|
||||||
gemini_file = filesystem_path / ".gemini"
|
gemini_file = filesystem_path / ".gemini"
|
||||||
if gemini_file.exists():
|
if gemini_file.exists():
|
||||||
yield from self.load_file(gemini_file, "text/gemini")
|
send_status(STATUS_SUCCESS, "text/gemini")
|
||||||
|
yield from self.load_file(gemini_file)
|
||||||
else:
|
else:
|
||||||
|
send_status(STATUS_SUCCESS, "text/gemini")
|
||||||
yield from self.list_directory(url_path, filesystem_path)
|
yield from self.list_directory(url_path, filesystem_path)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.send_status(STATUS_NOT_FOUND, "Not Found")
|
return send_status(STATUS_NOT_FOUND, "Not Found")
|
||||||
|
|
||||||
def load_file(self, filesystem_path: pathlib.Path, mimetype: str):
|
def load_file(self, filesystem_path: pathlib.Path):
|
||||||
self.send_status(STATUS_SUCCESS, mimetype)
|
|
||||||
with filesystem_path.open("rb") as fp:
|
with filesystem_path.open("rb") as fp:
|
||||||
data = fp.read(1024)
|
data = fp.read(1024)
|
||||||
while data:
|
while data:
|
||||||
|
@ -141,8 +152,6 @@ class StaticDirectoryApp:
|
||||||
data = fp.read(1024)
|
data = fp.read(1024)
|
||||||
|
|
||||||
def list_directory(self, url_path: pathlib.Path, filesystem_path: pathlib.Path):
|
def list_directory(self, url_path: pathlib.Path, filesystem_path: pathlib.Path):
|
||||||
self.send_status(STATUS_SUCCESS, "text/gemini")
|
|
||||||
|
|
||||||
yield f"Directory: /{url_path}\r\n".encode()
|
yield f"Directory: /{url_path}\r\n".encode()
|
||||||
if url_path.parent != url_path:
|
if url_path.parent != url_path:
|
||||||
yield f"=>/{url_path.parent}\t..\r\n".encode()
|
yield f"=>/{url_path.parent}\t..\r\n".encode()
|
||||||
|
@ -184,7 +193,6 @@ class GeminiRequestHandler:
|
||||||
self.writer: typing.Optional[asyncio.StreamWriter] = None
|
self.writer: typing.Optional[asyncio.StreamWriter] = None
|
||||||
self.received_timestamp: typing.Optional[datetime.datetime] = None
|
self.received_timestamp: typing.Optional[datetime.datetime] = None
|
||||||
self.remote_addr: typing.Optional[str] = None
|
self.remote_addr: typing.Optional[str] = None
|
||||||
self.raw_url: typing.Optional[str] = None
|
|
||||||
self.url: typing.Optional[urllib.parse.ParseResult] = None
|
self.url: typing.Optional[urllib.parse.ParseResult] = None
|
||||||
self.status: typing.Optional[int] = None
|
self.status: typing.Optional[int] = None
|
||||||
self.meta: typing.Optional[str] = None
|
self.meta: typing.Optional[str] = None
|
||||||
|
@ -213,19 +221,6 @@ class GeminiRequestHandler:
|
||||||
self.write_status(STATUS_BAD_REQUEST, "Could not understand request line")
|
self.write_status(STATUS_BAD_REQUEST, "Could not understand request line")
|
||||||
return await self.close_connection()
|
return await self.close_connection()
|
||||||
|
|
||||||
# Discard proxy requests, may revisit this in a later version
|
|
||||||
if self.url.scheme and self.url.scheme != "gemini":
|
|
||||||
self.write_status(
|
|
||||||
STATUS_PROXY_REQUEST_REFUSED, 'URL scheme must be "gemini://"'
|
|
||||||
)
|
|
||||||
return await self.close_connection()
|
|
||||||
elif self.url.hostname and self.url.hostname != self.server.hostname:
|
|
||||||
self.write_status(
|
|
||||||
STATUS_PROXY_REQUEST_REFUSED,
|
|
||||||
f'URL hostname must be "{self.server.hostname}"',
|
|
||||||
)
|
|
||||||
return await self.close_connection()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
environ = self.build_environ()
|
environ = self.build_environ()
|
||||||
app = self.app(environ, self.write_status)
|
app = self.app(environ, self.write_status)
|
||||||
|
@ -246,7 +241,6 @@ class GeminiRequestHandler:
|
||||||
"SERVER_PORT": self.server.port,
|
"SERVER_PORT": self.server.port,
|
||||||
"REMOTE_ADDR": self.remote_addr,
|
"REMOTE_ADDR": self.remote_addr,
|
||||||
"HOSTNAME": self.server.hostname,
|
"HOSTNAME": self.server.hostname,
|
||||||
"RAW_URL": self.raw_url,
|
|
||||||
"URL": self.url,
|
"URL": self.url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,12 +255,7 @@ class GeminiRequestHandler:
|
||||||
if len(data) > 1024:
|
if len(data) > 1024:
|
||||||
raise ValueError("URL exceeds max length of 1024 bytes")
|
raise ValueError("URL exceeds max length of 1024 bytes")
|
||||||
|
|
||||||
self.raw_url = data.decode()
|
self.url = data.decode()
|
||||||
self.url = urllib.parse.urlparse(self.raw_url)
|
|
||||||
if not self.url.netloc:
|
|
||||||
# URL does not contain a scheme and was not prefixed with // per RFC 1808
|
|
||||||
# TODO: Suggest spec should enforce // when scheme is omitted
|
|
||||||
self.url = urllib.parse.urlparse(f"//{self.raw_url}")
|
|
||||||
|
|
||||||
def write_status(self, status: int, meta: str) -> None:
|
def write_status(self, status: int, meta: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -323,7 +312,7 @@ class GeminiRequestHandler:
|
||||||
self.server.log_message(
|
self.server.log_message(
|
||||||
f"{self.remote_addr} "
|
f"{self.remote_addr} "
|
||||||
f"[{self.received_timestamp:%d/%b/%Y:%H:%M:%S +0000}] "
|
f"[{self.received_timestamp:%d/%b/%Y:%H:%M:%S +0000}] "
|
||||||
f'"{self.raw_url}" '
|
f'"{self.url}" '
|
||||||
f"{self.status} "
|
f"{self.status} "
|
||||||
f'"{self.meta}" '
|
f'"{self.meta}" '
|
||||||
f"{self.response_size}"
|
f"{self.response_size}"
|
||||||
|
@ -341,16 +330,25 @@ class GeminiServer:
|
||||||
self,
|
self,
|
||||||
host: str,
|
host: str,
|
||||||
port: int,
|
port: int,
|
||||||
ssl_context: ssl.SSLContext,
|
certfile: typing.Optional[str],
|
||||||
|
keyfile: typing.Optional[str],
|
||||||
hostname: str,
|
hostname: str,
|
||||||
app: typing.Callable,
|
app: typing.Callable,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.ssl_context = ssl_context
|
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
|
if not certfile:
|
||||||
|
certfile, keyfile = self.generate_tls_certificate(hostname)
|
||||||
|
|
||||||
|
self.ssl_context = ssl.SSLContext()
|
||||||
|
self.ssl_context.verify_mode = ssl.CERT_NONE
|
||||||
|
self.ssl_context.check_hostname = False
|
||||||
|
self.ssl_context.load_cert_chain(certfile, keyfile)
|
||||||
|
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""
|
"""
|
||||||
The main asynchronous server loop.
|
The main asynchronous server loop.
|
||||||
|
@ -385,8 +383,8 @@ class GeminiServer:
|
||||||
"""
|
"""
|
||||||
print(message, file=sys.stderr)
|
print(message, file=sys.stderr)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def generate_tls_certificate(hostname: str) -> typing.Tuple[str, str]:
|
def generate_tls_certificate(hostname: str) -> typing.Tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Utility function to generate a self-signed SSL certificate key pair if
|
Utility function to generate a self-signed SSL certificate key pair if
|
||||||
one isn't provided. Results may vary depending on your version of OpenSSL.
|
one isn't provided. Results may vary depending on your version of OpenSSL.
|
||||||
|
@ -406,9 +404,10 @@ def generate_tls_certificate(hostname: str) -> typing.Tuple[str, str]:
|
||||||
return str(certfile), str(keyfile)
|
return str(certfile), str(keyfile)
|
||||||
|
|
||||||
|
|
||||||
def run_server() -> None:
|
def build_argument_parser() -> argparse.ArgumentParser:
|
||||||
"""
|
"""
|
||||||
Entry point for running the command line directory server.
|
Construct the default argument parser when launching the server from
|
||||||
|
the command line.
|
||||||
"""
|
"""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="jetforce",
|
prog="jetforce",
|
||||||
|
@ -418,25 +417,32 @@ def run_server() -> None:
|
||||||
)
|
)
|
||||||
parser.add_argument("--host", help="Address to bind server to", default="127.0.0.1")
|
parser.add_argument("--host", help="Address to bind server to", default="127.0.0.1")
|
||||||
parser.add_argument("--port", help="Port to bind server to", type=int, default=1965)
|
parser.add_argument("--port", help="Port to bind server to", type=int, default=1965)
|
||||||
parser.add_argument("--tls-certfile", help="TLS certificate file", metavar="FILE")
|
parser.add_argument(
|
||||||
parser.add_argument("--tls-keyfile", help="TLS private key file", metavar="FILE")
|
"--tls-certfile", dest="certfile", help="TLS certificate file", metavar="FILE"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tls-keyfile", dest="keyfile", help="TLS private key file", metavar="FILE"
|
||||||
|
)
|
||||||
parser.add_argument("--hostname", help="Server hostname", default="localhost")
|
parser.add_argument("--hostname", help="Server hostname", default="localhost")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def run_server() -> None:
|
||||||
|
"""
|
||||||
|
Entry point for running the command line static directory server.
|
||||||
|
"""
|
||||||
|
parser = build_argument_parser()
|
||||||
parser.add_argument("--dir", help="local directory to serve", default="/var/gemini")
|
parser.add_argument("--dir", help="local directory to serve", default="/var/gemini")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
certfile, keyfile = args.tls_certfile, args.tls_keyfile
|
app = StaticDirectoryApplication(args.dir)
|
||||||
if not certfile:
|
|
||||||
certfile, keyfile = generate_tls_certificate(args.hostname)
|
|
||||||
|
|
||||||
ssl_context = ssl.SSLContext()
|
|
||||||
ssl_context.load_cert_chain(certfile, keyfile)
|
|
||||||
|
|
||||||
server = GeminiServer(
|
server = GeminiServer(
|
||||||
host=args.host,
|
host=args.host,
|
||||||
port=args.port,
|
port=args.port,
|
||||||
ssl_context=ssl_context,
|
certfile=args.certfile,
|
||||||
|
keyfile=args.keyfile,
|
||||||
hostname=args.hostname,
|
hostname=args.hostname,
|
||||||
app=StaticDirectoryApp.serve_directory(args.dir),
|
app=app,
|
||||||
)
|
)
|
||||||
asyncio.run(server.run())
|
asyncio.run(server.run())
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,13 @@ def run_client():
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--port", help="Optional port to connect to, will default to the URL"
|
"--port", help="Optional port to connect to, will default to the URL"
|
||||||
)
|
)
|
||||||
|
parser.add_argument("--certfile", help="Optional client certificate")
|
||||||
|
parser.add_argument("--keyfile", help="Optional client key")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.certfile:
|
||||||
|
context.load_cert_chain(args.certfile, args.keyfile)
|
||||||
|
|
||||||
fetch(args.url, args.host, args.port)
|
fetch(args.url, args.host, args.port)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue