From 948456550546e6cfc673ad4e6a9d4bc88026835d Mon Sep 17 00:00:00 2001 From: NataS Date: Mon, 22 Apr 2024 22:33:45 -0600 Subject: [PATCH] Kestrel Server Zitified Example of using ziti for Kestrel Kernel --- .../Controllers/MetricItemsController.cs | 126 ++++++++++++++++++ .../Controllers/WeatherForecastController.cs | 32 +++++ .../DelegatedZitiConnectionListenerFactory.cs | 98 ++++++++++++++ .../Models/MetricContext.cs | 13 ++ .../Models/MetricItem.cs | 8 ++ OpenZiti.NET.Samples.Kestrel/Program.cs | 27 ++++ OpenZiti.NET.Samples.Kestrel/README.md | 33 +++++ .../ServiceCollectionExtensions.cs | 17 +++ .../WeatherForecast.cs | 12 ++ .../ZitiRestServerCSharp.csproj | 21 +++ .../ZitiRestServerCSharp.sln | 25 ++++ 11 files changed, 412 insertions(+) create mode 100644 OpenZiti.NET.Samples.Kestrel/Controllers/MetricItemsController.cs create mode 100644 OpenZiti.NET.Samples.Kestrel/Controllers/WeatherForecastController.cs create mode 100644 OpenZiti.NET.Samples.Kestrel/DelegatedZitiConnectionListenerFactory.cs create mode 100644 OpenZiti.NET.Samples.Kestrel/Models/MetricContext.cs create mode 100644 OpenZiti.NET.Samples.Kestrel/Models/MetricItem.cs create mode 100644 OpenZiti.NET.Samples.Kestrel/Program.cs create mode 100644 OpenZiti.NET.Samples.Kestrel/README.md create mode 100644 OpenZiti.NET.Samples.Kestrel/ServiceCollectionExtensions.cs create mode 100644 OpenZiti.NET.Samples.Kestrel/WeatherForecast.cs create mode 100644 OpenZiti.NET.Samples.Kestrel/ZitiRestServerCSharp.csproj create mode 100644 OpenZiti.NET.Samples.Kestrel/ZitiRestServerCSharp.sln diff --git a/OpenZiti.NET.Samples.Kestrel/Controllers/MetricItemsController.cs b/OpenZiti.NET.Samples.Kestrel/Controllers/MetricItemsController.cs new file mode 100644 index 0000000..0399378 --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/Controllers/MetricItemsController.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using ZitiRestServerCSharp.Models; + +namespace ZitiRestServerCSharp.Controllers +{ + [Route("api/MetricItemsController")] + [ApiController] + public class MetricItemsController : ControllerBase + { + private readonly MetricContext _context; + + public MetricItemsController(MetricContext context) + { + _context = context; + } + + // GET: api/MetricItemsController + [HttpGet] + public async Task>> GetMetricItems() + { + if (_context.MetricItems == null) + { + return NotFound(); + } + return await _context.MetricItems.ToListAsync(); + } + + // GET: api/MetricItemsController/5 + [HttpGet("{id}")] + public async Task> GetMetricItem(long id) + { + Console.WriteLine("Starting Metrics -> Get -> id: " + id); + if (_context.MetricItems == null) + { + return NotFound(); + } + var metricItem = await _context.MetricItems.FindAsync(id); + + if (metricItem == null) + { + return NotFound(); + } + Console.WriteLine("Ending Metrics -> Get -> id: " + id); + return metricItem; + } + + // PUT: api/MetricItemsController/5 + // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 + [HttpPut("{id}")] + public async Task PutMetricItem(long id, MetricItem metricItem) + { + if (id != metricItem.Id) + { + return BadRequest(); + } + + _context.Entry(metricItem).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!MetricItemExists(id)) + { + return NotFound(); + } + else + { + throw; + } + } + + return NoContent(); + } + + // POST: api/MetricItemsController + // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 + [HttpPost] + public async Task> PostMetricItem(MetricItem metricItem) + { + Console.WriteLine("Starting Add MetricItem with values" + metricItem); + if (_context.MetricItems == null) + { + return Problem("Entity set 'MetricContext.MetricItems' is null."); + } + _context.MetricItems.Add(metricItem); + await _context.SaveChangesAsync(); + + Console.WriteLine("Creating values" + metricItem); + return CreatedAtAction(nameof(GetMetricItem), new { id = metricItem.Id }, metricItem); + } + + // DELETE: api/MetricItemsController/5 + [HttpDelete("{id}")] + public async Task DeleteMetricItem(long id) + { + if (_context.MetricItems == null) + { + return NotFound(); + } + var metricItem = await _context.MetricItems.FindAsync(id); + if (metricItem == null) + { + return NotFound(); + } + + _context.MetricItems.Remove(metricItem); + await _context.SaveChangesAsync(); + + return NoContent(); + } + + private bool MetricItemExists(long id) + { + return (_context.MetricItems?.Any(e => e.Id == id)).GetValueOrDefault(); + } + } +} diff --git a/OpenZiti.NET.Samples.Kestrel/Controllers/WeatherForecastController.cs b/OpenZiti.NET.Samples.Kestrel/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..7f1320a --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ZitiRestServerCSharp.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/OpenZiti.NET.Samples.Kestrel/DelegatedZitiConnectionListenerFactory.cs b/OpenZiti.NET.Samples.Kestrel/DelegatedZitiConnectionListenerFactory.cs new file mode 100644 index 0000000..04c99ec --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/DelegatedZitiConnectionListenerFactory.cs @@ -0,0 +1,98 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading.Channels; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +using OpenZiti; + +internal class ZitiConnectionListenerFactory : IConnectionListenerFactory +{ + private ILogger _logger; + private ZitiSocket _zitiSocket; + + public ZitiConnectionListenerFactory(ILogger logger) + { + _logger = logger; + } + + public async ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) + { + #region OptionZiti + API.SetLogLevel(ZitiLogLevel.INFO); + _zitiSocket = new ZitiSocket(SocketType.Stream); + ZitiContext ctx = new ZitiContext("C:\\OpenZiti\\CSharp-RestApi-Server.json"); + var svcName = "CSharp-Service"; + string terminator = ""; + + API.Bind(_zitiSocket, ctx, svcName, terminator); + Console.WriteLine("Bound to Ziti"); + API.Listen(_zitiSocket, 100); + Console.WriteLine("Listening on Ziti"); + #endregion + + return new SocketListener(_zitiSocket, _logger); + } + + class SocketListener : IConnectionListener + { + private ZitiSocket _zitiSocket; + private Socket _socket; + private readonly Channel _channel = Channel.CreateBounded(20); + private readonly SocketConnectionContextFactory _contextFactory; + + public SocketListener(ZitiSocket zitiSocket, ILogger logger) + { + _zitiSocket = zitiSocket; + _socket = zitiSocket.ToSocket(); + _contextFactory = new(new(), logger); + } + + public EndPoint EndPoint => _socket.LocalEndPoint!; + + public async ValueTask DisposeAsync() + { + } + + public ValueTask UnbindAsync(CancellationToken cancellationToken = default) + { + return default; + } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + try + { + while (true) + { + ZitiSocket client = API.Accept(_zitiSocket, out var caller); + Console.WriteLine("Accepted connection from an Authorized Ziti Client"); + var socket = client.ToSocket(); + if (socket.RemoteEndPoint is IPEndPoint remoteEndPoint) + { + string clientIpAddress = remoteEndPoint.Address.ToString(); + int remotePort = remoteEndPoint.Port; + Console.WriteLine($"Connection stablished with Authorized Ziti Client IP Address: {clientIpAddress}, Port: {remotePort}"); + } + if (socket.LocalEndPoint is IPEndPoint localEndPoint) + { + string localIpAddress = localEndPoint.Address.ToString(); + int localPort = localEndPoint.Port; + Console.WriteLine($"Connection stablished at IP Address: {localIpAddress}, Port: {localPort}"); + } + return _contextFactory.Create(socket); + } + } + catch (ObjectDisposedException) + { + } + catch (SocketException) + { + } + return null; + } + } +} \ No newline at end of file diff --git a/OpenZiti.NET.Samples.Kestrel/Models/MetricContext.cs b/OpenZiti.NET.Samples.Kestrel/Models/MetricContext.cs new file mode 100644 index 0000000..bd0ba65 --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/Models/MetricContext.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; + +namespace ZitiRestServerCSharp.Models; + +public class MetricContext : DbContext +{ + public MetricContext(DbContextOptions options) + : base(options) + { + } + + public DbSet MetricItems { get; set; } = null!; +} \ No newline at end of file diff --git a/OpenZiti.NET.Samples.Kestrel/Models/MetricItem.cs b/OpenZiti.NET.Samples.Kestrel/Models/MetricItem.cs new file mode 100644 index 0000000..23b5d86 --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/Models/MetricItem.cs @@ -0,0 +1,8 @@ +namespace ZitiRestServerCSharp.Models; +public class MetricItem +{ + public long Id { get; set; } + public string? SensorGuid { get; set; } + public string? Name { get; set; } + public int value { get; set; } +} \ No newline at end of file diff --git a/OpenZiti.NET.Samples.Kestrel/Program.cs b/OpenZiti.NET.Samples.Kestrel/Program.cs new file mode 100644 index 0000000..da9b295 --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/Program.cs @@ -0,0 +1,27 @@ +using OpenZiti; +using System.Net.Sockets; +using Microsoft.EntityFrameworkCore; +using ZitiRestServerCSharp.Models; + +var builder = WebApplication.CreateBuilder(args); + +builder.WebHost.UseDelegatedTransport(); + +builder.Services.AddControllers(); +builder.Services.AddDbContext(opt => + opt.UseInMemoryDatabase("Metrics")); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.UseSwagger(); +app.UseSwaggerUI(); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/OpenZiti.NET.Samples.Kestrel/README.md b/OpenZiti.NET.Samples.Kestrel/README.md new file mode 100644 index 0000000..6677b3f --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/README.md @@ -0,0 +1,33 @@ +![Ziggy using the sdk-csharp](https://raw.githubusercontent.com/openziti/branding/main/images/banners/CSharp.jpg) + +# Zitified Kestrel Sample + +This sample demonstrates how to use the OpenZiti SDK to secure a Kestrel server. +In this case, the server is a simple REST API listening in the Ziti Overlay, that returns some metric data. This data looks like this: +``` +{ + "sensorguid": "abcd1234", + "name": "temp", + "value": "6" +} +``` + +## OpenZiti Concepts Demonstrated + +This sample demonstrates some key OpenZiti concepts: +* Application-embedded zero trust server. +* Availability to natively integrate with the DotNet Core ecosystem. +* Offloading traffic from an identity. + +## Running the Sample + +To run the sample, you should be able to just run it directly and it will bootstrap the overlay network. The program expects to have your identity saved into the `C:\OpenZiti\CSharp-RestApi-Server.json` file. This location can be changed in the file `DelegatedZitiConnectionListenerFactory.cs`. You can run the code as: +``` +dotnet run --project OpenZiti.NET.Samples.Kestrel/ZitiRestServerCSharp.csproj +``` + +## Code Walkthrough + +There're a few key components in this sample: +* `DelegatedZitiConnectionListenerFactory.cs` - This is the main entry point of the application. It sets up the Kestrel server and the Ziti SDK using the identity provided. +* `ServiceCollectionExtension.cs` - This file contains the extension method to overide the default `IConnectionListenerFactory` with the `DelegatedZitiConnectionListenerFactory`. diff --git a/OpenZiti.NET.Samples.Kestrel/ServiceCollectionExtensions.cs b/OpenZiti.NET.Samples.Kestrel/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..dab189f --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using System.Net; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Core; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class ServiceCollectionExtensions + { + public static IWebHostBuilder UseDelegatedTransport(this IWebHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices(services => + { + services.AddSingleton(); + }); + } + } +} \ No newline at end of file diff --git a/OpenZiti.NET.Samples.Kestrel/WeatherForecast.cs b/OpenZiti.NET.Samples.Kestrel/WeatherForecast.cs new file mode 100644 index 0000000..103c884 --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/WeatherForecast.cs @@ -0,0 +1,12 @@ + namespace ZitiRestServerCSharp; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/OpenZiti.NET.Samples.Kestrel/ZitiRestServerCSharp.csproj b/OpenZiti.NET.Samples.Kestrel/ZitiRestServerCSharp.csproj new file mode 100644 index 0000000..fccb0d4 --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/ZitiRestServerCSharp.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/OpenZiti.NET.Samples.Kestrel/ZitiRestServerCSharp.sln b/OpenZiti.NET.Samples.Kestrel/ZitiRestServerCSharp.sln new file mode 100644 index 0000000..808bbc4 --- /dev/null +++ b/OpenZiti.NET.Samples.Kestrel/ZitiRestServerCSharp.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZitiRestServerCSharp", "ZitiRestServerCSharp.csproj", "{BF2A5560-1B4C-4C2B-837C-A7810D954D84}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BF2A5560-1B4C-4C2B-837C-A7810D954D84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF2A5560-1B4C-4C2B-837C-A7810D954D84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF2A5560-1B4C-4C2B-837C-A7810D954D84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF2A5560-1B4C-4C2B-837C-A7810D954D84}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2E5C150F-4A3D-443D-ACB0-B3054BB1B28D} + EndGlobalSection +EndGlobal