Compare commits

...

5 Commits

Author SHA1 Message Date
Jaakko Keränen 305b86ebd2
Bumped version to v0.7 2024-04-13 15:20:24 +03:00
Jaakko Keränen 8ff7ab3d17
CGI: Use path wildcards with `bin_root` executables; look for "index.gmi" files 2024-04-13 15:20:10 +03:00
Jaakko Keränen a9923d43fc
Skip the TLS SNI check when request hostname has a literal IP address 2024-04-13 14:27:37 +03:00
Jaakko Keränen d1905e688c
Respond with status 53 if an unknown hostname is requested 2024-04-13 14:05:45 +03:00
Jaakko Keränen cf91752b5b
Bumped version to 0.6.2; updated Project Gemini URL 2024-04-13 14:04:58 +03:00
4 changed files with 56 additions and 16 deletions

View File

@ -47,6 +47,14 @@ The log can be viewed via journalctl (or syslog):
## Change log
### v0.7
* CGI: Fixed contents of `PATH_INFO`: it is now URL-decoded and the script path is removed so only the part following the script path is included in the environment variable (RFC 3875, section 4.1.5).
* CGI: `bin_root` applies a wildcard to all found CGI executables so `PATH_INFO` can be used.
* CGI: `bin_root` looks for executable "index.gmi" files in requested directories to provide the directory index.
* Skip the TLS SNI check when request hostname has a literal IP address. (SNI is not supposed to be used with literal addresses.)
* Respond with status 53 if an unknown hostname is requested. This helps reveal configuration errors where the correct hostnames are not specified.
### v0.6
* Added `stream` to the `[cgi.*]` section to enable streaming output mode where the output of the CGI child process is immediately sent to the client without any buffering. Streaming is not supported if the server is using multiple processes.

View File

@ -12,7 +12,7 @@ Extensibility is achieved with Python modules that get loaded at launch
from the configured directories. A set of built-in extension modules is
provided for common functionality like CGI and for serving static files.
The supported protocols are `Gemini <https://gemini.circumlunar.space>`_ and
The supported protocols are `Gemini <https://geminiprotocol.net>`_ and
`Titan <https://transjovian.org/titan>`_. Both are accepted via the same
TCP port.
@ -210,6 +210,10 @@ bin_root : path
with ``,titan`` (note: a comma), the entrypoint will use the Titan
protocol instead of Gemini. The ``,titan`` suffix is omitted from the URL.
Executable files named `index.gmi` are assumed to be directory indices, so
a request for the directory `DIR` will check for `DIR/index.gmi` and use
it for generating the index page.
cgi.*
-----
@ -502,7 +506,7 @@ from .gemini import Server, Cache, Context, Identity
from .markdown import to_gemtext as markdown_to_gemtext
__version__ = '0.6.1'
__version__ = '0.7.0'
__all__ = [
'Config', 'Cache', 'Context', 'Identity',
'get_mime_type', 'markdown_to_gemtext'

View File

@ -7,6 +7,7 @@ import importlib
import os.path
import select
import socket
import ipaddress
import multiprocessing as mp
import subprocess
import threading
@ -381,12 +382,23 @@ def handle_gemini_or_titan_request(request_data):
report_error(stream, 59, "Invalid port number")
return
if not stream.get_servername():
# Server name indication is required.
report_error(stream, 59, "Missing TLS server name indication")
return
if stream.get_servername().decode() != hostname:
report_error(stream, 53, "Proxy request refused")
return
# The hostname may be a literal IPv4/IPv6 address.
try:
ipaddress.ip_address(hostname)
# No error during parsing, looks like a literal address.
except ValueError:
# Server name indication is required.
report_error(stream, 59, "Missing TLS server name indication")
return
else:
sni_name = stream.get_servername().decode()
if sni_name != hostname:
# SNI servername does not match the hostname in the URL. Misbehaving client?
report_error(stream, 53, "Proxy request refused")
return
if sni_name not in worker.cfg.hostnames():
report_error(stream, 53, f"Proxy request refused (domain \"{sni_name}\")")
return
try:
status, meta, body, from_cache = worker.context.call_entrypoint(Request(

View File

@ -17,8 +17,8 @@ class CgiContext:
self.port = port
self.args = args
self.base_path = url_path
if self.base_path.endswith('/*'):
self.base_path = self.base_path[:-2]
if self.base_path.endswith('*'):
self.base_path = self.base_path[:-1]
self.work_dir = work_dir
self.is_streaming = is_streaming
@ -31,7 +31,11 @@ class CgiContext:
env_vars['REMOTE_ADDR'] = '%s:%d' % req.remote_address
if req.query != None:
env_vars['QUERY_STRING'] = req.query
env_vars['PATH_INFO'] = req.path
# PATH_INFO contains any additional subdirectories deeper than the script location.
path_info = urllib.parse.unquote(req.path)
if path_info.startswith(self.base_path):
path_info = path_info[len(self.base_path):]
env_vars['PATH_INFO'] = path_info
env_vars['SCRIPT_NAME'] = self.base_path
env_vars['SERVER_SOFTWARE'] = 'GmCapsule/' + gmcapsule.__version__
env_vars['SERVER_PROTOCOL'] = req.scheme.upper()
@ -110,13 +114,25 @@ class CgiTreeMapper:
def __call__(self, url_path):
# Check if url_path is a valid CGI entrypoint and return
# a CgiContext for it.
fn = str(self.root_dir) + url_path
root_dir = str(self.root_dir)
fn = root_dir + url_path
if self.protocol == 'titan':
fn += ',titan'
if os.path.isdir(fn):
return None
if os.access(fn, os.X_OK):
return CgiContext(self.port, url_path, [fn], work_dir=os.path.dirname(fn))
# Look for an executable up the path.
par_path = fn
par_url = url_path
while len(par_path) > len(root_dir):
# An executable 'index.gmi' is used for generating the index page.
if os.path.isdir(par_path):
if os.access(os.path.join(par_path, 'index.gmi'), os.X_OK):
return CgiContext(self.port, par_url + '*', [os.path.join(par_path, 'index.gmi')],
work_dir=par_path)
else:
return None
if os.access(par_path, os.X_OK):
return CgiContext(self.port, par_url + '*', [par_path], work_dir=os.path.dirname(par_path))
par_path = os.path.dirname(par_path)
par_url = os.path.dirname(par_url)
return None