Просмотр исходного кода

DCBA-serving endpoints: ARTIFACTS.usml, repo.dcbar, .dcba downloads

Add the HTTP endpoints that serve DCBA artifact repositories alongside the
existing USM source-repository ones:

- ArtifactsListingEndpoint (GET /repo/{name}/ARTIFACTS.usml) streams the
  DCBA listing verbatim from public/ with Content-Type application/json,
  the artifact-repo counterpart of PackagesListingEndpoint, so client
  checksums and signatures stay valid.
- DcbaDefinitionEndpoint (GET /repo/{name}/repo.dcbar) serves a rewritten
  copy of the on-disk .dcbar (whatever its real filename) with the url
  member replaced by the derived base plus repo/<name>/, mirroring
  RepositoryDefinitionEndpoint for .usmr; the route name repo.dcbar is
  canonical.
- PackageDownloadEndpoint (GET /repo/{name}/{file}) now serves both .usmc
  and .dcba: the suffix selects the repository kind (.usmc only from USM
  source repos, .dcba only from DCBA artifact repos; wrong kind answers
  404 with a clear message). Traversal guards and the exact
  content-length / application/octet-stream / DO_NOT_COMPRESS download
  behaviour are unchanged.
- PackagesListingEndpoint answers 404 (not a 500 cross-kind guard) when
  asked for PACKAGES.usml on a DCBA repository; ArtifactsListingEndpoint
  does the mirror-image check, since the listing simply does not exist in
  a repository of the other kind. Unknown repos stay 404, load failures
  stay 500.
- Route registration keeps every exact route (/repo/{name}/PACKAGES.usml,
  repo.usmr, ARTIFACTS.usml, repo.dcbar) ahead of the /repo/{name}/{file}
  catch-all; the comment above the block is updated.

Verified with the shim recipe against a locally built libusm: fixtures
(one .usmr repo with a real .usmc, one .dcbar repo with two .dcba
artifacts and a signed ARTIFACTS.usml) served over one parent; ARTIFACTS
usml 200 and byte-identical with the right content type, repo.dcbar 200
with the rewritten url, .dcba downloads byte-exact; ARTIFACTS.usml on the
usmc repo, PACKAGES.usml and .usmc on the DCBA repo, .dcba on the USM
repo, unknown repos and .. / traversal attempts all answer 404; existing
usmr/usmc/PACKAGES behaviour unchanged; usm dcba add-repo against the
running instance resolves both artifacts.
saddle-Clanker-Saddle 1 месяц назад
Родитель
Сommit
f0138c23ce

+ 2 - 0
meson.build

@@ -63,7 +63,9 @@ app_sources = files(
     'src/entrypoints/PackageDetailEntrypoint.vala',
     'src/actions/SearchPackagesAction.vala',
     'src/endpoints/PackagesListingEndpoint.vala',
+    'src/endpoints/ArtifactsListingEndpoint.vala',
     'src/endpoints/RepositoryDefinitionEndpoint.vala',
+    'src/endpoints/DcbaDefinitionEndpoint.vala',
     'src/endpoints/PackageDownloadEndpoint.vala',
     'src/endpoints/InstallerScriptEndpoint.vala',
     # spry:sources-end

+ 52 - 0
src/endpoints/ArtifactsListingEndpoint.vala

@@ -0,0 +1,52 @@
+using Astralis;
+
+namespace UsmWeb {
+
+    /**
+     * `GET /repo/{name}/ARTIFACTS.usml` — streams that DCBA repository's
+     * artifact listing verbatim so DCBA clients resolve against exactly the
+     * bytes on disk (signature and checksums stay valid), the
+     * artifact-repository counterpart of {@link PackagesListingEndpoint}.
+     *
+     * A USM source repository carries no `ARTIFACTS.usml`, so asking one
+     * for it answers 404 rather than surfacing the cross-kind guard as a
+     * server error: the resource simply does not exist in that repository.
+     */
+    public class ArtifactsListingEndpoint : Object, Endpoint {
+
+        private RepositoryService repositories = Inversion.inject<RepositoryService>();
+
+        public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws Error {
+            string name = "";
+            route_context.mapped_parameters.try_get("name", out name);
+
+            RepositoryEntry? entry;
+            try {
+                entry = repositories.find(name);
+            } catch (Error e) {
+                return new HttpStringResult(@"Repository listing unavailable: $(e.message)",
+                    StatusCode.INTERNAL_SERVER_ERROR);
+            }
+            if (entry == null) {
+                return new HttpStringResult(@"No repository named \"$name\"", StatusCode.NOT_FOUND);
+            }
+            if (((!)entry).kind != RepositoryKind.DCBA) {
+                return new HttpStringResult(
+                    @"No ARTIFACTS.usml in \"$name\": that repository is a USM source repository, not a DCBA artifact repository",
+                    StatusCode.NOT_FOUND);
+            }
+
+            var path = ((!)entry).artifacts_listing_location();
+            var file = File.new_for_path(path);
+            if (!file.query_exists()) {
+                return new HttpStringResult("ARTIFACTS.usml not found", StatusCode.NOT_FOUND);
+            }
+
+            var info = yield file.query_info_async(
+                FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE);
+            var result = new HttpStreamResult(yield file.read_async(), info.get_size());
+            result.set_header("Content-Type", "application/json");
+            return result;
+        }
+    }
+}

+ 65 - 0
src/endpoints/DcbaDefinitionEndpoint.vala

@@ -0,0 +1,65 @@
+using Astralis;
+using Json;
+
+namespace UsmWeb {
+
+    /**
+     * `GET /repo/{name}/repo.dcbar` — serves a rewritten copy of that DCBA
+     * repository's on-disk descriptor (whatever its real filename is) with
+     * its embedded `url` replaced by the derived base ({@link DerivedBase})
+     * plus `repo/<name>/`, the artifact-repository counterpart of
+     * {@link RepositoryDefinitionEndpoint}: a client that drops this file
+     * into its repos.d resolves artifacts from this app no matter which
+     * host it was downloaded through. The route name `repo.dcbar` is the
+     * canonical one regardless of the on-disk descriptor's filename.
+     *
+     * A USM source repository has no DCBA descriptor to serve, so asking
+     * one for `repo.dcbar` answers 404.
+     */
+    public class DcbaDefinitionEndpoint : GLib.Object, Endpoint {
+
+        private RepositoryService repositories = Inversion.inject<RepositoryService>();
+        private UsmWebConfig config = Inversion.inject<UsmWebConfig>();
+
+        public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws Error {
+            string name = "";
+            route_context.mapped_parameters.try_get("name", out name);
+
+            RepositoryEntry? entry;
+            try {
+                entry = repositories.find(name);
+            } catch (Error e) {
+                return new HttpStringResult(@"Repository definition unavailable: $(e.message)",
+                    StatusCode.INTERNAL_SERVER_ERROR);
+            }
+            if (entry == null) {
+                return new HttpStringResult(@"No repository named \"$name\"", StatusCode.NOT_FOUND);
+            }
+            if (((!)entry).kind != RepositoryKind.DCBA) {
+                return new HttpStringResult(
+                    @"No repo.dcbar for \"$name\": that repository is a USM source repository, not a DCBA artifact repository",
+                    StatusCode.NOT_FOUND);
+            }
+
+            string contents;
+            try {
+                FileUtils.get_contents(((!)entry).descriptor_path, out contents);
+            } catch (Error e) {
+                return new HttpStringResult(@"Repository definition unavailable: $(e.message)",
+                    StatusCode.INTERNAL_SERVER_ERROR);
+            }
+
+            var repo = ((!)entry).repo();
+            var base_uri = DerivedBase.derive(http_context.request, config);
+
+            var parser = new Parser();
+            parser.load_from_data(contents);
+            var root = parser.get_root();
+            root.get_object().set_string_member("url", base_uri + "repo/" + repo.name + "/");
+
+            var result = new HttpStringResult(Json.to_string(root, false));
+            result.set_header("Content-Type", "application/json");
+            return result;
+        }
+    }
+}

+ 37 - 10
src/endpoints/PackageDownloadEndpoint.vala

@@ -3,15 +3,21 @@ using Astralis;
 namespace UsmWeb {
 
     /**
-     * `GET /repo/{name}/{file}.usmc` — streams a package archive from that
-     * repository's `public/` directory with an exact content-length, exactly
-     * what a USM client fetching through this app expects (the bytes match
-     * the listing's sha512).
+     * `GET /repo/{name}/{file}` — streams a package archive (`.usmc`) or a
+     * pre-built artifact (`.dcba`) from that repository's `public/`
+     * directory with an exact content-length, exactly what a USM client
+     * fetching through this app expects (the bytes match the listings'
+     * checksums).
      *
-     * The route pattern matches any third segment, so it must be registered
-     * after the exact `/repo/{name}/PACKAGES.usml` and
-     * `/repo/{name}/repo.usmr` routes; requests that do not name a `.usmc`
+     * The suffix also selects the repository kind: `.usmc` is served only
+     * from USM source repositories and `.dcba` only from DCBA artifact
+     * repositories — asking the wrong kind answers 404, since the file
+     * has no meaning there. Requests that do not name a recognized file
      * inside `public/` answer 404.
+     *
+     * The route pattern matches any third segment, so it must be registered
+     * after the exact `/repo/{name}/PACKAGES.usml`, `/repo/{name}/repo.usmr`,
+     * `/repo/{name}/ARTIFACTS.usml` and `/repo/{name}/repo.dcbar` routes.
      */
     public class PackageDownloadEndpoint : Object, Endpoint {
 
@@ -23,7 +29,13 @@ namespace UsmWeb {
             route_context.mapped_parameters.try_get("name", out name);
             route_context.mapped_parameters.try_get("file", out file);
 
-            if (!file.has_suffix(".usmc") || file.contains("/") || file.contains("..")) {
+            if (file.contains("/") || file.contains("..")) {
+                return new HttpStringResult("Not Found", StatusCode.NOT_FOUND);
+            }
+
+            bool is_usmc = file.has_suffix(".usmc");
+            bool is_dcba = file.has_suffix(".dcba");
+            if (!is_usmc && !is_dcba) {
                 return new HttpStringResult("Not Found", StatusCode.NOT_FOUND);
             }
 
@@ -38,10 +50,25 @@ namespace UsmWeb {
                 return new HttpStringResult(@"No repository named \"$name\"", StatusCode.NOT_FOUND);
             }
 
-            var path = ((!)entry).package_location(file);
+            var kind = ((!)entry).kind;
+            if (is_usmc && kind != RepositoryKind.USM) {
+                return new HttpStringResult(
+                    @"No package \"$file\" in \"$name\": that repository is a DCBA artifact repository and carries no .usmc packages",
+                    StatusCode.NOT_FOUND);
+            }
+            if (is_dcba && kind != RepositoryKind.DCBA) {
+                return new HttpStringResult(
+                    @"No artifact \"$file\" in \"$name\": that repository is a USM source repository and carries no .dcba artifacts",
+                    StatusCode.NOT_FOUND);
+            }
+
+            var path = is_dcba
+                ? ((!)entry).artifact_location(file)
+                : ((!)entry).package_location(file);
             var handle = File.new_for_path(path);
             if (!handle.query_exists()) {
-                return new HttpStringResult("No such package", StatusCode.NOT_FOUND);
+                return new HttpStringResult(is_dcba ? "No such artifact" : "No such package",
+                    StatusCode.NOT_FOUND);
             }
 
             var info = yield handle.query_info_async(

+ 9 - 0
src/endpoints/PackagesListingEndpoint.vala

@@ -6,6 +6,10 @@ namespace UsmWeb {
      * `GET /repo/{name}/PACKAGES.usml` — streams that repository's listing
      * verbatim so USM clients resolve against exactly the bytes on disk
      * (signature and checksums stay valid).
+     *
+     * A DCBA artifact repository carries no `PACKAGES.usml`, so asking one
+     * for it answers 404 rather than surfacing the cross-kind guard as a
+     * server error: the resource simply does not exist in that repository.
      */
     public class PackagesListingEndpoint : Object, Endpoint {
 
@@ -25,6 +29,11 @@ namespace UsmWeb {
             if (entry == null) {
                 return new HttpStringResult(@"No repository named \"$name\"", StatusCode.NOT_FOUND);
             }
+            if (((!)entry).kind != RepositoryKind.USM) {
+                return new HttpStringResult(
+                    @"No PACKAGES.usml in \"$name\": that repository is a DCBA artifact repository, not a USM source repository",
+                    StatusCode.NOT_FOUND);
+            }
 
             var path = ((!)entry).listing_location();
             var file = File.new_for_path(path);

+ 5 - 2
src/main.vala

@@ -50,12 +50,15 @@ int main(string[] args) {
         statum.add_resource<MainCssResource>();
         // spry:resources-end
 
-        // USM-serving endpoints. The exact /repo/{name}/PACKAGES.usml and
-        // /repo/{name}/repo.usmr routes must precede /repo/{name}/{file}
+        // USM-serving endpoints. The exact /repo/{name}/PACKAGES.usml,
+        // /repo/{name}/repo.usmr, /repo/{name}/ARTIFACTS.usml and
+        // /repo/{name}/repo.dcbar routes must precede /repo/{name}/{file}
         // (which also matches them), and the /{file} catch-all — serving
         // only /install-usm.sh — must come last.
         application.add_endpoint<PackagesListingEndpoint>(new EndpointRoute("/repo/{name}/PACKAGES.usml"));
         application.add_endpoint<RepositoryDefinitionEndpoint>(new EndpointRoute("/repo/{name}/repo.usmr"));
+        application.add_endpoint<ArtifactsListingEndpoint>(new EndpointRoute("/repo/{name}/ARTIFACTS.usml"));
+        application.add_endpoint<DcbaDefinitionEndpoint>(new EndpointRoute("/repo/{name}/repo.dcbar"));
         application.add_endpoint<PackageDownloadEndpoint>(new EndpointRoute("/repo/{name}/{file}"));
         if (config.installer == InstallerSource.PATH) {
             application.add_endpoint<InstallerScriptEndpoint>(new EndpointRoute("/{file}"));