Azure Event Grid Edge Samples, SDK

This commit is contained in:
Vidya Kukke 2019-10-25 10:49:30 -07:00
Коммит 786c9743d5
87 изменённых файлов: 4992 добавлений и 0 удалений

330
.gitignore поставляемый Normal file
Просмотреть файл

@ -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"]

21
LICENSE Normal file
Просмотреть файл

@ -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/ .

14
README.md Normal file
Просмотреть файл

@ -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; }
}
}

17
SDK/Contracts/Topic.cs Normal file
Просмотреть файл

@ -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>";
}
}
}
}

105
SDK/EventGridEdgeClient.cs Normal file
Просмотреть файл

@ -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);
}
}
}
}

97
SDK/EventsAPI.cs Normal file
Просмотреть файл

@ -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);
}
}
}
}
}
}

32
SDK/Extensions.cs Normal file
Просмотреть файл

@ -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);
}
}
}
}

44
SDK/SDK.csproj Normal file
Просмотреть файл

@ -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>

66
SDK/SubscriptionsAPI.cs Normal file
Просмотреть файл

@ -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);
}
}
}
}

66
SDK/TopicsAPI.cs Normal file
Просмотреть файл

@ -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.");
}
}
}
}