fix: Go-пакеты со слешами в имени ломали роутинг

Использован :path в FastAPI-роутах, имя+версия парсятся из URL.
Шаблоны urlencode-ят имена пакетов при генерации ссылок.
This commit is contained in:
Marker689
2026-05-10 06:41:00 +03:00
parent 6523f55dcd
commit 22dc87851a
5 changed files with 22 additions and 11 deletions

View File

@@ -2,6 +2,7 @@
import csv import csv
import io import io
from urllib.parse import unquote
from fastapi import APIRouter, Depends, Query, Response from fastapi import APIRouter, Depends, Query, Response
from sqlalchemy import select from sqlalchemy import select
@@ -108,17 +109,20 @@ async def export_packages_csv(
) )
@router.get("/{name}/{version}") @router.get("/{name:path}")
async def get_package( async def get_package(
name: str, name: str,
version: str,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
): ):
parts = name.rsplit("/", 1)
pkg_name = unquote(parts[0])
pkg_version = unquote(parts[1]) if len(parts) == 2 else ""
scans = ( scans = (
( (
await session.execute( await session.execute(
select(Scan) select(Scan)
.where(Scan.package_name == name, Scan.package_version == version) .where(Scan.package_name == pkg_name, Scan.package_version == pkg_version)
.order_by(Scan.started_at.desc()) .order_by(Scan.started_at.desc())
) )
) )

View File

@@ -1,5 +1,7 @@
"""Web UI routes — Jinja2 + htmx pages.""" """Web UI routes — Jinja2 + htmx pages."""
from urllib.parse import unquote
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from jinja2 import Environment, PackageLoader, select_autoescape from jinja2 import Environment, PackageLoader, select_autoescape
@@ -154,20 +156,25 @@ async def packages_list(
) )
@router.get("/packages/{name}/{version}", response_class=HTMLResponse) @router.get("/packages/{name:path}", response_class=HTMLResponse)
async def package_detail( async def package_detail(
name: str, name: str,
version: str,
request: Request, request: Request,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
): ):
# name:path captures the entire path after /packages/
# e.g. "eviltest/0.1.0" or "github.com/attacker/evilmodule/v0.1.0"
parts = name.rsplit("/", 1)
pkg_name = unquote(parts[0])
pkg_version = unquote(parts[1]) if len(parts) == 2 else ""
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
scans = ( scans = (
( (
await session.execute( await session.execute(
select(Scan) select(Scan)
.where(Scan.package_name == name, Scan.package_version == version) .where(Scan.package_name == pkg_name, Scan.package_version == pkg_version)
.options(selectinload(Scan.findings)) .options(selectinload(Scan.findings))
.order_by(Scan.started_at.desc()) .order_by(Scan.started_at.desc())
) )
@@ -185,8 +192,8 @@ async def package_detail(
return _render( return _render(
"package_detail.html", "package_detail.html",
pkg_name=name, pkg_name=pkg_name,
pkg_version=version, pkg_version=pkg_version,
scans=scans, scans=scans,
findings=all_findings, findings=all_findings,
request=request, request=request,

View File

@@ -21,7 +21,7 @@
<tbody> <tbody>
{% for p in packages %} {% for p in packages %}
<tr> <tr>
<td><a href="/packages/{{ p.pkg_name }}/{{ p.pkg_ver }}">{{ p.pkg_name }}</a></td> <td><a href="/packages/{{ p.pkg_name | urlencode }}/{{ p.pkg_ver | urlencode }}">{{ p.pkg_name }}</a></td>
<td>{{ p.pkg_ver }}</td> <td>{{ p.pkg_ver }}</td>
<td>{{ p.ecosystem }}</td> <td>{{ p.ecosystem }}</td>
<td>{{ p.repository }}</td> <td>{{ p.repository }}</td>

View File

@@ -52,7 +52,7 @@
<tbody> <tbody>
{% for s in latest_scans %} {% for s in latest_scans %}
<tr> <tr>
<td><a href="/packages/{{ s.package_name }}/{{ s.package_version }}">{{ s.package_name }}</a></td> <td><a href="/packages/{{ s.package_name | urlencode }}/{{ s.package_version | urlencode }}">{{ s.package_name }}</a></td>
<td>{{ s.package_version }}</td> <td>{{ s.package_version }}</td>
<td><small>{{ s.repository }}</small></td> <td><small>{{ s.repository }}</small></td>
<td> <td>

View File

@@ -14,7 +14,7 @@
<article class="scan-info-block"> <article class="scan-info-block">
<div class="scan-info-grid"> <div class="scan-info-grid">
<div><strong>Package</strong><br><a href="/packages/{{ scan.package_name }}/{{ scan.package_version }}">{{ scan.package_name }}</a></div> <div><strong>Package</strong><br><a href="/packages/{{ scan.package_name | urlencode }}/{{ scan.package_version | urlencode }}">{{ scan.package_name }}</a></div>
<div><strong>Version</strong><br>{{ scan.package_version }}</div> <div><strong>Version</strong><br>{{ scan.package_version }}</div>
<div><strong>Ecosystem</strong><br>{{ scan.ecosystem }}</div> <div><strong>Ecosystem</strong><br>{{ scan.ecosystem }}</div>
<div><strong>Repository</strong><br>{{ scan.repository }}</div> <div><strong>Repository</strong><br>{{ scan.repository }}</div>