diff --git a/MQTTnet.sln b/MQTTnet.sln
index 4504baaa..a7d20079 100644
--- a/MQTTnet.sln
+++ b/MQTTnet.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.28729.10
+# Visual Studio 15
+VisualStudioVersion = 15.0.28307.645
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Tests", "Tests\MQTTnet.Core.Tests\MQTTnet.Tests.csproj", "{A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}"
EndProject
diff --git a/Source/MQTTnet.Server/Configuration/SettingsModel.cs b/Source/MQTTnet.Server/Configuration/MqttSettingsModel.cs
similarity index 96%
rename from Source/MQTTnet.Server/Configuration/SettingsModel.cs
rename to Source/MQTTnet.Server/Configuration/MqttSettingsModel.cs
index ce5336d2..8faa8087 100644
--- a/Source/MQTTnet.Server/Configuration/SettingsModel.cs
+++ b/Source/MQTTnet.Server/Configuration/MqttSettingsModel.cs
@@ -1,9 +1,9 @@
namespace MQTTnet.Server.Configuration
{
///
- /// Main Settings Model
+ /// MQTT settings Model
///
- public class SettingsModel
+ public class MqttSettingsModel
{
///
/// Set default connection timeout in seconds
diff --git a/Source/MQTTnet.Server/Configuration/RetainedApplicationMessagesModel.cs b/Source/MQTTnet.Server/Configuration/RetainedApplicationMessagesModel.cs
index 7abae83f..a9342177 100644
--- a/Source/MQTTnet.Server/Configuration/RetainedApplicationMessagesModel.cs
+++ b/Source/MQTTnet.Server/Configuration/RetainedApplicationMessagesModel.cs
@@ -6,6 +6,6 @@
public int WriteInterval { get; set; } = 10;
- public string Filename { get; set; } = "RetainedApplicationMessages.json";
+ public string Path { get; set; }
}
}
diff --git a/Source/MQTTnet.Server/Configuration/ScriptingSettingsModel.cs b/Source/MQTTnet.Server/Configuration/ScriptingSettingsModel.cs
new file mode 100644
index 00000000..bb71a152
--- /dev/null
+++ b/Source/MQTTnet.Server/Configuration/ScriptingSettingsModel.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MQTTnet.Server.Configuration
+{
+ public class ScriptingSettingsModel
+ {
+ public List IncludePaths { get; set; }
+ }
+}
diff --git a/Source/MQTTnet.Server/Controllers/ClientsController.cs b/Source/MQTTnet.Server/Controllers/ClientsController.cs
index afc75119..6898375b 100644
--- a/Source/MQTTnet.Server/Controllers/ClientsController.cs
+++ b/Source/MQTTnet.Server/Controllers/ClientsController.cs
@@ -4,12 +4,14 @@ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MQTTnet.Server.Mqtt;
using MQTTnet.Server.Status;
namespace MQTTnet.Server.Controllers
{
+ [Authorize]
[ApiController]
public class ClientsController : ControllerBase
{
@@ -57,5 +59,21 @@ namespace MQTTnet.Server.Controllers
await client.DisconnectAsync();
return StatusCode((int)HttpStatusCode.NoContent);
}
+
+ [Route("api/v1/clients/{clientId}/statistics")]
+ [HttpDelete]
+ public async Task DeleteClientStatistics(string clientId)
+ {
+ clientId = HttpUtility.UrlDecode(clientId);
+
+ var client = (await _mqttServerService.GetClientStatusAsync()).FirstOrDefault(c => c.ClientId == clientId);
+ if (client == null)
+ {
+ return new StatusCodeResult((int)HttpStatusCode.NotFound);
+ }
+
+ client.ResetStatistics();
+ return StatusCode((int)HttpStatusCode.NoContent);
+ }
}
}
diff --git a/Source/MQTTnet.Server/Controllers/RetainedApplicationMessagesController.cs b/Source/MQTTnet.Server/Controllers/RetainedApplicationMessagesController.cs
index 30110b38..9c9f2734 100644
--- a/Source/MQTTnet.Server/Controllers/RetainedApplicationMessagesController.cs
+++ b/Source/MQTTnet.Server/Controllers/RetainedApplicationMessagesController.cs
@@ -4,11 +4,13 @@ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MQTTnet.Server.Mqtt;
namespace MQTTnet.Server.Controllers
{
+ [Authorize]
[ApiController]
public class RetainedApplicationMessagesController : ControllerBase
{
diff --git a/Source/MQTTnet.Server/Controllers/ScriptsController.cs b/Source/MQTTnet.Server/Controllers/ScriptsController.cs
index 1b2a7032..23cff06a 100644
--- a/Source/MQTTnet.Server/Controllers/ScriptsController.cs
+++ b/Source/MQTTnet.Server/Controllers/ScriptsController.cs
@@ -1,11 +1,65 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using MQTTnet.Server.Scripting;
+using MQTTnet.Server.Web;
namespace MQTTnet.Server.Controllers
{
- public class ScriptsController
+ [Authorize]
+ [ApiController]
+ public class ScriptsController : Controller
{
+ private readonly PythonScriptHostService _pythonScriptHostService;
+
+ public ScriptsController(PythonScriptHostService pythonScriptHostService)
+ {
+ _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService));
+ }
+
+ [Route("api/v1/scripts")]
+ [HttpGet]
+ public ActionResult> GetScriptUids()
+ {
+ return _pythonScriptHostService.GetScriptUids();
+ }
+
+ [Route("api/v1/scripts/uid")]
+ [HttpGet]
+ public async Task> GetScript(string uid)
+ {
+ var script = await _pythonScriptHostService.ReadScriptAsync(uid, HttpContext.RequestAborted);
+ if (script == null)
+ {
+ return NotFound();
+ }
+
+ return Content(script);
+ }
+
+ [Route("api/v1/scripts/uid")]
+ [HttpPost]
+ public Task PostScript(string uid)
+ {
+ var code = HttpContext.Request.ReadBodyAsString();
+ return _pythonScriptHostService.WriteScriptAsync(uid, code, CancellationToken.None);
+ }
+
+ [Route("api/v1/scripts/uid")]
+ [HttpDelete]
+ public Task DeleteScript(string uid)
+ {
+ return _pythonScriptHostService.DeleteScriptAsync(uid);
+ }
+
+ [Route("api/v1/scripts/initialize")]
+ [HttpPost]
+ public Task PostInitializeScripts()
+ {
+ return _pythonScriptHostService.TryInitializeScriptsAsync();
+ }
}
}
diff --git a/Source/MQTTnet.Server/Controllers/SessionsController.cs b/Source/MQTTnet.Server/Controllers/SessionsController.cs
index 415d4b9c..463c0047 100644
--- a/Source/MQTTnet.Server/Controllers/SessionsController.cs
+++ b/Source/MQTTnet.Server/Controllers/SessionsController.cs
@@ -4,12 +4,14 @@ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MQTTnet.Server.Mqtt;
using MQTTnet.Server.Status;
namespace MQTTnet.Server.Controllers
{
+ [Authorize]
[ApiController]
public class SessionsController : ControllerBase
{
diff --git a/Source/MQTTnet.Server/MQTTnet.Server.csproj b/Source/MQTTnet.Server/MQTTnet.Server.csproj
index 8d150a39..edf5e274 100644
--- a/Source/MQTTnet.Server/MQTTnet.Server.csproj
+++ b/Source/MQTTnet.Server/MQTTnet.Server.csproj
@@ -20,6 +20,7 @@
+ c564f0de-28b4-45bf-b726-4d665d705653
@@ -28,6 +29,13 @@
1701;1702
+
+
+
+
+
+
+
Always
@@ -54,23 +62,23 @@
Always
-
+
Always
-
- Always
-
-
+
Always
Always
+
+ Always
+
-
+
diff --git a/Source/MQTTnet.Server/Mqtt/CustomMqttFactory.cs b/Source/MQTTnet.Server/Mqtt/CustomMqttFactory.cs
index 06bf5467..3eba2ca4 100644
--- a/Source/MQTTnet.Server/Mqtt/CustomMqttFactory.cs
+++ b/Source/MQTTnet.Server/Mqtt/CustomMqttFactory.cs
@@ -12,7 +12,7 @@ namespace MQTTnet.Server.Mqtt
{
private readonly MqttFactory _mqttFactory;
- public CustomMqttFactory(SettingsModel settings, ILogger logger)
+ public CustomMqttFactory(MqttSettingsModel settings, ILogger logger)
{
if (settings == null) throw new ArgumentNullException(nameof(settings));
if (logger == null) throw new ArgumentNullException(nameof(logger));
diff --git a/Source/MQTTnet.Server/Mqtt/MqttServerService.cs b/Source/MQTTnet.Server/Mqtt/MqttServerService.cs
index 42ec60b8..ce340313 100644
--- a/Source/MQTTnet.Server/Mqtt/MqttServerService.cs
+++ b/Source/MQTTnet.Server/Mqtt/MqttServerService.cs
@@ -23,7 +23,7 @@ namespace MQTTnet.Server.Mqtt
{
private readonly ILogger _logger;
- private readonly SettingsModel _settings;
+ private readonly MqttSettingsModel _settings;
private readonly MqttApplicationMessageInterceptor _mqttApplicationMessageInterceptor;
private readonly MqttServerStorage _mqttServerStorage;
private readonly MqttClientConnectedHandler _mqttClientConnectedHandler;
@@ -37,7 +37,7 @@ namespace MQTTnet.Server.Mqtt
private readonly MqttWebSocketServerAdapter _webSocketServerAdapter;
public MqttServerService(
- SettingsModel settings,
+ MqttSettingsModel mqttSettings,
CustomMqttFactory mqttFactory,
MqttClientConnectedHandler mqttClientConnectedHandler,
MqttClientDisconnectedHandler mqttClientDisconnectedHandler,
@@ -50,7 +50,7 @@ namespace MQTTnet.Server.Mqtt
PythonScriptHostService pythonScriptHostService,
ILogger logger)
{
- _settings = settings ?? throw new ArgumentNullException(nameof(settings));
+ _settings = mqttSettings ?? throw new ArgumentNullException(nameof(mqttSettings));
_mqttClientConnectedHandler = mqttClientConnectedHandler ?? throw new ArgumentNullException(nameof(mqttClientConnectedHandler));
_mqttClientDisconnectedHandler = mqttClientDisconnectedHandler ?? throw new ArgumentNullException(nameof(mqttClientDisconnectedHandler));
_mqttClientSubscribedTopicHandler = mqttClientSubscribedTopicHandler ?? throw new ArgumentNullException(nameof(mqttClientSubscribedTopicHandler));
diff --git a/Source/MQTTnet.Server/Mqtt/MqttServerStorage.cs b/Source/MQTTnet.Server/Mqtt/MqttServerStorage.cs
index 39b65d51..49875be9 100644
--- a/Source/MQTTnet.Server/Mqtt/MqttServerStorage.cs
+++ b/Source/MQTTnet.Server/Mqtt/MqttServerStorage.cs
@@ -14,27 +14,28 @@ namespace MQTTnet.Server.Mqtt
{
private readonly List _messages = new List();
- private readonly SettingsModel _settings;
+ private readonly MqttSettingsModel _mqttSettings;
private readonly ILogger _logger;
- private string _filename;
+ private string _path;
private bool _messagesHaveChanged;
- public MqttServerStorage(SettingsModel settings, ILogger logger)
+ public MqttServerStorage(MqttSettingsModel mqttSettings, ILogger logger)
{
- _settings = settings ?? throw new ArgumentNullException(nameof(settings));
+ _mqttSettings = mqttSettings ?? throw new ArgumentNullException(nameof(mqttSettings));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void Configure()
{
- if (_settings.RetainedApplicationMessages?.Persist != true)
+ if (_mqttSettings.RetainedApplicationMessages?.Persist != true ||
+ string.IsNullOrEmpty(_mqttSettings.RetainedApplicationMessages.Path))
{
_logger.LogInformation("Persisting of retained application messages is disabled.");
return;
}
- _filename = ExpandFilename();
+ _path = ExpandPath();
// The retained application messages are stored in a separate thread.
// This is mandatory because writing them to a slow storage (like RaspberryPi SD card)
@@ -61,7 +62,7 @@ namespace MQTTnet.Server.Mqtt
{
try
{
- await Task.Delay(TimeSpan.FromSeconds(_settings.RetainedApplicationMessages.WriteInterval)).ConfigureAwait(false);
+ await Task.Delay(TimeSpan.FromSeconds(_mqttSettings.RetainedApplicationMessages.WriteInterval)).ConfigureAwait(false);
List messages;
lock (_messages)
@@ -76,7 +77,7 @@ namespace MQTTnet.Server.Mqtt
}
var json = JsonConvert.SerializeObject(messages);
- await File.WriteAllTextAsync(_filename, json, Encoding.UTF8).ConfigureAwait(false);
+ await File.WriteAllTextAsync(_path, json, Encoding.UTF8).ConfigureAwait(false);
_logger.LogInformation($"{messages.Count} retained MQTT messages written.");
}
@@ -89,19 +90,19 @@ namespace MQTTnet.Server.Mqtt
public async Task> LoadRetainedMessagesAsync()
{
- if (_settings.RetainedApplicationMessages?.Persist != true)
+ if (_mqttSettings.RetainedApplicationMessages?.Persist != true)
{
return null;
}
- if (!File.Exists(_filename))
+ if (!File.Exists(_path))
{
return null;
}
try
{
- var json = await File.ReadAllTextAsync(_filename).ConfigureAwait(false);
+ var json = await File.ReadAllTextAsync(_path).ConfigureAwait(false);
var applicationMessages = JsonConvert.DeserializeObject>(json);
_logger.LogInformation($"{applicationMessages.Count} retained MQTT messages loaded.");
@@ -115,17 +116,17 @@ namespace MQTTnet.Server.Mqtt
}
}
- private string ExpandFilename()
+ private string ExpandPath()
{
- var filename = _settings.RetainedApplicationMessages.Filename;
+ var path = _mqttSettings.RetainedApplicationMessages.Path;
- var uri = new Uri(filename, UriKind.RelativeOrAbsolute);
+ var uri = new Uri(path, UriKind.RelativeOrAbsolute);
if (!uri.IsAbsoluteUri)
{
- filename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filename);
+ path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path);
}
- return filename;
+ return path;
}
}
}
diff --git a/Source/MQTTnet.Server/Program.cs b/Source/MQTTnet.Server/Program.cs
index 69b09499..f5df1d02 100644
--- a/Source/MQTTnet.Server/Program.cs
+++ b/Source/MQTTnet.Server/Program.cs
@@ -2,6 +2,7 @@
using System.Reflection;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
+using MQTTnet.Server.Web;
namespace MQTTnet.Server
{
diff --git a/Source/MQTTnet.Server/README.md b/Source/MQTTnet.Server/README.md
new file mode 100644
index 00000000..782ba959
--- /dev/null
+++ b/Source/MQTTnet.Server/README.md
@@ -0,0 +1,10 @@
+# Starting portable version
+The portable version requires a local installation of the .net core runtime. With that runtime installed the server can be started via the following comand.
+
+dotnet .\MQTTnet.Server.dll
+
+# Starting self contained versions
+The self contained versions are fully portable versions including the .net core runtime. The server can be started using the contained executable files.
+
+Windows: MQTTnet.Server.exe
+Linux: MQTTnet.Server (must be set to _executable_ first)
diff --git a/Source/MQTTnet.Server/RetainedApplicationMessages.json b/Source/MQTTnet.Server/RetainedApplicationMessages.json
deleted file mode 100644
index 04c16db3..00000000
--- a/Source/MQTTnet.Server/RetainedApplicationMessages.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"Topic":"a","Payload":"YWFzYXNhc2E=","QualityOfServiceLevel":0,"Retain":true,"UserProperties":[],"ContentType":null,"ResponseTopic":null,"PayloadFormatIndicator":null,"MessageExpiryInterval":null,"TopicAlias":null,"CorrelationData":null,"SubscriptionIdentifier":null},{"Topic":"fgdfgd","Payload":"YWFzYXNhc2E=","QualityOfServiceLevel":0,"Retain":true,"UserProperties":[],"ContentType":null,"ResponseTopic":null,"PayloadFormatIndicator":null,"MessageExpiryInterval":null,"TopicAlias":null,"CorrelationData":null,"SubscriptionIdentifier":null}]
\ No newline at end of file
diff --git a/Source/MQTTnet.Server/Scripting/PythonScriptHostService.cs b/Source/MQTTnet.Server/Scripting/PythonScriptHostService.cs
index 1acc58c4..23cb531a 100644
--- a/Source/MQTTnet.Server/Scripting/PythonScriptHostService.cs
+++ b/Source/MQTTnet.Server/Scripting/PythonScriptHostService.cs
@@ -4,9 +4,12 @@ using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;
+using MQTTnet.Server.Configuration;
namespace MQTTnet.Server.Scripting
{
@@ -14,11 +17,14 @@ namespace MQTTnet.Server.Scripting
{
private readonly IDictionary _proxyObjects = new ExpandoObject();
private readonly List _scriptInstances = new List();
+ private readonly string _scriptsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Scripts");
+ private readonly ScriptingSettingsModel _scriptingSettings;
private readonly ILogger _logger;
private readonly ScriptEngine _scriptEngine;
- public PythonScriptHostService(PythonIOStream pythonIOStream, ILogger logger)
+ public PythonScriptHostService(ScriptingSettingsModel scriptingSettings, PythonIOStream pythonIOStream, ILogger logger)
{
+ _scriptingSettings = scriptingSettings ?? throw new ArgumentNullException(nameof(scriptingSettings));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_scriptEngine = IronPython.Hosting.Python.CreateEngine();
@@ -29,11 +35,7 @@ namespace MQTTnet.Server.Scripting
{
AddSearchPaths(_scriptEngine);
- var scriptsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Scripts");
- foreach (var filename in Directory.GetFiles(scriptsDirectory, "*.py", SearchOption.AllDirectories).OrderBy(file => file))
- {
- TryInitializeScript(filename);
- }
+ TryInitializeScriptsAsync().GetAwaiter().GetResult();
}
public void RegisterProxyObject(string name, object @object)
@@ -58,35 +60,109 @@ namespace MQTTnet.Server.Scripting
}
catch (Exception exception)
{
- _logger.LogError(exception, $"Error while invoking function '{name}' at script '{pythonScriptInstance.Name}'.");
+ _logger.LogError(exception, $"Error while invoking function '{name}' at script '{pythonScriptInstance.Uid}'.");
}
}
}
}
- private void TryInitializeScript(string filename)
+ public List GetScriptUids()
{
- try
+ lock (_scriptInstances)
{
- var scriptName = new FileInfo(filename).Name;
-
- _logger.LogTrace($"Initializing Python script '{scriptName}'...");
- var code = File.ReadAllText(filename);
-
- var scriptInstance = CreateScriptInstance(scriptName, code);
- scriptInstance.InvokeOptionalFunction("initialize");
-
- _scriptInstances.Add(scriptInstance);
-
- _logger.LogInformation($"Initialized script '{scriptName}'.");
- }
- catch (Exception exception)
- {
- _logger.LogError(exception, $"Error while initializing script '{new FileInfo(filename).Name}'.");
+ return _scriptInstances.Select(si => si.Uid).ToList();
}
}
- private PythonScriptInstance CreateScriptInstance(string name, string code)
+ public Task ReadScriptAsync(string uid, CancellationToken cancellationToken)
+ {
+ if (uid == null) throw new ArgumentNullException(nameof(uid));
+
+ string path;
+
+ lock (_scriptInstances)
+ {
+ path = _scriptInstances.FirstOrDefault(si => si.Uid == uid)?.Path;
+ }
+
+ if (path == null || !File.Exists(path))
+ {
+ return null;
+ }
+
+ return File.ReadAllTextAsync(path, Encoding.UTF8, cancellationToken);
+ }
+
+ public async Task WriteScriptAsync(string uid, string code, CancellationToken cancellationToken)
+ {
+ var path = Path.Combine(_scriptsPath, uid + ".py");
+
+ await File.WriteAllTextAsync(path, code, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
+ await TryInitializeScriptsAsync().ConfigureAwait(false);
+ }
+
+ public async Task DeleteScriptAsync(string uid)
+ {
+ var path = Path.Combine(_scriptsPath, uid + ".py");
+
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ await TryInitializeScriptsAsync().ConfigureAwait(false);
+ }
+ }
+
+ public async Task TryInitializeScriptsAsync()
+ {
+ lock (_scriptInstances)
+ {
+ foreach (var scriptInstance in _scriptInstances)
+ {
+ try
+ {
+ scriptInstance.InvokeOptionalFunction("destroy");
+ }
+ catch (Exception exception)
+ {
+ _logger.LogWarning(exception, $"Error while unloading script '{scriptInstance.Uid}'.");
+ }
+ }
+
+ _scriptInstances.Clear();
+ }
+
+ foreach (var path in Directory.GetFiles(_scriptsPath, "*.py", SearchOption.AllDirectories).OrderBy(file => file))
+ {
+ await TryInitializeScriptAsync(path).ConfigureAwait(false);
+ }
+ }
+
+ private async Task TryInitializeScriptAsync(string path)
+ {
+ var uid = new FileInfo(path).Name.Replace(".py", string.Empty, StringComparison.OrdinalIgnoreCase);
+
+ try
+ {
+ _logger.LogTrace($"Initializing Python script '{uid}'...");
+ var code = await File.ReadAllTextAsync(path).ConfigureAwait(false);
+
+ var scriptInstance = CreateScriptInstance(uid, path, code);
+ scriptInstance.InvokeOptionalFunction("initialize");
+
+ lock (_scriptInstances)
+ {
+ _scriptInstances.Add(scriptInstance);
+ }
+
+ _logger.LogInformation($"Initialized script '{uid}'.");
+ }
+ catch (Exception exception)
+ {
+ _logger.LogError(exception, $"Error while initializing script '{uid}'.");
+ }
+ }
+
+ private PythonScriptInstance CreateScriptInstance(string uid, string path, string code)
{
var scriptScope = _scriptEngine.CreateScope();
@@ -96,31 +172,32 @@ namespace MQTTnet.Server.Scripting
scriptScope.SetVariable("mqtt_net_server", _proxyObjects);
compiledCode.Execute(scriptScope);
- return new PythonScriptInstance(name, scriptScope);
+ return new PythonScriptInstance(uid, path, scriptScope);
}
private void AddSearchPaths(ScriptEngine scriptEngine)
{
- var paths = new List
+ if (_scriptingSettings.IncludePaths?.Any() != true)
{
- Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Lib"),
- "/usr/lib/python2.7",
- @"C:\Python27\Lib"
- };
-
- AddSearchPaths(scriptEngine, paths);
- }
+ return;
+ }
- private void AddSearchPaths(ScriptEngine scriptEngine, IEnumerable paths)
- {
var searchPaths = scriptEngine.GetSearchPaths();
- foreach (var path in paths)
+ foreach (var path in _scriptingSettings.IncludePaths)
{
- if (Directory.Exists(path))
+ var effectivePath = path;
+
+ var uri = new Uri(effectivePath, UriKind.RelativeOrAbsolute);
+ if (!uri.IsAbsoluteUri)
{
- searchPaths.Add(path);
- _logger.LogInformation($"Added Python lib path: {path}");
+ effectivePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, effectivePath);
+ }
+
+ if (Directory.Exists(effectivePath))
+ {
+ searchPaths.Add(effectivePath);
+ _logger.LogInformation($"Added Python lib path: {effectivePath}");
}
}
diff --git a/Source/MQTTnet.Server/Scripting/PythonScriptInstance.cs b/Source/MQTTnet.Server/Scripting/PythonScriptInstance.cs
index 91a46340..5d28f1e5 100644
--- a/Source/MQTTnet.Server/Scripting/PythonScriptInstance.cs
+++ b/Source/MQTTnet.Server/Scripting/PythonScriptInstance.cs
@@ -8,14 +8,17 @@ namespace MQTTnet.Server.Scripting
{
private readonly ScriptScope _scriptScope;
- public PythonScriptInstance(string name, ScriptScope scriptScope)
+ public PythonScriptInstance(string uid, string path, ScriptScope scriptScope)
{
- _scriptScope = scriptScope;
+ Uid = uid;
+ Path = path;
- Name = name;
+ _scriptScope = scriptScope;
}
- public string Name { get; }
+ public string Uid { get; }
+
+ public string Path { get; }
public bool InvokeOptionalFunction(string name, params object[] parameters)
{
diff --git a/Source/MQTTnet.Server/Scripts/00_sample.py b/Source/MQTTnet.Server/Scripts/00_sample.py
index c5aa2889..ce508a91 100644
--- a/Source/MQTTnet.Server/Scripts/00_sample.py
+++ b/Source/MQTTnet.Server/Scripts/00_sample.py
@@ -7,9 +7,17 @@ def initialize():
It will be executed only one time.
"""
- print("Hello World from Sample script.")
+ print("Hello World from sample script.")
+def destroy():
+ """
+ This function is invoked when the script is unloaded due to a script file update etc.
+ """
+
+ print("Bye from sample script.")
+
+
def on_validate_client_connection(context):
"""
This function is invoked whenever a client wants to connect. It can be used to validate the connection.
diff --git a/Source/MQTTnet.Server/Scripts/readme.md b/Source/MQTTnet.Server/Scripts/readme.md
index fb4cc996..7a34835c 100644
--- a/Source/MQTTnet.Server/Scripts/readme.md
+++ b/Source/MQTTnet.Server/Scripts/readme.md
@@ -1,9 +1,11 @@
-This directory contains scripts which are loaded by the server and can be used to perform the following tasks.
+# MQTT Scripts
-1. Validation of client connections via credentials, client IDs etc.
-2. Manipulation of every processed message.
-3. Validation of subscription attempts.
-4. Publishing of custom application messages.
+This directory contains scripts which are loaded by the server and can be used to perform the following tasks.
+
+* Validation of client connections via credentials, client IDs etc.
+* Manipulation of every processed message.
+* Validation of subscription attempts.
+* Publishing of custom application messages.
All scripts are loaded and _MQTTnet Server_ will invoke functions according to predefined naming conventions.
If a function is implemented in multiple scripts the context will be moved throug all instances. This allows overriding of results or passing data to other (following) scripts.
diff --git a/Source/MQTTnet.Server/Web/BasicAuthenticationHandler.cs b/Source/MQTTnet.Server/Web/BasicAuthenticationHandler.cs
new file mode 100644
index 00000000..2525451a
--- /dev/null
+++ b/Source/MQTTnet.Server/Web/BasicAuthenticationHandler.cs
@@ -0,0 +1,111 @@
+using System;
+using System.IO;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using IronPython.Runtime;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+using Microsoft.Scripting;
+
+namespace MQTTnet.Server.Web
+{
+ public class AuthenticationHandler : AuthenticationHandler
+ {
+ private readonly ILogger _logger;
+
+ public AuthenticationHandler(IOptionsMonitor options, ILoggerFactory loggerFactory, UrlEncoder encoder, ISystemClock clock, ILogger logger)
+ : base(options, loggerFactory, encoder, clock)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ protected override async Task HandleAuthenticateAsync()
+ {
+ await Task.CompletedTask;
+
+ if (!Request.Headers.ContainsKey(HeaderNames.Authorization))
+ {
+ return AuthenticateResult.Fail("Missing Authorization Header");
+ }
+
+ try
+ {
+ var headerValue = Request.Headers[HeaderNames.Authorization];
+ var parsedHeaderValue = AuthenticationHeaderValue.Parse(Request.Headers[HeaderNames.Authorization]);
+
+ var scheme = parsedHeaderValue.Scheme;
+ var parameter = parsedHeaderValue.Parameter;
+ string username = null;
+ string password = null;
+
+ if (scheme.Equals("Basic", StringComparison.OrdinalIgnoreCase))
+ {
+ var credentialBytes = Convert.FromBase64String(parsedHeaderValue.Parameter);
+ var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
+ username = credentials[0];
+ password = credentials[1];
+ }
+
+ var context = new PythonDictionary
+ {
+ ["header_value"] = headerValue,
+ ["scheme"] = scheme,
+ ["parameter"] = parameter,
+ ["username"] = username,
+ ["password"] = password,
+ ["is_authenticated"] = false
+ };
+
+ if (!ValidateUser(context))
+ {
+ return AuthenticateResult.Fail("Invalid credentials.");
+ }
+
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, context.get("username") as string ?? string.Empty)
+ };
+
+ var identity = new ClaimsIdentity(claims, Scheme.Name);
+ var principal = new ClaimsPrincipal(identity);
+ var ticket = new AuthenticationTicket(principal, Scheme.Name);
+
+ return AuthenticateResult.Success(ticket);
+ }
+ catch (Exception exception)
+ {
+ _logger.LogWarning("Error while authenticating user.", exception);
+
+ return AuthenticateResult.Fail(exception);
+ }
+ }
+
+ private bool ValidateUser(PythonDictionary context)
+ {
+ var handlerPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "authorization_handler.py");
+ if (!File.Exists(handlerPath))
+ {
+ return false;
+ }
+
+ var code = File.ReadAllText(handlerPath);
+
+ var scriptEngine = IronPython.Hosting.Python.CreateEngine();
+ //scriptEngine.Runtime.IO.SetOutput(new PythonIOStream(_logger.), Encoding.UTF8);
+
+ var scriptScope = scriptEngine.CreateScope();
+ var scriptSource = scriptScope.Engine.CreateScriptSourceFromString(code, SourceCodeKind.File);
+ var compiledCode = scriptSource.Compile();
+ compiledCode.Execute(scriptScope);
+ var function = scriptScope.Engine.Operations.GetMember(scriptScope, "handle_authenticate");
+ scriptScope.Engine.Operations.Invoke(function, context);
+
+ return context.get("is_authenticated", false) as bool? == true;
+ }
+ }
+}
diff --git a/Source/MQTTnet.Server/Web/Extensions.cs b/Source/MQTTnet.Server/Web/Extensions.cs
new file mode 100644
index 00000000..ef136dc2
--- /dev/null
+++ b/Source/MQTTnet.Server/Web/Extensions.cs
@@ -0,0 +1,20 @@
+using System;
+using System.IO;
+using System.Text;
+using Microsoft.AspNetCore.Http;
+
+namespace MQTTnet.Server.Web
+{
+ public static class Extensions
+ {
+ public static string ReadBodyAsString(this HttpRequest request)
+ {
+ if (request == null) throw new ArgumentNullException(nameof(request));
+
+ using (var reader = new StreamReader(request.Body, Encoding.UTF8))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+}
diff --git a/Source/MQTTnet.Server/Startup.cs b/Source/MQTTnet.Server/Web/Startup.cs
similarity index 71%
rename from Source/MQTTnet.Server/Startup.cs
rename to Source/MQTTnet.Server/Web/Startup.cs
index 7135508e..9c0c06ef 100644
--- a/Source/MQTTnet.Server/Startup.cs
+++ b/Source/MQTTnet.Server/Web/Startup.cs
@@ -1,10 +1,13 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using Microsoft.Scripting.Utils;
using MQTTnet.Server.Configuration;
@@ -14,7 +17,7 @@ using MQTTnet.Server.Scripting;
using MQTTnet.Server.Scripting.DataSharing;
using Swashbuckle.AspNetCore.SwaggerUI;
-namespace MQTTnet.Server
+namespace MQTTnet.Server.Web
{
public class Startup
{
@@ -35,7 +38,7 @@ namespace MQTTnet.Server
MqttServerService mqttServerService,
PythonScriptHostService pythonScriptHostService,
DataSharingService dataSharingService,
- SettingsModel settings)
+ MqttSettingsModel mqttSettings)
{
if (environment.IsDevelopment())
{
@@ -46,12 +49,20 @@ namespace MQTTnet.Server
application.UseHsts();
}
+ application.UseCors(x => x
+ .AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader()
+ .AllowCredentials());
+
+ application.UseAuthentication();
+
application.UseStaticFiles();
application.UseHttpsRedirection();
application.UseMvc();
- ConfigureWebSocketEndpoint(application, mqttServerService, settings);
+ ConfigureWebSocketEndpoint(application, mqttServerService, mqttSettings);
dataSharingService.Configure();
pythonScriptHostService.Configure();
@@ -73,6 +84,8 @@ namespace MQTTnet.Server
public void ConfigureServices(IServiceCollection services)
{
+ services.AddCors();
+
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(options =>
@@ -80,7 +93,7 @@ namespace MQTTnet.Server
options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
});
- services.AddSingleton(ReadSettings());
+ ReadMqttSettings(services);
services.AddSingleton();
services.AddSingleton();
@@ -98,10 +111,21 @@ namespace MQTTnet.Server
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
-
+
services.AddSwaggerGen(c =>
{
c.DescribeAllEnumsAsStrings();
+ c.AddSecurityDefinition("Basic", new OpenApiSecurityScheme
+ {
+ Scheme = "Basic",
+ Name = HeaderNames.Authorization,
+ Type = SecuritySchemeType.Http,
+ In = ParameterLocation.Header
+ });
+ c.AddSecurityRequirement(new OpenApiSecurityRequirement
+ {
+ [new OpenApiSecurityScheme { Name = "Basic" }] = new List()
+ });
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "MQTTnet.Server API",
@@ -120,46 +144,52 @@ namespace MQTTnet.Server
},
});
});
+
+ services.AddAuthentication("Basic").AddScheme("Basic", null);
}
- private SettingsModel ReadSettings()
+ private void ReadMqttSettings(IServiceCollection services)
{
- var settings = new Configuration.SettingsModel();
- Configuration.Bind("MQTT", settings);
- return settings;
+ var mqttSettings = new MqttSettingsModel();
+ Configuration.Bind("MQTT", mqttSettings);
+ services.AddSingleton(mqttSettings);
+
+ var scriptingSettings = new ScriptingSettingsModel();
+ Configuration.Bind("Scripting", scriptingSettings);
+ services.AddSingleton(scriptingSettings);
}
private static void ConfigureWebSocketEndpoint(
IApplicationBuilder application,
MqttServerService mqttServerService,
- SettingsModel settings)
+ MqttSettingsModel mqttSettings)
{
- if (settings?.WebSocketEndPoint?.Enabled != true)
+ if (mqttSettings?.WebSocketEndPoint?.Enabled != true)
{
return;
}
- if (string.IsNullOrEmpty(settings.WebSocketEndPoint.Path))
+ if (string.IsNullOrEmpty(mqttSettings.WebSocketEndPoint.Path))
{
return;
}
var webSocketOptions = new WebSocketOptions
{
- KeepAliveInterval = TimeSpan.FromSeconds(settings.WebSocketEndPoint.KeepAliveInterval),
- ReceiveBufferSize = settings.WebSocketEndPoint.ReceiveBufferSize
+ KeepAliveInterval = TimeSpan.FromSeconds(mqttSettings.WebSocketEndPoint.KeepAliveInterval),
+ ReceiveBufferSize = mqttSettings.WebSocketEndPoint.ReceiveBufferSize
};
- if (settings.WebSocketEndPoint.AllowedOrigins?.Any() == true)
+ if (mqttSettings.WebSocketEndPoint.AllowedOrigins?.Any() == true)
{
- webSocketOptions.AllowedOrigins.AddRange(settings.WebSocketEndPoint.AllowedOrigins);
+ webSocketOptions.AllowedOrigins.AddRange(mqttSettings.WebSocketEndPoint.AllowedOrigins);
}
-
+
application.UseWebSockets(webSocketOptions);
application.Use(async (context, next) =>
{
- if (context.Request.Path == settings.WebSocketEndPoint.Path)
+ if (context.Request.Path == mqttSettings.WebSocketEndPoint.Path)
{
if (context.WebSockets.IsWebSocketRequest)
{
diff --git a/Source/MQTTnet.Server/Web/authorization_handler.py b/Source/MQTTnet.Server/Web/authorization_handler.py
new file mode 100644
index 00000000..28cff2c1
--- /dev/null
+++ b/Source/MQTTnet.Server/Web/authorization_handler.py
@@ -0,0 +1,10 @@
+def handle_authenticate(context):
+ """
+ This function is invoked whenever a user tries to access protected HTTP resources.
+ This function must exist and return a proper value. Otherwise the request is denied.
+ """
+
+ username = context["username"]
+ password = context["password"]
+
+ context["is_authenticated"] = True # Change this to _False_ in case of invalid credentials.
\ No newline at end of file
diff --git a/Source/MQTTnet.Server/appsettings.json b/Source/MQTTnet.Server/appsettings.json
index 2ee0eaf3..b6bc55e4 100644
--- a/Source/MQTTnet.Server/appsettings.json
+++ b/Source/MQTTnet.Server/appsettings.json
@@ -37,15 +37,22 @@
"AllowedOrigins": [] // List of strings with URLs.
},
"CommunicationTimeout": 15, // In seconds.
- "ConnectionBacklog": 0, /* Set 0 to disable */
+ "ConnectionBacklog": 10, // Set 0 to disable
"EnablePersistentSessions": false,
"MaxPendingMessagesPerClient": 250,
"RetainedApplicationMessages": {
- "Persist": true,
- "Filename": "RetainedApplicationMessages.json",
+ "Persist": false,
+ "Path": "RetainedApplicationMessages.json",
"WriteInterval": 10 // In seconds.
},
- "EnableDebugLogging": true
+ "EnableDebugLogging": false
+ },
+ "Scripting": {
+ "IncludePaths": [
+ "Lib",
+ "/usr/lib/python2.7",
+ "C:\\Python27\\Lib"
+ ]
},
"Logging": {
"LogLevel": {
diff --git a/Source/MQTTnet.Server/run.bat b/Source/MQTTnet.Server/run.bat
deleted file mode 100644
index 016447e6..00000000
--- a/Source/MQTTnet.Server/run.bat
+++ /dev/null
@@ -1,2 +0,0 @@
-@echo off
-START "MQTTnet Server" dotnet .\MQTTnet.Server.dll
\ No newline at end of file
diff --git a/Source/MQTTnet.Server/run.sh b/Source/MQTTnet.Server/run.sh
deleted file mode 100644
index 1f755ad2..00000000
--- a/Source/MQTTnet.Server/run.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-echo "Starting MQTTnet Server.."
-dotnet .\MQTTnet.Server.dll
\ No newline at end of file
diff --git a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj
index f3114e32..d60256ed 100644
--- a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj
+++ b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj
@@ -12,7 +12,7 @@
-
+