parent
17c9444b80
commit
cd57adec3e
|
@ -0,0 +1,32 @@
|
||||||
|
name: Jetforce
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: [ "3.7", "3.8", "3.9", "3.10-dev" ]
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install packages
|
||||||
|
run: |
|
||||||
|
pip install .
|
||||||
|
pip install mypy pytest
|
||||||
|
- name: Check types
|
||||||
|
run: |
|
||||||
|
mypy --ignore-missing-imports jetforce/
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
pytest -v tests/
|
|
@ -172,6 +172,7 @@ class StaticDirectoryApplication(JetforceApplication):
|
||||||
env=cgi_env,
|
env=cgi_env,
|
||||||
bufsize=0,
|
bufsize=0,
|
||||||
)
|
)
|
||||||
|
proc.stdout = typing.cast(typing.IO[bytes], proc.stdout)
|
||||||
|
|
||||||
status_line = proc.stdout.readline(self.CGI_MAX_RESPONSE_HEADER_SIZE)
|
status_line = proc.stdout.readline(self.CGI_MAX_RESPONSE_HEADER_SIZE)
|
||||||
if len(status_line) == self.CGI_MAX_RESPONSE_HEADER_SIZE:
|
if len(status_line) == self.CGI_MAX_RESPONSE_HEADER_SIZE:
|
||||||
|
@ -194,6 +195,8 @@ class StaticDirectoryApplication(JetforceApplication):
|
||||||
Non-blocking read from the stdout of the CGI process and pipe it
|
Non-blocking read from the stdout of the CGI process and pipe it
|
||||||
to the socket transport.
|
to the socket transport.
|
||||||
"""
|
"""
|
||||||
|
proc.stdout = typing.cast(typing.IO[bytes], proc.stdout)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
proc.poll()
|
proc.poll()
|
||||||
|
|
||||||
|
|
|
@ -105,15 +105,10 @@ class GeminiServer(Factory):
|
||||||
"""
|
"""
|
||||||
return GeminiProtocol(self, self.app)
|
return GeminiProtocol(self, self.app)
|
||||||
|
|
||||||
def run(self) -> None:
|
def initialize(self) -> None:
|
||||||
"""
|
"""
|
||||||
This is the main server loop.
|
Install the server into the twisted reactor.
|
||||||
"""
|
"""
|
||||||
self.log_message(ABOUT)
|
|
||||||
self.log_message(f"Server hostname is {self.hostname}")
|
|
||||||
self.log_message(f"TLS Certificate File: {self.certfile}")
|
|
||||||
self.log_message(f"TLS Private Key File: {self.keyfile}")
|
|
||||||
|
|
||||||
certificate_options = GeminiCertificateOptions(
|
certificate_options = GeminiCertificateOptions(
|
||||||
certfile=self.certfile,
|
certfile=self.certfile,
|
||||||
keyfile=self.keyfile,
|
keyfile=self.keyfile,
|
||||||
|
@ -131,4 +126,13 @@ class GeminiServer(Factory):
|
||||||
)
|
)
|
||||||
endpoint.listen(self).addCallback(self.on_bind_interface)
|
endpoint.listen(self).addCallback(self.on_bind_interface)
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""
|
||||||
|
This is the main server loop.
|
||||||
|
"""
|
||||||
|
self.log_message(ABOUT)
|
||||||
|
self.log_message(f"Server hostname is {self.hostname}")
|
||||||
|
self.log_message(f"TLS Certificate File: {self.certfile}")
|
||||||
|
self.log_message(f"TLS Private Key File: {self.keyfile}")
|
||||||
|
self.initialize()
|
||||||
self.reactor.run()
|
self.reactor.run()
|
||||||
|
|
|
@ -39,10 +39,6 @@ def fetch(
|
||||||
sys.stdout.buffer.flush()
|
sys.stdout.buffer.flush()
|
||||||
data = fp.read(1024)
|
data = fp.read(1024)
|
||||||
|
|
||||||
# Send a close_notify alert
|
|
||||||
# ssock.setblocking(False)
|
|
||||||
# ssock.unwrap()
|
|
||||||
|
|
||||||
|
|
||||||
def run_client() -> None:
|
def run_client() -> None:
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
@ -66,7 +62,7 @@ def run_client() -> None:
|
||||||
|
|
||||||
if args.tls_keylog:
|
if args.tls_keylog:
|
||||||
# This is a "private" variable that the stdlib exposes for debugging
|
# This is a "private" variable that the stdlib exposes for debugging
|
||||||
context.keylog_filename = args.tls_keylog
|
context.keylog_filename = args.tls_keylog # type: ignore
|
||||||
|
|
||||||
fetch(args.url, args.host, args.port, args.tls_enable_sni)
|
fetch(args.url, args.host, args.port, args.tls_enable_sni)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
echo "20 text/plain"
|
||||||
|
echo $QUERY_STRING
|
|
@ -0,0 +1 @@
|
||||||
|
Jetforce rules!
|
|
@ -0,0 +1,101 @@
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
import socket
|
||||||
|
import unittest
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from twisted.internet import reactor
|
||||||
|
from jetforce import StaticDirectoryApplication, GeminiServer
|
||||||
|
|
||||||
|
ROOT_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiTestServer(GeminiServer):
|
||||||
|
|
||||||
|
real_port: int
|
||||||
|
|
||||||
|
def on_bind_interface(self, port):
|
||||||
|
"""
|
||||||
|
Capture the port number that the test server actually binds to.
|
||||||
|
"""
|
||||||
|
sock_ip, sock_port, *_ = port.socket.getsockname()
|
||||||
|
self.real_port = sock_port
|
||||||
|
|
||||||
|
def log_access(self, message: str) -> None:
|
||||||
|
"""Suppress logging"""
|
||||||
|
|
||||||
|
def log_message(self, message: str) -> None:
|
||||||
|
"""Suppress logging"""
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionalTestCase(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
This class will spin up a complete test jetforce server and serve it
|
||||||
|
on a local TCP port in a new thread. The tests will send real gemini
|
||||||
|
connection strings to the server and check the validity of the response
|
||||||
|
body from end-to-end.
|
||||||
|
"""
|
||||||
|
|
||||||
|
server: GeminiTestServer
|
||||||
|
thread: Thread
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
app = StaticDirectoryApplication(root_directory=ROOT_DIR)
|
||||||
|
cls.server = GeminiTestServer(app=app, port=0)
|
||||||
|
cls.server.initialize()
|
||||||
|
|
||||||
|
cls.thread = Thread(target=reactor.run, args=(False,))
|
||||||
|
cls.thread.start()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
reactor.callFromThread(reactor.stop)
|
||||||
|
cls.thread.join(timeout=5)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def request(cls, data):
|
||||||
|
"""
|
||||||
|
Send bytes to the server using a TCP/IP socket.
|
||||||
|
"""
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
with socket.create_connection((cls.server.host, cls.server.real_port)) as sock:
|
||||||
|
with context.wrap_socket(sock) as ssock:
|
||||||
|
ssock.sendall(data)
|
||||||
|
fp = ssock.makefile("rb")
|
||||||
|
return fp.read()
|
||||||
|
|
||||||
|
def test_index(self):
|
||||||
|
resp = self.request(b"gemini://localhost\r\n")
|
||||||
|
self.assertEqual(resp, b"20 text/gemini\r\nJetforce rules!\n")
|
||||||
|
|
||||||
|
def test_invalid_path(self):
|
||||||
|
resp = self.request(b"gemini://localhost/invalid\r\n")
|
||||||
|
self.assertEqual(resp, b"51 Not Found\r\n")
|
||||||
|
|
||||||
|
def test_invalid_hostname(self):
|
||||||
|
resp = self.request(b"gemini://example.com\r\n")
|
||||||
|
self.assertEqual(resp, b"53 This server does not allow proxy requests\r\n")
|
||||||
|
|
||||||
|
def test_invalid_port(self):
|
||||||
|
resp = self.request(b"gemini://localhost:1111\r\n")
|
||||||
|
self.assertEqual(resp, b"53 This server does not allow proxy requests\r\n")
|
||||||
|
|
||||||
|
def test_directory_redirect(self):
|
||||||
|
resp = self.request(b"gemini://localhost/cgi-bin\r\n")
|
||||||
|
self.assertEqual(resp, b"31 gemini://localhost/cgi-bin/\r\n")
|
||||||
|
|
||||||
|
def test_directory(self):
|
||||||
|
resp = self.request(b"gemini://localhost/cgi-bin/\r\n")
|
||||||
|
self.assertEqual(resp.splitlines(keepends=True)[0], b"20 text/gemini\r\n")
|
||||||
|
|
||||||
|
def test_cgi_script(self):
|
||||||
|
resp = self.request(b"gemini://localhost/cgi-bin/echo.cgi?hello%20world\r\n")
|
||||||
|
self.assertEqual(resp, b"20 text/plain\r\nhello%20world\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue