From ffc1d950da6a32d5bcd682b863a810edcd6375a1 Mon Sep 17 00:00:00 2001 From: Christian Kratky Date: Sun, 9 Jun 2019 00:10:26 +0200 Subject: [PATCH] Add authorization for MQTTnet.Server --- MQTTnet.sln | 4 +- ...{SettingsModel.cs => MqttSettingsModel.cs} | 4 +- .../RetainedApplicationMessagesModel.cs | 2 +- .../Configuration/ScriptingSettingsModel.cs | 9 ++ .../Controllers/ClientsController.cs | 18 +++ .../RetainedApplicationMessagesController.cs | 2 + .../Controllers/ScriptsController.cs | 58 ++++++- .../Controllers/SessionsController.cs | 2 + Source/MQTTnet.Server/MQTTnet.Server.csproj | 18 ++- .../MQTTnet.Server/Mqtt/CustomMqttFactory.cs | 2 +- .../MQTTnet.Server/Mqtt/MqttServerService.cs | 6 +- .../MQTTnet.Server/Mqtt/MqttServerStorage.cs | 33 ++-- Source/MQTTnet.Server/Program.cs | 1 + Source/MQTTnet.Server/README.md | 10 ++ .../RetainedApplicationMessages.json | 1 - .../Scripting/PythonScriptHostService.cs | 143 ++++++++++++++---- .../Scripting/PythonScriptInstance.cs | 11 +- Source/MQTTnet.Server/Scripts/00_sample.py | 10 +- Source/MQTTnet.Server/Scripts/readme.md | 12 +- .../Web/BasicAuthenticationHandler.cs | 111 ++++++++++++++ Source/MQTTnet.Server/Web/Extensions.cs | 20 +++ Source/MQTTnet.Server/{ => Web}/Startup.cs | 68 ++++++--- .../Web/authorization_handler.py | 10 ++ Source/MQTTnet.Server/appsettings.json | 15 +- Source/MQTTnet.Server/run.bat | 2 - Source/MQTTnet.Server/run.sh | 3 - .../MQTTnet.TestApp.NetCore.csproj | 2 +- 27 files changed, 472 insertions(+), 105 deletions(-) rename Source/MQTTnet.Server/Configuration/{SettingsModel.cs => MqttSettingsModel.cs} (96%) create mode 100644 Source/MQTTnet.Server/Configuration/ScriptingSettingsModel.cs create mode 100644 Source/MQTTnet.Server/README.md delete mode 100644 Source/MQTTnet.Server/RetainedApplicationMessages.json create mode 100644 Source/MQTTnet.Server/Web/BasicAuthenticationHandler.cs create mode 100644 Source/MQTTnet.Server/Web/Extensions.cs rename Source/MQTTnet.Server/{ => Web}/Startup.cs (71%) create mode 100644 Source/MQTTnet.Server/Web/authorization_handler.py delete mode 100644 Source/MQTTnet.Server/run.bat delete mode 100644 Source/MQTTnet.Server/run.sh diff --git a/MQTTnet.sln b/MQTTnet.sln index 4504baa..a7d2007 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 ce5336d..8faa808 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 7abae83..a934217 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 0000000..bb71a15 --- /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 afc7511..6898375 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 30110b3..9c9f273 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 1b2a703..23cff06 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 415d4b9..463c004 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 8d150a3..edf5e27 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 - + diff --git a/Source/MQTTnet.Server/Mqtt/CustomMqttFactory.cs b/Source/MQTTnet.Server/Mqtt/CustomMqttFactory.cs index 06bf546..3eba2ca 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 42ec60b..ce34031 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 39b65d5..49875be 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 69b0949..f5df1d0 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 0000000..782ba95 --- /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 04c16db..0000000 --- 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 1acc58c..23cb531 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; + return _scriptInstances.Select(si => si.Uid).ToList(); + } + } - _logger.LogTrace($"Initializing Python script '{scriptName}'..."); - var code = File.ReadAllText(filename); + public Task ReadScriptAsync(string uid, CancellationToken cancellationToken) + { + if (uid == null) throw new ArgumentNullException(nameof(uid)); - var scriptInstance = CreateScriptInstance(scriptName, code); - scriptInstance.InvokeOptionalFunction("initialize"); + string path; - _scriptInstances.Add(scriptInstance); + lock (_scriptInstances) + { + path = _scriptInstances.FirstOrDefault(si => si.Uid == uid)?.Path; + } - _logger.LogInformation($"Initialized script '{scriptName}'."); + 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 '{new FileInfo(filename).Name}'."); + _logger.LogError(exception, $"Error while initializing script '{uid}'."); } } - private PythonScriptInstance CreateScriptInstance(string name, string code) + 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) + { + effectivePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, effectivePath); + } + + if (Directory.Exists(effectivePath)) { - searchPaths.Add(path); - _logger.LogInformation($"Added Python lib path: {path}"); + 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 91a4634..5d28f1e 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 c5aa288..ce508a9 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 fb4cc99..7a34835 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 0000000..2525451 --- /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 0000000..ef136dc --- /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 7135508..9c0c06e 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 0000000..28cff2c --- /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 2ee0eaf..b6bc55e 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 016447e..0000000 --- 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 1f755ad..0000000 --- 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 f3114e3..d60256e 100644 --- a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj +++ b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj @@ -12,7 +12,7 @@ - +