Azure Event Grid Edge Samples, SDK
This commit is contained in:
Коммит
786c9743d5
|
@ -0,0 +1,330 @@
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
**/Properties/launchSettings.json
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# CodeRush
|
||||||
|
.cr/
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
|
@ -0,0 +1,23 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<RootNamespace>Microsoft.Azure.EventGridEdge.Samples.Common</RootNamespace>
|
||||||
|
<AssemblyName>Microsoft.EventGridEdge.Samples.Common</AssemblyName>
|
||||||
|
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
<RuntimeFrameworkVersion>2.1.4</RuntimeFrameworkVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Portable.BouncyCastle" Version="1.8.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" AllowExplicitVersion="true" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<!-- https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0001.md -->
|
||||||
|
<NoWarn>$(NoWarn),1573,1591,1712</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,80 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Auth
|
||||||
|
{
|
||||||
|
public class HttpBufferedStream : Stream
|
||||||
|
{
|
||||||
|
private const char CR = '\r';
|
||||||
|
private const char LF = '\n';
|
||||||
|
private readonly BufferedStream innerStream;
|
||||||
|
|
||||||
|
public HttpBufferedStream(Stream stream)
|
||||||
|
{
|
||||||
|
this.innerStream = new BufferedStream(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanRead => this.innerStream.CanRead;
|
||||||
|
|
||||||
|
public override bool CanSeek => this.innerStream.CanSeek;
|
||||||
|
|
||||||
|
public override bool CanWrite => this.innerStream.CanWrite;
|
||||||
|
|
||||||
|
public override long Length => this.innerStream.Length;
|
||||||
|
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get => this.innerStream.Position;
|
||||||
|
set => this.innerStream.Position = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush() => this.innerStream.Flush();
|
||||||
|
|
||||||
|
public override Task FlushAsync(CancellationToken cancellationToken) => this.innerStream.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count) => this.innerStream.Read(buffer, offset, count);
|
||||||
|
|
||||||
|
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.innerStream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<string> ReadLineAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int position = 0;
|
||||||
|
byte[] buffer = new byte[1];
|
||||||
|
bool crFound = false;
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int length = await this.innerStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
|
||||||
|
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
throw new IOException("Unexpected end of stream.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crFound && (char)buffer[position] == LF)
|
||||||
|
{
|
||||||
|
builder.Remove(builder.Length - 1, 1);
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append((char)buffer[position]);
|
||||||
|
crFound = (char)buffer[position] == CR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin) => this.innerStream.Seek(offset, origin);
|
||||||
|
|
||||||
|
public override void SetLength(long value) => this.innerStream.SetLength(value);
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count) => this.innerStream.Write(buffer, offset, count);
|
||||||
|
|
||||||
|
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.innerStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing) => this.innerStream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Auth
|
||||||
|
{
|
||||||
|
public class HttpSerializer
|
||||||
|
{
|
||||||
|
private const char SP = ' ';
|
||||||
|
private const char CR = '\r';
|
||||||
|
private const char LF = '\n';
|
||||||
|
private const char ProtocolVersionSeparator = '/';
|
||||||
|
private const string Protocol = "HTTP";
|
||||||
|
private const char HeaderSeparator = ':';
|
||||||
|
private const string ContentLengthHeaderName = "content-length";
|
||||||
|
|
||||||
|
public byte[] SerializeRequest(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
this.PreProcessRequest(request);
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
// request-line = method SP request-target SP HTTP-version CRLF
|
||||||
|
builder.Append(request.Method);
|
||||||
|
builder.Append(SP);
|
||||||
|
builder.Append(request.RequestUri.IsAbsoluteUri ? request.RequestUri.PathAndQuery : Uri.EscapeUriString(request.RequestUri.ToString()));
|
||||||
|
builder.Append(SP);
|
||||||
|
builder.Append($"{Protocol}{ProtocolVersionSeparator}");
|
||||||
|
builder.Append(new Version(1, 1).ToString(2));
|
||||||
|
builder.Append(CR);
|
||||||
|
builder.Append(LF);
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
builder.Append(request.Headers);
|
||||||
|
|
||||||
|
if (request.Content != null)
|
||||||
|
{
|
||||||
|
long? contentLength = request.Content.Headers.ContentLength;
|
||||||
|
if (contentLength.HasValue)
|
||||||
|
{
|
||||||
|
request.Content.Headers.ContentLength = contentLength.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(request.Content.Headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers end
|
||||||
|
builder.Append(CR);
|
||||||
|
builder.Append(LF);
|
||||||
|
|
||||||
|
return Encoding.ASCII.GetBytes(builder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> DeserializeResponseAsync(HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var httpResponse = new HttpResponseMessage();
|
||||||
|
|
||||||
|
await this.SetResponseStatusLineAsync(httpResponse, bufferedStream, cancellationToken);
|
||||||
|
await this.SetHeadersAndContentAsync(httpResponse, bufferedStream, cancellationToken);
|
||||||
|
|
||||||
|
return httpResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetHeadersAndContentAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IList<string> headers = new List<string>();
|
||||||
|
string line = await bufferedStream.ReadLineAsync(cancellationToken);
|
||||||
|
while (!string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
headers.Add(line);
|
||||||
|
line = await bufferedStream.ReadLineAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.Content = new StreamContent(bufferedStream);
|
||||||
|
foreach (string header in headers)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(header))
|
||||||
|
{
|
||||||
|
// headers end
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int headerSeparatorPosition = header.IndexOf(HeaderSeparator, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (headerSeparatorPosition <= 0)
|
||||||
|
{
|
||||||
|
throw new HttpRequestException($"Header is invalid {header}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string headerName = header.Substring(0, headerSeparatorPosition).Trim();
|
||||||
|
string headerValue = header.Substring(headerSeparatorPosition + 1).Trim();
|
||||||
|
|
||||||
|
bool headerAdded = httpResponse.Headers.TryAddWithoutValidation(headerName, headerValue);
|
||||||
|
if (!headerAdded)
|
||||||
|
{
|
||||||
|
if (string.Equals(headerName, ContentLengthHeaderName, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
if (!long.TryParse(headerValue, out long contentLength))
|
||||||
|
{
|
||||||
|
throw new HttpRequestException($"Header value is invalid for {headerName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await httpResponse.Content.LoadIntoBufferAsync(contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.Content.Headers.TryAddWithoutValidation(headerName, headerValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetResponseStatusLineAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
string statusLine = await bufferedStream.ReadLineAsync(cancellationToken);
|
||||||
|
if (string.IsNullOrWhiteSpace(statusLine))
|
||||||
|
{
|
||||||
|
throw new HttpRequestException("Response is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] statusLineParts = statusLine.Split(new[] { SP }, 3);
|
||||||
|
if (statusLineParts.Length < 3)
|
||||||
|
{
|
||||||
|
throw new HttpRequestException("Status line is not valid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] httpVersion = statusLineParts[0].Split(new[] { ProtocolVersionSeparator }, 2);
|
||||||
|
if (httpVersion.Length < 2 || !Version.TryParse(httpVersion[1], out Version versionNumber))
|
||||||
|
{
|
||||||
|
throw new HttpRequestException($"Version is not valid {statusLineParts[0]}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.Version = versionNumber;
|
||||||
|
|
||||||
|
if (!Enum.TryParse(statusLineParts[1], out HttpStatusCode statusCode))
|
||||||
|
{
|
||||||
|
throw new HttpRequestException($"StatusCode is not valid {statusLineParts[1]}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.StatusCode = statusCode;
|
||||||
|
httpResponse.ReasonPhrase = statusLineParts[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PreProcessRequest(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(request.Headers.Host))
|
||||||
|
{
|
||||||
|
request.Headers.Host = $"{request.RequestUri.DnsSafeHost}:{request.RequestUri.Port}";
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Headers.ConnectionClose = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Auth
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unix domain message handler.
|
||||||
|
/// </summary>
|
||||||
|
public class HttpUdsMessageHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
private readonly Uri providerUri;
|
||||||
|
|
||||||
|
public HttpUdsMessageHandler(Uri providerUri)
|
||||||
|
{
|
||||||
|
this.providerUri = providerUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (Socket socket = await this.GetConnectedSocketAsync())
|
||||||
|
{
|
||||||
|
using (var stream = new HttpBufferedStream(new NetworkStream(socket, true)))
|
||||||
|
{
|
||||||
|
var serializer = new HttpSerializer();
|
||||||
|
byte[] requestBytes = serializer.SerializeRequest(request);
|
||||||
|
|
||||||
|
await stream.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken);
|
||||||
|
if (request.Content != null)
|
||||||
|
{
|
||||||
|
await request.Content.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await serializer.DeserializeResponseAsync(stream, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Socket> GetConnectedSocketAsync()
|
||||||
|
{
|
||||||
|
var endpoint = new UnixDomainSocketEndPoint(this.providerUri.LocalPath);
|
||||||
|
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
|
||||||
|
await socket.ConnectAsync(endpoint);
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Auth
|
||||||
|
{
|
||||||
|
public enum PrivateKeyType
|
||||||
|
{
|
||||||
|
[System.Runtime.Serialization.EnumMember(Value = "ref")]
|
||||||
|
Ref = 0,
|
||||||
|
|
||||||
|
[System.Runtime.Serialization.EnumMember(Value = "key")]
|
||||||
|
Key = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IdentityCertificateRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("expiration", Required = Newtonsoft.Json.Required.Always)]
|
||||||
|
public DateTime Expiration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ServerCertificateRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("commonName", Required = Newtonsoft.Json.Required.Always)]
|
||||||
|
public string CommonName { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("expiration", Required = Newtonsoft.Json.Required.Always)]
|
||||||
|
public DateTime Expiration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CertificateResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("privateKey", Required = Newtonsoft.Json.Required.Always)]
|
||||||
|
public PrivateKey PrivateKey { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("certificate", Required = Newtonsoft.Json.Required.Always)]
|
||||||
|
public string Certificate { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("expiration", Required = Newtonsoft.Json.Required.Always)]
|
||||||
|
public DateTime Expiration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PrivateKey
|
||||||
|
{
|
||||||
|
[JsonProperty("type", Required = Required.Always)]
|
||||||
|
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||||
|
public PrivateKeyType Type { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("ref", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string Ref { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("bytes", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string Bytes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TrustBundleResponse
|
||||||
|
{
|
||||||
|
[Newtonsoft.Json.JsonProperty("certificate", Required = Newtonsoft.Json.Required.Always)]
|
||||||
|
public string Certificate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Auth
|
||||||
|
{
|
||||||
|
internal class IoTEdgeConstants
|
||||||
|
{
|
||||||
|
public const string ModuleGenerationId = "IOTEDGE_MODULEGENERATIONID";
|
||||||
|
public const string ModuleId = "IOTEDGE_MODULEID";
|
||||||
|
public const string WorkloadUri = "IOTEDGE_WORKLOADURI";
|
||||||
|
public const string WorkloadApiVersion = "IOTEDGE_APIVERSION";
|
||||||
|
public const string EdgeGatewayHostName = "IOTEDGE_GATEWAYHOSTNAME";
|
||||||
|
public const string UnixScheme = "unix";
|
||||||
|
public const int DefaultServerCertificateValidityInDays = 90;
|
||||||
|
public const int DefaultIdentityCertificateValidityInDays = 7;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,397 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
|
using Org.BouncyCastle.OpenSsl;
|
||||||
|
using Org.BouncyCastle.Pkcs;
|
||||||
|
using Org.BouncyCastle.Security;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Auth
|
||||||
|
{
|
||||||
|
public class IoTSecurity
|
||||||
|
{
|
||||||
|
public void ImportCertificate(IEnumerable<X509Certificate2> certificates)
|
||||||
|
{
|
||||||
|
if (certificates != null)
|
||||||
|
{
|
||||||
|
StoreName storeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StoreName.CertificateAuthority : StoreName.Root;
|
||||||
|
StoreLocation storeLocation = StoreLocation.CurrentUser;
|
||||||
|
using (var store = new X509Store(storeName, storeLocation))
|
||||||
|
{
|
||||||
|
store.Open(OpenFlags.ReadWrite);
|
||||||
|
foreach (X509Certificate2 cert in certificates)
|
||||||
|
{
|
||||||
|
store.Add(cert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(X509Certificate2, IEnumerable<X509Certificate2>)> GetClientCertificateAsync()
|
||||||
|
{
|
||||||
|
Uri workloadUri = this.GetWorkloadUri();
|
||||||
|
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
|
||||||
|
Uri workloadRequestUri = this.GetIdentityCertificateRequestUri(workloadUri);
|
||||||
|
|
||||||
|
int certificateValidityInDays = IoTEdgeConstants.DefaultIdentityCertificateValidityInDays;
|
||||||
|
DateTime expirationTime = DateTime.UtcNow.AddDays(certificateValidityInDays);
|
||||||
|
var identityCertificateRequest = new IdentityCertificateRequest() { Expiration = expirationTime };
|
||||||
|
|
||||||
|
var errorMessage = "Failed to retrieve ClientCertificate from IoTEdge Security Daemon.";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (HttpClient httpClient = this.GetHttpClient(workloadUri))
|
||||||
|
{
|
||||||
|
string requestString = JsonConvert.SerializeObject(identityCertificateRequest);
|
||||||
|
var content = new StringContent(requestString);
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
|
||||||
|
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, workloadRequestUri))
|
||||||
|
{
|
||||||
|
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
httpRequest.Content = content;
|
||||||
|
|
||||||
|
using (HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None))
|
||||||
|
{
|
||||||
|
if (httpResponse.StatusCode == HttpStatusCode.Created)
|
||||||
|
{
|
||||||
|
string responseData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responseData);
|
||||||
|
|
||||||
|
IEnumerable<string> rawCerts = this.ParseResponse(cr.Certificate);
|
||||||
|
if (rawCerts.FirstOrDefault() == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Did not receive an identity certificate from IoTEdge daemon!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.CreateX509Certificates(rawCerts, cr.PrivateKey.Bytes, moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
throw new Exception(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to retrieve client certificate from IoTEdge Security Daemon. Reason: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain)> GetServerCertificateAsync()
|
||||||
|
{
|
||||||
|
Uri workloadUri = this.GetWorkloadUri();
|
||||||
|
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
|
||||||
|
Uri workloadRequestUri = this.GetServerCertificateRequestUri(workloadUri);
|
||||||
|
|
||||||
|
ServerCertificateRequest scRequest = this.GetServerCertificateRequest(IoTEdgeConstants.DefaultServerCertificateValidityInDays);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (HttpClient httpClient = this.GetHttpClient(workloadUri))
|
||||||
|
{
|
||||||
|
string scrString = JsonConvert.SerializeObject(scRequest);
|
||||||
|
var content = new StringContent(scrString);
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
|
||||||
|
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, workloadRequestUri))
|
||||||
|
{
|
||||||
|
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
httpRequest.Content = content;
|
||||||
|
|
||||||
|
using (HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (httpResponse.StatusCode == HttpStatusCode.Created)
|
||||||
|
{
|
||||||
|
string responseData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responseData);
|
||||||
|
IEnumerable<string> rawCerts = this.ParseResponse(cr.Certificate);
|
||||||
|
if (rawCerts.FirstOrDefault() == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to retrieve serverCertificate from IoTEdge Security daemon. Reason: Security daemon return empty response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
(X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain) = this.CreateX509Certificates(rawCerts, cr.PrivateKey.Bytes, moduleId);
|
||||||
|
return (serverCertificate, certificateChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
string errorData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
throw new Exception(errorData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to retrieve server certificate from IoTEdge Security Daemon. Reason: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri GetWorkloadUri() => new Uri(Environment.GetEnvironmentVariable(IoTEdgeConstants.WorkloadUri));
|
||||||
|
|
||||||
|
private string GetIoTEdgeEnvironmentVariable(string envVarName) => Environment.GetEnvironmentVariable(envVarName);
|
||||||
|
|
||||||
|
private Uri GetIdentityCertificateRequestUri(Uri workloadUri)
|
||||||
|
{
|
||||||
|
string workloadApiVersion = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.WorkloadApiVersion);
|
||||||
|
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
|
||||||
|
|
||||||
|
string urlEncodedModuleId = WebUtility.UrlEncode(moduleId);
|
||||||
|
string urlEncodedWorkloadApiVersion = WebUtility.UrlEncode(workloadApiVersion);
|
||||||
|
|
||||||
|
string workloadBaseUrl = this.GetBaseUrl(workloadUri).TrimEnd('/');
|
||||||
|
var workloadRequestUriBuilder = new StringBuilder(workloadBaseUrl);
|
||||||
|
workloadRequestUriBuilder.Append($"/modules/{urlEncodedModuleId}/certificate/identity?api-version={urlEncodedWorkloadApiVersion}");
|
||||||
|
return new Uri(workloadRequestUriBuilder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri GetServerCertificateRequestUri(Uri workloadUri)
|
||||||
|
{
|
||||||
|
string workloadApiVersion = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.WorkloadApiVersion);
|
||||||
|
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
|
||||||
|
string moduleGenerationId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleGenerationId);
|
||||||
|
|
||||||
|
string urlEncodedModuleId = WebUtility.UrlEncode(moduleId);
|
||||||
|
string urlEncodedModuleGenerationId = WebUtility.UrlEncode(moduleGenerationId);
|
||||||
|
string urlEncodedWorkloadApiVersion = WebUtility.UrlEncode(workloadApiVersion);
|
||||||
|
|
||||||
|
string workloadBaseUrl = this.GetBaseUrl(workloadUri).TrimEnd('/');
|
||||||
|
var workloadRequestUriBuilder = new StringBuilder(workloadBaseUrl);
|
||||||
|
workloadRequestUriBuilder.Append($"/modules/{urlEncodedModuleId}/genid/{urlEncodedModuleGenerationId}/certificate/server?api-version={urlEncodedWorkloadApiVersion}");
|
||||||
|
return new Uri(workloadRequestUriBuilder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri GetTrustBundleRequestUri(Uri workloadUri)
|
||||||
|
{
|
||||||
|
string workloadApiVersion = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.WorkloadApiVersion);
|
||||||
|
string urlEncodedWorkloadApiVersion = WebUtility.UrlEncode(workloadApiVersion);
|
||||||
|
|
||||||
|
string workloadBaseUrl = this.GetBaseUrl(workloadUri).TrimEnd('/');
|
||||||
|
var workloadRequestUriBuilder = new StringBuilder(workloadBaseUrl);
|
||||||
|
workloadRequestUriBuilder.Append($"/trust-bundle?api-version={urlEncodedWorkloadApiVersion}");
|
||||||
|
return new Uri(workloadRequestUriBuilder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerCertificateRequest GetServerCertificateRequest(int validityInDays = 90)
|
||||||
|
{
|
||||||
|
string edgeDeviceHostName = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.EdgeGatewayHostName);
|
||||||
|
DateTime expirationTime = DateTime.UtcNow.AddDays(validityInDays);
|
||||||
|
|
||||||
|
return new ServerCertificateRequest()
|
||||||
|
{
|
||||||
|
CommonName = edgeDeviceHostName,
|
||||||
|
Expiration = expirationTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetBaseUrl(Uri workloadUri)
|
||||||
|
{
|
||||||
|
if (workloadUri.Scheme.Equals(IoTEdgeConstants.UnixScheme, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return $"http://{workloadUri.Segments.Last()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return workloadUri.OriginalString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpClient GetHttpClient(Uri workloadUri)
|
||||||
|
{
|
||||||
|
if (workloadUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || workloadUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new HttpClient();
|
||||||
|
}
|
||||||
|
else if (workloadUri.Scheme.Equals(IoTEdgeConstants.UnixScheme, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new HttpClient(new HttpUdsMessageHandler(workloadUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"Unknow workloadUri schema specified. {workloadUri}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private IList<string> ParseResponse(string certificateChain)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(certificateChain))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Trusted certificates can not be null or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract each certificate's string. The final string from the split will either be empty
|
||||||
|
// or a non-certificate entry, so it is dropped.
|
||||||
|
string delimiter = "-----END CERTIFICATE-----";
|
||||||
|
string[] rawCerts = certificateChain.Split(new[] { delimiter }, StringSplitOptions.None);
|
||||||
|
return rawCerts.Take(count: rawCerts.Count() - 1).Select(c => $"{c}{delimiter}").ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private (X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain) CreateX509Certificates(IEnumerable<string> rawCerts, string privateKey, string moduleId)
|
||||||
|
{
|
||||||
|
string primaryCert = rawCerts.First();
|
||||||
|
RsaPrivateCrtKeyParameters keyParams = null;
|
||||||
|
|
||||||
|
IEnumerable<X509Certificate2> x509CertsChain = this.ConvertToX509(rawCerts.Skip(1));
|
||||||
|
|
||||||
|
IList<X509CertificateEntry> chainCertEntries = new List<X509CertificateEntry>();
|
||||||
|
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
|
||||||
|
// note: the seperator between the certificate and private key is added for safety to delinate the cert and key boundary
|
||||||
|
var sr = new StringReader(primaryCert + "\r\n" + privateKey);
|
||||||
|
var pemReader = new PemReader(sr);
|
||||||
|
object certObject = pemReader.ReadObject();
|
||||||
|
while (certObject != null)
|
||||||
|
{
|
||||||
|
if (certObject is Org.BouncyCastle.X509.X509Certificate x509Cert)
|
||||||
|
{
|
||||||
|
chainCertEntries.Add(new X509CertificateEntry(x509Cert));
|
||||||
|
}
|
||||||
|
|
||||||
|
// when processing certificates generated via openssl certObject type is of AsymmetricCipherKeyPair
|
||||||
|
if (certObject is AsymmetricCipherKeyPair)
|
||||||
|
{
|
||||||
|
certObject = ((AsymmetricCipherKeyPair)certObject).Private;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certObject is RsaPrivateCrtKeyParameters)
|
||||||
|
{
|
||||||
|
keyParams = (RsaPrivateCrtKeyParameters)certObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
certObject = pemReader.ReadObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyParams == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Private key is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
store.SetKeyEntry(moduleId, new AsymmetricKeyEntry(keyParams), chainCertEntries.ToArray());
|
||||||
|
using (var p12File = new MemoryStream())
|
||||||
|
{
|
||||||
|
store.Save(p12File, Array.Empty<char>(), new SecureRandom());
|
||||||
|
var x509PrimaryCert = new X509Certificate2(p12File.ToArray());
|
||||||
|
return (x509PrimaryCert, x509CertsChain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<X509Certificate2>> GetTrustBundleAsync()
|
||||||
|
{
|
||||||
|
Uri workloadUri = this.GetWorkloadUri();
|
||||||
|
using (HttpClient httpClient = this.GetHttpClient(workloadUri))
|
||||||
|
{
|
||||||
|
Uri workloadRequestUri = this.GetTrustBundleRequestUri(workloadUri);
|
||||||
|
|
||||||
|
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, workloadRequestUri))
|
||||||
|
{
|
||||||
|
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
using (HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None))
|
||||||
|
{
|
||||||
|
if (httpResponse.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
string responseData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
TrustBundleResponse trustBundleResponse = JsonConvert.DeserializeObject<TrustBundleResponse>(responseData);
|
||||||
|
IEnumerable<string> rawCerts = this.ParseResponse(trustBundleResponse.Certificate);
|
||||||
|
if (rawCerts.FirstOrDefault() == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to retrieve trustbundle from security daemon.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ConvertToX509(rawCerts);
|
||||||
|
}
|
||||||
|
|
||||||
|
string errorData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
throw new Exception($"Failed to retrieve trustbundle from security daemon. Reason: {errorData}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ValidateClientCertificateAsync(X509Certificate2 clientCertificate)
|
||||||
|
{
|
||||||
|
// Please add validation more validations as appropriate
|
||||||
|
|
||||||
|
if (this.IsCACertificate(clientCertificate))
|
||||||
|
{
|
||||||
|
throw new Exception("Cannot use CA certificate for client authentication!");
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<X509Certificate2> trustedCertificates = await this.GetTrustBundleAsync();
|
||||||
|
|
||||||
|
using (X509Chain chain = new X509Chain())
|
||||||
|
{
|
||||||
|
foreach (X509Certificate2 trustedClientCert in trustedCertificates)
|
||||||
|
{
|
||||||
|
chain.ChainPolicy.ExtraStore.Add(trustedClientCert);
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
// IoTEdge generates a self-signed certificate by default, that is not rooted in a root certificate that is trusted by the trust provider hence this flag is needed
|
||||||
|
// so that build returns true if root terminates in a self-signed certificate
|
||||||
|
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chain.Build(clientCertificate))
|
||||||
|
{
|
||||||
|
var errorMessageBuilder = new StringBuilder();
|
||||||
|
foreach (X509ChainStatus cs in chain.ChainStatus)
|
||||||
|
{
|
||||||
|
errorMessageBuilder.AppendFormat(CultureInfo.InvariantCulture, $"ChainStatus: {cs.Status}, ChainStatusInfo: {cs.StatusInformation}");
|
||||||
|
errorMessageBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"ClientCertificate is not valid! Reason: Failed chain validation. Details: {errorMessageBuilder}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate2[] ConvertToX509(IEnumerable<string> rawCerts)
|
||||||
|
{
|
||||||
|
return rawCerts
|
||||||
|
.Select(c => Encoding.UTF8.GetBytes(c))
|
||||||
|
.Select(c => new X509Certificate2(c))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsCACertificate(X509Certificate2 certificate)
|
||||||
|
{
|
||||||
|
// https://tools.ietf.org/html/rfc3280#section-4.2.1.3
|
||||||
|
// The keyCertSign bit is asserted when the subject public key is
|
||||||
|
// used for verifying a signature on public key certificates. If the
|
||||||
|
// keyCertSign bit is asserted, then the cA bit in the basic
|
||||||
|
// constraints extension (section 4.2.1.10) MUST also be asserted.
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc3280#section-4.2.1.10
|
||||||
|
// The cA boolean indicates whether the certified public key belongs to
|
||||||
|
// a CA. If the cA boolean is not asserted, then the keyCertSign bit in
|
||||||
|
// the key usage extension MUST NOT be asserted.
|
||||||
|
X509ExtensionCollection extensionCollection = certificate.Extensions;
|
||||||
|
foreach (X509Extension extension in extensionCollection)
|
||||||
|
{
|
||||||
|
if (extension is X509BasicConstraintsExtension basicConstraintExtension)
|
||||||
|
{
|
||||||
|
if (basicConstraintExtension.CertificateAuthority)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Publisher
|
||||||
|
{
|
||||||
|
public class CustomHttpClientFactory : IHttpClientFactory
|
||||||
|
{
|
||||||
|
private readonly X509Certificate2 trustedRootCA;
|
||||||
|
private readonly X509Certificate2 clientCert;
|
||||||
|
|
||||||
|
public CustomHttpClientFactory(X509Certificate2 trustedRootCA, X509Certificate2 clientCert)
|
||||||
|
{
|
||||||
|
this.trustedRootCA = trustedRootCA;
|
||||||
|
this.clientCert = clientCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient CreateClient(string name)
|
||||||
|
{
|
||||||
|
var httpClientHandler = new HttpClientHandler()
|
||||||
|
{
|
||||||
|
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => this.ValidateCertificate(trustedRootCA, cert, chain, errors),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.clientCert != null)
|
||||||
|
{
|
||||||
|
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
|
||||||
|
httpClientHandler.ClientCertificates.Add(this.clientCert);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HttpClient(httpClientHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateCertificate(X509Certificate2 trustedCertificateRoot, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslErrors)
|
||||||
|
{
|
||||||
|
SslPolicyErrors terminatingErrors = sslErrors & ~SslPolicyErrors.RemoteCertificateChainErrors;
|
||||||
|
if (terminatingErrors != SslPolicyErrors.None)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Server certificate validation failed due to {terminatingErrors}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.ChainPolicy.ExtraStore.Add(trustedCertificateRoot);
|
||||||
|
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||||
|
if (!chain.Build(new X509Certificate2(certificate)))
|
||||||
|
{
|
||||||
|
var errorMessageBuilder = new StringBuilder();
|
||||||
|
foreach (X509ChainStatus cs in chain.ChainStatus)
|
||||||
|
{
|
||||||
|
errorMessageBuilder.AppendFormat(CultureInfo.InvariantCulture, $"ChainStatus: {cs.Status}, ChainStatusInfo: {cs.StatusInformation}");
|
||||||
|
errorMessageBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Server certificate failed chain validation due to {errorMessageBuilder}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Reviewied")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "Reviewed")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Reviewed")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Reviewed")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Reviewed")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Do not directly await a Task", Justification = "Reviewed")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "Reviewed")]
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Publisher
|
||||||
|
{
|
||||||
|
public class GridConfiguration
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
public int PublishIntervalInSeconds { get; set; }
|
||||||
|
|
||||||
|
public int InitialDelayInSeconds { get; set; }
|
||||||
|
|
||||||
|
public ClientAuthOptions ClientAuth { get; set; }
|
||||||
|
|
||||||
|
public TopicOptions Topic { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClientAuthOptions
|
||||||
|
{
|
||||||
|
public string Source { get; set; }
|
||||||
|
|
||||||
|
public string Token1 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TopicOptions
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string Schema { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"configuration": {
|
||||||
|
"enableEventGrid": true,
|
||||||
|
"eventGrid": {
|
||||||
|
"url": "https://eventgridmodule:4438",
|
||||||
|
"publishIntervalInSeconds": 30,
|
||||||
|
"initialDelayInSeconds": 60,
|
||||||
|
"topic": {
|
||||||
|
"name": "egtopic",
|
||||||
|
"schema": "eventgridschema"
|
||||||
|
},
|
||||||
|
"clientAuth": {
|
||||||
|
"source": "IoTEdge",
|
||||||
|
"token1": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,308 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Azure.EventGridEdge.SDK;
|
||||||
|
using Microsoft.Azure.EventGridEdge.Samples.Auth;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Publisher
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
private static readonly MediaTypeHeaderValue ApplicationJsonMTHV = new MediaTypeHeaderValue("application/json");
|
||||||
|
|
||||||
|
public static async Task Main()
|
||||||
|
{
|
||||||
|
var resetEvent = new ManualResetEventSlim();
|
||||||
|
|
||||||
|
// signals to long running components when to power down (either due to a Ctrl+C, or Ctrl-Break, or SIGTERM, or SIGKILL)
|
||||||
|
CancellationTokenSource lifetimeCts = SetupGracefulShutdown(resetEvent);
|
||||||
|
|
||||||
|
GridConfiguration gridConfig = GetGridConfiguration();
|
||||||
|
EventGridEdgeClient egClient = GetEventGridClientAsync(gridConfig).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// certificate issued by IoT Edge takes a while to be current so will wait for a bit
|
||||||
|
int delay = gridConfig.InitialDelayInSeconds * 1000;
|
||||||
|
Thread.Sleep(delay);
|
||||||
|
|
||||||
|
// wait for eventgrid module to come up
|
||||||
|
await WaitUntilEventGridModuleIsUpAndTopicExistsAsync(gridConfig, egClient, lifetimeCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// setup eventgrid topic and publish
|
||||||
|
await PublishEventsAsync(gridConfig, egClient, lifetimeCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
resetEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GridConfiguration GetGridConfiguration()
|
||||||
|
{
|
||||||
|
IConfiguration configuration = new ConfigurationBuilder()
|
||||||
|
.AddJsonFile("HostSettings.json", optional: false, reloadOnChange: true)
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
IConfigurationSection hostConfigurationSection = configuration.GetSection("configuration");
|
||||||
|
if (!hostConfigurationSection.GetValue("enableEventGrid", true))
|
||||||
|
{
|
||||||
|
throw new Exception("Need to set configuration:enableEventGrid=true to come up!");
|
||||||
|
}
|
||||||
|
|
||||||
|
IConfigurationSection eventGridSection = hostConfigurationSection.GetSection("eventGrid");
|
||||||
|
GridConfiguration gridConfig = eventGridSection.Get<GridConfiguration>();
|
||||||
|
ValidateConfiguration(gridConfig);
|
||||||
|
|
||||||
|
return gridConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateConfiguration(GridConfiguration gridConfig)
|
||||||
|
{
|
||||||
|
if (gridConfig == null)
|
||||||
|
{
|
||||||
|
throw new Exception("GridConfiguration is null. Please configure the section configuration:eventgrid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(gridConfig.Url))
|
||||||
|
{
|
||||||
|
throw new Exception("Please configure the section configuration:eventgrid:url");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gridConfig.Topic == null ||
|
||||||
|
string.IsNullOrEmpty(gridConfig.Topic.Name) ||
|
||||||
|
string.IsNullOrEmpty(gridConfig.Topic.Schema))
|
||||||
|
{
|
||||||
|
throw new Exception("Please configure configuration:eventgrid:topic:name, configuration:eventgrid:topic:schema");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Enum.TryParse<InputSchema>(gridConfig.Topic.Schema, ignoreCase: true, out InputSchema inputSchema))
|
||||||
|
{
|
||||||
|
throw new Exception("Unknown value specified in configuration:eventgrid:topic:schema");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gridConfig.ClientAuth == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Please configure configuration:eventgrid:clientAuth");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(gridConfig.ClientAuth.Source))
|
||||||
|
{
|
||||||
|
throw new Exception("Please configure configuration:eventgrid:clientAuth:source");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gridConfig.ClientAuth.Source.Equals("IoTEdge", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// nothing to configure more
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (gridConfig.ClientAuth.Source.Equals("BearerToken", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.IsNullOrEmpty(gridConfig.ClientAuth.Token1))
|
||||||
|
{
|
||||||
|
throw new Exception("Please configure configuration:eventgrid:clientAuth:token1");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Unknown value configured for configuration:eventgrid:clientAuth:token1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task PublishEventsAsync(
|
||||||
|
GridConfiguration gridConfig,
|
||||||
|
EventGridEdgeClient egClient,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Will publish events every {gridConfig.PublishIntervalInSeconds} seconds");
|
||||||
|
|
||||||
|
string topicName = gridConfig.Topic.Name;
|
||||||
|
InputSchema inputSchema = GetTopicInputSchema(gridConfig);
|
||||||
|
int publishIntervalInSeconds = gridConfig.PublishIntervalInSeconds;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (CancellationTokenSource cts = new CancellationTokenSource(30 * 1000))
|
||||||
|
{
|
||||||
|
EventGridEvent evtPayload = (EventGridEvent)CreateEvent(topicName, inputSchema);
|
||||||
|
await egClient.Events.PublishJsonAsync(topicName: topicName, evtPayload.Id, payload: evtPayload, contentType: ApplicationJsonMTHV, cts.Token).ConfigureAwait(false);
|
||||||
|
Console.WriteLine($"Published event {JsonConvert.SerializeObject(evtPayload)} to eventgrid module ...");
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to publish event to topic {topicName}. Reason: {e.ToString()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(publishIntervalInSeconds * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<EventGridEdgeClient> GetEventGridClientAsync(GridConfiguration gridConfig)
|
||||||
|
{
|
||||||
|
string[] urlTokens = gridConfig.Url.Split(":");
|
||||||
|
if (urlTokens.Length != 3)
|
||||||
|
{
|
||||||
|
throw new Exception($"URL should be of the form '<protocol>://<moduleName>:<portNo>' ");
|
||||||
|
}
|
||||||
|
|
||||||
|
string baseUrl = urlTokens[0] + ":" + urlTokens[1];
|
||||||
|
int port = int.Parse(urlTokens[2], CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
if (gridConfig.ClientAuth.Source.Equals("IoTEdge", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
IoTSecurity iotSecurity = new IoTSecurity();
|
||||||
|
(X509Certificate2 identityCertificate, IEnumerable<X509Certificate2> chain) = await iotSecurity.GetClientCertificateAsync();
|
||||||
|
return new EventGridEdgeClient(baseUrl, port, new CustomHttpClientFactory(chain.First(), identityCertificate));
|
||||||
|
}
|
||||||
|
else if (gridConfig.ClientAuth.Source.Equals("BearerToken", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
EventGridEdgeClient egClient = new EventGridEdgeClient(baseUrl, port);
|
||||||
|
|
||||||
|
HttpRequestHeaders defaultMgmtRequestHeaders = egClient.HttpClient.DefaultRequestHeaders;
|
||||||
|
defaultMgmtRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", $"{gridConfig.ClientAuth.Token1}");
|
||||||
|
|
||||||
|
HttpRequestHeaders defaultRuntimeRequestHeaders = egClient.HttpClient.DefaultRequestHeaders;
|
||||||
|
defaultRuntimeRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", $"{gridConfig.ClientAuth.Token1}");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Cannot create eventgrid client!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task WaitUntilEventGridModuleIsUpAndTopicExistsAsync(
|
||||||
|
GridConfiguration gridConfig,
|
||||||
|
EventGridEdgeClient egClient,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
InputSchema inputSchema = GetTopicInputSchema(gridConfig);
|
||||||
|
Topic topic = new Topic
|
||||||
|
{
|
||||||
|
Name = gridConfig.Topic.Name,
|
||||||
|
Properties = new TopicProperties()
|
||||||
|
{
|
||||||
|
InputSchema = inputSchema,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (CancellationTokenSource cts = new CancellationTokenSource(30 * 1000))
|
||||||
|
{
|
||||||
|
var createdTopic = await egClient.Topics.PutTopicAsync(topicName: topic.Name, topic: topic, cts.Token).ConfigureAwait(false);
|
||||||
|
Console.WriteLine($"Successfully created topic with name {topic.Name} so event grid must be up...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (EventGridApiException e)
|
||||||
|
{
|
||||||
|
LogAndBackoff(topic.Name, e);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException e)
|
||||||
|
{
|
||||||
|
LogAndBackoff(topic.Name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreateEvent(string topicName, InputSchema inputSchema)
|
||||||
|
{
|
||||||
|
Random random = new Random();
|
||||||
|
switch (inputSchema)
|
||||||
|
{
|
||||||
|
case InputSchema.EventGridSchema:
|
||||||
|
string subject = $"sensor:{random.Next(1, 100)}";
|
||||||
|
double temperature = random.NextDouble();
|
||||||
|
double pressure = random.NextDouble();
|
||||||
|
double humidity = random.Next(1, 25);
|
||||||
|
return new EventGridEvent()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
Topic = topicName,
|
||||||
|
Subject = subject,
|
||||||
|
EventType = "sensor.temperature",
|
||||||
|
DataVersion = "1.0",
|
||||||
|
EventTime = DateTime.UtcNow,
|
||||||
|
Data = new
|
||||||
|
{
|
||||||
|
Machine = new { Temperature = temperature, Pressure = pressure },
|
||||||
|
Ambient = new { Temperature = temperature, Humidity= humidity },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogAndBackoff(string topicName, Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to create topic with name {topicName}. Reason: {e.ToString()}");
|
||||||
|
Console.WriteLine("Retrying in 30 seconds...");
|
||||||
|
Thread.Sleep(30 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource SetupGracefulShutdown(ManualResetEventSlim resetEvent)
|
||||||
|
{
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.ProcessExit += (sender, args) => Shutdown();
|
||||||
|
|
||||||
|
Console.CancelKeyPress += (sender, args) =>
|
||||||
|
{
|
||||||
|
// Cancel this event so that the process doesn't get killed immediately, and we wait for graceful shutdown.
|
||||||
|
args.Cancel = true;
|
||||||
|
|
||||||
|
Shutdown();
|
||||||
|
};
|
||||||
|
|
||||||
|
return cts;
|
||||||
|
|
||||||
|
void Shutdown()
|
||||||
|
{
|
||||||
|
if (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel(throwOnFirstException: false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Cancelling gracefulShutdownCts failed, Swallowing the exception. Ex:\n{ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEvent.Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InputSchema GetTopicInputSchema(GridConfiguration gridConfig)
|
||||||
|
{
|
||||||
|
if (gridConfig.Topic == null || string.IsNullOrEmpty(gridConfig.Topic.Schema))
|
||||||
|
{
|
||||||
|
throw new Exception("Need to configure eventgrid's topic:schema");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (InputSchema)Enum.Parse(typeof(InputSchema), gridConfig.Topic.Schema, ignoreCase: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>Microsoft.Azure.EventGridEdge.Samples.Publisher</RootNamespace>
|
||||||
|
<AssemblyName>aegp</AssemblyName>
|
||||||
|
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
<RuntimeFrameworkVersion>2.1.4</RuntimeFrameworkVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" AllowExplicitVersion="true" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" AllowExplicitVersion="true" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.1.1" AllowExplicitVersion="true" />
|
||||||
|
<PackageReference Include="Portable.BouncyCastle" Version="1.8.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\auth\Auth.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\SDK\SDK.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="HostSettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<!-- https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0001.md -->
|
||||||
|
<NoWarn>$(NoWarn),1573,1591,1712</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,10 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore /src/IoTModules/c#/publisher/*.csproj
|
||||||
|
RUN dotnet publish /src/IoTModules/c#/publisher/*.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/runtime:2.1-alpine3.7
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /src/IoTModules/c#/publisher/out/ .
|
||||||
|
CMD ["dotnet", "aegp.dll"]
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore /src/IoTModules/c#/publisher/*.csproj
|
||||||
|
RUN dotnet publish /src/IoTModules/c#/publisher/*.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/runtime:2.1-bionic-arm32v7
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /src/IoTModules/c#/publisher/out/ .
|
||||||
|
|
||||||
|
CMD ["dotnet", "aegp.dll"]
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore \src\IoTModules\c#\publisher\Publisher.csproj
|
||||||
|
RUN dotnet publish \src\IoTModules\c#\publisher\Publisher.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/runtime:2.1.10-nanoserver-1809
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /src/IoTModules/c#/publisher/out/ .
|
||||||
|
|
||||||
|
CMD ["dotnet", "aegp.dll"]
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||||
|
{
|
||||||
|
public class CustomHttpClientFactory : IHttpClientFactory
|
||||||
|
{
|
||||||
|
private readonly X509Certificate2 rootCA;
|
||||||
|
private readonly X509Certificate2 clientCert;
|
||||||
|
|
||||||
|
public CustomHttpClientFactory(X509Certificate2 rootCA, X509Certificate2 clientCert)
|
||||||
|
{
|
||||||
|
this.rootCA = rootCA;
|
||||||
|
this.clientCert = clientCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient CreateClient(string name)
|
||||||
|
{
|
||||||
|
var httpClientHandler = new HttpClientHandler()
|
||||||
|
{
|
||||||
|
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => ValidateCertificate(this.rootCA, cert, chain, errors),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.clientCert != null)
|
||||||
|
{
|
||||||
|
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
|
||||||
|
httpClientHandler.ClientCertificates.Add(this.clientCert);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HttpClient(httpClientHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateCertificate(X509Certificate2 trustedCertificateRoot, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslErrors)
|
||||||
|
{
|
||||||
|
SslPolicyErrors terminatingErrors = sslErrors & ~SslPolicyErrors.RemoteCertificateChainErrors;
|
||||||
|
if (terminatingErrors != SslPolicyErrors.None)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Server certificate validation failed due to {terminatingErrors}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.ChainPolicy.ExtraStore.Add(trustedCertificateRoot);
|
||||||
|
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||||
|
if (!chain.Build(new X509Certificate2(certificate)))
|
||||||
|
{
|
||||||
|
var errorMessageBuilder = new StringBuilder();
|
||||||
|
foreach (X509ChainStatus cs in chain.ChainStatus)
|
||||||
|
{
|
||||||
|
errorMessageBuilder.AppendFormat(CultureInfo.InvariantCulture, $"ChainStatus: {cs.Status}, ChainStatusInfo: {cs.StatusInformation}");
|
||||||
|
errorMessageBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Server certificate failed chain validation due to {errorMessageBuilder}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||||
|
{
|
||||||
|
public enum EventSchema
|
||||||
|
{
|
||||||
|
EventGridSchema = 0,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Azure.EventGridEdge.SDK;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||||
|
{
|
||||||
|
public class EventsHandler
|
||||||
|
{
|
||||||
|
private readonly JsonSerializer jsonSerializer = new JsonSerializer();
|
||||||
|
|
||||||
|
public void HandleEvents(Stream requestStream)
|
||||||
|
{
|
||||||
|
using (var sr = new StreamReader(requestStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
|
||||||
|
using (var jtr = new JsonTextReader(sr))
|
||||||
|
{
|
||||||
|
List<EventGridEvent> outputEvents = this.jsonSerializer.Deserialize<List<EventGridEvent>>(jtr);
|
||||||
|
foreach (EventGridEvent outputEvent in outputEvents)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Received Event: {JsonConvert.SerializeObject(outputEvent)}");
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
// attributes that are applied to this project.
|
||||||
|
// Project-level suppressions either have no target or are given
|
||||||
|
// a specific target and scoped to a namespace, type, member, etc.
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Reviewied")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "Reviewed")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Reviewed")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "Reviewed")]
|
||||||
|
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Reviewed")]
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||||
|
{
|
||||||
|
public class GridConfiguration
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
public int InitialDelayInSeconds { get; set; }
|
||||||
|
|
||||||
|
public TopicOptions Topic { get; set; }
|
||||||
|
|
||||||
|
public SubscriptionOptions Subscription { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TopicOptions
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscriptionOptions
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string EventSchema { get; set; }
|
||||||
|
|
||||||
|
public string Url { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"configuration": {
|
||||||
|
"enableEventGrid": true,
|
||||||
|
"eventGrid": {
|
||||||
|
"url": "https://eventgridmodule:4438",
|
||||||
|
"initialDelayInSeconds": 60,
|
||||||
|
"topic": {
|
||||||
|
"name": "egtopic"
|
||||||
|
},
|
||||||
|
"subscription": {
|
||||||
|
"name": "egsubscribermodule",
|
||||||
|
"eventSchema": "eventgridschema",
|
||||||
|
"url": "https://egsubscribermodule:4430"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"requestTimeoutInSeconds": 30,
|
||||||
|
"captureStartupErrors": "true",
|
||||||
|
"detailedErrors": "true",
|
||||||
|
"kestrel": {
|
||||||
|
"addServerHeader": false,
|
||||||
|
"keepAliveTimeoutInSeconds": 300,
|
||||||
|
"endpoints": {
|
||||||
|
"http": {
|
||||||
|
"url": "http://*:8080"
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"url": "https://*:4430"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Warning",
|
||||||
|
"System.Net.Http.HttpClient": "Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Azure.EventGridEdge.SDK;
|
||||||
|
using Microsoft.Azure.EventGridEdge.Samples.Auth;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static async Task Main()
|
||||||
|
{
|
||||||
|
var resetEvent = new ManualResetEventSlim();
|
||||||
|
|
||||||
|
// signals to long running components when to power down (either due to a Ctrl+C, or Ctrl-Break, or SIGTERM, or SIGKILL)
|
||||||
|
CancellationTokenSource lifetimeCts = SetupGracefulShutdown(resetEvent);
|
||||||
|
|
||||||
|
GridConfiguration gridConfig = GetGridConfiguration();
|
||||||
|
EventGridEdgeClient egClient = GetEventGridClientAsync(gridConfig).GetAwaiter().GetResult();
|
||||||
|
SubscriberHost host = SetupSubscriberHostAsync(lifetimeCts).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// certificate issued by IoT Edge takes a while to be current so will wait for a bit
|
||||||
|
Thread.Sleep(120 * 1000);
|
||||||
|
|
||||||
|
// wait for topic to exist
|
||||||
|
await WaitUntilEventGridModuleIsUpAndTopicExistsAsync(egClient, gridConfig.Topic.Name).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// register subscription
|
||||||
|
await RegisterSubscriptionAsync(egClient, gridConfig).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// wait until shutdown
|
||||||
|
await host.WaitForShutdownAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// signal to gracefully shutdown
|
||||||
|
resetEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GridConfiguration GetGridConfiguration()
|
||||||
|
{
|
||||||
|
IConfiguration configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddJsonFile("HostSettings.json", optional: false, reloadOnChange: true)
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
IConfigurationSection hostConfigurationSection = configuration.GetSection("configuration");
|
||||||
|
if (!hostConfigurationSection.GetValue("enableEventGrid", true))
|
||||||
|
{
|
||||||
|
throw new Exception("Need to set configuration:enableEventGrid=true to come up!");
|
||||||
|
}
|
||||||
|
|
||||||
|
IConfigurationSection eventGridSection = hostConfigurationSection.GetSection("eventGrid");
|
||||||
|
GridConfiguration gridConfig = eventGridSection.Get<GridConfiguration>();
|
||||||
|
ValidateConfiguration(gridConfig);
|
||||||
|
return gridConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<SubscriberHost> SetupSubscriberHostAsync(CancellationTokenSource lifetimeCts)
|
||||||
|
{
|
||||||
|
IoTSecurity iotSecurity = new IoTSecurity();
|
||||||
|
|
||||||
|
// get server certificate to configure with
|
||||||
|
(X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain) =
|
||||||
|
await iotSecurity.GetServerCertificateAsync().ConfigureAwait(false);
|
||||||
|
iotSecurity.ImportCertificate(new List<X509Certificate2>() { serverCertificate });
|
||||||
|
iotSecurity.ImportCertificate(certificateChain);
|
||||||
|
|
||||||
|
Console.WriteLine($"Server Certificate issue is valid from {serverCertificate.NotBefore}, {serverCertificate.NotAfter}");
|
||||||
|
|
||||||
|
// start subscriber webhost
|
||||||
|
SubscriberHost host = new SubscriberHost(serverCertificate, lifetimeCts);
|
||||||
|
await host.StartAsync().ConfigureAwait(false);
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task RegisterSubscriptionAsync(EventGridEdgeClient egClient, GridConfiguration gridConfig)
|
||||||
|
{
|
||||||
|
string topicName = gridConfig.Topic.Name;
|
||||||
|
|
||||||
|
// create subscription
|
||||||
|
EventSubscription eventSubscription = CreateEventSubscription(gridConfig);
|
||||||
|
using (CancellationTokenSource cts = new CancellationTokenSource(30 * 1000))
|
||||||
|
{
|
||||||
|
await egClient.Subscriptions.PutSubscriptionAsync(topicName: topicName, subscriptionName: eventSubscription.Name, eventSubscription: eventSubscription, cts.Token).ConfigureAwait(false);
|
||||||
|
Console.WriteLine($"Successfully created subscription {JsonConvert.SerializeObject(eventSubscription)} for topic {topicName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task WaitUntilEventGridModuleIsUpAndTopicExistsAsync(EventGridEdgeClient egClient, string topicName)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (CancellationTokenSource cts = new CancellationTokenSource(30 * 1000))
|
||||||
|
{
|
||||||
|
var topic = await egClient.Topics.GetTopicAsync(topicName: topicName, cts.Token).ConfigureAwait(false);
|
||||||
|
Console.WriteLine($"Successfully retrieved topic with name {topicName} so event grid must be up...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (EventGridApiException e)
|
||||||
|
{
|
||||||
|
LogAndBackoff(topicName, e);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException e)
|
||||||
|
{
|
||||||
|
LogAndBackoff(topicName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<EventGridEdgeClient> GetEventGridClientAsync(GridConfiguration gridConfig)
|
||||||
|
{
|
||||||
|
IoTSecurity iotSecurity = new IoTSecurity();
|
||||||
|
|
||||||
|
// get the client certificate to use when communicating with eventgrid
|
||||||
|
(X509Certificate2 clientCertificate, IEnumerable<X509Certificate2> chain) = await iotSecurity.GetClientCertificateAsync().ConfigureAwait(false);
|
||||||
|
Console.WriteLine($"Client Certificate issue is valid from {clientCertificate.NotBefore}, {clientCertificate.NotAfter}");
|
||||||
|
string[] urlTokens = gridConfig.Url.Split(":");
|
||||||
|
if (urlTokens.Length != 3)
|
||||||
|
{
|
||||||
|
throw new Exception($"URL should be of the form '<protocol>://<moduleName>:<portNo>' ");
|
||||||
|
}
|
||||||
|
|
||||||
|
string baseUrl = urlTokens[0] + ":" + urlTokens[1];
|
||||||
|
int port = int.Parse(urlTokens[2], CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
return new EventGridEdgeClient(baseUrl, port, new CustomHttpClientFactory(chain.First(), clientCertificate));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogAndBackoff(string topicName, Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to retrieve topic with name {topicName}. Reason: {e.ToString()}");
|
||||||
|
Console.WriteLine("Retrying in 30 seconds...");
|
||||||
|
Thread.Sleep(30 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource SetupGracefulShutdown(ManualResetEventSlim resetEvent)
|
||||||
|
{
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.ProcessExit += (sender, args) => Shutdown();
|
||||||
|
|
||||||
|
Console.CancelKeyPress += (sender, args) =>
|
||||||
|
{
|
||||||
|
// Cancel this event so that the process doesn't get killed immediately, and we wait for graceful shutdown.
|
||||||
|
args.Cancel = true;
|
||||||
|
|
||||||
|
Shutdown();
|
||||||
|
};
|
||||||
|
|
||||||
|
return cts;
|
||||||
|
|
||||||
|
void Shutdown()
|
||||||
|
{
|
||||||
|
if (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel(throwOnFirstException: false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Cancelling gracefulShutdownCts failed, Swallowing the exception. Ex:\n{ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEvent.Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EventSubscription CreateEventSubscription(GridConfiguration gridConfig)
|
||||||
|
{
|
||||||
|
string subscriptionName = gridConfig.Subscription.Name;
|
||||||
|
string subscriptionEventSchema = gridConfig.Subscription.EventSchema;
|
||||||
|
string subscriptionUrl = gridConfig.Subscription.Url;
|
||||||
|
|
||||||
|
return new EventSubscription()
|
||||||
|
{
|
||||||
|
Name = subscriptionName,
|
||||||
|
Properties = new EventSubscriptionProperties()
|
||||||
|
{
|
||||||
|
EventDeliverySchema = (EventDeliverySchema)Enum.Parse(typeof(EventDeliverySchema), subscriptionEventSchema, true),
|
||||||
|
Destination = new WebHookEventSubscriptionDestination()
|
||||||
|
{
|
||||||
|
EndpointType = "WebHook",
|
||||||
|
Properties = new WebHookEventSubscriptionDestinationProperties()
|
||||||
|
{
|
||||||
|
EndpointUrl = subscriptionUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Topic = gridConfig.Topic.Name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateConfiguration(GridConfiguration gridConfig)
|
||||||
|
{
|
||||||
|
if (gridConfig == null)
|
||||||
|
{
|
||||||
|
throw new Exception("GridConfiguration is null. Please configure the section configuration:eventGrid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(gridConfig.Url))
|
||||||
|
{
|
||||||
|
throw new Exception("Please configure the section configuration:eventGrid:url");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gridConfig.Topic == null ||
|
||||||
|
string.IsNullOrEmpty(gridConfig.Topic.Name))
|
||||||
|
{
|
||||||
|
throw new Exception("Please configure configuration:eventGrid:topic:name");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gridConfig.Subscription == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Please configure configuration:eventGrid:subscription");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(gridConfig.Subscription.Name) ||
|
||||||
|
string.IsNullOrEmpty(gridConfig.Subscription.EventSchema) ||
|
||||||
|
string.IsNullOrEmpty(gridConfig.Subscription.Url))
|
||||||
|
{
|
||||||
|
throw new Exception("Please configure configuration:eventGrid:subscription:name, configuration:eventGrid:subscription:url, configuration:eventGrid:subscription:eventSchema");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>Microsoft.Azure.EventGridEdge.Samples.Subscriber</RootNamespace>
|
||||||
|
<AssemblyName>aegs</AssemblyName>
|
||||||
|
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
<RuntimeFrameworkVersion>2.1.4</RuntimeFrameworkVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" AllowExplicitVersion="true" />
|
||||||
|
<PackageReference Include="Portable.BouncyCastle" Version="1.8.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\auth\Auth.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\SDK\SDK.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="HostSettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<!-- https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0001.md -->
|
||||||
|
<NoWarn>$(NoWarn),1573,1591,1712</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,86 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||||
|
{
|
||||||
|
public class SubscriberHost
|
||||||
|
{
|
||||||
|
private readonly CancellationTokenSource lifetimeCts;
|
||||||
|
private readonly IWebHost subscriberHost;
|
||||||
|
|
||||||
|
public SubscriberHost(X509Certificate2 serverCertificate, CancellationTokenSource lifetimeCts)
|
||||||
|
{
|
||||||
|
this.lifetimeCts = lifetimeCts;
|
||||||
|
|
||||||
|
IConfiguration configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddJsonFile("HostSettings.json", optional: false, reloadOnChange: true)
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
this.subscriberHost = HostBuilder
|
||||||
|
.GetHostBuilder(serverCertificate, configuration.GetSection("api"))
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync()
|
||||||
|
{
|
||||||
|
await this.subscriberHost.StartAsync().ConfigureAwait(false);
|
||||||
|
PrintAsciiArt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WaitForShutdownAsync()
|
||||||
|
{
|
||||||
|
var shutdownTasks = new List<Task>
|
||||||
|
{
|
||||||
|
Task.Run(() => this.subscriberHost.WaitForShutdownAsync(this.lifetimeCts.Token)),
|
||||||
|
};
|
||||||
|
|
||||||
|
while (shutdownTasks.Count > 0)
|
||||||
|
{
|
||||||
|
Task completedTask = await Task.WhenAny(shutdownTasks).ConfigureAwait(false);
|
||||||
|
if (completedTask.IsFaulted)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"WaitForShutdown faulted with exception:{completedTask.Exception}");
|
||||||
|
|
||||||
|
if (!this.lifetimeCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.lifetimeCts.Cancel(throwOnFirstException: false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Cancelling lifetimeCts failed. Swallowing the exception. Ex:\n{ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdownTasks.Remove(completedTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintAsciiArt()
|
||||||
|
{
|
||||||
|
string asciiArt = @"
|
||||||
|
*************************************************************************************************************************************************
|
||||||
|
| ___ ______ __ ______ _ __ _____ __ _ __ |
|
||||||
|
| / |____ __ __________ / ____/ _____ ____ / /_ / ____/____(_)___/ / / ___/__ __/ /_ _______________(_) /_ ___ _____ |
|
||||||
|
| / /| /_ / / / / / ___/ _ \ / __/ | | / / _ \/ __ \/ __/ / / __/ ___/ / __ / \__ \/ / / / __ \/ ___/ ___/ ___/ / __ \/ _ \/ ___/ |
|
||||||
|
| / ___ |/ /_/ /_/ / / / __/ / /___ | |/ / __/ / / / /_ / /_/ / / / / /_/ / ___/ / /_/ / /_/ (__ ) /__/ / / / /_/ / __/ / |
|
||||||
|
| /_/ |_/___/\__,_/_/ \___/ /_____/ |___/\___/_/ /_/\__/ \____/_/ /_/\__,_/ /____/\__,_/_.___/____/\___/_/ /_/_.___/\___/_/ |
|
||||||
|
| |
|
||||||
|
*************************************************************************************************************************************************";
|
||||||
|
Console.WriteLine(asciiArt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||||
|
{
|
||||||
|
public static class HostBuilder
|
||||||
|
{
|
||||||
|
public static IWebHostBuilder GetHostBuilder(X509Certificate2 serverCertificate, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
IWebHostBuilder hostBuilder = new WebHostBuilder()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseConfiguration(configuration)
|
||||||
|
.UseKestrel((KestrelServerOptions options) =>
|
||||||
|
{
|
||||||
|
IConfigurationSection kestrelConfig = configuration.GetSection("kestrel");
|
||||||
|
options.Configure(kestrelConfig);
|
||||||
|
options.AddServerHeader = kestrelConfig.GetValue("addServerHeader", false);
|
||||||
|
options.Limits.MaxRequestBodySize = kestrelConfig.GetValue("maxRequestBodySize", 1024 * 1034); // allow 10 extra KB over the 1 MB payload
|
||||||
|
options.Limits.KeepAliveTimeout = TimeSpan.FromSeconds(kestrelConfig.GetValue("keepAliveTimeoutInSeconds", 120)); // default of 120 seconds
|
||||||
|
options.ConfigureHttpsDefaults((HttpsConnectionAdapterOptions o) =>
|
||||||
|
{
|
||||||
|
o.ServerCertificate = serverCertificate;
|
||||||
|
o.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
// this is needed because IoTEdge generates a self signed certificate that is not rooted in a root certificate that is trusted by the trust provider.
|
||||||
|
// Kestrel rejects the request automatically because of this. We return true here so that client validation can happen when routing requests.
|
||||||
|
o.ClientCertificateValidation = (X509Certificate2 arg1, X509Chain arg2, SslPolicyErrors arg3) => true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ConfigureLogging((ILoggingBuilder logging) =>
|
||||||
|
{
|
||||||
|
IConfigurationSection loggingConfig = configuration.GetSection("logging");
|
||||||
|
logging
|
||||||
|
.AddConfiguration(loggingConfig)
|
||||||
|
.AddConsole();
|
||||||
|
})
|
||||||
|
.UseStartup<HostStartup>();
|
||||||
|
|
||||||
|
return hostBuilder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Azure.EventGridEdge.Samples.Auth;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
|
||||||
|
{
|
||||||
|
public class HostStartup : IStartup
|
||||||
|
{
|
||||||
|
private readonly EventsHandler eventsHandler = new EventsHandler();
|
||||||
|
private readonly IoTSecurity iotSecurity = new IoTSecurity();
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.Use(this.RouteRequestAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IServiceProvider ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
return services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RouteRequestAsync(HttpContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
string method = context.Request.Method;
|
||||||
|
if (method.Equals(HttpMethods.Post, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
HttpRequest request = context.Request;
|
||||||
|
X509Certificate2 clientCert = await request.HttpContext.Connection.GetClientCertificateAsync().ConfigureAwait(false);
|
||||||
|
if (clientCert == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Client certificate not provided!");
|
||||||
|
}
|
||||||
|
|
||||||
|
await iotSecurity.ValidateClientCertificateAsync(clientCert);
|
||||||
|
|
||||||
|
// TODO: Verify it is eventgrid instance indeed!
|
||||||
|
using (var cts = new CancellationTokenSource(1000 * 30))
|
||||||
|
{
|
||||||
|
this.eventsHandler.HandleEvents(request.Body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await next().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
ARG base_tag=2.1.10-alpine3.7
|
||||||
|
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore /src/IoTModules/c#/subscriber/*.csproj
|
||||||
|
RUN dotnet publish /src/IoTModules/c#/subscriber/*.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/aspnet:${base_tag}
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /src/IoTModules/c#/subscriber/out/ .
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
EXPOSE 4430/tcp
|
||||||
|
|
||||||
|
CMD ["dotnet", "aegs.dll"]
|
|
@ -0,0 +1,14 @@
|
||||||
|
ARG base_tag=2.1.10-bionic-arm32v7
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore /src/IoTModules/c#/subscriber/*.csproj
|
||||||
|
RUN dotnet publish /src/IoTModules/c#/subscriber/*.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/aspnet:${base_tag}
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /src/IoTModules/c#/subscriber/out/ .
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
EXPOSE 4430/tcp
|
||||||
|
CMD ["dotnet", "aegs.dll"]
|
|
@ -0,0 +1,14 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore \src\IoTModules\c#\subscriber\Subscriber.csproj
|
||||||
|
RUN dotnet publish \src\IoTModules\c#\subscriber\Subscriber.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.10-nanoserver-1809
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /src/IoTModules/c#/subscriber/out/ .
|
||||||
|
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
EXPOSE 4430/tcp
|
||||||
|
|
||||||
|
CMD ["dotnet", "aegs.dll"]
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE
|
|
@ -0,0 +1,76 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 15
|
||||||
|
VisualStudioVersion = 15.0.28307.572
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Publisher", "IoTModules\c#\publisher\Publisher.csproj", "{BF992658-AF4A-4C41-A6C5-1F075F5D51E7}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Subscriber", "IoTModules\c#\subscriber\Subscriber.csproj", "{5CA757E4-191B-4025-8BF7-24A25D8F882E}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Auth", "IoTModules\c#\auth\Auth.csproj", "{C5BBA16E-48AE-4C2E-A840-BD75BD35EF13}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IoTModules", "IoTModules", "{C8E6EBAA-DA12-4ACC-897B-E674F24615E7}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "QuickStart", "QuickStart", "{1A29FBDF-CDBA-4BD0-9FE5-D40D95FF988D}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Publisher", "QuickStart\c#\publisher\Publisher.csproj", "{D337DD41-196D-49A9-B903-77F2E4F319D3}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Subscriber", "QuickStart\c#\subscriber\Subscriber.csproj", "{3814D171-75B4-4324-8478-DBCCC9CBB5E3}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDK", "SDK", "{AEE9042C-0BC5-4302-9582-9AFA340EF555}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDK", "SDK\SDK.csproj", "{83EC7C9C-AB4E-480C-BC14-1BF688EAA28A}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecurityDaemonClient", "SecurityDaemonClient\SecurityDaemonClient.csproj", "{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{BF992658-AF4A-4C41-A6C5-1F075F5D51E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BF992658-AF4A-4C41-A6C5-1F075F5D51E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BF992658-AF4A-4C41-A6C5-1F075F5D51E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BF992658-AF4A-4C41-A6C5-1F075F5D51E7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5CA757E4-191B-4025-8BF7-24A25D8F882E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5CA757E4-191B-4025-8BF7-24A25D8F882E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5CA757E4-191B-4025-8BF7-24A25D8F882E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5CA757E4-191B-4025-8BF7-24A25D8F882E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C5BBA16E-48AE-4C2E-A840-BD75BD35EF13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C5BBA16E-48AE-4C2E-A840-BD75BD35EF13}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C5BBA16E-48AE-4C2E-A840-BD75BD35EF13}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C5BBA16E-48AE-4C2E-A840-BD75BD35EF13}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D337DD41-196D-49A9-B903-77F2E4F319D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D337DD41-196D-49A9-B903-77F2E4F319D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D337DD41-196D-49A9-B903-77F2E4F319D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D337DD41-196D-49A9-B903-77F2E4F319D3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3814D171-75B4-4324-8478-DBCCC9CBB5E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3814D171-75B4-4324-8478-DBCCC9CBB5E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3814D171-75B4-4324-8478-DBCCC9CBB5E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3814D171-75B4-4324-8478-DBCCC9CBB5E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{83EC7C9C-AB4E-480C-BC14-1BF688EAA28A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{83EC7C9C-AB4E-480C-BC14-1BF688EAA28A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{83EC7C9C-AB4E-480C-BC14-1BF688EAA28A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{83EC7C9C-AB4E-480C-BC14-1BF688EAA28A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{BF992658-AF4A-4C41-A6C5-1F075F5D51E7} = {C8E6EBAA-DA12-4ACC-897B-E674F24615E7}
|
||||||
|
{5CA757E4-191B-4025-8BF7-24A25D8F882E} = {C8E6EBAA-DA12-4ACC-897B-E674F24615E7}
|
||||||
|
{C5BBA16E-48AE-4C2E-A840-BD75BD35EF13} = {C8E6EBAA-DA12-4ACC-897B-E674F24615E7}
|
||||||
|
{D337DD41-196D-49A9-B903-77F2E4F319D3} = {1A29FBDF-CDBA-4BD0-9FE5-D40D95FF988D}
|
||||||
|
{3814D171-75B4-4324-8478-DBCCC9CBB5E3} = {1A29FBDF-CDBA-4BD0-9FE5-D40D95FF988D}
|
||||||
|
{83EC7C9C-AB4E-480C-BC14-1BF688EAA28A} = {AEE9042C-0BC5-4302-9582-9AFA340EF555}
|
||||||
|
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8} = {AEE9042C-0BC5-4302-9582-9AFA340EF555}
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {475D2ECB-88BD-47E1-B351-D17F2ACF95B8}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
|
@ -0,0 +1,108 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Azure.EventGridEdge.SDK;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.QuickStart.Publisher
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
private static readonly string eventGridBaseAddress = "http://eventgridmodule";
|
||||||
|
private static readonly int eventGridPort = 5888;
|
||||||
|
private static readonly TimeSpan initialDelay = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
private static readonly string topicName = "quickstarttopic";
|
||||||
|
private static readonly InputSchema topicSchema = InputSchema.CustomEventSchema;
|
||||||
|
|
||||||
|
private static readonly string subscriptionName= "quickstartsub";
|
||||||
|
private static readonly EventDeliverySchema deliverySchema = EventDeliverySchema.CustomEventSchema;
|
||||||
|
private static readonly string subscriberUrl = "http://subscriber:80/api/subscriber";
|
||||||
|
|
||||||
|
private static readonly MediaTypeHeaderValue ApplicationJsonMTHV = new MediaTypeHeaderValue("application/json");
|
||||||
|
|
||||||
|
public static async Task Main()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\nWaiting a few minute(s) to create topic '{topicName}' ...\n");
|
||||||
|
Thread.Sleep(initialDelay);
|
||||||
|
|
||||||
|
Console.WriteLine($"EventGrid Module's URL: {eventGridBaseAddress}:{eventGridPort}");
|
||||||
|
EventGridEdgeClient egClient = new EventGridEdgeClient(eventGridBaseAddress, eventGridPort);
|
||||||
|
|
||||||
|
// create topic
|
||||||
|
Topic topic = new Topic()
|
||||||
|
{
|
||||||
|
Name = topicName,
|
||||||
|
Properties = new TopicProperties()
|
||||||
|
{
|
||||||
|
InputSchema = topicSchema
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var createdTopic = await egClient.Topics.PutTopicAsync(topicName: topicName, topic: topic, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
Console.WriteLine($"Created topic with Name: {topic.Name}, Schema: {topic.Properties.InputSchema} ...");
|
||||||
|
|
||||||
|
// the recommendation is to create subscribers from subscription modules or a "management" module. for the purposes of quickstart we are creating it here.
|
||||||
|
EventSubscription eventSubscription = new EventSubscription()
|
||||||
|
{
|
||||||
|
Name = subscriptionName,
|
||||||
|
Properties = new EventSubscriptionProperties
|
||||||
|
{
|
||||||
|
Topic = topicName,
|
||||||
|
EventDeliverySchema = deliverySchema,
|
||||||
|
Destination = new WebHookEventSubscriptionDestination()
|
||||||
|
{
|
||||||
|
EndpointType = "Webhook",
|
||||||
|
Properties = new WebHookEventSubscriptionDestinationProperties()
|
||||||
|
{
|
||||||
|
EndpointUrl = subscriberUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var createdSubscription = await egClient.Subscriptions.PutSubscriptionAsync(topicName: topicName, subscriptionName: subscriptionName, eventSubscription: eventSubscription, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
Console.WriteLine($"Created subscription with Name: {createdSubscription.Name}, Schema: {topic.Properties.InputSchema}, EndpointUrl: {subscriberUrl} for topic: {topic.Name} ...");
|
||||||
|
|
||||||
|
Console.WriteLine($"\nWaiting a few minute(s) before publishing events ...\n");
|
||||||
|
Thread.Sleep(initialDelay);
|
||||||
|
|
||||||
|
// keep publishing events
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
EventGridEvent evt = GetEvent();
|
||||||
|
Console.WriteLine($"\nPublishing event: {JsonConvert.SerializeObject(evt)}");
|
||||||
|
egClient.Events.PublishJsonAsync(topicName: topicName, (new List<EventGridEvent>() { evt }), ApplicationJsonMTHV, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EventGridEvent GetEvent()
|
||||||
|
{
|
||||||
|
Random random = new Random();
|
||||||
|
string subject = $"sensor:{random.Next(1, 100)}";
|
||||||
|
double temperature = random.NextDouble();
|
||||||
|
double pressure = random.NextDouble();
|
||||||
|
double humidity = random.Next(1, 25);
|
||||||
|
return new EventGridEvent()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
Topic = topicName,
|
||||||
|
Subject = subject,
|
||||||
|
EventType = "sensor.temperature",
|
||||||
|
DataVersion = "1.0",
|
||||||
|
EventTime = DateTime.UtcNow,
|
||||||
|
Data = new
|
||||||
|
{
|
||||||
|
Machine = new { Temperature = temperature, Pressure = pressure },
|
||||||
|
Ambient = new { Temperature = temperature, Humidity = humidity },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>Microsoft.Azure.EventGridEdge.QuickStart.Publisher</RootNamespace>
|
||||||
|
<AssemblyName>aegp</AssemblyName>
|
||||||
|
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
<RuntimeFrameworkVersion>2.1.4</RuntimeFrameworkVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\SDK\SDK.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="HostSettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<!-- https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0001.md -->
|
||||||
|
<NoWarn>$(NoWarn),1573,1591,1712</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore /src/QuickStart/c#/publisher/*.csproj
|
||||||
|
RUN dotnet publish /src/QuickStart/c#/publisher/*.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/runtime:2.1-alpine3.7
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /src/QuickStart/c#/publisher/out/ .
|
||||||
|
|
||||||
|
CMD ["dotnet", "aegp.dll"]
|
|
@ -0,0 +1,12 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore /src/QuickStart/c#/publisher/*.csproj
|
||||||
|
RUN dotnet publish /src/QuickStart/c#/publisher/*.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/runtime:2.1-bionic-arm32v7
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /src/QuickStart/c#/publisher/out/ .
|
||||||
|
|
||||||
|
CMD ["dotnet", "aegp.dll"]
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore \src\QuickStart\c#\publisher\publisher.csproj
|
||||||
|
RUN dotnet publish \src\QuickStart\c#\publisher\publisher.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/runtime:2.1.10-nanoserver-1809
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /src/QuickStart/c#/publisher/out/ .
|
||||||
|
|
||||||
|
CMD ["dotnet", "aegp.dll"]
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright(c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Azure.WebJobs;
|
||||||
|
using Microsoft.Azure.WebJobs.Extensions.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.QuickStart.Subscriber
|
||||||
|
{
|
||||||
|
public static class Subscriber
|
||||||
|
{
|
||||||
|
[FunctionName("subscriber")]
|
||||||
|
public static async Task<IActionResult> Run(
|
||||||
|
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]
|
||||||
|
HttpRequest req, ILogger log)
|
||||||
|
{
|
||||||
|
|
||||||
|
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
|
||||||
|
dynamic data = JsonConvert.DeserializeObject(requestBody);
|
||||||
|
log.LogInformation($"Received event data {data}\n");
|
||||||
|
return new OkResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<RootNamespace>Microsoft.Azure.EventGridEdge.QuickStart.Subscriber</RootNamespace>
|
||||||
|
<AzureFunctionsVersion></AzureFunctionsVersion>
|
||||||
|
<AssemblyName>subscriber</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netcoreapp2.1|AnyCPU'">
|
||||||
|
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
|
||||||
|
<TreatSpecificWarningsAsErrors />
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.28" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="host.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="local.settings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,15 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore /src/QuickStart/c#/subscriber/*.csproj
|
||||||
|
RUN dotnet publish /src/QuickStart/c#/subscriber/*.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/azure-functions/dotnet:2.0-iot-edge
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV AzureWebJobsScriptRoot=/app
|
||||||
|
ENV AzureFunctionsJobHost__Logging__Console__IsEnabled=true
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
COPY --from=build-env /src/QuickStart/c#/subscriber/out/ .
|
|
@ -0,0 +1,15 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore /src/QuickStart/c#/subscriber/*.csproj
|
||||||
|
RUN dotnet publish /src/QuickStart/c#/subscriber/*.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/azure-functions/dotnet:2.0-arm32v7
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV AzureWebJobsScriptRoot=/app
|
||||||
|
ENV AzureFunctionsJobHost__Logging__Console__IsEnabled=true
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
COPY --from=build-env /src/QuickStart/c#/subscriber/out/ .
|
|
@ -0,0 +1,15 @@
|
||||||
|
FROM microsoft/dotnet:2.1-sdk AS build-env
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore \src\QuickStart\c#\subscriber\Subscriber.csproj
|
||||||
|
RUN dotnet publish \src\QuickStart\c#\subscriber\Subscriber.csproj -c Release -o out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/azure-functions/dotnet:2.0-nanoserver-1809
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV AzureWebJobsScriptRoot=/app
|
||||||
|
ENV AzureFunctionsJobHost__Logging__Console__IsEnabled=true
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
COPY --from=build-env /src/QuickStart/c#/subscriber/out/ .
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||||
|
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||||
|
the rights to use your contribution. For details, visit https://cla.microsoft.com.
|
||||||
|
|
||||||
|
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
|
||||||
|
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
|
||||||
|
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||||
|
|
||||||
|
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||||
|
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||||
|
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
|
@ -0,0 +1,222 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class AdvancedFilter
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public AdvancedFilterOperatorType OperatorType { get; set; }
|
||||||
|
|
||||||
|
public string Key { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BoolEqualsAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public BoolEqualsAdvancedFilter(string key, bool value)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoolEqualsAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.BoolEquals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NumberLessThanAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public NumberLessThanAdvancedFilter(string key, decimal value)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberLessThanAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.NumberLessThan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NumberGreaterThanAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public NumberGreaterThanAdvancedFilter(string key, decimal value)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberGreaterThanAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.NumberGreaterThan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NumberLessThanOrEqualsAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public NumberLessThanOrEqualsAdvancedFilter(string key, decimal value)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberLessThanOrEqualsAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.NumberLessThanOrEquals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NumberGreaterThanOrEqualsAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public NumberGreaterThanOrEqualsAdvancedFilter(string key, decimal value)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberGreaterThanOrEqualsAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.NumberGreaterThanOrEquals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NumberInAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public NumberInAdvancedFilter(string key, params decimal[] values)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberInAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.NumberIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal[] Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NumberNotInAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public NumberNotInAdvancedFilter(string key, params decimal[] values)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberNotInAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.NumberNotIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal[] Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StringInAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public StringInAdvancedFilter(string key, params string[] values)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringInAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.StringIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StringNotInAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public StringNotInAdvancedFilter(string key, params string[] values)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringNotInAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.StringNotIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StringBeginsWithAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public StringBeginsWithAdvancedFilter(string key, params string[] values)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBeginsWithAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.StringBeginsWith;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StringEndsWithAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public StringEndsWithAdvancedFilter(string key, params string[] values)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringEndsWithAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.StringEndsWith;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StringContainsAdvancedFilter : AdvancedFilter
|
||||||
|
{
|
||||||
|
public StringContainsAdvancedFilter(string key, params string[] values)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringContainsAdvancedFilter()
|
||||||
|
{
|
||||||
|
this.OperatorType = AdvancedFilterOperatorType.StringContains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Values { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class AdvancedFilterJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType) => typeof(AdvancedFilter[]).IsAssignableFrom(objectType);
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonToken.StartArray)
|
||||||
|
{
|
||||||
|
return Array.Empty<AdvancedFilter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var array = JArray.Load(reader);
|
||||||
|
if (array.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<AdvancedFilter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<AdvancedFilter>();
|
||||||
|
foreach (JToken token in array)
|
||||||
|
{
|
||||||
|
if (token.Type != JTokenType.Object)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj = (JObject)token;
|
||||||
|
var operatorType = (AdvancedFilterOperatorType)Enum.Parse(typeof(AdvancedFilterOperatorType), obj.GetValue("operatorType", StringComparison.OrdinalIgnoreCase).ToString(), true);
|
||||||
|
Type deserializeClass = AdvancedFilterTypeConverter.OperatorTypeToAdvancedFilter(operatorType);
|
||||||
|
var filter = (AdvancedFilter)obj.ToObject(deserializeClass, serializer);
|
||||||
|
result.Add(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public enum AdvancedFilterOperatorType
|
||||||
|
{
|
||||||
|
NumberIn,
|
||||||
|
NumberNotIn,
|
||||||
|
NumberLessThan,
|
||||||
|
NumberGreaterThan,
|
||||||
|
NumberLessThanOrEquals,
|
||||||
|
NumberGreaterThanOrEquals,
|
||||||
|
BoolEquals,
|
||||||
|
StringIn,
|
||||||
|
StringNotIn,
|
||||||
|
StringBeginsWith,
|
||||||
|
StringEndsWith,
|
||||||
|
StringContains,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public static class AdvancedFilterTypeConverter
|
||||||
|
{
|
||||||
|
public static Type OperatorTypeToAdvancedFilter(AdvancedFilterOperatorType operatorType)
|
||||||
|
{
|
||||||
|
switch (operatorType)
|
||||||
|
{
|
||||||
|
case AdvancedFilterOperatorType.NumberIn:
|
||||||
|
return typeof(NumberInAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.NumberNotIn:
|
||||||
|
return typeof(NumberNotInAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.NumberLessThan:
|
||||||
|
return typeof(NumberLessThanAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.NumberLessThanOrEquals:
|
||||||
|
return typeof(NumberLessThanOrEqualsAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.NumberGreaterThan:
|
||||||
|
return typeof(NumberGreaterThanAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.NumberGreaterThanOrEquals:
|
||||||
|
return typeof(NumberGreaterThanOrEqualsAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.BoolEquals:
|
||||||
|
return typeof(BoolEqualsAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.StringIn:
|
||||||
|
return typeof(StringInAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.StringNotIn:
|
||||||
|
return typeof(StringNotInAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.StringBeginsWith:
|
||||||
|
return typeof(StringBeginsWithAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.StringEndsWith:
|
||||||
|
return typeof(StringEndsWithAdvancedFilter);
|
||||||
|
|
||||||
|
case AdvancedFilterOperatorType.StringContains:
|
||||||
|
return typeof(StringContainsAdvancedFilter);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentException($"Advanced Filter operatorType {operatorType} not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AdvancedFilterOperatorType AdvancedFilterToOperatorType(Type type)
|
||||||
|
{
|
||||||
|
if (type == typeof(NumberInAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.NumberIn;
|
||||||
|
}
|
||||||
|
else if (type == typeof(NumberNotInAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.NumberNotIn;
|
||||||
|
}
|
||||||
|
else if (type == typeof(NumberLessThanAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.NumberLessThan;
|
||||||
|
}
|
||||||
|
else if (type == typeof(NumberLessThanOrEqualsAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.NumberLessThanOrEquals;
|
||||||
|
}
|
||||||
|
else if (type == typeof(NumberGreaterThanAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.NumberGreaterThan;
|
||||||
|
}
|
||||||
|
else if (type == typeof(NumberGreaterThanOrEqualsAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.NumberGreaterThanOrEquals;
|
||||||
|
}
|
||||||
|
else if (type == typeof(BoolEqualsAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.BoolEquals;
|
||||||
|
}
|
||||||
|
else if (type == typeof(StringInAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.StringIn;
|
||||||
|
}
|
||||||
|
else if (type == typeof(StringNotInAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.StringNotIn;
|
||||||
|
}
|
||||||
|
else if (type == typeof(StringBeginsWithAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.StringBeginsWith;
|
||||||
|
}
|
||||||
|
else if (type == typeof(StringEndsWithAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.StringEndsWith;
|
||||||
|
}
|
||||||
|
else if (type == typeof(StringContainsAdvancedFilter))
|
||||||
|
{
|
||||||
|
return AdvancedFilterOperatorType.StringContains;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Unknown type {type.Name}. Cannot map it to an {nameof(AdvancedFilterOperatorType)}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class CaseInsensitiveDictionaryConverter : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
public override bool CanRead => true;
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType) => typeof(Dictionary<string, string>).IsAssignableFrom(objectType);
|
||||||
|
|
||||||
|
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonToken.Null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string path = reader.Path;
|
||||||
|
var obj = JObject.Load(reader);
|
||||||
|
Dictionary<string, string> defaultDictionary = obj.ToObject<Dictionary<string, string>>(serializer);
|
||||||
|
return new Dictionary<string, string>(defaultDictionary, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class CloudEvent : CloudEvent<object>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CloudEvent<T> : IEquatable<CloudEvent<T>>
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Source { get; set; }
|
||||||
|
|
||||||
|
public string SpecVersion { get; set; }
|
||||||
|
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public string DataContentType { get; set; }
|
||||||
|
|
||||||
|
public string DataSchema { get; set; }
|
||||||
|
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
public DateTime? Time { get; set; }
|
||||||
|
|
||||||
|
public T Data { get; set; }
|
||||||
|
|
||||||
|
#pragma warning disable CA1707 // Identifiers should not contain underscores
|
||||||
|
public T Data_Base64 { get; set; }
|
||||||
|
#pragma warning restore CA1707 // Identifiers should not contain underscores
|
||||||
|
|
||||||
|
public bool Equals(CloudEvent<T> other)
|
||||||
|
{
|
||||||
|
return StringEquals(this.Id, other.Id) &&
|
||||||
|
StringEquals(this.Source, other.Source) &&
|
||||||
|
StringEquals(this.SpecVersion, other.SpecVersion) &&
|
||||||
|
StringEquals(this.Type, other.Type) &&
|
||||||
|
StringEquals(this.DataContentType, other.DataContentType) &&
|
||||||
|
StringEquals(this.DataSchema, other.DataSchema) &&
|
||||||
|
StringEquals(this.Subject, other.Subject) &&
|
||||||
|
this.Time.Equals(other.Time);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (object.ReferenceEquals(this, obj))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (obj is CloudEvent<T> ce)
|
||||||
|
{
|
||||||
|
return this.Equals(ce);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1307 // Specify StringComparison
|
||||||
|
public override int GetHashCode() => this.Id?.GetHashCode() ?? base.GetHashCode();
|
||||||
|
#pragma warning restore CA1307 // Specify StringComparison
|
||||||
|
|
||||||
|
private static bool StringEquals(string str1, string str2)
|
||||||
|
{
|
||||||
|
return (str1 is null && str2 is null) ||
|
||||||
|
(!(str1 is null) && !(str2 is null) && str1.Equals(str2, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class CustomEventSubscriptionDestination : EventSubscriptionDestination
|
||||||
|
{
|
||||||
|
public CustomEventSubscriptionDestination(string endpointType)
|
||||||
|
{
|
||||||
|
this.EndpointType = endpointType;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter))]
|
||||||
|
public Dictionary<string, string> Properties { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public static class EndpointTypes
|
||||||
|
{
|
||||||
|
public const string WebHook = nameof(WebHook);
|
||||||
|
public const string EventGrid = nameof(EventGrid);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public enum EventDeliverySchema
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Events are delivered to the destination using the Event Grid event schema.
|
||||||
|
/// </summary>
|
||||||
|
EventGridSchema = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event Payloads are treated as byte arrays without any assumptions about their structure/format,
|
||||||
|
/// and thus not validated / parsed / checked for errors.
|
||||||
|
/// </summary>
|
||||||
|
CustomEventSchema,
|
||||||
|
|
||||||
|
#pragma warning disable CA1707 // Identifiers should not contain underscores
|
||||||
|
/// <summary>
|
||||||
|
/// Events are delivered in the CloudEvent_1_0 schema
|
||||||
|
/// </summary>
|
||||||
|
CloudEventSchemaV1_0,
|
||||||
|
#pragma warning restore CA1707 // Identifiers should not contain underscores
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class EventGridEvent : EventGridEvent<object>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EventGridEvent<T> : IEquatable<EventGridEvent<T>>
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Topic { get; set; }
|
||||||
|
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
public string EventType { get; set; }
|
||||||
|
|
||||||
|
public string DataVersion { get; set; }
|
||||||
|
|
||||||
|
public string MetadataVersion { get; set; }
|
||||||
|
|
||||||
|
public DateTime EventTime { get; set; }
|
||||||
|
|
||||||
|
public T Data { get; set; }
|
||||||
|
|
||||||
|
public bool Equals(EventGridEvent<T> other)
|
||||||
|
{
|
||||||
|
return StringEquals(this.Id, other.Id) &&
|
||||||
|
StringEquals(this.Topic, other.Topic) &&
|
||||||
|
StringEquals(this.Subject, other.Subject) &&
|
||||||
|
StringEquals(this.EventType, other.EventType) &&
|
||||||
|
StringEquals(this.DataVersion, other.DataVersion) &&
|
||||||
|
StringEquals(this.MetadataVersion, other.MetadataVersion) &&
|
||||||
|
this.EventTime.Equals(other.EventTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (object.ReferenceEquals(this, obj))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (obj is EventGridEvent<T> ege)
|
||||||
|
{
|
||||||
|
return this.Equals(ege);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1307 // Specify StringComparison
|
||||||
|
public override int GetHashCode() => this.Id?.GetHashCode() ?? base.GetHashCode();
|
||||||
|
#pragma warning restore CA1307 // Specify StringComparison
|
||||||
|
|
||||||
|
private static bool StringEquals(string str1, string str2)
|
||||||
|
{
|
||||||
|
return (str1 is null && str2 is null) ||
|
||||||
|
(!(str1 is null) && !(str2 is null) && str1.Equals(str2, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class EventGridEventSubscriptionDestination : EventSubscriptionDestination
|
||||||
|
{
|
||||||
|
public EventGridEventSubscriptionDestination()
|
||||||
|
{
|
||||||
|
this.EndpointType = EndpointTypes.EventGrid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebHook Properties of the event subscription destination.
|
||||||
|
/// </summary>
|
||||||
|
public EventGridEventSubscriptionDestinationProperties Properties { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class EventGridEventSubscriptionDestinationProperties
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The URL that represents the endpoint of the destination of an event subscription.
|
||||||
|
/// </summary>
|
||||||
|
public string EndpointUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The authentication key to the event grid user topic.
|
||||||
|
/// </summary>
|
||||||
|
public string SasKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the Event Grid User Topic / Domain Topic.
|
||||||
|
/// </summary>
|
||||||
|
public string TopicName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls the max events to batch to this subscription.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxEventsPerBatch { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls the preferred batch size in Kilobytes to be used to deliver to this subscription.
|
||||||
|
/// </summary>
|
||||||
|
public int? PreferredBatchSizeInKilobytes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class EventSubscription
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the resource.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public EventSubscriptionProperties Properties { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public abstract class EventSubscriptionDestination
|
||||||
|
{
|
||||||
|
public string EndpointType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class EventSubscriptionDestinationConverter : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType) => typeof(EventSubscriptionDestination).IsAssignableFrom(objectType);
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonToken.StartObject)
|
||||||
|
{
|
||||||
|
var obj = JObject.Load(reader);
|
||||||
|
if (obj.TryGetValue("endpointType", StringComparison.OrdinalIgnoreCase, out JToken token) &&
|
||||||
|
token != null &&
|
||||||
|
token.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
string endpointType = token.ToString();
|
||||||
|
Type deserializeClass = GetEventSubscriptionDestinationType(endpointType);
|
||||||
|
var destination = (EventSubscriptionDestination)obj.ToObject(deserializeClass, serializer);
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Couldn't find an endpointType value on the subscription destination.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
private static Type GetEventSubscriptionDestinationType(string endpointType)
|
||||||
|
{
|
||||||
|
if (endpointType.Equals(EndpointTypes.WebHook, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return typeof(WebHookEventSubscriptionDestination);
|
||||||
|
}
|
||||||
|
else if (endpointType.Equals(EndpointTypes.EventGrid, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return typeof(EventGridEventSubscriptionDestination);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(endpointType))
|
||||||
|
{
|
||||||
|
return typeof(CustomEventSubscriptionDestination);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Unknown endpoint type: {endpointType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class EventSubscriptionFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An optional string to filter events for an event subscription based on a resource path prefix.
|
||||||
|
/// The format of this depends on the publisher of the events.
|
||||||
|
/// Wildcard characters are not supported in this path.
|
||||||
|
/// </summary>
|
||||||
|
// e.g. "blobservices/default/containers/blobContainer1/folder1/folder2"
|
||||||
|
public string SubjectBeginsWith { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An optional string to filter events for an event subscription based on a resource path suffix.
|
||||||
|
/// Wildcard characters are not supported in this path.
|
||||||
|
/// </summary>
|
||||||
|
// e.g. ".jpg"
|
||||||
|
public string SubjectEndsWith { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A list of applicable event types that need to be part of the event subscription.
|
||||||
|
/// If it is desired to subscribe to all event types, the string "all" needs to be specified as an element in this list.
|
||||||
|
/// </summary>
|
||||||
|
// e.g. "*" or "resourceCreated"
|
||||||
|
public List<string> IncludedEventTypes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies if the SubjectBeginsWith and SubjectEndsWith properties of the filter
|
||||||
|
/// should be compared in a case sensitive manner.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSubjectCaseSensitive { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(AdvancedFilterJsonConverter))]
|
||||||
|
public AdvancedFilter[] AdvancedFilters { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class EventSubscriptionProperties
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the topic of the event subscription.
|
||||||
|
/// </summary>
|
||||||
|
public string Topic { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about the destination where events have to be delivered for the event subscription.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(EventSubscriptionDestinationConverter))]
|
||||||
|
public EventSubscriptionDestination Destination { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about the filter for the event subscription.
|
||||||
|
/// </summary>
|
||||||
|
public EventSubscriptionFilter Filter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The event delivery schema for the event subscription.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public EventDeliverySchema? EventDeliverySchema { get; set; }
|
||||||
|
|
||||||
|
// The following two properties aren't wired up yet, so commenting it out.
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// Expiration time of the event subscription.
|
||||||
|
///// </summary>
|
||||||
|
// public DateTime? ExpirationTimeUtc { get; set; }
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// The retry policy for events. This can be used to configure maximum number of delivery attempts
|
||||||
|
///// and time to live for events.
|
||||||
|
///// </summary>
|
||||||
|
public RetryPolicy RetryPolicy { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public enum InputSchema
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Events will be published in the Event Grid event schema.
|
||||||
|
/// </summary>
|
||||||
|
EventGridSchema = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event Payloads are treated as byte arrays without any assumptions about their structure/format,
|
||||||
|
/// and thus not validated / parsed / checked for errors.
|
||||||
|
/// </summary>
|
||||||
|
CustomEventSchema,
|
||||||
|
|
||||||
|
#pragma warning disable CA1707 // Identifiers should not contain underscores
|
||||||
|
/// <summary>
|
||||||
|
/// Events will be published in the CloudEventSchemaV1_0
|
||||||
|
/// </summary>
|
||||||
|
CloudEventSchemaV1_0,
|
||||||
|
#pragma warning restore CA1707 // Identifiers should not contain underscores
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class RetryPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of delivery retry attempts for events.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxDeliveryAttempts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Time To Live (in minutes) for events.
|
||||||
|
/// </summary>
|
||||||
|
public int? EventExpiryInMinutes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class Topic
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public TopicProperties Properties { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class TopicProperties
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoint for the topic.
|
||||||
|
/// </summary>
|
||||||
|
public string Endpoint { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This determines the format that Event Grid should expect for incoming events published to the topic.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public InputSchema? InputSchema { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class WebHookEventSubscriptionDestination : EventSubscriptionDestination
|
||||||
|
{
|
||||||
|
public WebHookEventSubscriptionDestination()
|
||||||
|
{
|
||||||
|
this.EndpointType = EndpointTypes.WebHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebHook Properties of the event subscription destination.
|
||||||
|
/// </summary>
|
||||||
|
public WebHookEventSubscriptionDestinationProperties Properties { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class WebHookEventSubscriptionDestinationProperties
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The URL that represents the endpoint of the destination of an event subscription.
|
||||||
|
/// </summary>
|
||||||
|
public string EndpointUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The base URL that represents the endpoint of the destination of an event subscription.
|
||||||
|
/// </summary>
|
||||||
|
public string EndpointBaseUrl { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls the max events to batch to this subscription.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxEventsPerBatch { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls the preferred batch size in Kilobytes to be used to deliver to this subscription.
|
||||||
|
/// </summary>
|
||||||
|
public int? PreferredBatchSizeInKilobytes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public sealed class EventGridApiException : Exception
|
||||||
|
{
|
||||||
|
internal EventGridApiException(HttpRequestMessage request, HttpResponseMessage response, string responsePayload)
|
||||||
|
: base(GetMessage(request, response, responsePayload))
|
||||||
|
{
|
||||||
|
this.RequestUri = request.RequestUri;
|
||||||
|
this.RequestMethod = request.Method.Method;
|
||||||
|
this.ResponseStatusCode = response?.StatusCode;
|
||||||
|
this.ResponseReasonPhrase = response?.ReasonPhrase;
|
||||||
|
this.ResponsePayload = responsePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri RequestUri { get; }
|
||||||
|
|
||||||
|
public string RequestMethod { get; }
|
||||||
|
|
||||||
|
public HttpStatusCode? ResponseStatusCode { get; }
|
||||||
|
|
||||||
|
public string ResponseReasonPhrase { get; }
|
||||||
|
|
||||||
|
public string ResponsePayload { get; }
|
||||||
|
|
||||||
|
private static string GetMessage(HttpRequestMessage request, HttpResponseMessage response, string responsePayload)
|
||||||
|
{
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
return $"REQUEST: Method={request.Method.Method} Url={request.RequestUri} \nRESPONSE: statusCode={response.StatusCode} reasonPhrase=<{response.ReasonPhrase}> payload={responsePayload}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return $"REQUEST: Method={request.Method.Method} Url={request.RequestUri} \nRESPONSE: <null>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public sealed class EventGridEdgeClient
|
||||||
|
{
|
||||||
|
private static readonly MediaTypeHeaderValue JsonContentType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
|
||||||
|
private readonly JsonSerializer jsonSerializer;
|
||||||
|
private readonly RecyclableMemoryStreamManager recyclableMemoryStreamManager;
|
||||||
|
private readonly UTF8Encoding encoding;
|
||||||
|
|
||||||
|
public EventGridEdgeClient(string baseAddress, int port)
|
||||||
|
: this(baseAddress, port, null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventGridEdgeClient(string baseAddress, int port, IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
this.BaseUri = new Uri($"{baseAddress}:{port}");
|
||||||
|
|
||||||
|
this.encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||||
|
|
||||||
|
this.HttpClient = httpClientFactory?.CreateClient() ?? new HttpClient();
|
||||||
|
this.HttpClient.BaseAddress = this.BaseUri;
|
||||||
|
|
||||||
|
this.jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.None,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
FloatParseHandling = FloatParseHandling.Decimal,
|
||||||
|
Converters = new JsonConverter[]
|
||||||
|
{
|
||||||
|
new StringEnumConverter(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
|
||||||
|
|
||||||
|
this.Topics = new TopicsAPI(this);
|
||||||
|
this.Subscriptions = new SubscriptionsAPI(this);
|
||||||
|
this.Events = new EventsAPI(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri BaseUri { get; }
|
||||||
|
|
||||||
|
public TopicsAPI Topics { get; }
|
||||||
|
|
||||||
|
public SubscriptionsAPI Subscriptions { get; }
|
||||||
|
|
||||||
|
public EventsAPI Events { get; }
|
||||||
|
|
||||||
|
public HttpClient HttpClient { get; }
|
||||||
|
|
||||||
|
public StreamContent CreateJsonContent<T>(T item, [CallerMemberName] string callerMemberName = "", MediaTypeHeaderValue contentType = null)
|
||||||
|
{
|
||||||
|
var stream = new RecyclableMemoryStream(this.recyclableMemoryStreamManager, callerMemberName);
|
||||||
|
using (var sw = new StreamWriter(stream, this.encoding, 1024, leaveOpen: true))
|
||||||
|
using (var jw = new JsonTextWriter(sw))
|
||||||
|
{
|
||||||
|
this.jsonSerializer.Serialize(jw, item);
|
||||||
|
sw.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
long finalPosition = stream.Position;
|
||||||
|
stream.Position = 0;
|
||||||
|
|
||||||
|
// the stream will get disposed when streamContent is disposed off.
|
||||||
|
var streamContent = new StreamContent(stream);
|
||||||
|
streamContent.Headers.ContentType = contentType == null ? JsonContentType : contentType;
|
||||||
|
streamContent.Headers.ContentLength = finalPosition;
|
||||||
|
return streamContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> DeserializeAsync<T>(HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
using (Stream stream = await response.Content.ReadAsStreamAsync())
|
||||||
|
{
|
||||||
|
return this.Deserialize<T>(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal T Deserialize<T>(Stream stream)
|
||||||
|
{
|
||||||
|
using (var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
|
||||||
|
using (var jr = new JsonTextReader(sr))
|
||||||
|
{
|
||||||
|
return this.jsonSerializer.Deserialize<T>(jr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class EventsAPI
|
||||||
|
{
|
||||||
|
private const string ApiVersionSuffix = "?api-version=2019-01-01-preview";
|
||||||
|
private readonly EventGridEdgeClient client;
|
||||||
|
|
||||||
|
internal EventsAPI(EventGridEdgeClient client)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PublishJsonAsync<T>(string topicName, string eventId, T payload, MediaTypeHeaderValue contentType, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (StreamContent streamContent = this.client.CreateJsonContent(payload, nameof(this.PublishJsonAsync), contentType))
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Post, $"topics/{UrlEncoder.Default.Encode(topicName)}/events/{eventId}{ApiVersionSuffix}") { Content = streamContent })
|
||||||
|
{
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PublishJsonAsync<T>(string topicName, IEnumerable<T> payload, MediaTypeHeaderValue contentType, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (StreamContent streamContent = this.client.CreateJsonContent(payload, nameof(this.PublishJsonAsync), contentType))
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Post, $"topics/{UrlEncoder.Default.Encode(topicName)}/events{ApiVersionSuffix}") { Content = streamContent })
|
||||||
|
{
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PublishRawAsync(string topicName, string eventId, byte[] payload, MediaTypeHeaderValue contentType, Dictionary<string, string> httpHeaders, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (var byteArrayContent = new ByteArrayContent(payload))
|
||||||
|
{
|
||||||
|
byteArrayContent.Headers.ContentType = contentType;
|
||||||
|
byteArrayContent.Headers.ContentLength = payload.Length;
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Post, $"topics/{UrlEncoder.Default.Encode(topicName)}/events/{eventId}{ApiVersionSuffix}") { Content = byteArrayContent })
|
||||||
|
{
|
||||||
|
if (httpHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<string, string> httpHeader in httpHeaders)
|
||||||
|
{
|
||||||
|
request.Headers.Add(httpHeader.Key, httpHeader.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PublishRawAsync(string topicName, byte[] payload, MediaTypeHeaderValue contentType, Dictionary<string, string> httpHeaders, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (var byteArrayContent = new ByteArrayContent(payload))
|
||||||
|
{
|
||||||
|
byteArrayContent.Headers.ContentType = contentType;
|
||||||
|
byteArrayContent.Headers.ContentLength = payload.Length;
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Post, $"topics/{UrlEncoder.Default.Encode(topicName)}/events{ApiVersionSuffix}") { Content = byteArrayContent })
|
||||||
|
{
|
||||||
|
if (httpHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<string, string> httpHeader in httpHeaders)
|
||||||
|
{
|
||||||
|
request.Headers.Add(httpHeader.Key, httpHeader.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public static class SdkExtensions
|
||||||
|
{
|
||||||
|
public static async Task ThrowIfFailedAsync(this HttpResponseMessage response, HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
string responsePayload = "!!!EMPTY PAYLOAD";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
responsePayload = await response.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
responsePayload = $"!!!payload-read-failed with inner exception:{ex}";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new EventGridApiException(request, response, responsePayload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<RootNamespace>$(RootNamespacePrefix)SDK</RootNamespace>
|
||||||
|
<AssemblyName>$(RootNamespace)</AssemblyName>
|
||||||
|
<TargetFrameworks>netcoreapp2.1;netstandard2.0</TargetFrameworks>
|
||||||
|
<TargetsForTfmSpecificBuildOutput Condition="'$(TargetFramework)'=='netcoreapp2.1'">$(TargetsForTfmSpecificBuildOutput);IncludeP2PAssets</TargetsForTfmSpecificBuildOutput>
|
||||||
|
|
||||||
|
<!-- nuget pack default properties
|
||||||
|
Id/PackageId defaults to AssemblyName
|
||||||
|
Version/PackageVersion is assigned by nbgv
|
||||||
|
-->
|
||||||
|
<Authors>Microsoft</Authors>
|
||||||
|
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||||
|
<PackageProjectUrl>https://azure.microsoft.com/en-us/services/event-grid/</PackageProjectUrl>
|
||||||
|
<Description></Description>
|
||||||
|
<PackageIconUrl>http://go.microsoft.com/fwlink/?LinkID=288890</PackageIconUrl>
|
||||||
|
<PackageTags>Azure;EventGrid;IoT;Edge</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="2.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="1.2.2" />
|
||||||
|
<PackageReference Include="System.Text.Encodings.Web" Version="4.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)'=='netcoreapp2.1'">
|
||||||
|
<ProjectReference Include="..\SecurityDaemonClient\SecurityDaemonClient.csproj" PrivateAssets="all"/>
|
||||||
|
<!-- Explicitly add SecurityDaemonClient's dependencies here otherwise the generated nuspec won't have these listed.-->
|
||||||
|
<!-- Ideally SecurityDaemonClient should be its' own nuget. -->
|
||||||
|
<PackageReference Include="Portable.BouncyCastle" Version="1.8.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="[5.2.6,5.3.0)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="IncludeP2PAssets">
|
||||||
|
<ItemGroup>
|
||||||
|
<BuildOutputInPackage Include="$(OutputPath)$(RootNamespacePrefix)SecurityDaemonClient.dll" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
</Project>
|
|
@ -0,0 +1,66 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class SubscriptionsAPI
|
||||||
|
{
|
||||||
|
private const string ApiVersionSuffix = "?api-version=2019-01-01-preview";
|
||||||
|
private readonly EventGridEdgeClient client;
|
||||||
|
|
||||||
|
internal SubscriptionsAPI(EventGridEdgeClient client)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EventSubscription> PutSubscriptionAsync(string topicName, string subscriptionName, EventSubscription eventSubscription, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (StreamContent streamContent = this.client.CreateJsonContent(eventSubscription))
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Put, $"topics/{UrlEncoder.Default.Encode(topicName)}/eventSubscriptions/{UrlEncoder.Default.Encode(subscriptionName)}{ApiVersionSuffix}") { Content = streamContent })
|
||||||
|
{
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
return await this.client.DeserializeAsync<EventSubscription>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EventSubscription> GetEventSubscriptionAsync(string topicName, string subscriptionName, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Get, $"topics/{UrlEncoder.Default.Encode(topicName)}/eventSubscriptions/{UrlEncoder.Default.Encode(subscriptionName)}{ApiVersionSuffix}"))
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
return await this.client.DeserializeAsync<EventSubscription>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<EventSubscription>> GetEventSubscriptionsAsync(string topicName, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Get, $"topics/{UrlEncoder.Default.Encode(topicName)}/eventSubscriptions/{ApiVersionSuffix}"))
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
return await this.client.DeserializeAsync<IEnumerable<EventSubscription>>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteEventSubscriptionAsync(string topicName, string subscriptionName, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Delete, $"topics/{UrlEncoder.Default.Encode(topicName)}/eventSubscriptions/{UrlEncoder.Default.Encode(subscriptionName)}{ApiVersionSuffix}"))
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.SDK
|
||||||
|
{
|
||||||
|
public class TopicsAPI
|
||||||
|
{
|
||||||
|
public const string ApiVersionSuffix = "?api-version=2019-01-01-preview";
|
||||||
|
private readonly EventGridEdgeClient client;
|
||||||
|
|
||||||
|
internal TopicsAPI(EventGridEdgeClient client)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Topic> PutTopicAsync(string topicName, Topic topic, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (StreamContent streamContent = this.client.CreateJsonContent(topic))
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Put, $"topics/{UrlEncoder.Default.Encode(topicName)}{ApiVersionSuffix}") { Content = streamContent })
|
||||||
|
{
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
return await this.client.DeserializeAsync<Topic>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Topic> GetTopicAsync(string topicName, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Get, $"topics/{UrlEncoder.Default.Encode(topicName)}{ApiVersionSuffix}"))
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
return await this.client.DeserializeAsync<Topic>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Topic>> GetTopicsAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Get, $"topics{ApiVersionSuffix}"))
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
return await this.client.DeserializeAsync<IEnumerable<Topic>>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteTopicAsync(string topicName, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Delete, $"topics/{UrlEncoder.Default.Encode(topicName)}{ApiVersionSuffix}"))
|
||||||
|
using (HttpResponseMessage response = await this.client.HttpClient.SendAsync(request, token))
|
||||||
|
{
|
||||||
|
await response.ThrowIfFailedAsync(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
public class CertificateResponse
|
||||||
|
{
|
||||||
|
public PrivateKey PrivateKey { get; set; }
|
||||||
|
|
||||||
|
public string Certificate { get; set; }
|
||||||
|
|
||||||
|
public DateTime? Expiration { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
public class IdentityCertificateRequest
|
||||||
|
{
|
||||||
|
public DateTime Expiration { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
public class PrivateKey
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public PrivateKeyType? Type { get; set; }
|
||||||
|
|
||||||
|
public string Ref { get; set; }
|
||||||
|
|
||||||
|
public string Bytes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
public enum PrivateKeyType
|
||||||
|
{
|
||||||
|
Ref = 0,
|
||||||
|
Key = 1,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
public class ServerCertificateRequest
|
||||||
|
{
|
||||||
|
public string CommonName { get; set; }
|
||||||
|
|
||||||
|
public DateTime Expiration { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
public class TrustBundleResponse
|
||||||
|
{
|
||||||
|
public string Certificate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,287 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using Newtonsoft.Json.Serialization;
|
||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
|
using Org.BouncyCastle.OpenSsl;
|
||||||
|
using Org.BouncyCastle.Pkcs;
|
||||||
|
using Org.BouncyCastle.Security;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
[Obsolete("This is a pubternal API that's being made public as a stop-gap measure. It will be removed from the Event Grid SDK nuget package as soon IoT Edge SDK ships with a built-in a security daemon client.")]
|
||||||
|
public sealed class SecurityDaemonClient : IDisposable
|
||||||
|
{
|
||||||
|
private const string UnixScheme = "unix";
|
||||||
|
private const int DefaultServerCertificateValidityInDays = 90;
|
||||||
|
private const int DefaultIdentityCertificateValidityInDays = 7;
|
||||||
|
private readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.None,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
FloatParseHandling = FloatParseHandling.Decimal,
|
||||||
|
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||||
|
Converters = new JsonConverter[] { new StringEnumConverter() },
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly string moduleGenerationId;
|
||||||
|
private readonly string edgeGatewayHostName;
|
||||||
|
private readonly string workloadApiVersion;
|
||||||
|
|
||||||
|
private readonly HttpClient httpClient;
|
||||||
|
private readonly Uri getTrustBundleUri;
|
||||||
|
private readonly Uri postIdentityCertificateRequestUri;
|
||||||
|
private readonly Uri postServerCertificateRequestUri;
|
||||||
|
private readonly string asString;
|
||||||
|
|
||||||
|
public SecurityDaemonClient()
|
||||||
|
{
|
||||||
|
this.ModuleId = Environment.GetEnvironmentVariable("IOTEDGE_MODULEID");
|
||||||
|
this.DeviceId = Environment.GetEnvironmentVariable("IOTEDGE_DEVICEID");
|
||||||
|
string iotHubHostName = Environment.GetEnvironmentVariable("IOTEDGE_IOTHUBHOSTNAME");
|
||||||
|
this.IotHubName = iotHubHostName.Split('.').FirstOrDefault();
|
||||||
|
|
||||||
|
this.moduleGenerationId = Environment.GetEnvironmentVariable("IOTEDGE_MODULEGENERATIONID");
|
||||||
|
this.edgeGatewayHostName = Environment.GetEnvironmentVariable("IOTEDGE_GATEWAYHOSTNAME");
|
||||||
|
this.workloadApiVersion = Environment.GetEnvironmentVariable("IOTEDGE_APIVERSION");
|
||||||
|
string workloadUriString = Environment.GetEnvironmentVariable("IOTEDGE_WORKLOADURI");
|
||||||
|
|
||||||
|
Validate.ArgumentNotNullOrEmpty(this.ModuleId, nameof(this.ModuleId));
|
||||||
|
Validate.ArgumentNotNullOrEmpty(this.DeviceId, nameof(this.DeviceId));
|
||||||
|
Validate.ArgumentNotNullOrEmpty(this.IotHubName, nameof(this.IotHubName));
|
||||||
|
Validate.ArgumentNotNullOrEmpty(this.moduleGenerationId, nameof(this.moduleGenerationId));
|
||||||
|
Validate.ArgumentNotNullOrEmpty(this.edgeGatewayHostName, nameof(this.edgeGatewayHostName));
|
||||||
|
Validate.ArgumentNotNullOrEmpty(this.workloadApiVersion, nameof(this.workloadApiVersion));
|
||||||
|
Validate.ArgumentNotNullOrEmpty(workloadUriString, nameof(workloadUriString));
|
||||||
|
|
||||||
|
var workloadUri = new Uri(workloadUriString);
|
||||||
|
|
||||||
|
string baseUrlForRequests;
|
||||||
|
if (workloadUri.Scheme.Equals(SecurityDaemonClient.UnixScheme, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
baseUrlForRequests = $"http://{workloadUri.Segments.Last()}";
|
||||||
|
this.httpClient = new HttpClient(new HttpUdsMessageHandler(workloadUri));
|
||||||
|
}
|
||||||
|
else if (workloadUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
workloadUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
baseUrlForRequests = workloadUriString;
|
||||||
|
this.httpClient = new HttpClient();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Unknown workloadUri scheme specified. {workloadUri}");
|
||||||
|
}
|
||||||
|
|
||||||
|
baseUrlForRequests = baseUrlForRequests.TrimEnd();
|
||||||
|
this.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
|
string encodedApiVersion = UrlEncoder.Default.Encode(this.workloadApiVersion);
|
||||||
|
string encodedModuleId = UrlEncoder.Default.Encode(this.ModuleId);
|
||||||
|
string encodedModuleGenerationId = UrlEncoder.Default.Encode(this.moduleGenerationId);
|
||||||
|
|
||||||
|
this.getTrustBundleUri = new Uri($"{baseUrlForRequests}/trust-bundle?api-version={encodedApiVersion}");
|
||||||
|
this.postIdentityCertificateRequestUri = new Uri($"{baseUrlForRequests}/modules/{encodedModuleId}/certificate/identity?api-version={encodedApiVersion}");
|
||||||
|
this.postServerCertificateRequestUri = new Uri($"{baseUrlForRequests}/modules/{encodedModuleId}/genid/{encodedModuleGenerationId}/certificate/server?api-version={encodedApiVersion}");
|
||||||
|
|
||||||
|
var settings = new
|
||||||
|
{
|
||||||
|
this.ModuleId,
|
||||||
|
this.DeviceId,
|
||||||
|
IotHubHostName = iotHubHostName,
|
||||||
|
ModuleGenerationId = this.moduleGenerationId,
|
||||||
|
GatewayHostName = this.edgeGatewayHostName,
|
||||||
|
WorkloadUri = workloadUriString,
|
||||||
|
WorkloadApiVersion = this.workloadApiVersion,
|
||||||
|
};
|
||||||
|
this.asString = $"{nameof(SecurityDaemonClient)}{JsonConvert.SerializeObject(settings, Formatting.None, this.jsonSettings)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string IotHubName { get; }
|
||||||
|
|
||||||
|
public string DeviceId { get; }
|
||||||
|
|
||||||
|
public string ModuleId { get; }
|
||||||
|
|
||||||
|
public void Dispose() => this.httpClient.Dispose();
|
||||||
|
|
||||||
|
public override string ToString() => this.asString;
|
||||||
|
|
||||||
|
public Task<(X509Certificate2 serverCert, X509Certificate2[] certChain)> GetServerCertificateAsync(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return this.GetServerCertificateAsync(TimeSpan.FromDays(SecurityDaemonClient.DefaultServerCertificateValidityInDays), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(X509Certificate2 serverCert, X509Certificate2[] certChain)> GetServerCertificateAsync(TimeSpan validity, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var request = new ServerCertificateRequest
|
||||||
|
{
|
||||||
|
CommonName = this.edgeGatewayHostName,
|
||||||
|
Expiration = DateTime.UtcNow.Add(validity),
|
||||||
|
};
|
||||||
|
|
||||||
|
string requestString = JsonConvert.SerializeObject(request, Formatting.None, this.jsonSettings);
|
||||||
|
using (var content = new StringContent(requestString, Encoding.UTF8, "application/json"))
|
||||||
|
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, this.postServerCertificateRequestUri) { Content = content })
|
||||||
|
using (HttpResponseMessage httpResponse = await this.httpClient.SendAsync(httpRequest, token))
|
||||||
|
{
|
||||||
|
string responsePayload = await httpResponse.Content.ReadAsStringAsync();
|
||||||
|
if (httpResponse.StatusCode == HttpStatusCode.Created)
|
||||||
|
{
|
||||||
|
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responsePayload, this.jsonSettings);
|
||||||
|
return this.CreateX509Certificates(cr);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve server certificate from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' ResponsePayload='{responsePayload}' Request={requestString} This={this}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<(X509Certificate2 identityCert, X509Certificate2[] certChain)> GetIdentityCertificateAsync(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return this.GetIdentityCertificateAsync(TimeSpan.FromDays(SecurityDaemonClient.DefaultIdentityCertificateValidityInDays), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(X509Certificate2 identityCert, X509Certificate2[] certChain)> GetIdentityCertificateAsync(TimeSpan validity, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var request = new IdentityCertificateRequest
|
||||||
|
{
|
||||||
|
Expiration = DateTime.UtcNow.Add(validity),
|
||||||
|
};
|
||||||
|
|
||||||
|
string requestString = JsonConvert.SerializeObject(request, Formatting.None, this.jsonSettings);
|
||||||
|
using (var content = new StringContent(requestString, Encoding.UTF8, "application/json"))
|
||||||
|
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, this.postIdentityCertificateRequestUri) { Content = content })
|
||||||
|
using (HttpResponseMessage httpResponse = await this.httpClient.SendAsync(httpRequest, token))
|
||||||
|
{
|
||||||
|
string responsePayload = await httpResponse.Content.ReadAsStringAsync();
|
||||||
|
if (httpResponse.StatusCode == HttpStatusCode.Created)
|
||||||
|
{
|
||||||
|
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responsePayload, this.jsonSettings);
|
||||||
|
return this.CreateX509Certificates(cr);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve identity certificate from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' ResponsePayload='{responsePayload}' Request={requestString} This={this}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<X509Certificate2[]> GetTrustBundleAsync(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, this.getTrustBundleUri))
|
||||||
|
using (HttpResponseMessage httpResponse = await this.httpClient.SendAsync(httpRequest, token))
|
||||||
|
{
|
||||||
|
string responsePayload = await httpResponse.Content.ReadAsStringAsync();
|
||||||
|
if (httpResponse.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
TrustBundleResponse trustBundleResponse = JsonConvert.DeserializeObject<TrustBundleResponse>(responsePayload, this.jsonSettings);
|
||||||
|
Validate.ArgumentNotNullOrEmpty(trustBundleResponse.Certificate, nameof(trustBundleResponse.Certificate));
|
||||||
|
|
||||||
|
string[] rawCerts = ParseCertificateResponse(trustBundleResponse.Certificate);
|
||||||
|
if (rawCerts.FirstOrDefault() == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve the certificate trust bundle from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' Reason='Security daemon returned an empty response' This={this}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertToX509(rawCerts);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve the certificate trust bundle from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' ResponsePayload='{responsePayload}' This={this}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509Certificate2[] ConvertToX509(IEnumerable<string> rawCerts) => rawCerts.Select(c => new X509Certificate2(Encoding.UTF8.GetBytes(c))).ToArray();
|
||||||
|
|
||||||
|
private static string[] ParseCertificateResponse(string certificateChain, [CallerMemberName] string callerMemberName = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(certificateChain))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Trusted certificates can not be null or empty for {callerMemberName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract each certificate's string. The final string from the split will either be empty
|
||||||
|
// or a non-certificate entry, so it is dropped.
|
||||||
|
string delimiter = "-----END CERTIFICATE-----";
|
||||||
|
string[] rawCerts = certificateChain.Split(new[] { delimiter }, StringSplitOptions.None);
|
||||||
|
return rawCerts.Take(count: rawCerts.Length - 1).Select(c => $"{c}{delimiter}").ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private (X509Certificate2 primaryCert, X509Certificate2[] certChain) CreateX509Certificates(CertificateResponse cr, [CallerMemberName] string callerMemberName = default)
|
||||||
|
{
|
||||||
|
Validate.ArgumentNotNullOrEmpty(cr.Certificate, nameof(cr.Certificate));
|
||||||
|
Validate.ArgumentNotNull(cr.Expiration, nameof(cr.Expiration));
|
||||||
|
Validate.ArgumentNotNull(cr.PrivateKey, nameof(cr.PrivateKey));
|
||||||
|
Validate.ArgumentNotNull(cr.PrivateKey.Type, nameof(cr.PrivateKey.Type));
|
||||||
|
Validate.ArgumentNotNull(cr.PrivateKey.Bytes, nameof(cr.PrivateKey.Bytes));
|
||||||
|
|
||||||
|
string[] rawCerts = ParseCertificateResponse(cr.Certificate);
|
||||||
|
if (rawCerts.Length == 0 ||
|
||||||
|
string.IsNullOrWhiteSpace(rawCerts[0]))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve certificate from IoTEdge Security daemon for {callerMemberName}. Reason: Security daemon returned an empty response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string primaryCert = rawCerts[0];
|
||||||
|
X509Certificate2[] certChain = ConvertToX509(rawCerts.Skip(1));
|
||||||
|
|
||||||
|
RsaPrivateCrtKeyParameters keyParams = null;
|
||||||
|
|
||||||
|
var chainCertEntries = new List<X509CertificateEntry>();
|
||||||
|
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
|
||||||
|
|
||||||
|
// note: the seperator between the certificate and private key is added for safety to delineate the cert and key boundary
|
||||||
|
using (var sr = new StringReader(primaryCert + "\r\n" + cr.PrivateKey.Bytes))
|
||||||
|
{
|
||||||
|
var pemReader = new PemReader(sr);
|
||||||
|
object certObject;
|
||||||
|
while ((certObject = pemReader.ReadObject()) != null)
|
||||||
|
{
|
||||||
|
if (certObject is Org.BouncyCastle.X509.X509Certificate x509Cert)
|
||||||
|
{
|
||||||
|
chainCertEntries.Add(new X509CertificateEntry(x509Cert));
|
||||||
|
}
|
||||||
|
|
||||||
|
// when processing certificates generated via openssl certObject type is of AsymmetricCipherKeyPair
|
||||||
|
if (certObject is AsymmetricCipherKeyPair ackp)
|
||||||
|
{
|
||||||
|
certObject = ackp.Private;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certObject is RsaPrivateCrtKeyParameters rpckp)
|
||||||
|
{
|
||||||
|
keyParams = rpckp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyParams == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Private key was not found for {callerMemberName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
store.SetKeyEntry(this.ModuleId, new AsymmetricKeyEntry(keyParams), chainCertEntries.ToArray());
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
store.Save(ms, Array.Empty<char>(), new SecureRandom());
|
||||||
|
var x509PrimaryCert = new X509Certificate2(ms.ToArray());
|
||||||
|
return (x509PrimaryCert, certChain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<RootNamespace>$(RootNamespacePrefix)SecurityDaemonClient</RootNamespace>
|
||||||
|
<AssemblyName>$(RootNamespace)</AssemblyName>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Portable.BouncyCastle" Version="1.8.5" />
|
||||||
|
<PackageReference Include="System.Text.Encodings.Web" Version="4.5.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="2.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="[5.2.6,5.3.0)" />
|
||||||
|
<!-- Add ANY new dependencies to the SDK.csproj's section where SecurityDaemonClient is referenced by the SDK.dll-->
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,57 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
[Obsolete("This is a pubternal API that's being made public as a stop-gap measure. It will be removed from the Event Grid SDK nuget package as soon IoT Edge SDK ships with a built-in a security daemon client.")]
|
||||||
|
public class SecurityDaemonHttpClientFactory : IHttpClientFactory
|
||||||
|
{
|
||||||
|
private readonly Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> callback;
|
||||||
|
|
||||||
|
public SecurityDaemonHttpClientFactory(X509Certificate2 identityCertificate)
|
||||||
|
: this(identityCertificate, ServiceCertificateValidationCallback)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public SecurityDaemonHttpClientFactory(X509Certificate2 identityCertificate, Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCallback)
|
||||||
|
{
|
||||||
|
this.IdentityCertificate = identityCertificate;
|
||||||
|
this.callback = serverCertificateCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate2 IdentityCertificate { get; }
|
||||||
|
|
||||||
|
public static async Task<SecurityDaemonHttpClientFactory> CreateAsync(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
using (var iotEdgeClient = new SecurityDaemonClient())
|
||||||
|
{
|
||||||
|
(X509Certificate2 identityCertificate, _) = await iotEdgeClient.GetIdentityCertificateAsync(token);
|
||||||
|
return new SecurityDaemonHttpClientFactory(identityCertificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Reliability", "CA2000: DisposeObjectsBeforeLosingScope", Justification = "The HttpClient owns the lifetime of the handler")]
|
||||||
|
public HttpClient CreateClient(string name)
|
||||||
|
{
|
||||||
|
var httpClientHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = this.callback };
|
||||||
|
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
|
||||||
|
httpClientHandler.ClientCertificates.Add(this.IdentityCertificate);
|
||||||
|
return new HttpClient(httpClientHandler, disposeHandler: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ServiceCertificateValidationCallback(HttpRequestMessage request, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors errors)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
internal class HttpBufferedStream : Stream
|
||||||
|
{
|
||||||
|
private const char CR = '\r';
|
||||||
|
private const char LF = '\n';
|
||||||
|
private readonly BufferedStream innerStream;
|
||||||
|
|
||||||
|
public HttpBufferedStream(Stream stream)
|
||||||
|
{
|
||||||
|
this.innerStream = new BufferedStream(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanRead => this.innerStream.CanRead;
|
||||||
|
|
||||||
|
public override bool CanSeek => this.innerStream.CanSeek;
|
||||||
|
|
||||||
|
public override bool CanWrite => this.innerStream.CanWrite;
|
||||||
|
|
||||||
|
public override long Length => this.innerStream.Length;
|
||||||
|
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get => this.innerStream.Position;
|
||||||
|
set => this.innerStream.Position = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush() => this.innerStream.Flush();
|
||||||
|
|
||||||
|
public override Task FlushAsync(CancellationToken cancellationToken) => this.innerStream.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count) => this.innerStream.Read(buffer, offset, count);
|
||||||
|
|
||||||
|
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.innerStream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<string> ReadLineAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int position = 0;
|
||||||
|
byte[] buffer = new byte[1];
|
||||||
|
bool crFound = false;
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int length = await this.innerStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
|
||||||
|
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
throw new IOException("Unexpected end of stream.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crFound && (char)buffer[position] == LF)
|
||||||
|
{
|
||||||
|
builder.Remove(builder.Length - 1, 1);
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append((char)buffer[position]);
|
||||||
|
crFound = (char)buffer[position] == CR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin) => this.innerStream.Seek(offset, origin);
|
||||||
|
|
||||||
|
public override void SetLength(long value) => this.innerStream.SetLength(value);
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count) => this.innerStream.Write(buffer, offset, count);
|
||||||
|
|
||||||
|
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.innerStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing) => this.innerStream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
internal class HttpRequestResponseSerializer
|
||||||
|
{
|
||||||
|
private const char SP = ' ';
|
||||||
|
private const char CR = '\r';
|
||||||
|
private const char LF = '\n';
|
||||||
|
private const char ProtocolVersionSeparator = '/';
|
||||||
|
private const string Protocol = "HTTP";
|
||||||
|
private const string HeaderSeparator = ":";
|
||||||
|
private const string ContentLengthHeaderName = "content-length";
|
||||||
|
|
||||||
|
public byte[] SerializeRequest(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
Validate.ArgumentNotNull(request, nameof(request));
|
||||||
|
Validate.ArgumentNotNull(request.RequestUri, nameof(request.RequestUri));
|
||||||
|
|
||||||
|
PreProcessRequest(request);
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
// request-line = method SP request-target SP HTTP-version CRLF
|
||||||
|
builder.Append(request.Method);
|
||||||
|
builder.Append(SP);
|
||||||
|
builder.Append(request.RequestUri.IsAbsoluteUri ? request.RequestUri.PathAndQuery : Uri.EscapeUriString(request.RequestUri.ToString()));
|
||||||
|
builder.Append(SP);
|
||||||
|
builder.Append($"{Protocol}{ProtocolVersionSeparator}");
|
||||||
|
builder.Append(new Version(1, 1).ToString(2));
|
||||||
|
builder.Append(CR);
|
||||||
|
builder.Append(LF);
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
builder.Append(request.Headers);
|
||||||
|
|
||||||
|
if (request.Content != null)
|
||||||
|
{
|
||||||
|
long? contentLength = request.Content.Headers.ContentLength;
|
||||||
|
if (contentLength.HasValue)
|
||||||
|
{
|
||||||
|
request.Content.Headers.ContentLength = contentLength.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(request.Content.Headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers end
|
||||||
|
builder.Append(CR);
|
||||||
|
builder.Append(LF);
|
||||||
|
|
||||||
|
return Encoding.ASCII.GetBytes(builder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> DeserializeResponseAsync(HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var httpResponse = new HttpResponseMessage();
|
||||||
|
await SetResponseStatusLineAsync(httpResponse, bufferedStream, cancellationToken);
|
||||||
|
await SetHeadersAndContentAsync(httpResponse, bufferedStream, cancellationToken);
|
||||||
|
return httpResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SetHeadersAndContentAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IList<string> headers = new List<string>();
|
||||||
|
string line = await bufferedStream.ReadLineAsync(cancellationToken);
|
||||||
|
while (!string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
headers.Add(line);
|
||||||
|
line = await bufferedStream.ReadLineAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.Content = new StreamContent(bufferedStream);
|
||||||
|
foreach (string header in headers)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(header))
|
||||||
|
{
|
||||||
|
// headers end
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int headerSeparatorPosition = header.IndexOf(HeaderSeparator, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (headerSeparatorPosition <= 0)
|
||||||
|
{
|
||||||
|
throw new HttpRequestException($"Header is invalid {header}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string headerName = header.Substring(0, headerSeparatorPosition).Trim();
|
||||||
|
string headerValue = header.Substring(headerSeparatorPosition + 1).Trim();
|
||||||
|
|
||||||
|
bool headerAdded = httpResponse.Headers.TryAddWithoutValidation(headerName, headerValue);
|
||||||
|
if (!headerAdded)
|
||||||
|
{
|
||||||
|
if (string.Equals(headerName, ContentLengthHeaderName, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
if (!long.TryParse(headerValue, out long contentLength))
|
||||||
|
{
|
||||||
|
throw new HttpRequestException($"Header value is invalid for {headerName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await httpResponse.Content.LoadIntoBufferAsync(contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.Content.Headers.TryAddWithoutValidation(headerName, headerValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SetResponseStatusLineAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
string statusLine = await bufferedStream.ReadLineAsync(cancellationToken);
|
||||||
|
if (string.IsNullOrWhiteSpace(statusLine))
|
||||||
|
{
|
||||||
|
throw new HttpRequestException("Response is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] statusLineParts = statusLine.Split(new[] { SP }, 3);
|
||||||
|
if (statusLineParts.Length < 3)
|
||||||
|
{
|
||||||
|
throw new HttpRequestException("Status line is not valid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] httpVersion = statusLineParts[0].Split(new[] { ProtocolVersionSeparator }, 2);
|
||||||
|
if (httpVersion.Length < 2 || !Version.TryParse(httpVersion[1], out Version versionNumber))
|
||||||
|
{
|
||||||
|
throw new HttpRequestException($"Version is not valid {statusLineParts[0]}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.Version = versionNumber;
|
||||||
|
|
||||||
|
if (!Enum.TryParse(statusLineParts[1], out HttpStatusCode statusCode))
|
||||||
|
{
|
||||||
|
throw new HttpRequestException($"StatusCode is not valid {statusLineParts[1]}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.StatusCode = statusCode;
|
||||||
|
httpResponse.ReasonPhrase = statusLineParts[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PreProcessRequest(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(request.Headers.Host))
|
||||||
|
{
|
||||||
|
request.Headers.Host = $"{request.RequestUri.DnsSafeHost}:{request.RequestUri.Port}";
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Headers.ConnectionClose = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unix domain message handler.
|
||||||
|
/// </summary>
|
||||||
|
internal class HttpUdsMessageHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
private readonly Uri providerUri;
|
||||||
|
|
||||||
|
public HttpUdsMessageHandler(Uri providerUri)
|
||||||
|
{
|
||||||
|
Validate.ArgumentNotNull(providerUri, nameof(providerUri));
|
||||||
|
this.providerUri = providerUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Validate.ArgumentNotNull(request, nameof(request));
|
||||||
|
|
||||||
|
using (Socket socket = await this.GetConnectedSocketAsync())
|
||||||
|
{
|
||||||
|
using (var stream = new HttpBufferedStream(new NetworkStream(socket, true)))
|
||||||
|
{
|
||||||
|
var serializer = new HttpRequestResponseSerializer();
|
||||||
|
byte[] requestBytes = serializer.SerializeRequest(request);
|
||||||
|
|
||||||
|
await stream.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken);
|
||||||
|
if (request.Content != null)
|
||||||
|
{
|
||||||
|
await request.Content.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await serializer.DeserializeResponseAsync(stream, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Socket> GetConnectedSocketAsync()
|
||||||
|
{
|
||||||
|
var endpoint = new UnixDomainSocketEndPoint(this.providerUri.LocalPath);
|
||||||
|
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
|
||||||
|
await socket.ConnectAsync(endpoint);
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.EventGridEdge.IotEdge
|
||||||
|
{
|
||||||
|
internal static class Validate
|
||||||
|
{
|
||||||
|
public static void ArgumentNotNull(object value, string paramName)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(paramName, $"The argument {paramName} is null.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ArgumentNotNullOrEmpty(string value, string paramName)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(paramName, $"The argument {paramName} is null.");
|
||||||
|
}
|
||||||
|
else if (value.Length == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(paramName, $"The argument {paramName} is empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче