├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── images
├── migration-scenario.png
└── network-server-scenario.png
└── src
├── IoTHubGateway.Server
├── Constants.cs
├── Controllers
│ └── GatewayController.cs
├── IoTHubGateway.Server.csproj
├── Program.cs
├── ServerOptions.cs
├── Services
│ ├── CloudToMessageListenerJobHostedService.cs
│ ├── DeviceConnectionException.cs
│ ├── GatewayService.cs
│ ├── IGatewayService.cs
│ └── RegisteredDevices.cs
├── Startup.cs
├── appsettings.Development.json
└── appsettings.json
├── IoTHubGateway.sln
├── SampleClients
└── CSharpClient
│ ├── CSharpClient.csproj
│ └── Program.cs
└── Tests
└── IoTHubGateway.Server.Tests
├── GatewayControllerTest.cs
└── IoTHubGateway.Server.Tests.csproj
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | # tools/**
279 | # !tools/packages.config
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: csharp
2 | dist: trusty
3 | mono: none
4 | dotnet: 2.0.0
5 |
6 | install:
7 | - dotnet restore src/
8 |
9 | script:
10 | - dotnet build src/
11 | - dotnet test src/Tests/IoTHubGateway.Server.Tests
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Francisco Beltrao
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Azure IoT Hub AMQP Server Gateway Sample
2 |
3 | Travis: [](https://travis-ci.org/fbeltrao/IoTHubGateway)
4 |
5 | ## Introduction
6 |
7 | The best way to connect any device to IoT Hub when building an IoT solution is to directly connect it using one of the provided [Microsoft Azure IoT Device SDKs](https://github.com/Azure/azure-iot-sdks).
8 |
9 | Azure IoT Hub supports multiple protocols and offers extensive developer integration from REST API to [full device and service SDKs](https://github.com/Azure/azure-iot-sdks) in C, Python, Node.js, Java and .NET. In most cases, there is no need to build your own integration code from scratch.
10 |
11 | In some cases however, for example when you need to do protocol translation or your client devices are simply not capable to connect directly to IoT hub, you need a gateway to bridge the gap.
12 |
13 | If you just need to do protocol translation the right approach is to create a gateway as documented here: https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-protocol-gateway
14 |
15 | If you need a gateway on the edge then the best way to implement this gateway scenario is to use IoT Edge as outlined in these [use cases and patterns](https://docs.microsoft.com/en-us/azure/iot-edge/iot-edge-as-gateway).
16 |
17 | There is another scenario which we want to discuss here and which is not covered in the articles above, where you need to implement an application gateway at the server level and not at the Edge or the gateway may not support IoT Edge due to processing power or memory constraints or due to the lack of container support.
18 |
19 | One of these cases could be a migration scenario where you are unable to change the code running on the devices. In this case you need to implement an intermediary gateway that the devices can connect to without knowing about IoT hub.
20 |
21 | 
22 |
23 | Another use for the server gateway for example is to act as a [LoRaWAN](https://www.lora-alliance.org/technology) to IoT Hub connector. The LoRaWAN network server could include this gateway code to forward all the messages from the connected LoRa devices to IoT Hub.
24 |
25 | 
26 |
27 | ## Note
28 |
29 | The code provided in this sample is not production ready and was built for demonstration and illustration purposes. Hence, the code has sample quality. We have done some basic and scalability testing but no in depth validation.
30 |
31 | ## Solution Approach
32 |
33 | In this sample, we created an ASP.NET Core solution to serve as our gateway.
34 |
35 | It is crucial that a gateway solution should be able to multiplex device connections instead of creating single connections to IoT Hub per device. IoT Hub supports multiplexing only over HTTP and AMQP. For our sample gateway, we decided to use AMQP for its efficiency. The [Azure IoT Hub SDK for .NET](https://github.com/Azure/azure-iot-sdk-csharp) already implements support for connection pooling. Support for the other language SDKs are planned.
36 |
37 | An alternative solution is to implement the same pattern without the use of the SDK and working directly at the AMQP level, here you can find a Node.js implementation: https://github.com/vjrantal/azure-iot-multiplexing-gateway
38 |
39 | ## Flow
40 |
41 | When a device first wants to send a message through the gateway, in our example it calls a REST API, but this could be achieved using any accessible endpoint or protocol that the gateway supports. At this point, the gateway creates a new [DeviceClient](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.deviceclient?view=azure-dotnet) in order to connect to IoT Hub using the connection pool settings "Pooling = true" in [AmqpConnectionPoolSettings](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.amqpconnectionpoolsettings?view=azure-dotnet).
42 |
43 | ```csharp
44 | var newDeviceClient = DeviceClient.Create(
45 | this.gatewayOptions.IoTHubHostName,
46 | auth,
47 | new ITransportSettings[]
48 | {
49 | new AmqpTransportSettings(TransportType.Amqp_Tcp_Only)
50 | {
51 | AmqpConnectionPoolSettings = new AmqpConnectionPoolSettings()
52 | {
53 | Pooling = true
54 | }
55 | }
56 | }
57 | );
58 | ```
59 |
60 | This forces every device client to share the same connection. By default, 995 devices are supported per connection. The SDK will add additional connections to the pool automatically if needed.
61 |
62 | At this point, we are ready to send the message from the gateway to IoT Hub.
63 |
64 | To further optimize our sample, we don't want to create a new [DeviceClient](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.deviceclient?view=azure-dotnet) for every subsequent message the device wants to send. Instead, we keep it in a cache with a time to live (TTL) we can specify. If no new message comes in within this specified time, 1 hour in our case, the cache system will remove the [DeviceClient](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.deviceclient?view=azure-dotnet) from the cache.
65 |
66 | ## Authentication
67 |
68 | In our example, we support two authentication mechanisms. In both cases, Iot Hub will see the message as being sent by the device itself rather than by the gateway.
69 |
70 | ### Authentication using a Shared Access Policy Key
71 |
72 | Using the authentication method [DeviceAuthenticationWithSharedAccessPolicyKey](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithsharedaccesspolicykey?view=azure-dotnet) A single connection string allows the gateway to forward messages from all clients to IoT hub without individual authentication for each of the devices. In this case, the gateway authenticates in behalf of the devices but does not impersonate them. This is the simplest solution, but it's not secure.
73 |
74 | You need to have a custom authentication mechanism in place between the devices and the gateway in this scenario to ensure security. This is useful in the scenario of a LoRaWAN network server, where an authenticaiton mechanism is already in place between the LoRa devices and their LoRaWAN gateway or server.
75 |
76 | ### Authentication using Tokens
77 |
78 | Using the authentication method [DeviceAuthenticationWithToken](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithtoken?view=azure-dotnet) allows every device to authenticate to IoT hub directly, while still retaining the ability to share the connection among devices with pooling. In this case, the device has the SAS Key and generates a temporary token that it sends through the gateway to IoT Hub. IoT Hub is authenticating the device and not the gateway itself, effectively making it an end-to-end authentication. The time-to-live set for the caching needs to be the same as the one for the SAS token.
79 |
80 | ## Sending messages from Cloud to Devices
81 |
82 | ### Direct Method
83 |
84 | IoT Hub allows to do a callback to the connected device by calling a method on the device. For more information see ["Use direct methods"](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-csharp-csharp-direct-methods).
85 |
86 | In the gateway it's possible to listen for this event called by IoT hub. This call is synchronous. Depending on the protocol and the connection between device and our gateway, you need to implement your own callback solution which varies greatly based on the solution architecture and hence is out of the scope of this sample. For demonstration purposes, we simply log the method call to the console. For example, you could call the REST API of the LoRaWAN server from this event handler and pass the message downstream to the device.
87 |
88 | ### Cloud to Device Messages
89 |
90 | For details Cloud to Device messages, see ["Send messages from the cloud to your device with IoT Hub"](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-csharp-csharp-c2d)
91 |
92 | We have implemented a basic Cloud to Device message handling, but this implementation is not optimal as the more devices that are connected the slower it will become. We have run a quick test with 5’000 DeviceClient instances. It is an approach that might not be advised for large device deployments.
93 |
94 | ### Device Twins
95 |
96 | For details on how Device Twins work, see ["Understand and use device twins in IoT Hub"](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-device-twins)
97 |
98 | Support is not implemented in this gateway sample at the moment.
99 |
100 | ## Scaling
101 |
102 | This solution was tested in a single instance, connecting 20'000 devices, sending and receiving messages. No problems were found. Since we open a device connection and keep in memory you should use affinity if you deploy the application in multiple instances, so that each device always communicates with the same instance.
103 |
104 | If you enable cloud to device messages pay attention to the option "CloudMessageParallelism", as it dictate how fast your deployment will handle cloud messages. Direct methods don't have the same problem as the IoT SDK notifies when a direct method call is received.
105 |
106 | ## Configuration Options
107 |
108 | Customization of the gateway is available through the configuration. The available options are:
109 |
110 | |Setting|Description|Default|
111 | |--|--|--|
112 | |DeviceOperationTimeout|Device operation timeout (in milliseconds)|10 seconds (10'000)|
113 | |IoTHubHostName|IoT Hub host name. Something like xxxxx.azure-devices.net|""|
114 | |AccessPolicyName|The IoT Hub access policy name. A common value is iothubowner|""|
115 | |AccessPolicyKey|The IoT Hub access policy key. Get it from Azure Portal|""|
116 | |MaxPoolSize|The maximum pool size|ushort.MaxValue|
117 | |SharedAccessPolicyKeyEnabled|Allows or not the usage of shared access policy keys.If you enable it make sure that access to the API is protected otherwise anyone will be able to impersonate devices|false|
118 | |DefaultDeviceCacheInMinutes|Default device client cache in duration in minutes|60 minutes|
119 | |DirectMethodEnabled|Enable/disables direct method (cloud -> device)|false|
120 | |DirectMethodCallback|Gets/sets the callback to handle device direct methods|null|
121 | |CloudMessagesEnabled|Enable/disables cloud messages in the gateway. Cloud messages are retrieved in a background job|false|
122 | |CloudMessageParallelism|Degree of parallelism used to check for cloud messages|10|
123 | |CloudMessageCallback|Gets/sets the callback to handle cloud messages|null|
124 | ## Contributors
125 |
126 | Sascha Corti, Ronnie Saurenmann, Francisco Beltrao
127 |
--------------------------------------------------------------------------------
/images/migration-scenario.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fbeltrao/IoTHubGateway/94e8bd18e9407470532acede42ed1b7337e13cf2/images/migration-scenario.png
--------------------------------------------------------------------------------
/images/network-server-scenario.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fbeltrao/IoTHubGateway/94e8bd18e9407470532acede42ed1b7337e13cf2/images/network-server-scenario.png
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/Constants.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace IoTHubGateway.Server
7 | {
8 | public static class Constants
9 | {
10 | ///
11 | /// Name of request header containing the device sas token
12 | ///
13 | public const string SasTokenHeaderName = "sas_token";
14 |
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/Controllers/GatewayController.cs:
--------------------------------------------------------------------------------
1 | using IoTHubGateway.Server.Services;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.Extensions.Options;
4 | using System;
5 | using System.Threading.Tasks;
6 |
7 | namespace IoTHubGateway.Server.Controllers
8 | {
9 | [Route("api")]
10 | public class GatewayController : Controller
11 | {
12 | private readonly IGatewayService gatewayService;
13 | private readonly ServerOptions options;
14 | private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
15 |
16 | public GatewayController(IGatewayService gatewayService, ServerOptions options)
17 | {
18 | this.gatewayService = gatewayService;
19 | this.options = options;
20 | }
21 |
22 | ///
23 | /// Sends a message for the given device
24 | ///
25 | /// Device identifier
26 | /// Payload (JSON format)
27 | ///
28 | [HttpPost("{deviceId}")]
29 | public async Task Send(string deviceId, [FromBody] dynamic payload)
30 | {
31 | if (string.IsNullOrEmpty(deviceId))
32 | return BadRequest(new { error = "Missing deviceId" });
33 |
34 | if (payload == null)
35 | return BadRequest(new { error = "Missing payload" });
36 |
37 | var sasToken = this.ControllerContext.HttpContext.Request.Headers[Constants.SasTokenHeaderName].ToString();
38 | if (!string.IsNullOrEmpty(sasToken))
39 | {
40 | var tokenExpirationDate = ResolveTokenExpiration(sasToken);
41 | if (!tokenExpirationDate.HasValue)
42 | tokenExpirationDate = DateTime.UtcNow.AddMinutes(20);
43 |
44 | await gatewayService.SendDeviceToCloudMessageByToken(deviceId, payload.ToString(), sasToken, tokenExpirationDate.Value);
45 | }
46 | else
47 | {
48 | if (!this.options.SharedAccessPolicyKeyEnabled)
49 | return BadRequest(new { error = "Shared access is not enabled" });
50 | await gatewayService.SendDeviceToCloudMessageBySharedAccess(deviceId, payload.ToString());
51 | }
52 |
53 | return Ok();
54 | }
55 |
56 | ///
57 | /// Expirations is available as parameter "se" as a unix time in our sample application
58 | ///
59 | ///
60 | ///
61 | private DateTime? ResolveTokenExpiration(string sasToken)
62 | {
63 | // TODO: Implement in more reliable way (regex or another built-in class)
64 | const string field = "se=";
65 | var index = sasToken.LastIndexOf(field);
66 | if (index >= 0)
67 | {
68 | var unixTime = sasToken.Substring(index + field.Length);
69 | if (int.TryParse(unixTime, out var unixTimeInt))
70 | {
71 | return epoch.AddSeconds(unixTimeInt);
72 | }
73 | }
74 |
75 | return null;
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/IoTHubGateway.Server.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.Logging;
10 |
11 | namespace IoTHubGateway.Server
12 | {
13 | public class Program
14 | {
15 | public static void Main(string[] args)
16 | {
17 | BuildWebHost(args).Run();
18 | }
19 |
20 | public static IWebHost BuildWebHost(string[] args) =>
21 | WebHost.CreateDefaultBuilder(args)
22 | .UseStartup()
23 | .Build();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/ServerOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Devices.Client;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace IoTHubGateway.Server
8 | {
9 | ///
10 | /// Defines the server options
11 | ///
12 | public class ServerOptions
13 | {
14 |
15 | ///
16 | /// Device operation timeout (in milliseconds)
17 | /// Default: 10000 (10 seconds)
18 | ///
19 | public int DeviceOperationTimeout { get; set; } = 1000 * 10;
20 |
21 | ///
22 | /// IoT Hub host name. Something like xxxxx.azure-devices.net
23 | ///
24 | public string IoTHubHostName { get; set; }
25 |
26 | ///
27 | /// The IoT Hub access policy name. A common value is iothubowner
28 | ///
29 | public string AccessPolicyName { get; set; }
30 |
31 | ///
32 | /// The IoT Hub access policy key. Get it from Azure Portal
33 | ///
34 | public string AccessPolicyKey { get; set; }
35 |
36 | ///
37 | /// The maximum pool size (default is )
38 | ///
39 | public int MaxPoolSize { get; set; } = ushort.MaxValue;
40 |
41 | ///
42 | /// Allows or not the usage of shared access policy keys
43 | /// If you enable it make sure that access to the API is protected otherwise anyone will be able to impersonate devices
44 | ///
45 | public bool SharedAccessPolicyKeyEnabled { get; set; }
46 |
47 | ///
48 | /// Default device client cache in duration in minutes
49 | /// 60 minutes by default
50 | ///
51 | public int DefaultDeviceCacheInMinutes = 60;
52 |
53 | ///
54 | /// Enable/disables direct method (cloud -> device)
55 | ///
56 | public bool DirectMethodEnabled { get; set; } = false;
57 |
58 | ///
59 | /// Gets/sets the callback to handle device direct methods
60 | ///
61 | public MethodCallback DirectMethodCallback { get; set; }
62 |
63 | ///
64 | /// Enable/disables cloud messages in the gateway
65 | /// Cloud messages are retrieved in a background job
66 | /// Default: false / disabled
67 | ///
68 | public bool CloudMessagesEnabled { get; set; }
69 |
70 | ///
71 | /// Degree of parallelism used to check for cloud messages
72 | ///
73 | public int CloudMessageParallelism { get; set; } = 10;
74 |
75 |
76 | ///
77 | /// Gets/sets the callback to handle cloud messages
78 | ///
79 | public Action CloudMessageCallback { get; set; }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/Services/CloudToMessageListenerJobHostedService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Devices.Client;
2 | using Microsoft.Extensions.Caching.Memory;
3 | using Microsoft.Extensions.Hosting;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Options;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | namespace IoTHubGateway.Server.Services
13 | {
14 | public class CloudToMessageListenerJobHostedService : IHostedService
15 | {
16 | private readonly IMemoryCache cache;
17 | private readonly ILogger logger;
18 | private readonly RegisteredDevices registeredDevices;
19 | private readonly ServerOptions serverOptions;
20 |
21 | public CloudToMessageListenerJobHostedService(
22 | IMemoryCache cache,
23 | ILogger logger,
24 | RegisteredDevices registeredDevices,
25 | ServerOptions serverOptions)
26 | {
27 | this.cache = cache;
28 | this.logger = logger;
29 | this.registeredDevices = registeredDevices;
30 | this.serverOptions = serverOptions;
31 | }
32 |
33 |
34 | ///
35 | /// Checks for device messages
36 | ///
37 | ///
38 | private void CheckDeviceMessages()
39 | {
40 | var deviceIdList = this.registeredDevices.GetDeviceIdList();
41 | Parallel.ForEach(deviceIdList, new ParallelOptions() { MaxDegreeOfParallelism = serverOptions.CloudMessageParallelism }, (deviceId) =>
42 | {
43 | try
44 | {
45 | if (this.cache.TryGetValue(deviceId, out var deviceClient))
46 | {
47 | var message = deviceClient.ReceiveAsync(TimeSpan.FromMilliseconds(1)).GetAwaiter().GetResult();
48 | if (message != null)
49 | {
50 | try
51 | {
52 | this.serverOptions.CloudMessageCallback(deviceId, message);
53 |
54 | deviceClient.CompleteAsync(message).GetAwaiter().GetResult();
55 | }
56 | catch (Exception handlingMessageException)
57 | {
58 | logger.LogError(handlingMessageException, $"Error handling message from {deviceId}");
59 | }
60 | }
61 | }
62 | }
63 | catch (Exception ex)
64 | {
65 | logger.LogError(ex, $"Error receiving message from {deviceId}");
66 | }
67 | });
68 | }
69 |
70 | ///
71 | /// Starts job
72 | ///
73 | ///
74 | ///
75 | public Task StartAsync(CancellationToken stoppingToken)
76 | {
77 | logger.LogDebug($"{nameof(CloudToMessageListenerJobHostedService)} is starting.");
78 |
79 | if (this.serverOptions.CloudMessageCallback == null)
80 | {
81 | logger.LogInformation($"{nameof(CloudToMessageListenerJobHostedService)} not executing as no handler was defined in {nameof(ServerOptions)}.{nameof(ServerOptions.CloudMessageCallback)}.");
82 | }
83 | else
84 | {
85 | stoppingToken.Register(() => logger.LogDebug($" {nameof(CloudToMessageListenerJobHostedService)} background task is stopping."));
86 |
87 | while (!stoppingToken.IsCancellationRequested)
88 | {
89 | CheckDeviceMessages();
90 | }
91 | }
92 |
93 | logger.LogDebug($"{nameof(CloudToMessageListenerJobHostedService)} background task is stopping.");
94 |
95 | return Task.CompletedTask;
96 | }
97 |
98 | ///
99 | /// Ends the job
100 | ///
101 | ///
102 | ///
103 | public Task StopAsync(CancellationToken stoppingToken)
104 | {
105 | return Task.CompletedTask;
106 | }
107 |
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/Services/DeviceConnectionException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace IoTHubGateway.Server.Services
5 | {
6 | ///
7 | /// Exception raised when the device could not connected
8 | ///
9 | [Serializable]
10 | public class DeviceConnectionException : Exception
11 | {
12 | public DeviceConnectionException()
13 | {
14 | }
15 |
16 | public DeviceConnectionException(string message) : base(message)
17 | {
18 | }
19 |
20 | public DeviceConnectionException(string message, Exception innerException) : base(message, innerException)
21 | {
22 | }
23 |
24 | protected DeviceConnectionException(SerializationInfo info, StreamingContext context) : base(info, context)
25 | {
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/Services/GatewayService.cs:
--------------------------------------------------------------------------------
1 |
2 | using Microsoft.Azure.Devices.Client;
3 | using Microsoft.Extensions.Caching.Memory;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Options;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Text;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 | using System.Web;
13 |
14 | namespace IoTHubGateway.Server.Services
15 | {
16 | ///
17 | /// IoT Hub Device client multiplexer based on AMQP
18 | ///
19 | public class GatewayService : IGatewayService
20 | {
21 | private readonly ServerOptions serverOptions;
22 | private readonly IMemoryCache cache;
23 | private readonly ILogger logger;
24 | RegisteredDevices registeredDevices;
25 |
26 | ///
27 | /// Sliding expiration for each device client connection
28 | /// Default: 30 minutes
29 | ///
30 | public TimeSpan DeviceConnectionCacheSlidingExpiration { get; set; }
31 |
32 | ///
33 | /// Constructor
34 | ///
35 | public GatewayService(ServerOptions serverOptions, IMemoryCache cache, ILogger logger, RegisteredDevices registeredDevices)
36 | {
37 | this.serverOptions = serverOptions;
38 | this.cache = cache;
39 | this.logger = logger;
40 | this.registeredDevices = registeredDevices;
41 | this.DeviceConnectionCacheSlidingExpiration = TimeSpan.FromMinutes(serverOptions.DefaultDeviceCacheInMinutes);
42 | }
43 |
44 | public async Task SendDeviceToCloudMessageByToken(string deviceId, string payload, string sasToken, DateTime tokenExpiration)
45 | {
46 | var deviceClient = await ResolveDeviceClient(deviceId, sasToken, tokenExpiration);
47 | if (deviceClient == null)
48 | throw new DeviceConnectionException($"Failed to connect to device {deviceId}");
49 |
50 | try
51 | {
52 | await deviceClient.SendEventAsync(new Message(Encoding.UTF8.GetBytes(payload))
53 | {
54 | ContentEncoding = "utf-8",
55 | ContentType = "application/json"
56 | });
57 |
58 | this.logger.LogInformation($"Event sent to device {deviceId} using device token. Payload: {payload}");
59 | }
60 | catch (Exception ex)
61 | {
62 | this.logger.LogError(ex, $"Could not send device message to IoT Hub (device: {deviceId})");
63 | throw;
64 | }
65 |
66 | }
67 |
68 | public async Task SendDeviceToCloudMessageBySharedAccess(string deviceId, string payload)
69 | {
70 | var deviceClient = await ResolveDeviceClient(deviceId);
71 |
72 | try
73 | {
74 | await deviceClient.SendEventAsync(new Message(Encoding.UTF8.GetBytes(payload))
75 | {
76 | ContentEncoding = "utf-8",
77 | ContentType = "application/json"
78 | });
79 |
80 | this.logger.LogInformation($"Event sent to device {deviceId} using shared access. Payload: {payload}");
81 | }
82 | catch (Exception ex)
83 | {
84 | this.logger.LogError(ex, $"Could not send device message to IoT Hub (device: {deviceId})");
85 | throw;
86 | }
87 |
88 | }
89 |
90 | private async Task ResolveDeviceClient(string deviceId, string sasToken = null, DateTime? tokenExpiration = null)
91 | {
92 | try
93 | {
94 | var deviceClient = await cache.GetOrCreateAsync(deviceId, async (cacheEntry) =>
95 | {
96 | IAuthenticationMethod auth = null;
97 | if (string.IsNullOrEmpty(sasToken))
98 | {
99 | auth = new DeviceAuthenticationWithSharedAccessPolicyKey(deviceId, this.serverOptions.AccessPolicyName, this.serverOptions.AccessPolicyKey);
100 | }
101 | else
102 | {
103 | auth = new DeviceAuthenticationWithToken(deviceId, sasToken);
104 | }
105 |
106 | var newDeviceClient = DeviceClient.Create(
107 | this.serverOptions.IoTHubHostName,
108 | auth,
109 | new ITransportSettings[]
110 | {
111 | new AmqpTransportSettings(TransportType.Amqp_Tcp_Only)
112 | {
113 | AmqpConnectionPoolSettings = new AmqpConnectionPoolSettings()
114 | {
115 | Pooling = true,
116 | MaxPoolSize = (uint)this.serverOptions.MaxPoolSize,
117 | }
118 | }
119 | }
120 | );
121 |
122 | newDeviceClient.OperationTimeoutInMilliseconds = (uint)this.serverOptions.DeviceOperationTimeout;
123 |
124 | await newDeviceClient.OpenAsync();
125 |
126 | if (this.serverOptions.DirectMethodEnabled)
127 | await newDeviceClient.SetMethodDefaultHandlerAsync(this.serverOptions.DirectMethodCallback, deviceId);
128 |
129 | if (!tokenExpiration.HasValue)
130 | tokenExpiration = DateTime.UtcNow.AddMinutes(this.serverOptions.DefaultDeviceCacheInMinutes);
131 | cacheEntry.SetAbsoluteExpiration(tokenExpiration.Value);
132 | cacheEntry.RegisterPostEvictionCallback(this.CacheEntryRemoved, deviceId);
133 |
134 | this.logger.LogInformation($"Connection to device {deviceId} has been established, valid until {tokenExpiration.Value.ToString()}");
135 |
136 |
137 | registeredDevices.AddDevice(deviceId);
138 |
139 | return newDeviceClient;
140 | });
141 |
142 |
143 | return deviceClient;
144 | }
145 | catch (Exception ex)
146 | {
147 | this.logger.LogError(ex, $"Could not connect device {deviceId}");
148 | }
149 |
150 | return null;
151 | }
152 |
153 | private void CacheEntryRemoved(object key, object value, EvictionReason reason, object state)
154 | {
155 | this.registeredDevices.RemoveDevice(key);
156 | }
157 | }
158 | }
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/Services/IGatewayService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace IoTHubGateway.Server.Services
7 | {
8 | ///
9 | /// Gateway to IoT Hub service
10 | ///
11 | public interface IGatewayService
12 | {
13 | ///
14 | /// Sends device to cloud message using device token
15 | ///
16 | ///
17 | ///
18 | ///
19 | ///
20 | ///
21 | Task SendDeviceToCloudMessageByToken(string deviceId, string payload, string sasToken, DateTime dateTime);
22 |
23 | ///
24 | /// Sends device to cloud message using shared access token
25 | ///
26 | ///
27 | ///
28 | ///
29 | Task SendDeviceToCloudMessageBySharedAccess(string deviceId, string payload);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/Services/RegisteredDevices.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace IoTHubGateway.Server.Services
7 | {
8 | ///
9 | /// Maintains a list of registered devices to enabled cloud messages background job to query them
10 | ///
11 | public class RegisteredDevices
12 | {
13 | System.Collections.Concurrent.ConcurrentDictionary devices = new System.Collections.Concurrent.ConcurrentDictionary();
14 |
15 | public void AddDevice(string deviceId)
16 | {
17 | devices.AddOrUpdate(deviceId, deviceId, (key, existing) =>
18 | {
19 | return deviceId;
20 | });
21 | }
22 |
23 | public void RemoveDevice(object key)
24 | {
25 | devices.Remove(key.ToString(), out var value);
26 | }
27 |
28 | public ICollection GetDeviceIdList()
29 | {
30 | return this.devices.Keys;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using IoTHubGateway.Server.Services;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.Azure.Devices.Client;
10 | using Microsoft.Extensions.Configuration;
11 | using Microsoft.Extensions.DependencyInjection;
12 | using Microsoft.Extensions.Hosting;
13 | using Microsoft.Extensions.Logging;
14 | using Microsoft.Extensions.Options;
15 |
16 | namespace IoTHubGateway.Server
17 | {
18 | public class Startup
19 | {
20 | public Startup(IConfiguration configuration)
21 | {
22 | Configuration = configuration;
23 | }
24 |
25 | public IConfiguration Configuration { get; }
26 |
27 | // This method gets called by the runtime. Use this method to add services to the container.
28 | public void ConfigureServices(IServiceCollection services)
29 | {
30 | services.AddOptions();
31 | services.AddMemoryCache();
32 |
33 | // A single instance of registered devices must be kept
34 | services.AddSingleton();
35 |
36 | // Resolve server options
37 | var options = new ServerOptions();
38 | Configuration.GetSection(nameof(ServerOptions)).Bind(options);
39 | services.AddSingleton(options);
40 |
41 |
42 | if (options.CloudMessagesEnabled)
43 | {
44 | services.AddSingleton();
45 | }
46 |
47 | #if DEBUG
48 | SetupDebugListeners(options);
49 |
50 | #endif
51 |
52 | services.AddSingleton();
53 | services.AddMvc();
54 | }
55 |
56 | #if DEBUG
57 | private void SetupDebugListeners(ServerOptions options)
58 | {
59 | if (options.DirectMethodEnabled && options.DirectMethodCallback == null)
60 | {
61 | options.DirectMethodCallback = (methodRequest, userContext) =>
62 | {
63 | var deviceId = (string)userContext;
64 | Console.WriteLine($"[{DateTime.Now.ToString()}] Device method for {deviceId}.{methodRequest.Name}({methodRequest.DataAsJson}) received");
65 |
66 | var responseBody = "{ succeeded: true }";
67 | MethodResponse methodResponse = new MethodResponse(Encoding.UTF8.GetBytes(responseBody), 200);
68 |
69 | return Task.FromResult(methodResponse);
70 | };
71 | }
72 |
73 | if (options.CloudMessagesEnabled && options.CloudMessageCallback == null)
74 | {
75 | options.CloudMessageCallback = (deviceId, message) =>
76 | {
77 | Console.WriteLine($"[{DateTime.Now.ToString()}] Cloud message for {deviceId} received");
78 | };
79 | }
80 | }
81 | #endif
82 |
83 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
84 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
85 | {
86 | if (env.IsDevelopment())
87 | {
88 | app.UseDeveloperExceptionPage();
89 | }
90 |
91 |
92 | app.UseMvc();
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "IncludeScopes": false,
4 | "LogLevel": {
5 | "Default": "Debug",
6 | "System": "Information",
7 | "Microsoft": "Information"
8 | }
9 | },
10 |
11 | "ServerOptions": {
12 | "IoTHubHostName": "xxx.azure-devices.net",
13 | "AccessPolicyName": "iothubowner",
14 | "AccessPolicyKey": "xxxxxxx",
15 | "SharedAccessPolicyKeyEnabled": true,
16 | "DirectMethodEnabled": true,
17 | "CloudMessagesEnabled": true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.Server/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "IncludeScopes": false,
4 | "Debug": {
5 | "LogLevel": {
6 | "Default": "Warning"
7 | }
8 | },
9 | "Console": {
10 | "LogLevel": {
11 | "Default": "Warning"
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/IoTHubGateway.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27428.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTHubGateway.Server", "IoTHubGateway.Server\IoTHubGateway.Server.csproj", "{B274B563-F76B-46D2-B5E9-FCEEA34A3C59}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SampleClients", "SampleClients", "{BBBB9EA1-FD6A-4F5B-89B6-3CB4775897C5}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D57B650E-3B97-426D-B057-17D73853817E}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTHubGateway.Server.Tests", "Tests\IoTHubGateway.Server.Tests\IoTHubGateway.Server.Tests.csproj", "{B65CC92E-61D7-414A-9E8E-554BE4F12910}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpClient", "SampleClients\CSharpClient\CSharpClient.csproj", "{07873032-0046-4BFE-A40D-251AA805168C}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Release|Any CPU = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {B274B563-F76B-46D2-B5E9-FCEEA34A3C59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {B274B563-F76B-46D2-B5E9-FCEEA34A3C59}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {B274B563-F76B-46D2-B5E9-FCEEA34A3C59}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {B274B563-F76B-46D2-B5E9-FCEEA34A3C59}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {B65CC92E-61D7-414A-9E8E-554BE4F12910}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {B65CC92E-61D7-414A-9E8E-554BE4F12910}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {B65CC92E-61D7-414A-9E8E-554BE4F12910}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {B65CC92E-61D7-414A-9E8E-554BE4F12910}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {07873032-0046-4BFE-A40D-251AA805168C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {07873032-0046-4BFE-A40D-251AA805168C}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {07873032-0046-4BFE-A40D-251AA805168C}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {07873032-0046-4BFE-A40D-251AA805168C}.Release|Any CPU.Build.0 = Release|Any CPU
34 | EndGlobalSection
35 | GlobalSection(SolutionProperties) = preSolution
36 | HideSolutionNode = FALSE
37 | EndGlobalSection
38 | GlobalSection(NestedProjects) = preSolution
39 | {B65CC92E-61D7-414A-9E8E-554BE4F12910} = {D57B650E-3B97-426D-B057-17D73853817E}
40 | {07873032-0046-4BFE-A40D-251AA805168C} = {BBBB9EA1-FD6A-4F5B-89B6-3CB4775897C5}
41 | EndGlobalSection
42 | GlobalSection(ExtensibilityGlobals) = postSolution
43 | EnterpriseLibraryConfigurationToolBinariesPathV6 = packages\EnterpriseLibrary.TransientFaultHandling.6.0.1304.0\lib\portable-net45+win+wp8
44 | SolutionGuid = {1F383F61-9ECE-48E3-913B-0C55B9AF3629}
45 | EndGlobalSection
46 | EndGlobal
47 |
--------------------------------------------------------------------------------
/src/SampleClients/CSharpClient/CSharpClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp2.0
6 | latest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/SampleClients/CSharpClient/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Devices.Client;
2 | using System;
3 | using System.Net.Http;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace CSharpClient
8 | {
9 | class Program
10 | {
11 | static async Task Main(string[] args)
12 | {
13 | Console.WriteLine("");
14 | Console.ReadLine();
15 | var hostName = "";
16 | var deviceId = "";
17 |
18 | var sasToken = new SharedAccessSignatureBuilder()
19 | {
20 | Key = "",
21 | Target = $"{hostName}.azure-devices.net/devices/{deviceId}",
22 | TimeToLive = TimeSpan.FromMinutes(20)
23 | }
24 | .ToSignature();
25 |
26 | using (var client = new HttpClient())
27 | {
28 | client.DefaultRequestHeaders.Add("sas_token", sasToken);
29 | while (true)
30 | {
31 | var postResponse = await client.PostAsync($"http://localhost:32527/api/{deviceId}", new StringContent("{ content: 'from_rest_call' }", Encoding.UTF8, "application/json"));
32 | Console.WriteLine($"Response: {postResponse.StatusCode.ToString()}");
33 |
34 | await Task.Delay(200);
35 |
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Tests/IoTHubGateway.Server.Tests/GatewayControllerTest.cs:
--------------------------------------------------------------------------------
1 | using IoTHubGateway.Server.Controllers;
2 | using IoTHubGateway.Server.Services;
3 | using Microsoft.AspNetCore.Http;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Microsoft.Extensions.Options;
6 | using Moq;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Net.Http;
10 | using System.Text;
11 | using System.Threading.Tasks;
12 | using Xunit;
13 |
14 |
15 | namespace IoTHubGateway.Server.Tests
16 | {
17 | public class GatewayControllerTest
18 | {
19 | private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
20 |
21 | GatewayController SetupWithSasToken(GatewayController controller, string sas_token)
22 | {
23 | var controllerContext = new ControllerContext();
24 | var httpContext = new DefaultHttpContext();
25 | httpContext.Request.Headers.Add(Constants.SasTokenHeaderName, sas_token);
26 | controller.ControllerContext.HttpContext = httpContext;
27 |
28 | return controller;
29 | }
30 |
31 | [Fact]
32 | public async Task Send_WithoutDeviceToken_ReturnsBadRequest_If_SharedAccessPolicy_Is_Not_Enabled()
33 | {
34 | var gatewayService = new Mock();
35 | var options = new ServerOptions()
36 | {
37 | SharedAccessPolicyKeyEnabled = false,
38 | };
39 |
40 | var target = new GatewayController(gatewayService.Object, options);
41 | target.ControllerContext.HttpContext = new DefaultHttpContext();
42 |
43 | var result = await target.Send("device-1", new { payload = 1 });
44 | Assert.IsType(result);
45 | }
46 |
47 |
48 | [Fact]
49 | public async Task Send_WithDeviceToken_ReturnsOk_If_SharedAccessPolicy_Is_Not_Enabled()
50 | {
51 | var gatewayService = new Mock();
52 | var options = new ServerOptions()
53 | {
54 | SharedAccessPolicyKeyEnabled = false,
55 | };
56 |
57 | var target = new GatewayController(gatewayService.Object, options);
58 | target.ControllerContext.HttpContext = new DefaultHttpContext();
59 | target.ControllerContext.HttpContext.Request.Headers[Constants.SasTokenHeaderName] = "a-token";
60 |
61 | var result = await target.Send("device-1", new { payload = 1 });
62 | Assert.IsType(result);
63 | }
64 |
65 |
66 | [Fact]
67 | public async Task Send_WithoutDeviceToken_SendsUsingSharedAccessPolicy()
68 | {
69 | var gatewayService = new Mock();
70 | gatewayService.Setup(x => x.SendDeviceToCloudMessageBySharedAccess(It.IsAny(), It.IsAny()))
71 | .Returns(Task.CompletedTask)
72 | .Verifiable();
73 | var options = new ServerOptions()
74 | {
75 | SharedAccessPolicyKeyEnabled = true,
76 | };
77 |
78 | var target = new GatewayController(gatewayService.Object, options);
79 | target.ControllerContext.HttpContext = new DefaultHttpContext();
80 |
81 | var result = await target.Send("device-1", new { payload = 1 });
82 | Assert.IsType(result);
83 | gatewayService.Verify();
84 | }
85 |
86 | [Fact]
87 | public async Task Send_WithDeviceToken_SendsUsingSharedAccessPolicy()
88 | {
89 | var gatewayService = new Mock();
90 | gatewayService.Setup(x => x.SendDeviceToCloudMessageByToken(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
91 | .Returns(Task.CompletedTask)
92 | .Verifiable();
93 |
94 | var options = new ServerOptions()
95 | {
96 | SharedAccessPolicyKeyEnabled = true,
97 | };
98 |
99 | var target = new GatewayController(gatewayService.Object, options);
100 | target.ControllerContext.HttpContext = new DefaultHttpContext();
101 | target.ControllerContext.HttpContext.Request.Headers.Add(Constants.SasTokenHeaderName, "a-token");
102 |
103 | var result = await target.Send("device-1", new { payload = 1 });
104 | Assert.IsType(result);
105 | gatewayService.Verify();
106 | }
107 |
108 |
109 | ///
110 | /// Gateway accepts expired tokens as it needs to take into account grace period / clock disparities
111 | ///
112 | ///
113 | [Fact]
114 | public async Task Send_WithDeviceToken_And_TokenExpirationDateInPast_Returns_Ok()
115 | {
116 | var gatewayService = new Mock();
117 | var options = new ServerOptions()
118 | {
119 | SharedAccessPolicyKeyEnabled = false,
120 | };
121 |
122 | var target = new GatewayController(gatewayService.Object, options);
123 | target.ControllerContext.HttpContext = new DefaultHttpContext();
124 | target.ControllerContext.HttpContext.Request.Headers.Add(Constants.SasTokenHeaderName, $"a-token&se={((long)DateTime.UtcNow.AddDays(-1).Subtract(epoch).TotalSeconds).ToString()}");
125 |
126 | var result = await target.Send("device-1", new { payload = 1 });
127 | Assert.IsType(result);
128 | }
129 |
130 |
131 | [Fact]
132 | public async Task Send_WithDeviceToken_And_ValidTokenExpirationDate_Returns_OK()
133 | {
134 | var gatewayService = new Mock();
135 |
136 | var tokenExpirationDate = DateTime.UtcNow.AddMinutes(10);
137 |
138 | gatewayService.Setup(x => x.SendDeviceToCloudMessageByToken(It.IsAny(), It.IsAny(), It.IsAny(), It.Is(v => IsInSameSecond(v, tokenExpirationDate))))
139 | .Returns(Task.CompletedTask)
140 | .Verifiable();
141 |
142 | var options = new ServerOptions()
143 | {
144 | SharedAccessPolicyKeyEnabled = false,
145 | };
146 |
147 | var target = new GatewayController(gatewayService.Object, options);
148 | target.ControllerContext.HttpContext = new DefaultHttpContext();
149 | target.ControllerContext.HttpContext.Request.Headers.Add(Constants.SasTokenHeaderName, $"a-token&se={((long)tokenExpirationDate.Subtract(epoch).TotalSeconds).ToString()}");
150 |
151 | var result = await target.Send("device-1", new { payload = 1 });
152 | Assert.IsType(result);
153 | gatewayService.Verify();
154 | }
155 |
156 | private bool IsInSameSecond(DateTime date1, DateTime date2)
157 | {
158 | return date1.Subtract(date2).TotalSeconds < 1.0;
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Tests/IoTHubGateway.Server.Tests/IoTHubGateway.Server.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------