diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b79a87..13fd872 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,7 @@ jobs: fail-fast: false matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + flask-version: ["2.1", "2.2"] os: [ubuntu-latest] steps: - name: Check out repository @@ -23,7 +24,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install packages run: | - pip install . + pip install . Flask==${{ matrix.flask-version}} pip install .[test] - name: Run mypy run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 141d4a5..ba8fcb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,15 @@ ## v3.0.0 (unreleased) -This version removes the ability to establish TLS connections -over gopher. The code for this was particularly annoying to -monkey-patch, and the feature never gained significant adoption -beyond a proof-of-concept. - -- Added support for python 3.9, 3.10, 3.11. -- Dropped support for python 3.5, 3.6. -- Migrated build from travis.ci to github actions. +This version pulls in some long overdue dependency updates and adds +support for the latest versions of Flask and Python. + +- Supported Python versions: `3.7`, `3.8`, `3.9`, `3.10`, `3.11`. +- Supported Flask versions: `2.1`, `2.2` + +This version also removes the capability to negotiate TLS over +gopher. The code for this was particularly annoying to monkey-patch, +and the feature never gained traction to make it worth maintaining. + - Removed the `make_gopher_ssl_server` function. - Removed the following WSGI server classes: - ``GopherBaseWSGIServer`` diff --git a/demo/run_server.py b/demo/run_server.py index d448363..90dff51 100755 --- a/demo/run_server.py +++ b/demo/run_server.py @@ -158,6 +158,7 @@ def demo_form(field): # Check if there was a new field added to the request request_query = request.args.to_dict() + if field in form_fields: request_query[field] = request.environ["SEARCH_TEXT"] @@ -167,7 +168,7 @@ def demo_form(field): if name in request_query: lines.append(f"{description:<13}: {request_query[name]}") else: - url = url_for("demo_form", field=name, **request_query) + url = url_for("demo_form", _external=False, field=name, **request_query) lines.append(gopher.menu.query(f"{description:<13}:", url)) # Add the buttons at the bottom of the form @@ -177,7 +178,7 @@ def demo_form(field): else: lines.append("clear") if request_query.keys() == form_fields.keys(): - url = url_for("demo_form", field="submit", **request_query) + url = url_for("demo_form", _external=False, field="submit", **request_query) lines.append(gopher.menu.dir("submit", url)) else: lines.append("submit") diff --git a/demo/templates/index.gopher b/demo/templates/index.gopher index eee5037..1fe9a39 100644 --- a/demo/templates/index.gopher +++ b/demo/templates/index.gopher @@ -24,7 +24,6 @@ This page's source code is open source and available for download at: {{ menu.dir('Directory Listings', url_for('demo_directory')) }} {{ menu.dir('Interactive Forms', url_for('demo_form')) }} {{ menu.dir('Client Sessions', url_for('demo_session')) }} -{{ menu.dir('SSL Encryption', url_for('demo_ssl')) }} {{ menu.dir('Server Environment', url_for('demo_environ')) }} {% endblock %} diff --git a/flask_gopher/flask_gopher.py b/flask_gopher/flask_gopher.py index 18938e8..aaa710f 100644 --- a/flask_gopher/flask_gopher.py +++ b/flask_gopher/flask_gopher.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import mimetypes import os import re @@ -12,29 +14,32 @@ from typing import cast from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit -from flask import _request_ctx_stack as request_ctx_stack # noqa -from flask import current_app, render_template, request, url_for -from flask.helpers import safe_join, send_file +from flask import current_app, g, render_template, request, url_for +from flask.helpers import send_file from flask.sessions import SecureCookieSession, SecureCookieSessionInterface from itsdangerous import BadSignature, URLSafeSerializer from jinja2.filters import escape from pyfiglet import FigletError, figlet_format from tabulate import tabulate -from werkzeug.exceptions import BadRequest, HTTPException +from werkzeug.exceptions import HTTPException, NotFound from werkzeug.local import LocalProxy -from werkzeug.serving import BaseWSGIServer, WSGIRequestHandler +from werkzeug.security import safe_join +from werkzeug.serving import WSGIRequestHandler from .__version__ import __version__ @LocalProxy -def menu(): +def _menu(): """ Shortcut for gopher.menu """ return current_app.extensions["gopher"].menu +menu = cast("GopherMenu", _menu) + + def render_menu(*lines): """ Shortcut for gopher.render_menu @@ -481,13 +486,11 @@ def menu(self): initialized with the same host/port that the request's url_adapter is using. """ - ctx = request_ctx_stack.top - if ctx is not None: - if not hasattr(ctx, "gopher_menu"): - host = request.environ["SERVER_NAME"] - port = request.environ["SERVER_PORT"] - ctx.gopher_menu = self.menu_class(host, port) - return ctx.gopher_menu + if not hasattr(g, "_flask_gopher_menu"): + host = request.environ["SERVER_NAME"] + port = request.environ["SERVER_PORT"] + g._flask_gopher_menu = self.menu_class(host, port) + return g._flask_gopher_menu def render_menu(self, *lines): """ @@ -584,11 +587,13 @@ def url_for(endpoint, _external=False, _type=1, **values): if not _external: return url_for(endpoint, **values) - values["_scheme"] = "gopher" - url = url_for(endpoint, _external=_external, **values) - parts = url.split("/") - parts.insert(3, str(_type)) - url = "/".join(parts) + url = url_for(endpoint, _external=True, **values) + if request.scheme == "gopher": + scheme, rest = url.split(":", maxsplit=1) + url = f"gopher:{rest}" + parts = url.split("/") + parts.insert(3, str(_type)) + url = "/".join(parts) return url @@ -678,8 +683,7 @@ def make_environ(self): # header or the SERVER_NAME env variable to match it. # Go look at werkzeug.routing.Map.bind_to_environ() try: - server = cast(BaseWSGIServer, self.server) - server_name = server.app.config.get("SERVER_NAME") + server_name = self.server.app.config.get("SERVER_NAME") # type: ignore except Exception: pass else: @@ -780,6 +784,9 @@ def load_file(self, filename): can only be invoked from inside of a flask view. """ abs_filename = safe_join(self.local_directory, filename) + if abs_filename is None: + raise NotFound() + if not os.path.isabs(abs_filename): abs_filename = os.path.join(current_app.root_path, abs_filename) @@ -789,7 +796,7 @@ def load_file(self, filename): data = self._parse_directory(filename, abs_filename) return self.result_class(True, data) else: - raise BadRequest() + raise NotFound() def _parse_directory(self, folder, abs_folder): """ diff --git a/setup.py b/setup.py index 596e1c4..a957820 100644 --- a/setup.py +++ b/setup.py @@ -31,15 +31,11 @@ def long_description(): include_package_data=True, platforms="any", install_requires=[ - "Flask>=0.11", + "Flask>=2.1", # pyfiglet v0.8.0 removes a bunch of fonts due to licensing issues :( "pyfiglet<=0.7.6", "tabulate", "pyopenssl", - # https://github.com/michael-lazar/flask-gopher/issues/9 - "werkzeug<1.0.0", - # https://stackoverflow.com/questions/72191560 - "markupsafe==2.0.1", ], extras_require={ "test": [ diff --git a/tests/test_flask_gopher.py b/tests/test_flask_gopher.py index 4796d64..d0d91c7 100644 --- a/tests/test_flask_gopher.py +++ b/tests/test_flask_gopher.py @@ -283,7 +283,6 @@ def test_http_get(self): self.assertEqual(resp.status, 200) self.assertIn("Content-Type", resp.headers) self.assertIn("Content-Length", resp.headers) - self.assertTrue(resp.headers["Server"].startswith("Flask-Gopher")) self.assertEqual(resp.read(), b"Hello World!") def test_render_menu(self):