CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 5s
CI / Tests (Python 3.12) (push) Successful in 2m48s
CI / Tests (Python 3.13) (push) Successful in 2m49s
CI / Tests (Python 3.14) (push) Successful in 2m43s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 5s
141 lines
4.4 KiB
Python
141 lines
4.4 KiB
Python
# Copyright 2026 Logan Fick
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Web helpers for Owlbot browser session flows."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from http import HTTPStatus
|
|
from pathlib import Path
|
|
|
|
import jinja2
|
|
from aiohttp import web
|
|
|
|
from owlbot import __version__
|
|
from owlbot.sessions import (
|
|
SESSION_COOKIE_NAME,
|
|
BrowserSession,
|
|
ConnectTokenRedemptionError,
|
|
SessionManager,
|
|
)
|
|
|
|
_TEMPLATE_DIR = Path(__file__).resolve().parent.parent / "templates"
|
|
_TEMPLATE_ENV = jinja2.Environment(
|
|
loader=jinja2.FileSystemLoader(_TEMPLATE_DIR),
|
|
autoescape=True,
|
|
)
|
|
_TEMPLATE_ENV.globals["owlbot_version"] = __version__
|
|
|
|
|
|
def _render_template(template_name: str, **context: object) -> str:
|
|
template = _TEMPLATE_ENV.get_template(template_name)
|
|
return template.render(**context)
|
|
|
|
|
|
def connect_guidance_response(
|
|
*,
|
|
status: int,
|
|
command_prefix: str = "!",
|
|
title: str = "Connect to Owlbot",
|
|
message: str | None = None,
|
|
command_message: str | None = None,
|
|
) -> web.Response:
|
|
"""Return a simple connect guidance page for protected routes."""
|
|
body_message = message or "Run this command in chat, then try again."
|
|
return web.Response(
|
|
status=status,
|
|
text=_render_template(
|
|
"connect_error.html",
|
|
title=title,
|
|
body_message=body_message,
|
|
command_message=command_message,
|
|
command_prefix=command_prefix,
|
|
),
|
|
content_type="text/html",
|
|
)
|
|
|
|
|
|
def browser_session_status_response(
|
|
session: BrowserSession | None,
|
|
*,
|
|
command_prefix: str,
|
|
) -> web.Response:
|
|
"""Return the browser session status page."""
|
|
return web.Response(
|
|
text=_render_template(
|
|
"session_status.html",
|
|
session=session,
|
|
command_prefix=command_prefix,
|
|
),
|
|
content_type="text/html",
|
|
)
|
|
|
|
|
|
def register_browser_session_routes(
|
|
app: web.Application,
|
|
*,
|
|
session_manager: SessionManager,
|
|
command_prefix: str,
|
|
cookie_secure: bool = False,
|
|
) -> None:
|
|
"""Register the shared browser-connection endpoints on the app."""
|
|
|
|
async def redeem(request: web.Request) -> web.Response:
|
|
token = request.match_info["token"]
|
|
replacing_session_id = request.cookies.get(SESSION_COOKIE_NAME)
|
|
connect_token = session_manager.get_connect_token(token)
|
|
if connect_token is None:
|
|
return connect_guidance_response(
|
|
status=HTTPStatus.FORBIDDEN,
|
|
command_prefix=command_prefix,
|
|
title="Invalid or expired link",
|
|
message="This connection link is invalid or has expired.",
|
|
)
|
|
|
|
try:
|
|
session = session_manager.redeem_connect_token(
|
|
token,
|
|
replacing_session_id=replacing_session_id,
|
|
)
|
|
except ConnectTokenRedemptionError:
|
|
return connect_guidance_response(
|
|
status=HTTPStatus.FORBIDDEN,
|
|
command_prefix=command_prefix,
|
|
title="Invalid or expired link",
|
|
message="This connection link is invalid or has expired.",
|
|
)
|
|
|
|
response = web.HTTPFound(location=connect_token.destination_path)
|
|
response.set_cookie(
|
|
SESSION_COOKIE_NAME,
|
|
session.session_id,
|
|
httponly=True,
|
|
max_age=int((session.expires_at - session.created_at).total_seconds()),
|
|
samesite="Lax",
|
|
secure=cookie_secure,
|
|
path="/owlbot",
|
|
)
|
|
raise response
|
|
|
|
async def status(request: web.Request) -> web.Response:
|
|
session_id = request.cookies.get(SESSION_COOKIE_NAME)
|
|
session = session_manager.get_session(session_id)
|
|
return browser_session_status_response(
|
|
session,
|
|
command_prefix=command_prefix,
|
|
)
|
|
|
|
app.router.add_get("/owlbot/connect", status)
|
|
app.router.add_get("/owlbot/connect/{token}", redeem)
|