{"id":19244366,"url":"https://github.com/nanoframework/nanoframework.webserver","last_synced_at":"2025-04-04T20:14:52.669Z","repository":{"id":37978518,"uuid":"304084607","full_name":"nanoframework/nanoFramework.WebServer","owner":"nanoframework","description":":package: Web server for nanoFramework packed with features: REST api using attributes, multithread requests, parameters in query URL, static files serving.","archived":false,"fork":false,"pushed_at":"2024-10-23T00:41:26.000Z","size":671,"stargazers_count":34,"open_issues_count":1,"forks_count":19,"subscribers_count":6,"default_branch":"main","last_synced_at":"2024-10-29T21:06:16.652Z","etag":null,"topics":["csharp","dotnet","esp32","nanoframework","nxp","rest","rest-api","restful-api","restful-webservices","stm32","webserver"],"latest_commit_sha":null,"homepage":"https://www.nanoframework.net","language":"C#","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nanoframework.png","metadata":{"funding":{"open_collective":"nanoframework","github":"nanoframework"},"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-14T17:14:44.000Z","updated_at":"2024-10-26T13:29:04.000Z","dependencies_parsed_at":"2023-10-11T19:37:31.539Z","dependency_job_id":"b37b21f4-b026-4809-b383-6e4b52cf08cd","html_url":"https://github.com/nanoframework/nanoFramework.WebServer","commit_stats":{"total_commits":445,"total_committers":12,"mean_commits":"37.083333333333336","dds":0.3932584269662921,"last_synced_commit":"30ff89806eef7e5617e0e812b9f71a2343575775"},"previous_names":[],"tags_count":188,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanoframework%2FnanoFramework.WebServer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanoframework%2FnanoFramework.WebServer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanoframework%2FnanoFramework.WebServer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanoframework%2FnanoFramework.WebServer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nanoframework","download_url":"https://codeload.github.com/nanoframework/nanoFramework.WebServer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247242680,"owners_count":20907134,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","dotnet","esp32","nanoframework","nxp","rest","rest-api","restful-api","restful-webservices","stm32","webserver"],"created_at":"2024-11-09T17:23:05.786Z","updated_at":"2025-04-04T20:14:52.651Z","avatar_url":"https://github.com/nanoframework.png","language":"C#","readme":"[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=nanoframework_lib-nanoframework.WebServer\u0026metric=alert_status)](https://sonarcloud.io/dashboard?id=nanoframework_lib-nanoframework.WebServer) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=nanoframework_lib-nanoframework.WebServer\u0026metric=reliability_rating)](https://sonarcloud.io/dashboard?id=nanoframework_lib-nanoframework.WebServer) [![NuGet](https://img.shields.io/nuget/dt/nanoFramework.WebServer.svg?label=NuGet\u0026style=flat\u0026logo=nuget)](https://www.nuget.org/packages/nanoFramework.WebServer/) [![#yourfirstpr](https://img.shields.io/badge/first--timers--only-friendly-blue.svg)](https://github.com/nanoframework/Home/blob/main/CONTRIBUTING.md) [![Discord](https://img.shields.io/discord/478725473862549535.svg?logo=discord\u0026logoColor=white\u0026label=Discord\u0026color=7289DA)](https://discord.gg/gCyBu8T)\n\n![nanoFramework logo](https://raw.githubusercontent.com/nanoframework/Home/main/resources/logo/nanoFramework-repo-logo.png)\n\n-----\n\n### Welcome to the .NET **nanoFramework** WebServer repository\n\n## Build status\n\n| Component | Build Status | NuGet Package |\n|:-|---|---|\n| nanoFramework.WebServer | [![Build Status](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_apis/build/status/nanoFramework.WebServer?repoName=nanoframework%2FnanoFramework.WebServer\u0026branchName=main)](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_build/latest?definitionId=65\u0026repoName=nanoframework%2FnanoFramework.WebServer\u0026branchName=main) | [![NuGet](https://img.shields.io/nuget/v/nanoFramework.WebServer.svg?label=NuGet\u0026style=flat\u0026logo=nuget)](https://www.nuget.org/packages/nanoFramework.WebServer/) |\n| nanoFramework.WebServer.FileSystem | [![Build Status](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_apis/build/status/nanoFramework.WebServer?repoName=nanoframework%2FnanoFramework.WebServer\u0026branchName=main)](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_build/latest?definitionId=65\u0026repoName=nanoframework%2FnanoFramework.WebServer\u0026branchName=main) | [![NuGet](https://img.shields.io/nuget/v/nanoFramework.WebServer.FileSystem.svg?label=NuGet\u0026style=flat\u0026logo=nuget)](https://www.nuget.org/packages/nanoFramework.WebServer.FileSystem/) |\n\n## .NET nanoFramework WebServer\n\nThis library was coded by [Laurent Ellerbach](https://github.com/Ellerbach) who generously offered it to the .NET **nanoFramework** project.\n\nThis is a simple nanoFramework WebServer. Features:\n\n- Handle multi-thread requests\n- Serve static files from any storage using [`nanoFramework.WebServer.FileSystem` NuGet](https://www.nuget.org/packages/nanoFramework.WebServer.FileSystem). Requires a target device with support for storage (having `System.IO.FileSystem` capability).\n- Handle parameter in URL\n- Possible to have multiple WebServer running at the same time\n- supports GET/PUT and any other word\n- Supports any type of header\n- Supports content in POST\n- Reflection for easy usage of controllers and notion of routes\n- Helpers to return error code directly facilitating REST API\n- HTTPS support\n- [URL decode/encode](https://github.com/nanoframework/lib-nanoFramework.System.Net.Http/blob/develop/nanoFramework.System.Net.Http/Http/System.Net.HttpUtility.cs)\n\nLimitations:\n\n- Does not support any zip in the request or response stream\n\n## Usage\n\nYou just need to specify a port and a timeout for the queries and add an event handler when a request is incoming. With this first way, you will have an event raised every time you'll receive a request.\n\n```csharp\nusing (WebServer server = new WebServer(80, HttpProtocol.Http)\n{\n    // Add a handler for commands that are received by the server.\n    server.CommandReceived += ServerCommandReceived;\n\n    // Start the server.\n    server.Start();\n\n    Thread.Sleep(Timeout.Infinite);\n}\n```\n\nYou can as well pass a controller where you can use decoration for the routes and method supported.\n\n```csharp\nusing (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ControllerPerson), typeof(ControllerTest) }))\n{\n    // Start the server.\n    server.Start();\n\n    Thread.Sleep(Timeout.Infinite);\n}\n```\n\nIn this case, you're passing 2 classes where you have public methods decorated which will be called every time the route is found.\n\nWith the previous example, a very simple and straight forward Test controller will look like that:\n\n```csharp\npublic class ControllerTest\n{\n    [Route(\"test\"), Route(\"Test2\"), Route(\"tEst42\"), Route(\"TEST\")]\n    [CaseSensitive]\n    [Method(\"GET\")]\n    public void RoutePostTest(WebServerEventArgs e)\n    {\n        string route = $\"The route asked is {e.Context.Request.RawUrl.TrimStart('/').Split('/')[0]}\";\n        e.Context.Response.ContentType = \"text/plain\";\n        WebServer.OutPutStream(e.Context.Response, route);\n    }\n\n    [Route(\"test/any\")]\n    public void RouteAnyTest(WebServerEventArgs e)\n    {\n        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);\n    }\n}\n```\n\nIn this example, the `RoutePostTest` will be called every time the called url will be `test` or `Test2` or `tEst42` or `TEST`, the url can be with parameters and the method GET. Be aware that `Test` won't call the function, neither `test/`.\n\nThe `RouteAnyTest`is called whenever the url is `test/any` whatever the method is.\n\nThere is a more advance example with simple REST API to get a list of Person and add a Person. Check it in the [sample](./WebServer.Sample/ControllerPerson.cs).\n\n\u003e [!Important]\n\u003e\n\u003e By default the routes are not case sensitive and the attribute **must** be lowercase.\n\u003e If you want to use case sensitive routes like in the previous example, use the attribute `CaseSensitive`. As in the previous example, you **must** write the route as you want it to be responded to.\n\n## A simple GPIO controller REST API\n\nYou will find in simple [GPIO controller sample](https://github.com/nanoframework/Samples/tree/main/samples/Webserver/WebServer.GpioRest) REST API. The controller not case sensitive and is working like this:\n\n- To open the pin 2 as output: http://yoururl/open/2/output\n- To open pin 4 as input: http://yoururl/open/4/input\n- To write the value high to pin 2: http://yoururl/write/2/high\n  - You can use high or 1, it has the same effect and will place the pin in high value\n  - You can use low of 0, it has the same effect and will place the pin in low value\n- To read the pin 4: http://yoururl/read/4, you will get as a raw text `high`or `low`depending on the state\n\n## Authentication on controllers\n\nControllers support authentication. 3 types of authentications are currently implemented on controllers only:\n\n- Basic: the classic user and password following the HTTP standard. Usage:\n  - `[Authentication(\"Basic\")]` will use the default credential of the webserver\n  - `[Authentication(\"Basic:myuser mypassword\")]` will use myuser as a user and my password as a password. Note: the user cannot contains spaces.\n- APiKey in header: add ApiKey in headers with the API key. Usage:\n  - `[Authentication(\"ApiKey\")]` will use the default credential of the webserver\n  - `[Authentication(\"ApiKeyc:akey\")]` will use akey as ApiKey.\n- None: no authentication required. Usage:\n  - `[Authentication(\"None\")]` will use the default credential of the webserver\n\nThe Authentication attribute applies to both public Classes an public Methods.\n\nAs for the rest of the controller, you can add attributes to define them, override them. The following example gives an idea of what can be done:\n\n```csharp\n[Authentication(\"Basic\")]\nclass ControllerAuth\n{\n    [Route(\"authbasic\")]\n    public void Basic(WebServerEventArgs e)\n    {\n        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);\n    }\n\n    [Route(\"authbasicspecial\")]\n    [Authentication(\"Basic:user2 password\")]\n    public void Special(WebServerEventArgs e)\n    {\n        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);\n    }\n\n    [Authentication(\"ApiKey:superKey1234\")]\n    [Route(\"authapi\")]\n    public void Key(WebServerEventArgs e)\n    {\n        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);\n    }\n\n    [Route(\"authnone\")]\n    [Authentication(\"None\")]\n    public void None(WebServerEventArgs e)\n    {\n        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);\n    }\n\n    [Authentication(\"ApiKey\")]\n    [Route(\"authdefaultapi\")]\n    public void DefaultApi(WebServerEventArgs e)\n    {\n        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);\n    }\n}\n```\n\nAnd you can pass default credentials to the server:\n\n```csharp\nusing (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ControllerPerson), typeof(ControllerTest), typeof(ControllerAuth) }))\n{\n    // To test authentication with various scenarios\n    server.ApiKey = \"ATopSecretAPIKey1234\";\n    server.Credential = new NetworkCredential(\"topuser\", \"topPassword\");\n\n    // Start the server.\n    server.Start();\n\n    Thread.Sleep(Timeout.Infinite);\n}\n```\n\nWith the previous example the following happens:\n\n- All the controller by default, even when nothing is specified will use the controller credentials. In our case, the Basic authentication with the default user (topuser) and password (topPassword) will be used.\n  - When calling http://yoururl/authbasic from a browser, you will be prompted for the user and password, use the default one topuser and topPassword to get access\n  - When calling http://yoururl/authnone, you won't be prompted because the authentication has been overridden for no authentication\n  - When calling http://yoururl/authbasicspecial, the user and password are different from the defautl ones, user2 and password is the right couple here\n- If you would have define in the controller a specific user and password like `[Authentication(\"Basic:myuser mypassword\")]`, then the default one for all the controller would have been myuser and mypassword\n- When calling http://yoururl/authapi, you must pass the header `ApiKey` (case sensitive) with the value `superKey1234` to get authorized, this is overridden the default Basic authentication\n- When calling http://yoururl/authdefaultapi, the default key `ATopSecretAPIKey1234` will be used so you have to pass it in the headers of the request\n\nAll up, this is an example to show how to use authentication, it's been defined to allow flexibility.\n\nThe 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:\n\n```csharp\nclass MixedController\n{\n\n    [Route(\"sameroute\")]\n    [Authentication(\"Basic\")]\n    public void Basic(WebServerEventArgs e)\n    {\n        WebServer.OutPutStream(e.Context.Response, \"sameroute: Basic\");\n    }\n\n    [Authentication(\"ApiKey:superKey1234\")]\n    [Route(\"sameroute\")]\n    public void Key(WebServerEventArgs e)\n    {\n        WebServer.OutPutStream(e.Context.Response, \"sameroute: API key #1\");\n    }\n\n    [Authentication(\"ApiKey:superKey5678\")]\n    [Route(\"sameroute\")]\n    public void Key2(WebServerEventArgs e)\n    {\n        WebServer.OutPutStream(e.Context.Response, \"sameroute: API key #2\");\n    }\n\n    [Route(\"sameroute\")]\n    public void None(WebServerEventArgs e)\n    {\n        WebServer.OutPutStream(e.Context.Response, \"sameroute: Public\");\n    }\n}\n```\n\nThe webserver selects the route for a request:\n\n- If there are no matching methods, a not-found response (404) is returned.\n- 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.\n- If no authentication information is passed in the header of the request:\n\t- If one of the methods does not require authentication, that method is called.\n\t- 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.\n\nThe 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.\n\nHaving 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. \n\n## Managing incoming queries thru events\n\nVery basic usage is the following:\n\n```csharp\nprivate static void ServerCommandReceived(object source, WebServerEventArgs e)\n{\n    var url = e.Context.Request.RawUrl;\n    Debug.WriteLine($\"Command received: {url}, Method: {e.Context.Request.HttpMethod}\");\n\n    if (url.ToLower() == \"/sayhello\")\n    {\n        // This is simple raw text returned\n        WebServer.OutPutStream(e.Context.Response, \"It's working, url is empty, this is just raw text, /sayhello is just returning a raw text\");\n    }\n    else\n    {\n        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);\n    }\n}\n```\n\nYou can do more advance scenario like returning a full HTML page:\n\n```csharp\nWebServer.OutPutStream(e.Context.Response, \"\u003chtml\u003e\u003chead\u003e\" +\n    \"\u003ctitle\u003eHi from nanoFramework Server\u003c/title\u003e\u003c/head\u003e\u003cbody\u003eYou want me to say hello in a real HTML page!\u003cbr/\u003e\u003ca href='/useinternal'\u003eGenerate an internal text.txt file\u003c/a\u003e\u003cbr /\u003e\" +\n    \"\u003ca href='/Text.txt'\u003eDownload the Text.txt file\u003c/a\u003e\u003cbr\u003e\" +\n    \"Try this url with parameters: \u003ca href='/param.htm?param1=42\u0026second=24\u0026NAme=Ellerbach'\u003e/param.htm?param1=42\u0026second=24\u0026NAme=Ellerbach\u003c/a\u003e\u003c/body\u003e\u003c/html\u003e\");\n```\n\nAnd can get parameters from a URL a an example from the previous link on the param.html page:\n\n```csharp\nif (url.ToLower().IndexOf(\"/param.htm\") == 0)\n{\n    // Test with parameters\n    var parameters = WebServer.decryptParam(url);\n    string toOutput = \"\u003chtml\u003e\u003chead\u003e\" +\n        \"\u003ctitle\u003eHi from nanoFramework Server\u003c/title\u003e\u003c/head\u003e\u003cbody\u003eHere are the parameters of this URL: \u003cbr /\u003e\";\n    foreach (var par in parameters)\n    {\n        toOutput += $\"Parameter name: {par.Name}, Value: {par.Value}\u003cbr /\u003e\";\n    }\n    toOutput += \"\u003c/body\u003e\u003c/html\u003e\";\n    WebServer.OutPutStream(e.Context.Response, toOutput);\n}\n```\n\nAnd server static files:\n\n```csharp\n// E = USB storage\n// D = SD Card\n// I = Internal storage\n// Adjust this based on your configuration\nconst string DirectoryPath = \"I:\\\\\";\nstring[] _listFiles;\n\n// Gets the list of all files in a specific directory\n// See the MountExample for more details if you need to mount an SD card and adjust here\n// https://github.com/nanoframework/Samples/blob/main/samples/System.IO.FileSystem/MountExample/Program.cs\n_listFiles = Directory.GetFiles(DirectoryPath);\n// Remove the root directory\nfor (int i = 0; i \u003c _listFiles.Length; i++)\n{\n    _listFiles[i] = _listFiles[i].Substring(DirectoryPath.Length);\n}\n\nvar fileName = url.Substring(1);\n// Note that the file name is case sensitive\n// Very simple example serving a static file on an SD card                   \nforeach (var file in _listFiles)\n{\n    if (file == fileName)\n    {\n        WebServer.SendFileOverHTTP(e.Context.Response, DirectoryPath + file);\n        return;\n    }\n}\n\nWebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);\n```\n\n\u003e [!Important]\n\u003e\n\u003e Serving files requires the `nanoFramework.WebServer.FileSystem` nuget **AND** that the device supports storage so `System.IO.FileSystem`.\n\nAnd also **REST API** is supported, here is a comprehensive example:\n\n```csharp\nif (url.ToLower().IndexOf(\"/api/\") == 0)\n{\n    string ret = $\"Your request type is: {e.Context.Request.HttpMethod}\\r\\n\";\n    ret += $\"The request URL is: {e.Context.Request.RawUrl}\\r\\n\";\n    var parameters = WebServer.DecodeParam(e.Context.Request.RawUrl);\n    if (parameters != null)\n    {\n        ret += \"List of url parameters:\\r\\n\";\n        foreach (var param in parameters)\n        {\n            ret += $\"  Parameter name: {param.Name}, value: {param.Value}\\r\\n\";\n        }\n    }\n\n    if (e.Context.Request.Headers != null)\n    {\n        ret += $\"Number of headers: {e.Context.Request.Headers.Count}\\r\\n\";\n    }\n    else\n    {\n        ret += \"There is no header in this request\\r\\n\";\n    }\n\n    foreach (var head in e.Context.Request.Headers?.AllKeys)\n    {\n        ret += $\"  Header name: {head}, Values:\";\n        var vals = e.Context.Request.Headers.GetValues(head);\n        foreach (var val in vals)\n        {\n            ret += $\"{val} \";\n        }\n\n        ret += \"\\r\\n\";\n    }\n\n    if (e.Context.Request.ContentLength64 \u003e 0)\n    {\n\n        ret += $\"Size of content: {e.Context.Request.ContentLength64}\\r\\n\";\n\n        var contentTypes = e.Context.Request.Headers?.GetValues(\"Content-Type\");\n        var isMultipartForm = contentTypes != null \u0026\u0026 contentTypes.Length \u003e 0 \u0026\u0026 contentTypes[0].StartsWith(\"multipart/form-data;\");\n\n        if(isMultipartForm)\n        {\n            var form = e.Context.Request.ReadForm();\n            ret += $\"Received a form with {form.Parameters.Length} parameters and {form.Files.Length} files.\";\n        }\n        else \n        {\n            var body = e.Context.Request.ReadBody();\n\n            ret += $\"Request body hex string representation:\\r\\n\";\n            for (int i = 0; i \u003c body.Length; i++)\n            {\n                ret += body[i].ToString(\"X\") + \" \";\n            }\n        }\n\n    }\n\n    WebServer.OutPutStream(e.Context.Response, ret);\n}\n```\n\nThis API example is basic but as you get the method, you can choose what to do.\n\nAs you get the url, you can check for a specific controller called. And you have the parameters and the content payload!\n\nNotice the extension methods to read the body of the request:\n\n- ReadBody will read the data from the InputStream while the data is flowing in which might be in multiple passes depending on the size of the body\n- ReadForm allows to read a multipart/form-data form and returns the text key/value pairs as well as any files in the request\n\nExample of a result with call:\n\n![result](./doc/POSTcapture.jpg)\n\nAnd more! Check the complete example for more about this WebServer!\n\n## Using HTTPS\n\nYou will need to generate a certificate and keys:\n\n```csharp\nX509Certificate _myWebServerCertificate509 = new X509Certificate2(_myWebServerCrt, _myWebServerPrivateKey, \"1234\");\n\n// X509 RSA key PEM format 2048 bytes\n        // generate with openssl:\n        // \u003e openssl req -newkey rsa:2048 -nodes -keyout selfcert.key -x509 -days 365 -out selfcert.crt\n        // and paste selfcert.crt content below:\n        private const string _myWebServerCrt =\n@\"-----BEGIN CERTIFICATE-----\nMORETEXT\n-----END CERTIFICATE-----\";\n\n        // this one is generated with the command below. We need a password.\n        // \u003e openssl rsa -des3 -in selfcert.key -out selfcertenc.key\n        // the one below was encoded with '1234' as the password.\n        private const string _myWebServerPrivateKey =\n@\"-----BEGIN RSA PRIVATE KEY-----\nMORETEXTANDENCRYPTED\n-----END RSA PRIVATE KEY-----\";\n\nusing (WebServer server = new WebServer(443, HttpProtocol.Https)\n{\n    // Add a handler for commands that are received by the server.\n    server.CommandReceived += ServerCommandReceived;\n    server.HttpsCert = _myWebServerCertificate509;\n\n    server.SslProtocols = System.Net.Security.SslProtocols.Tls | System.Net.Security.SslProtocols.Tls11 | System.Net.Security.SslProtocols.Tls12;\n    // Start the server.\n    server.Start();\n\n    Thread.Sleep(Timeout.Infinite);\n}\n```\n\n\u003e [!IMPORTANT]\n\u003e Because the certificate above is not issued from a Certificate Authority it won't be recognized as a valid certificate. If you want to access the nanoFramework device with your browser, for example, you'll have to add the [CRT file](WebServer.Sample\\webserver-cert.crt) as a trusted one. On Windows, you just have to double click on the CRT file and then click \"Install Certificate...\".\n\nYou can of course use the routes as defined earlier. Both will work, event or route with the notion of controller.\n\n## WebServer status\n\nIt is possible to subscribe to an event to get the WebServer status. That can be useful to restart the server, put in place a retry mechanism or equivalent.\n\n```csharp\nserver.WebServerStatusChanged += WebServerStatusChanged;\n\nprivate static void WebServerStatusChanged(object obj, WebServerStatusEventArgs e)\n{\n    // Do whatever you need like restarting the server\n    Debug.WriteLine($\"The web server is now {(e.Status == WebServerStatus.Running ? \"running\" : \"stopped\" )}\");\n}\n```\n\n## E2E tests\n\nThere is a collection of postman tests `nanoFramework WebServer E2E Tests.postman_collection.json` in WebServerE2ETests which should be used for testing WebServer in real world scenario. Usage is simple:\n- Import json file into Postman\n- Deploy WebServerE2ETests to your device - copy IP\n- Set the `base_url` variable to match your device IP address \n- Choose request you want to test or run whole collection and check tests results.\n\nThe WebServerE2ETests project requires the name and credentials for the WiFi access point. That is stored in the WiFi.cs file that is not part of the git repository. Build the WebServerE2ETests to create a template for that file, then change the SSID and credentials. Your credentials will not be part of a commit.\n\n## Feedback and documentation\n\nFor documentation, providing feedback, issues and finding out how to contribute please refer to the [Home repo](https://github.com/nanoframework/Home).\n\nJoin our Discord community [here](https://discord.gg/gCyBu8T).\n\n## Credits\n\nThe list of contributors to this project can be found at [CONTRIBUTORS](https://github.com/nanoframework/Home/blob/main/CONTRIBUTORS.md).\n\n## License\n\nThe **nanoFramework** WebServer library is licensed under the [MIT license](LICENSE.md).\n\n## Code of Conduct\n\nThis project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behaviour in our community.\nFor more information see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).\n\n## .NET Foundation\n\nThis project is supported by the [.NET Foundation](https://dotnetfoundation.org).\n","funding_links":["https://opencollective.com/nanoframework","https://github.com/sponsors/nanoframework"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnanoframework%2Fnanoframework.webserver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnanoframework%2Fnanoframework.webserver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnanoframework%2Fnanoframework.webserver/lists"}