diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a5a9d5f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,213 @@ +# EditorConfig for Visual Studio 2022: https://learn.microsoft.com/en-us/visualstudio/ide/create-portable-custom-editor-options?view=vs-2022 + +# This is a top-most .editorconfig file +root = true + +#===================================================== +# +# nanoFramework specific settings +# +# +#===================================================== +[*] +# Generic EditorConfig settings +end_of_line = crlf +charset = utf-8-bom + +# Visual Studio spell checker +spelling_languages = en-us +spelling_checkable_types = strings,identifiers,comments +spelling_error_severity = information +spelling_exclusion_path = spelling_exclusion.dic + +#===================================================== +# +# Settings copied from the .NET runtime +# +# https://github.com/dotnet/runtime +# +#===================================================== +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_collection_expression = when_types_exactly_match +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# License header +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +[*.{csproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd,bat}] +end_of_line = crlf \ No newline at end of file diff --git a/README.md b/README.md index 2acd84b..b5e3148 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,53 @@ With the previous example the following happens: All up, this is an example to show how to use authentication, it's been defined to allow flexibility. +The webserver supports having multiple authentication methods or credentials for the same route. Each pair of authentication method plus credentials should have its own method in the controller: + +```csharp +class MixedController +{ + + [Route("sameroute")] + [Authentication("Basic")] + public void Basic(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "sameroute: Basic"); + } + + [Authentication("ApiKey:superKey1234")] + [Route("sameroute")] + public void Key(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "sameroute: API key #1"); + } + + [Authentication("ApiKey:superKey5678")] + [Route("sameroute")] + public void Key2(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "sameroute: API key #2"); + } + + [Route("sameroute")] + public void None(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "sameroute: Public"); + } +} +``` + +The webserver selects the route for a request: + +- If there are no matching methods, a not-found response (404) is returned. +- If authentication information is passed in the header of the request, then only methods that require authentication are considered. If one of the method's credentials matches the credentials passed in the request, that method is called. Otherwise a non-authorized response (401) will be returned. +- If no authentication information is passed in the header of the request: + - If one of the methods does not require authentication, that method is called. + - Otherwise a non-authorized response (401) will be returned. If one of the methods requires basic authentication, the `WWW-Authenticate` header is included to request credentials. + +The webserver does not support more than one matching method. Calling multiple methods most likely results in an exception as a subsequent method tries to modify a response that is already processed by the first method. The webserver does not know what to do and returns an internal server error (500). The body of the response lists the matching methods. + +Having multiple matching methods is considered a programming error. One way this occurs is if two methods in a controller accidentally have the same route. Returning an internal server error with the names of the methods makes it easy to discover the error. It is expected that the error is discovered and fixed in testing. Then the internal error will not occur in the application that is deployed to a device. + ## Managing incoming queries thru events Very basic usage is the following: diff --git a/nanoFramework.WebServer/WebServer.cs b/nanoFramework.WebServer/WebServer.cs index ea48b1d..b781f4f 100644 --- a/nanoFramework.WebServer/WebServer.cs +++ b/nanoFramework.WebServer/WebServer.cs @@ -1,7 +1,5 @@ -// -// Copyright (c) 2020 Laurent Ellerbach and the project contributors -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections; @@ -15,7 +13,6 @@ using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; - namespace nanoFramework.WebServer { /// @@ -408,7 +405,7 @@ namespace nanoFramework.WebServer _serverThread.Abort(); _serverThread = null; // Event is generate in the running thread - Debug.WriteLine("Stopped server in thread "); + Debug.WriteLine("Stopped server in thread "); } /// @@ -459,7 +456,7 @@ namespace nanoFramework.WebServer byte[] buf = new byte[MaxSizeBuffer]; using FileStream dataReader = new FileStream(strFilePath, FileMode.Open, FileAccess.Read); - + long fileLength = dataReader.Length; response.ContentType = contentType; response.ContentLength64 = fileLength; @@ -472,7 +469,7 @@ namespace nanoFramework.WebServer bytesToRead = bytesToRead < MaxSizeBuffer ? bytesToRead : MaxSizeBuffer; // Reads the data. - dataReader.Read(buf, 0,(int) bytesToRead); + dataReader.Read(buf, 0, (int)bytesToRead); // Writes data to browser response.OutputStream.Write(buf, 0, (int)bytesToRead); @@ -534,10 +531,16 @@ namespace nanoFramework.WebServer return; } + CallbackRoutes selectedRoute = null; + bool selectedRouteHasAuth = false; + string multipleCallback = null; + bool hasAuthRoutes = false; + string basicAuthNoCred = null; + bool authFailed = false; + // Variables used only within the "for". They are here for performance reasons bool mustAuthenticate; bool isAuthOk; - bool isRoute = false; foreach (CallbackRoutes route in _callbackRoutes) { @@ -546,56 +549,97 @@ namespace nanoFramework.WebServer continue; } - isRoute = true; - // Check auth first mustAuthenticate = route.Authentication != null && route.Authentication.AuthenticationType != AuthenticationType.None; - isAuthOk = !mustAuthenticate; - if (mustAuthenticate) { + hasAuthRoutes = true; if (route.Authentication.AuthenticationType == AuthenticationType.Basic) { - var credSite = route.Authentication.Credentials ?? Credential; var credReq = context.Request.Credentials; + if (credReq is null) + { + if (basicAuthNoCred is null) + { + basicAuthNoCred = route.Route; + } - isAuthOk = credReq != null && credSite != null + continue; + } + + var credSite = route.Authentication.Credentials ?? Credential; + + isAuthOk = credSite != null && (credSite.UserName == credReq.UserName) && (credSite.Password == credReq.Password); } else if (route.Authentication.AuthenticationType == AuthenticationType.ApiKey) { - var apikeySite = route.Authentication.ApiKey ?? ApiKey; var apikeyReq = GetApiKeyFromHeaders(context.Request.Headers); + if (apikeyReq is null) + { + continue; + } - isAuthOk = apikeyReq != null - && apikeyReq == apikeySite; + var apikeySite = route.Authentication.ApiKey ?? ApiKey; + + isAuthOk = apikeyReq == apikeySite; + } + else + { + isAuthOk = false; + } + + if (isAuthOk) + { + // This route can be used and has precedence over non-authenticated routes + if (!selectedRouteHasAuth) + { + selectedRoute = null; + multipleCallback = null; + } + + selectedRouteHasAuth = true; + } + else + { + authFailed = true; + continue; } } - - if (!isAuthOk) + else if (selectedRouteHasAuth || authFailed) { - if (route.Authentication != null && - route.Authentication.AuthenticationType == AuthenticationType.Basic) + // The selected route has authentication and/or a route exists with failed authentication. + // Those have precedence over non-authenticated routes + continue; + } + + if (selectedRoute is null) + { + selectedRoute = route; + } + else + { + multipleCallback ??= $"Multiple matching callbacks: {selectedRoute.Callback.DeclaringType.FullName}.{selectedRoute.Callback.Name}"; + multipleCallback += $", {route.Callback.DeclaringType.FullName}.{route.Callback.Name}"; + } + } + + + if (selectedRoute is null) + { + if (hasAuthRoutes) + { + if (!authFailed && basicAuthNoCred is not null) { context.Response.Headers.Add("WWW-Authenticate", - $"Basic realm=\"Access to {route.Route}\""); + $"Basic realm=\"Access to {basicAuthNoCred}\""); } context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; context.Response.ContentLength64 = 0; - - HandleContextResponse(context); - return; } - - InvokeRoute(route, context); - HandleContextResponse(context); - } - - if (!isRoute) - { - if (CommandReceived != null) + else if (CommandReceived != null) { // Starting a new thread to be able to handle a new request in parallel CommandReceived.Invoke(this, new WebServerEventArgs(context)); @@ -605,9 +649,19 @@ namespace nanoFramework.WebServer context.Response.StatusCode = (int)HttpStatusCode.NotFound; context.Response.ContentLength64 = 0; } - - HandleContextResponse(context); } + else if (multipleCallback is not null) + { + multipleCallback += "."; + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + OutPutStream(context.Response, multipleCallback); + } + else + { + InvokeRoute(selectedRoute, context); + } + + HandleContextResponse(context); }).Start(); } @@ -621,7 +675,7 @@ namespace nanoFramework.WebServer { // If we are here then set the server state to not running _cancel = true; - } + } WebServerStatusChanged?.Invoke(this, new WebServerStatusEventArgs(WebServerStatus.Stopped)); } diff --git a/spelling_exclusion.dic b/spelling_exclusion.dic new file mode 100644 index 0000000..8c0e1f8 --- /dev/null +++ b/spelling_exclusion.dic @@ -0,0 +1 @@ +nano diff --git a/tests/WebServerE2ETests/AuthController.cs b/tests/WebServerE2ETests/AuthController.cs index 8c76db3..5f6a3fa 100644 --- a/tests/WebServerE2ETests/AuthController.cs +++ b/tests/WebServerE2ETests/AuthController.cs @@ -1,8 +1,8 @@ -// Copyright (c) 2020 Laurent Ellerbach and the project contributors -// See LICENSE file in the project root for full license information. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. -using nanoFramework.WebServer; using System.Net; +using nanoFramework.WebServer; namespace WebServerE2ETests { diff --git a/tests/WebServerE2ETests/MixedController.cs b/tests/WebServerE2ETests/MixedController.cs new file mode 100644 index 0000000..1e2bcab --- /dev/null +++ b/tests/WebServerE2ETests/MixedController.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using nanoFramework.WebServer; + +namespace WebServerE2ETests +{ + class MixedController + { + #region ApiKey + public + [Route("authapikeyandpublic")] + [Authentication("ApiKey:superKey1234")] + public void ApiKeyAndPublicApiKey(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "ApiKey+Public: ApiKey"); + } + + [Route("authapikeyandpublic")] + public void ApiKeyAndPublicPublic(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "ApiKey+Public: Public"); + } + #endregion + + #region Basic + public + [Route("authbasicandpublic")] + [Authentication("Basic:user2 password")] + public void BasicAndPublicBasic(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "Basic+Public: Basic"); + } + + [Route("authbasicandpublic")] + public void BasicAndPublicPublic(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "Basic+Public: Public"); + } + #endregion + + #region Basic + ApiKey + Public + [Route("authapikeybasicandpublic")] + public void ApiKeyBasicAndPublicPublic(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "ApiKey+Basic+Public: Public"); + } + + [Route("authapikeybasicandpublic")] + [Authentication("Basic:user3 password")] + public void ApiKeyBasicAndPublicBasic3(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "ApiKey+Basic+Public: Basic user3"); + } + + [Route("authapikeybasicandpublic")] + [Authentication("Basic:user2 password")] + public void ApiKeyBasicAndPublicBasic2(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "ApiKey+Basic+Public: Basic user2"); + } + + [Authentication("ApiKey:superKey1234")] + [Route("authapikeybasicandpublic")] + public void ApiKeyBasicAndPublicApiKey(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "ApiKey+Basic+Public: ApiKey"); + } + + [Authentication("ApiKey:superKey42")] + [Route("authapikeybasicandpublic")] + public void ApiKeyBasicAndPublicApiKey2(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "ApiKey+Basic+Public: ApiKey 2"); + } + #endregion + + #region Multiple callbacks + [Route("authmultiple")] + public void MultiplePublic1(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "Multiple: Public1"); + } + + [Route("authmultiple")] + [Authentication("Basic:user2 password")] + public void MultipleBasic1(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "Multiple: Basic1"); + } + + [Route("authmultiple")] + public void MultiplePublic2(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "Multiple: Public2"); + } + + [Authentication("ApiKey:superKey1234")] + [Route("authmultiple")] + public void MultipleApiKey1(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "Multiple: ApiKey1"); + } + + [Route("authmultiple")] + [Authentication("Basic:user2 password")] + public void MultipleBasic2(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "Multiple: Basic2"); + } + + [Authentication("ApiKey:superKey1234")] + [Route("authmultiple")] + public void MultipleApiKey2(WebServerEventArgs e) + { + WebServer.OutPutStream(e.Context.Response, "Multiple: ApiKey2"); + } + #endregion + } +} + diff --git a/tests/WebServerE2ETests/Program.cs b/tests/WebServerE2ETests/Program.cs index 290b58d..23dc8d7 100644 --- a/tests/WebServerE2ETests/Program.cs +++ b/tests/WebServerE2ETests/Program.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2020 Laurent Ellerbach and the project contributors -// See LICENSE file in the project root for full license information. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. -using nanoFramework.Networking; -using nanoFramework.WebServer; using System; using System.Diagnostics; using System.IO; @@ -10,6 +8,8 @@ using System.Net; using System.Net.NetworkInformation; using System.Text; using System.Threading; +using nanoFramework.Networking; +using nanoFramework.WebServer; namespace WebServerE2ETests { @@ -29,7 +29,7 @@ namespace WebServerE2ETests } Debug.WriteLine($"Connected with wifi credentials. IP Address: {GetCurrentIPAddress()}"); - _server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(SimpleRouteController), typeof(AuthController) }); + _server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(SimpleRouteController), typeof(AuthController), typeof(MixedController) }); // To test authentication with various scenarios _server.ApiKey = "ATopSecretAPIKey1234"; _server.Credential = new NetworkCredential("topuser", "topPassword"); @@ -49,7 +49,7 @@ namespace WebServerE2ETests private static void WebServerStatusChanged(object obj, WebServerStatusEventArgs e) { - Debug.WriteLine($"The web server is now {(e.Status == WebServerStatus.Running ? "running" : "stopped" )}"); + Debug.WriteLine($"The web server is now {(e.Status == WebServerStatus.Running ? "running" : "stopped")}"); } private static void ServerCommandReceived(object obj, WebServerEventArgs e) diff --git a/tests/WebServerE2ETests/SimpleRouteController.cs b/tests/WebServerE2ETests/SimpleRouteController.cs index 1299b87..f1a1e5b 100644 --- a/tests/WebServerE2ETests/SimpleRouteController.cs +++ b/tests/WebServerE2ETests/SimpleRouteController.cs @@ -1,9 +1,9 @@ -// Copyright (c) 2020 Laurent Ellerbach and the project contributors -// See LICENSE file in the project root for full license information. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. -using nanoFramework.WebServer; using System.Diagnostics; using System.Net; +using nanoFramework.WebServer; namespace WebServerE2ETests { @@ -15,7 +15,7 @@ namespace WebServerE2ETests Debug.WriteLine($"{nameof(OutputWithOKCode)} {e.Context.Request.HttpMethod} {e.Context.Request.RawUrl}"); WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK); } - + [Route("notfoundcode")] public void OutputWithNotFoundCode(WebServerEventArgs e) { @@ -45,5 +45,19 @@ namespace WebServerE2ETests { WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK); } + + [Route("multiplecallback")] + public void FirstOfMultipleCallback(WebServerEventArgs e) + { + Debug.WriteLine($"{nameof(FirstOfMultipleCallback)} {e.Context.Request.HttpMethod} {e.Context.Request.RawUrl}"); + WebServer.OutPutStream(e.Context.Response, nameof(FirstOfMultipleCallback)); + } + + [Route("multiplecallback")] + public void SecondOfMultipleCallback(WebServerEventArgs e) + { + Debug.WriteLine($"{nameof(SecondOfMultipleCallback)} {e.Context.Request.HttpMethod} {e.Context.Request.RawUrl}"); + WebServer.OutPutStream(e.Context.Response, nameof(SecondOfMultipleCallback)); + } } } diff --git a/tests/WebServerE2ETests/WebServerE2ETests.nfproj b/tests/WebServerE2ETests/WebServerE2ETests.nfproj index 210b962..4f79e1d 100644 --- a/tests/WebServerE2ETests/WebServerE2ETests.nfproj +++ b/tests/WebServerE2ETests/WebServerE2ETests.nfproj @@ -18,6 +18,7 @@ + diff --git a/tests/WebServerE2ETests/nanoFramework WebServer E2E Tests.postman_collection.json b/tests/WebServerE2ETests/nanoFramework WebServer E2E Tests.postman_collection.json index 29da8c2..6cde2f8 100644 --- a/tests/WebServerE2ETests/nanoFramework WebServer E2E Tests.postman_collection.json +++ b/tests/WebServerE2ETests/nanoFramework WebServer E2E Tests.postman_collection.json @@ -214,6 +214,37 @@ }, "response": [] }, + { + "name": "SimpleRouteController_MultipleCallback", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 500 and message\", function () {\r", + " pm.response.to.have.status(500);\r", + " pm.response.to.have.body(\"Multiple matching callbacks: WebServerE2ETests.SimpleRouteController.FirstOfMultipleCallback, WebServerE2ETests.SimpleRouteController.SecondOfMultipleCallback.\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/multiplecallback", + "host": [ + "{{base_url}}" + ], + "path": [ + "multiplecallback" + ] + } + }, + "response": [] + }, { "name": "SimpleRouteController_OutputWithNotFoundCode", "event": [ @@ -874,6 +905,498 @@ } }, "response": [] + }, + { + "name": "MixedController_ApiKeyPublic_ApiKey", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 200 Body ApiKey\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.have.body(\"ApiKey+Public: ApiKey\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{auth_apikey}}", + "type": "string" + }, + { + "key": "key", + "value": "ApiKey", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authapikeyandpublic", + "host": [ + "{{base_url}}" + ], + "path": [ + "authapikeyandpublic" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_ApiKeyPublic_Public", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 200 Body Public\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.have.body(\"ApiKey+Public: Public\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authapikeyandpublic", + "host": [ + "{{base_url}}" + ], + "path": [ + "authapikeyandpublic" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_BasicPublic_Basic", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 200 Body Basic\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.have.body(\"Basic+Public: Basic\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{auth_basic_password}}", + "type": "string" + }, + { + "key": "username", + "value": "{{auth_basic_username}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authbasicandpublic", + "host": [ + "{{base_url}}" + ], + "path": [ + "authbasicandpublic" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_BasicPublic_Public", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 200 Body Public\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.have.body(\"Basic+Public: Public\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authbasicandpublic", + "host": [ + "{{base_url}}" + ], + "path": [ + "authbasicandpublic" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_ApiKeyBasicPublic_ApiKey", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 200 Body ApiKey\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.have.body(\"ApiKey+Basic+Public: ApiKey\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{auth_apikey}}", + "type": "string" + }, + { + "key": "key", + "value": "ApiKey", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authapikeybasicandpublic", + "host": [ + "{{base_url}}" + ], + "path": [ + "authapikeybasicandpublic" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_ApiKeyBasicPublic_ApiKey2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 200 Body ApiKey\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.have.body(\"ApiKey+Basic+Public: ApiKey 2\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{auth_apikey2}}", + "type": "string" + }, + { + "key": "key", + "value": "ApiKey", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authapikeybasicandpublic", + "host": [ + "{{base_url}}" + ], + "path": [ + "authapikeybasicandpublic" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_ApiKeyBasicPublic_Basic", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 200 Body Basic\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.have.body(\"ApiKey+Basic+Public: Basic user2\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{auth_basic_password}}", + "type": "string" + }, + { + "key": "username", + "value": "{{auth_basic_username}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authapikeybasicandpublic", + "host": [ + "{{base_url}}" + ], + "path": [ + "authapikeybasicandpublic" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_ApiKeyBasicPublic_Basic2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 200 Body Basic\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.have.body(\"ApiKey+Basic+Public: Basic user3\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{auth_basic_password}}", + "type": "string" + }, + { + "key": "username", + "value": "{{auth_basic_username2}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authapikeybasicandpublic", + "host": [ + "{{base_url}}" + ], + "path": [ + "authapikeybasicandpublic" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_ApiKeyBasicPublic_Public", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 200 Body Public\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.have.body(\"ApiKey+Basic+Public: Public\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authapikeybasicandpublic", + "host": [ + "{{base_url}}" + ], + "path": [ + "authapikeybasicandpublic" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_MultipleCallbacks_ApiKey", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 500 Body ApiKey\", function () {\r", + " pm.response.to.have.status(500);\r", + " pm.response.to.have.body(\"Multiple matching callbacks: WebServerE2ETests.MixedController.MultipleApiKey1, WebServerE2ETests.MixedController.MultipleApiKey2.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{auth_apikey}}", + "type": "string" + }, + { + "key": "key", + "value": "ApiKey", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authmultiple", + "host": [ + "{{base_url}}" + ], + "path": [ + "authmultiple" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_MultipleCallbacks_Basic", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 500 Body Basic\", function () {\r", + " pm.response.to.have.status(500);\r", + " pm.response.to.have.body(\"Multiple matching callbacks: WebServerE2ETests.MixedController.MultipleBasic1, WebServerE2ETests.MixedController.MultipleBasic2.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{auth_basic_password}}", + "type": "string" + }, + { + "key": "username", + "value": "{{auth_basic_username}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authmultiple", + "host": [ + "{{base_url}}" + ], + "path": [ + "authmultiple" + ] + } + }, + "response": [] + }, + { + "name": "MixedController_MultipleCallbacks_Public", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status 500 Body Public\", function () {\r", + " pm.response.to.have.status(500);\r", + " pm.response.to.have.body(\"Multiple matching callbacks: WebServerE2ETests.MixedController.MultiplePublic1, WebServerE2ETests.MixedController.MultiplePublic2.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/authmultiple", + "host": [ + "{{base_url}}" + ], + "path": [ + "authmultiple" + ] + } + }, + "response": [] } ], "event": [ @@ -906,6 +1429,11 @@ "value": "user2", "type": "string" }, + { + "key": "auth_basic_username2", + "value": "user3", + "type": "string" + }, { "key": "auth_basic_password", "value": "password", @@ -915,6 +1443,11 @@ "key": "auth_apikey", "value": "superKey1234", "type": "string" + }, + { + "key": "auth_apikey2", + "value": "superKey42", + "type": "string" } ] } \ No newline at end of file