From a831c492c4018df6e10881d84fe0680dae3b2569 Mon Sep 17 00:00:00 2001 From: Andre Lafleur Date: Thu, 2 Apr 2026 16:45:50 +0800 Subject: [PATCH 1/6] feat: add GWP Minimal API and Razor Pages hosting samples Two ASP.NET Core samples demonstrating server-side token proxy patterns for hosting the Genetec Web Player: - GwpMinimalApiSample: single Program.cs with static HTML page - GwpRazorPagesSample: Razor Pages with per-request CSP nonces and server-rendered configuration via JsonSerializer.Serialize Both samples include proper player lifecycle cleanup on failed start and cancellable startup with generation tracking. --- .../GwpMinimalApiSample.csproj | 14 + .../GwpMinimalApiSample/Program.cs | 62 ++ .../Properties/launchSettings.json | 12 + .../GwpMinimalApiSample/README.md | 82 +++ .../GwpMinimalApiSample/appsettings.json | 9 + .../GwpMinimalApiSample/wwwroot/index.html | 549 ++++++++++++++++++ .../GwpRazorPagesSample.csproj | 14 + .../GwpRazorPagesSample/Pages/Index.cshtml | 540 +++++++++++++++++ .../GwpRazorPagesSample/Pages/Index.cshtml.cs | 29 + .../GwpRazorPagesSample/Program.cs | 86 +++ .../Properties/launchSettings.json | 12 + .../GwpRazorPagesSample/README.md | 95 +++ .../GwpRazorPagesSample/appsettings.json | 9 + 13 files changed, 1513 insertions(+) create mode 100644 Samples/Genetec Web Player/GwpMinimalApiSample/GwpMinimalApiSample.csproj create mode 100644 Samples/Genetec Web Player/GwpMinimalApiSample/Program.cs create mode 100644 Samples/Genetec Web Player/GwpMinimalApiSample/Properties/launchSettings.json create mode 100644 Samples/Genetec Web Player/GwpMinimalApiSample/README.md create mode 100644 Samples/Genetec Web Player/GwpMinimalApiSample/appsettings.json create mode 100644 Samples/Genetec Web Player/GwpMinimalApiSample/wwwroot/index.html create mode 100644 Samples/Genetec Web Player/GwpRazorPagesSample/GwpRazorPagesSample.csproj create mode 100644 Samples/Genetec Web Player/GwpRazorPagesSample/Pages/Index.cshtml create mode 100644 Samples/Genetec Web Player/GwpRazorPagesSample/Pages/Index.cshtml.cs create mode 100644 Samples/Genetec Web Player/GwpRazorPagesSample/Program.cs create mode 100644 Samples/Genetec Web Player/GwpRazorPagesSample/Properties/launchSettings.json create mode 100644 Samples/Genetec Web Player/GwpRazorPagesSample/README.md create mode 100644 Samples/Genetec Web Player/GwpRazorPagesSample/appsettings.json diff --git a/Samples/Genetec Web Player/GwpMinimalApiSample/GwpMinimalApiSample.csproj b/Samples/Genetec Web Player/GwpMinimalApiSample/GwpMinimalApiSample.csproj new file mode 100644 index 0000000..a555ab3 --- /dev/null +++ b/Samples/Genetec Web Player/GwpMinimalApiSample/GwpMinimalApiSample.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + GwpMinimalApiSample + Genetec.Dap.CodeSamples + Sample project + Genetec Inc. + Copyright © Genetec Inc. 2025 + + + diff --git a/Samples/Genetec Web Player/GwpMinimalApiSample/Program.cs b/Samples/Genetec Web Player/GwpMinimalApiSample/Program.cs new file mode 100644 index 0000000..54ac4a7 --- /dev/null +++ b/Samples/Genetec Web Player/GwpMinimalApiSample/Program.cs @@ -0,0 +1,62 @@ +// Copyright 2025 Genetec Inc. +// Licensed under the Apache License, Version 2.0 + +using System.Net.Http.Headers; +using System.Text; + +var builder = WebApplication.CreateBuilder(args); + +var mediaGateway = builder.Configuration.GetSection("MediaGateway"); +var endpoint = mediaGateway["Endpoint"]?.TrimEnd('/') ?? throw new InvalidOperationException("MediaGateway:Endpoint is required."); +var username = mediaGateway["Username"] ?? throw new InvalidOperationException("MediaGateway:Username is required."); +var password = mediaGateway["Password"] ?? string.Empty; +var sdkCertificate = mediaGateway["SdkCertificate"] ?? throw new InvalidOperationException("MediaGateway:SdkCertificate is required."); +var serverVersion = mediaGateway["ServerVersion"]; + +var authorizationParameter = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username};{sdkCertificate}:{password}")); + +builder.Services.AddHttpClient("MediaGateway", client => +{ + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authorizationParameter); +}).ConfigurePrimaryHttpMessageHandler(() => +{ + var handler = new HttpClientHandler(); + if (builder.Environment.IsDevelopment()) + { + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + return handler; +}); + +var app = builder.Build(); + +app.UseStaticFiles(); + +app.MapGet("/api/config", () => Results.Ok(new +{ + mediaGatewayEndpoint = endpoint, + serverVersion = string.IsNullOrWhiteSpace(serverVersion) ? null : serverVersion.Trim(), +})); + +app.MapGet("/api/token/{cameraId}", async (string cameraId, IHttpClientFactory httpClientFactory) => +{ + if (string.IsNullOrWhiteSpace(cameraId)) + { + return Results.BadRequest("A camera GUID is required."); + } + + var client = httpClientFactory.CreateClient("MediaGateway"); + var response = await client.GetAsync($"{endpoint}/v2/token/{Uri.EscapeDataString(cameraId.Trim())}"); + + if (!response.IsSuccessStatusCode) + { + return Results.StatusCode((int)response.StatusCode); + } + + var token = await response.Content.ReadAsStringAsync(); + return Results.Text(token); +}); + +app.MapFallbackToFile("index.html"); + +app.Run(); diff --git a/Samples/Genetec Web Player/GwpMinimalApiSample/Properties/launchSettings.json b/Samples/Genetec Web Player/GwpMinimalApiSample/Properties/launchSettings.json new file mode 100644 index 0000000..a626612 --- /dev/null +++ b/Samples/Genetec Web Player/GwpMinimalApiSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "GwpMinimalApiSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:59205;http://localhost:59206" + } + } +} \ No newline at end of file diff --git a/Samples/Genetec Web Player/GwpMinimalApiSample/README.md b/Samples/Genetec Web Player/GwpMinimalApiSample/README.md new file mode 100644 index 0000000..867a9e1 --- /dev/null +++ b/Samples/Genetec Web Player/GwpMinimalApiSample/README.md @@ -0,0 +1,82 @@ +# GWP Minimal API Sample + +This sample demonstrates hosting the Genetec Web Player inside an ASP.NET Core Minimal API application: + +- ASP.NET Core serves a static HTML page that loads and runs GWP. +- A server-side `/api/token/{cameraId}` endpoint proxies token requests to the Media Gateway using credentials from `appsettings.json`. Media Gateway credentials never reach the browser. +- A `/api/config` endpoint provides the Media Gateway endpoint and server version to the page without exposing authentication details. +- The page loads `gwp.js` directly from the target Media Gateway and uses the browser environment GWP expects. + +## Why this shape + +GWP is a browser-oriented JavaScript library. It depends on DOM containers, HTML video and audio elements, canvas, WebSockets, and Media Source Extensions. ASP.NET Core serves the page and handles authentication server-side, while the browser provides the media runtime GWP requires. + +This is the simplest possible web hosting model for GWP: one `Program.cs`, one static HTML file, and one configuration file. + +## Run + +```powershell +dotnet run +``` + +Then open the URL shown in the console output (for example, `https://localhost:5001`). + +### Configuration + +Media Gateway settings are configured in `appsettings.json`: + +```json +{ + "MediaGateway": { + "Endpoint": "https://localhost/media", + "Username": "admin", + "Password": "", + "SdkCertificate": "KxsD11z743Hf5Gq9mv3+5ekxzemlCiUXkTFY5ba1NOGcLCmGstt2n0zYE9NsNimv", + "ServerVersion": "" + } +} +``` + +You can also use environment variables or user secrets: + +```powershell +dotnet user-secrets set "MediaGateway:Password" "your-password" +``` + +## Required environment setup + +### 1. Trust the Media Gateway certificate + +If the Media Gateway certificate is self-signed or otherwise untrusted, the browser will fail to load `gwp.js` or connect to the gateway. + +For development, the sample automatically allows certificate warnings when connecting to the Media Gateway from the server-side token endpoint. The browser must still trust the certificate for the `gwp.js` script load and WebSocket connections. Add the certificate to the browser's trust store or use a trusted certificate. + +### 2. Allow the page origin in Media Gateway CORS + +If strict CORS is enabled, add the ASP.NET application origin to `MediaGateway.gconfig`: + +```xml + + + + + + +``` + +Restart the Media Gateway role after the change. + +### 3. Use a matching GWP build + +The sample loads `gwp.js` from `${mediaGatewayEndpoint}/v2/files/gwp.js` so the player version matches the Security Center version. + +## Scope and limitations + +- This sample demonstrates a feasible hosting pattern. It is not a production-ready security design. +- The `/api/token/{cameraId}` endpoint has no authentication. Any client that can reach the server can request camera tokens. A real application must add its own user authentication layer in front of this endpoint. +- Media Gateway credentials are stored in `appsettings.json`. For production, use a secure configuration provider such as user secrets, Azure Key Vault, or environment variables. +- The default SDK certificate is the Genetec development certificate intended for SDK development only. +- A Content Security Policy meta tag restricts script sources, connections, and media to `self`, `https:`, `wss:`, and `blob:`. The policy allows `'unsafe-inline'` for scripts and styles because the page uses inline markup. The Razor Pages sample demonstrates how to use CSP nonces instead. +- Player startup is cancellable. Clicking Stop during script load or session establishment cancels the in-flight start and cleans up any partially created player. +- Browser autoplay rules apply to audio. +- Video rendering and overlays remain in the HTML layer, not the ASP.NET server. diff --git a/Samples/Genetec Web Player/GwpMinimalApiSample/appsettings.json b/Samples/Genetec Web Player/GwpMinimalApiSample/appsettings.json new file mode 100644 index 0000000..a5e1b2a --- /dev/null +++ b/Samples/Genetec Web Player/GwpMinimalApiSample/appsettings.json @@ -0,0 +1,9 @@ +{ + "MediaGateway": { + "Endpoint": "https://localhost/media", + "Username": "admin", + "Password": "", + "SdkCertificate": "KxsD11z743Hf5Gq9mv3+5ekxzemlCiUXkTFY5ba1NOGcLCmGstt2n0zYE9NsNimv", + "ServerVersion": "" + } +} diff --git a/Samples/Genetec Web Player/GwpMinimalApiSample/wwwroot/index.html b/Samples/Genetec Web Player/GwpMinimalApiSample/wwwroot/index.html new file mode 100644 index 0000000..c1255b5 --- /dev/null +++ b/Samples/Genetec Web Player/GwpMinimalApiSample/wwwroot/index.html @@ -0,0 +1,549 @@ + + + + + + + GWP Minimal API Sample + + + +
+
+
+

This page runs with the origin shown below. Add that origin to the Media Gateway allowed origins list when strict CORS is enabled.

+
+
+ +
+ + + +
+ +
+ + + + + + +
+ +
+ +
+
+
Idle
+
+ +
+
+
+
+ + + + diff --git a/Samples/Genetec Web Player/GwpRazorPagesSample/GwpRazorPagesSample.csproj b/Samples/Genetec Web Player/GwpRazorPagesSample/GwpRazorPagesSample.csproj new file mode 100644 index 0000000..ac3a5c7 --- /dev/null +++ b/Samples/Genetec Web Player/GwpRazorPagesSample/GwpRazorPagesSample.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + GwpRazorPagesSample + Genetec.Dap.CodeSamples + Sample project + Genetec Inc. + Copyright © Genetec Inc. 2025 + + + diff --git a/Samples/Genetec Web Player/GwpRazorPagesSample/Pages/Index.cshtml b/Samples/Genetec Web Player/GwpRazorPagesSample/Pages/Index.cshtml new file mode 100644 index 0000000..15ed63e --- /dev/null +++ b/Samples/Genetec Web Player/GwpRazorPagesSample/Pages/Index.cshtml @@ -0,0 +1,540 @@ +@page +@using System.Text.Json +@model Genetec.Dap.CodeSamples.Pages.IndexModel +@{ + Layout = null; + var nonce = Model.CspNonce; +} + + + + + + GWP Razor Pages Sample + + + +
+
+
+

This page runs with the origin shown below. Add that origin to the Media Gateway allowed origins list when strict CORS is enabled.

+
+
+ +
+ + + +
+ +
+ + + + + + +
+ +
+ +
+
+
Idle
+
+ +
+
+
+
+ + + + diff --git a/Samples/Genetec Web Player/GwpRazorPagesSample/Pages/Index.cshtml.cs b/Samples/Genetec Web Player/GwpRazorPagesSample/Pages/Index.cshtml.cs new file mode 100644 index 0000000..0d9d9a5 --- /dev/null +++ b/Samples/Genetec Web Player/GwpRazorPagesSample/Pages/Index.cshtml.cs @@ -0,0 +1,29 @@ +// Copyright 2025 Genetec Inc. +// Licensed under the Apache License, Version 2.0 + +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Genetec.Dap.CodeSamples.Pages; + +public class IndexModel : PageModel +{ + public string MediaGatewayEndpoint { get; private set; } = string.Empty; + + public string? ServerVersion { get; private set; } + + public string CspNonce { get; private set; } = string.Empty; + + private readonly MediaGatewayOptions m_options; + + public IndexModel(MediaGatewayOptions options) + { + m_options = options; + } + + public void OnGet() + { + MediaGatewayEndpoint = m_options.Endpoint; + ServerVersion = m_options.ServerVersion; + CspNonce = HttpContext.Items["CspNonce"] as string ?? string.Empty; + } +} diff --git a/Samples/Genetec Web Player/GwpRazorPagesSample/Program.cs b/Samples/Genetec Web Player/GwpRazorPagesSample/Program.cs new file mode 100644 index 0000000..3405cb6 --- /dev/null +++ b/Samples/Genetec Web Player/GwpRazorPagesSample/Program.cs @@ -0,0 +1,86 @@ +// Copyright 2025 Genetec Inc. +// Licensed under the Apache License, Version 2.0 + +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; + +var builder = WebApplication.CreateBuilder(args); + +var mediaGateway = builder.Configuration.GetSection("MediaGateway"); +var endpoint = mediaGateway["Endpoint"]?.TrimEnd('/') ?? throw new InvalidOperationException("MediaGateway:Endpoint is required."); +var username = mediaGateway["Username"] ?? throw new InvalidOperationException("MediaGateway:Username is required."); +var password = mediaGateway["Password"] ?? string.Empty; +var sdkCertificate = mediaGateway["SdkCertificate"] ?? throw new InvalidOperationException("MediaGateway:SdkCertificate is required."); +var serverVersion = mediaGateway["ServerVersion"]; + +var authorizationParameter = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username};{sdkCertificate}:{password}")); + +builder.Services.AddRazorPages(); + +builder.Services.AddHttpClient("MediaGateway", client => +{ + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authorizationParameter); +}).ConfigurePrimaryHttpMessageHandler(() => +{ + var handler = new HttpClientHandler(); + if (builder.Environment.IsDevelopment()) + { + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + return handler; +}); + +builder.Services.AddSingleton(new MediaGatewayOptions +{ + Endpoint = endpoint, + ServerVersion = string.IsNullOrWhiteSpace(serverVersion) ? null : serverVersion.Trim(), +}); + +var app = builder.Build(); + +app.Use(async (context, next) => +{ + var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16)); + context.Items["CspNonce"] = nonce; + + context.Response.Headers["Content-Security-Policy"] = string.Join("; ", + "default-src 'self' https:", + $"script-src 'self' https: 'nonce-{nonce}'", + $"style-src 'self' 'nonce-{nonce}'", + "connect-src 'self' https: wss:", + "media-src https: blob:"); + + await next(); +}); + +app.UseStaticFiles(); +app.UseRouting(); +app.MapRazorPages(); + +app.MapGet("/api/token/{cameraId}", async (string cameraId, IHttpClientFactory httpClientFactory) => +{ + if (string.IsNullOrWhiteSpace(cameraId)) + { + return Results.BadRequest("A camera GUID is required."); + } + + var client = httpClientFactory.CreateClient("MediaGateway"); + var response = await client.GetAsync($"{endpoint}/v2/token/{Uri.EscapeDataString(cameraId.Trim())}"); + + if (!response.IsSuccessStatusCode) + { + return Results.StatusCode((int)response.StatusCode); + } + + var token = await response.Content.ReadAsStringAsync(); + return Results.Text(token); +}); + +app.Run(); + +public class MediaGatewayOptions +{ + public required string Endpoint { get; init; } + public string? ServerVersion { get; init; } +} diff --git a/Samples/Genetec Web Player/GwpRazorPagesSample/Properties/launchSettings.json b/Samples/Genetec Web Player/GwpRazorPagesSample/Properties/launchSettings.json new file mode 100644 index 0000000..c65cb95 --- /dev/null +++ b/Samples/Genetec Web Player/GwpRazorPagesSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "GwpRazorPagesSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:65426;http://localhost:65427" + } + } +} \ No newline at end of file diff --git a/Samples/Genetec Web Player/GwpRazorPagesSample/README.md b/Samples/Genetec Web Player/GwpRazorPagesSample/README.md new file mode 100644 index 0000000..3557c50 --- /dev/null +++ b/Samples/Genetec Web Player/GwpRazorPagesSample/README.md @@ -0,0 +1,95 @@ +# GWP Razor Pages Sample + +This sample demonstrates hosting the Genetec Web Player inside an ASP.NET Core Razor Pages application with production-ready CSP nonce support: + +- ASP.NET Core serves a Razor Page that loads and runs GWP. +- The page is server-rendered, so the Media Gateway endpoint and server version are injected directly into the markup. No client-side configuration fetch is needed. +- A per-request cryptographic CSP nonce is generated in middleware, added to the `Content-Security-Policy` response header, and passed to each `