Files
satellite-provider/_docs/02_document/modules/api_program.md
T
Oleksandr Bezdieniezhnykh c74a2339aa
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
[AZ-505] AC-5 fix: enable TLS for HTTP/2 via ALPN
Kestrel with HttpProtocols.Http1AndHttp2 on a plaintext listener
silently downgrades to HTTP/1.1-only (logs "HTTP/2 is not enabled
... TLS is not enabled"), so AC-5's multiplexed-GET test failed
with HTTP_1_1_REQUIRED. ALPN cannot run over plaintext, so the
fix switches the dev listener to TLS on https://+:8080:

- scripts/run-tests.sh generates a self-signed dev cert idempotently
  (./certs/api.pfx + api.crt) via openssl in an alpine container;
  certs/ is gitignored.
- docker-compose.yml binds Kestrel to ASPNETCORE_URLS=https://+:8080
  with Kestrel__Certificates__Default__Path bound to the .pfx.
- docker-compose.tests.yml mounts api.crt into the integration-tests
  container's CA store and runs update-ca-certificates so HttpClient
  trusts the cert transparently; default API_URL is now https://api:8080.
- Drop the obsolete Http2UnencryptedSupport AppContext switch from
  Http2MultiplexingTests; ALPN over TLS handles negotiation.

Test-data fixes caught on the post-TLS rerun (independent of the TLS
switch but surfaced together):

- Http2MultiplexingTests: switch slippy coords from (154321, 95812)
  -- which Google Maps returns 404 for -- to (158485, 91707), the
  slippy projection of (47.461747, 37.647063) already exercised by
  JwtIntegrationTests.
- TileInventoryTests + LeafletPathIndexOnlyTests: SpecifyKind to
  Unspecified at the binding site for raw Npgsql seed paths writing
  into tiles.captured_at / created_at / updated_at (TIMESTAMP without
  tz). Npgsql v6+ refuses Kind=Utc into plain timestamp columns;
  production goes through Dapper and never hits this code path.
- MigrationTests Az503NewUniqueIndexCoversIntegerKeyAndFlightId:
  accept either idx_tiles_location_hash (migration 014) or its
  AZ-505 successor tiles_leaflet_path (migration 015) -- both have
  location_hash as the leading column, which is the AC-9 intent.

Docs updated to reflect the TLS+ALPN path: tile-inventory.md
Non-Goals, modules/api_program.md, module-layout.md, the AZ-505
task spec's Risk 3, and the cycle 6 implementation + completeness
reports. The full integration test suite passes (mode=full, exit 0).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 22:19:26 +03:00

12 KiB
Raw Blame History

Module: Api/Program.cs

Purpose

Application entry point. Configures DI container, sets up middleware, defines minimal API endpoints, runs database migrations on startup, and starts background services.

Public Interface

API Endpoints

Method Route Handler Description
GET /tiles/{z}/{x}/{y} ServeTile Slippy map tile server with in-memory caching. AZ-505 rewired the DB lookup to filter on location_hash (deterministic UUIDv5) so the read becomes an Index Only Scan against tiles_leaflet_path; the wire response is byte-identical to pre-AZ-505.
GET /api/satellite/tiles/latlon GetTileByLatLon Download single tile by lat/lon/zoom
POST /api/satellite/tiles/inventory GetTilesInventory Bulk tile-existence/metadata lookup (AZ-505) — body is XOR of tiles[{tileZoom,tileX,tileY}] (Form A) and locationHashes[uuid] (Form B), each capped at 5000 entries. Response is one entry per request entry, in input order. Contract: _docs/02_document/contracts/api/tile-inventory.md v1.0.0.
GET /api/satellite/tiles/mgrs GetSatelliteTilesByMgrs MGRS stub (returns empty)
POST /api/satellite/upload UploadUavTileBatch UAV tile batch upload (AZ-488) — multipart envelope, 5-rule quality gate, per-source UPSERT with source='uav'. Requires the RequiresGpsPermission policy.
POST /api/satellite/request RequestRegion Queue region for async tile processing
GET /api/satellite/region/{id} GetRegionStatus Get region processing status
POST /api/satellite/route CreateRoute Create route with intermediate points
GET /api/satellite/route/{id} GetRoute Get route with all points

Local Records (defined in Program.cs)

  • GetSatelliteTilesResponse, SatelliteTile — MGRS response stubs
  • DownloadTileResponse — tile download response
  • RequestRegionRequest — region request body
  • ParameterDescriptionFilter — Swagger operation filter

Api/DTOs (AZ-488)

  • UavTileBatchUploadRequest — multipart envelope with metadata (JSON string) and files (IFormFileCollection)

Common/DTO (AZ-488)

  • UavTileMetadata, UavTileBatchMetadataPayload — per-item metadata + envelope shape
  • UavTileBatchUploadResponse, UavTileUploadResultItem — per-item response shape
  • UavTileUploadStatus, UavTileRejectReasons — string-constant enumerations exposed in the v1.0.0 contract

Common/DTO (AZ-505)

  • TileInventoryRequest — XOR body envelope with Tiles (Form A) OR LocationHashes (Form B)
  • TileCoord{TileZoom, TileX, TileY} per-entry coord under Form A
  • TileInventoryResponse{Results: TileInventoryEntry[]} response shape; ordering matches request
  • TileInventoryEntry — per-entry response shape (Present, LocationHash, optional Id/CapturedAt/Source/FlightId/ResolutionMPerPx)
  • TileInventoryLimits.MaxEntriesPerRequest — hard cap (5000) consumed by request validation

Internal Logic

DI Registration

  1. Serilog configured from appsettings.json
  2. Connection string extracted from ConnectionStrings:DefaultConnection
  3. Config bindings: MapConfig, StorageConfig, ProcessingConfig, UavQualityConfig (AZ-488)
  4. Request size limits (AZ-488): KestrelServerOptions.Limits.MaxRequestBodySize and FormOptions.MultipartBodyLengthLimit are set to UavQualityConfig.MaxBatchSize × UavQualityConfig.MaxBytes (default 100 × 5 MiB = 500 MiB) so an oversized UAV batch is rejected at the framework layer before reaching the handler.
  5. Singletons: repositories (TileRepository, RegionRepository, RouteRepository), GoogleMapsDownloaderV2, ITileService, IRegionService, IRouteService, IUavTileQualityGate, IUavTileUploadHandler (AZ-488)
  6. IRegionRequestQueue with configurable capacity
  7. Hosted services: RegionProcessingService, RouteProcessingService
  8. CORS policy: TilesCors — configured origins from CorsConfig:AllowedOrigins, falls back to allow-any
  9. JSON options: camelCase, case-insensitive
  10. JWT authentication (AZ-487 + AZ-494): AddSatelliteJwt(builder.Configuration) (extension in SatelliteProvider.Api.Authentication) registers JwtBearer with TokenValidationParameters set per the suite auth contract: signature + lifetime + issuer + audience validation, 30 s clock skew, ≥ 32-byte HMAC key. The iss value comes from JWT_ISSUER env (fallback Jwt:Issuer config); the aud value comes from JWT_AUDIENCE env (fallback Jwt:Audience config). All three values (secret, iss, aud) are fail-fast — the API throws InvalidOperationException at startup if any is unset or whitespace-only. Production deploys MUST set the env vars with admin-team-confirmed values; appsettings.json ships empty so the fail-fast triggers. appsettings.Development.json ships clearly-tagged DEV-ONLY values (DEV-ONLY-iss-admin-azaion-local / DEV-ONLY-aud-satellite-provider) so local dev works out-of-the-box. Followed by AddAuthorization with the RequiresGpsPermission policy (AZ-488).
  11. Kestrel HTTP/2 (AZ-505): builder.WebHost.ConfigureKestrel(opts => opts.ConfigureEndpointDefaults(lo => lo.Protocols = HttpProtocols.Http1AndHttp2)). The dev listener is now https://+:8080 with a self-signed cert (./certs/api.pfx, generated idempotently by scripts/run-tests.sh and bound via ASPNETCORE_Kestrel__Certificates__Default__Path / __Password in docker-compose.yml). Kestrel needs TLS for HTTP/2 protocol negotiation; ALPN advertises both h2 and http/1.1 so HTTP/2-capable clients (browser Leaflet, HttpClient with Version20 + RequestVersionExact, httpx http2=True) multiplex tile reads on a single TLS connection, and legacy clients fall back to HTTP/1.1. The integration-test container trusts the dev cert via /usr/local/share/ca-certificates/ + update-ca-certificates. AZ-505 AC-5 verifies the multiplex semantics here; production termination is expected at the ingress (Envoy / nginx / ALB) — Kestrel can then drop to HTTP/2 cleartext behind it without changing this code.

Startup

  1. Database migration via DatabaseMigrator.RunMigrations() — throws on failure
  2. Creates tiles and ready directories
  3. Swagger enabled in Development mode
  4. Middleware chain (order matters): UseExceptionHandlerUseHttpsRedirectionUseCors("TilesCors")UseAuthenticationUseAuthorization → endpoint mapping.
  5. Every MapGet/MapPost endpoint is decorated with .RequireAuthorization(); the framework returns 401 before the handler runs for any anonymous, expired, or invalid-signature request.

ServeTile Handler

  1. Checks IMemoryCache for tile bytes (1h absolute, 30min sliding expiration)
  2. If cache miss: queries ITileRepository.GetByTileCoordinatesAsync — AZ-505 rewired this method to compute location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}") and filter by WHERE location_hash = $1, hitting tiles_leaflet_path as an Index Only Scan with Heap Fetches ≤ 1. Selection rule is unchanged (most-recent across sources/flights); wire response is byte-identical.
  3. If no DB record: downloads tile via GoogleMapsDownloaderV2.DownloadSingleTileAsync, creates TileEntity, inserts
  4. Returns image bytes with cache headers (Cache-Control: public, max-age=86400)

GetTilesInventory Handler (AZ-505)

  1. Validates XOR body shape: 400 if both tiles and locationHashes are populated, 400 if neither is populated, 400 if either exceeds TileInventoryLimits.MaxEntriesPerRequest (5000)
  2. Delegates to ITileService.GetInventoryAsync(request, ct)
  3. Service computes location_hash for Form A entries via Uuidv5.Create(TileNamespace, "{z}/{x}/{y}"), calls ITileRepository.GetTilesByLocationHashesAsync(IReadOnlyList<Guid>), re-aligns results back to input order
  4. Returns TileInventoryResponse with one entry per input — present=true entries carry id / capturedAt / source / flightId / resolutionMPerPx; present=false entries carry only locationHash
  5. Authenticated by .RequireAuthorization() (401 before handler for anonymous)

GetTileByLatLon Handler

Downloads a tile, persists it, returns metadata as DownloadTileResponse.

RequestRegion Handler

Validates size (10010000m), delegates to IRegionService.RequestRegionAsync.

UploadUavTileBatch Handler (AZ-488)

Buffers each IFormFile into memory, packages them as UavUploadFile records (filename, content-type, bytes), and delegates to IUavTileUploadHandler.HandleAsync. Envelope-level errors (mismatched batch, oversized batch, malformed metadata) are surfaced as HTTP 400 ProblemDetails; per-item rejects are returned in the HTTP 200 response payload. The endpoint is protected by .RequireAuthorization(SatellitePermissions.UavUploadPolicy) so 401 (no token) and 403 (no GPS permission) are returned before the handler runs.

Dependencies

All project references: Common, DataAccess, Services. NuGet: Serilog.AspNetCore (8.0.3 — fallback retained on .NET 10 per AZ-500 Risk #4: no 10.x line published as of cycle 4; documented in AGENTS.md), Swashbuckle.AspNetCore (10.1.7 — bumped from 6.6.2 by AZ-500 to land Microsoft.OpenApi 2.x compat required by ASP.NET Core 10), Microsoft.AspNetCore.OpenApi (10.0.7 — bumped from 8.0.25 by AZ-500), Microsoft.AspNetCore.Authentication.JwtBearer (10.0.7 — added at 8.0.21 by AZ-487, bumped to 8.0.25 by AZ-496, bumped to 10.0.7 by AZ-500), SixLabors.ImageSharp, Newtonsoft.Json.

Microsoft.OpenApi 2.x refactor note (AZ-500): the major bump (1.x → 2.x) drove three internal Swashbuckle-setup edits in this file — using Microsoft.OpenApi.Models;using Microsoft.OpenApi;; AddSecurityRequirement(...) rewritten to take a Func<OpenApiDocument, OpenApiSecurityRequirement> and use OpenApiSecuritySchemeReference("Bearer") instead of the removed OpenApiSecurityScheme.Reference shape; MapType<UavTileBatchUploadRequest> rewritten to use the new JsonSchemaType enum and IDictionary<string, IOpenApiSchema> properties bag. The Swagger document shape (paths, operations, the Bearer Authorize button, the multipart-batch upload schema) is preserved exactly — SwaggerDocument_AdvertisesBearerSecurityScheme and the AZ-353 swagger-ready integration assertions still pass. Eight ASPDEPR002 deprecation warnings (WithOpenApi(...)) remain — they're recorded in _docs/03_implementation/reviews/batch_01_cycle4_review.md as a follow-up PBI; the API is still fully functional in .NET 10 (deprecated, not removed).

Consumers

  • HTTP clients (external)
  • Integration tests (via HTTP)

Data Models

Defines several local request/response records that are not shared with other projects.

Configuration

All configuration sections are consumed here:

  • ConnectionStrings:DefaultConnection
  • MapConfig, StorageConfig, ProcessingConfig
  • UavQuality (AZ-488) — MinBytes, MaxBytes, MaxAgeDays, CapturedAtFutureSkewSeconds, MinLuminanceVariance, MaxBatchSize, LuminanceSampleSize. Drives the 5-rule quality gate AND the per-request body-size limits.
  • CorsConfig:AllowedOrigins
  • Jwt:Secret — HMAC-SHA256 signing key for JWT validation (AZ-487). Resolution: JWT_SECRET env var (preferred, opaque production secret) → Jwt:Secret configuration key (appsettings.Development.json placeholder only). Startup fails fast if the resolved value is unset, empty, or shorter than 32 bytes.
  • Jwt:Issuer — Expected iss claim value (AZ-494). Resolution: JWT_ISSUER env → Jwt:Issuer config. Startup fails fast if unset/empty.
  • Jwt:Audience — Expected aud claim value (AZ-494). Resolution: JWT_AUDIENCE env → Jwt:Audience config. Startup fails fast if unset/empty.
  • Serilog section

External Integrations

  • Google Maps (indirectly via GoogleMapsDownloaderV2)
  • PostgreSQL (via repositories and DatabaseMigrator)
  • File system (./tiles/, ./ready/)

Security

  • CORS configured (permissive by default when no origins specified)
  • Swagger only in Development; Bearer token "Authorize" button registered via AddSecurityDefinition/AddSecurityRequirement (AZ-487)
  • HTTPS redirection enabled
  • JWT bearer authentication (AZ-487) — every endpoint requires a valid HS256-signed token. Anonymous, expired, or signature-tampered requests return 401 before the handler runs.
  • Permission-claim policies (AZ-488) — POST /api/satellite/upload is wrapped in .RequireAuthorization(SatellitePermissions.UavUploadPolicy). The PermissionsAuthorizationHandler reads the permissions claim (repeated-string OR JSON-array shape) and returns 403 if GPS is not present.

Tests

Integration tests exercise all endpoints. Unit test project has only a dummy test.