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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче