├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── doxygen.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── RedHttpServer.sln ├── assets ├── logo.svg ├── red-large.png ├── red.doxy └── red.png └── src ├── ExampleServer ├── ExampleServer.csproj ├── Program.cs ├── markdown.md ├── pages │ └── statuspage.ecs └── public │ ├── index.html │ └── upload.html ├── FSharpTest ├── FSharpTest.fsproj └── Program.fs ├── RedHttpServer.Tests ├── BodyParserTests.cs ├── RedHttpServer.Tests.csproj ├── RoutingIntegrationTests.cs └── TestUtils.cs ├── RedHttpServer ├── Context.cs ├── Extensions │ ├── BodyParser.cs │ ├── BodyParserExtension.cs │ ├── JsonConverter.cs │ └── XmlConverter.cs ├── HandlerExceptionEventArgs.cs ├── HandlerType.cs ├── Handlers.cs ├── InContext.cs ├── Interfaces │ ├── IBodyConverter.cs │ ├── IBodyParser.cs │ ├── IJsonConverter.cs │ ├── IRedExtension.cs │ ├── IRedMiddleware.cs │ ├── IRedWebSocketMiddleware.cs │ ├── IRouter.cs │ └── IXmlConverter.cs ├── PluginCollection.cs ├── Properties │ └── AssemblyInfo.cs ├── RedHttpServer.cs ├── RedHttpServer.csproj ├── RedHttpServerException.cs ├── RedHttpServerPrivate.cs ├── Request.cs ├── Response.cs ├── Router.cs ├── UrlParameters.cs └── WebSocketDialog.cs └── Test ├── Program.cs ├── Test.csproj ├── TestRoutes.cs └── public └── files └── test.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 5 8 | ignore: 9 | - dependency-name: Microsoft.NET.Test.Sdk 10 | versions: 11 | - 16.8.3 12 | - 16.9.1 13 | - dependency-name: nunit 14 | versions: 15 | - 3.13.0 16 | - 3.13.1 17 | - dependency-name: System.Text.Json 18 | versions: 19 | - 5.0.1 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Setup .NET Core 11 | uses: actions/setup-dotnet@v1 12 | with: 13 | dotnet-version: 3.1.101 14 | - name: Test with dotnet 15 | run: dotnet test 16 | -------------------------------------------------------------------------------- /.github/workflows/doxygen.yml: -------------------------------------------------------------------------------- 1 | name: Update documentation 2 | on: 3 | pull_request: 4 | branches: gh-pages 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Prepare doxygen 13 | run: | 14 | sudo apt update 15 | sudo apt install -y doxygen graphviz 16 | 17 | - name: Clean old documentation 18 | run: | 19 | rm -r ./doxygen 20 | mkdir ./doxygen 21 | 22 | - name: Run doxygen 23 | run: doxygen assets/red.doxy 24 | 25 | - name: Push updated documentation 26 | run: | 27 | git add ./doxygen --all 28 | git commit -am "Update documentation" 29 | git push 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | doxy_output/ 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | .idea/ 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | artifacts/ 49 | 50 | *_i.c 51 | *_p.c 52 | *_i.h 53 | *.ilk 54 | *.meta 55 | *.obj 56 | *.pch 57 | *.pdb 58 | *.pgc 59 | *.pgd 60 | *.rsp 61 | *.sbr 62 | *.tlb 63 | *.tli 64 | *.tlh 65 | *.tmp 66 | *.tmp_proj 67 | *.log 68 | *.vspscc 69 | *.vssscc 70 | .builds 71 | *.pidb 72 | *.svclog 73 | *.scc 74 | 75 | # Chutzpah Test files 76 | _Chutzpah* 77 | 78 | # Visual C++ cache files 79 | ipch/ 80 | *.aps 81 | *.ncb 82 | *.opendb 83 | *.opensdf 84 | *.sdf 85 | *.cachefile 86 | *.VC.db 87 | *.VC.VC.opendb 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | *.sap 94 | 95 | # TFS 2012 Local Workspace 96 | $tf/ 97 | 98 | # Guidance Automation Toolkit 99 | *.gpState 100 | 101 | # ReSharper is a .NET coding add-in 102 | _ReSharper*/ 103 | *.[Rr]e[Ss]harper 104 | *.DotSettings.user 105 | 106 | # JustCode is a .NET coding add-in 107 | .JustCode 108 | 109 | # TeamCity is a build add-in 110 | _TeamCity* 111 | 112 | # DotCover is a Code Coverage Tool 113 | *.dotCover 114 | 115 | # NCrunch 116 | _NCrunch_* 117 | .*crunch*.local.xml 118 | nCrunchTemp_* 119 | 120 | # MightyMoose 121 | *.mm.* 122 | AutoTest.Net/ 123 | 124 | # Web workbench (sass) 125 | .sass-cache/ 126 | 127 | # Installshield output folder 128 | [Ee]xpress/ 129 | 130 | # DocProject is a documentation generator add-in 131 | DocProject/buildhelp/ 132 | DocProject/Help/*.HxT 133 | DocProject/Help/*.HxC 134 | DocProject/Help/*.hhc 135 | DocProject/Help/*.hhk 136 | DocProject/Help/*.hhp 137 | DocProject/Help/Html2 138 | DocProject/Help/html 139 | 140 | # Click-Once directory 141 | publish/ 142 | 143 | # Publish Web Output 144 | *.[Pp]ublish.xml 145 | *.azurePubxml 146 | # TODO: Comment the next line if you want to checkin your web deploy settings 147 | # but database connection strings (with potential passwords) will be unencrypted 148 | *.pubxml 149 | *.publishproj 150 | 151 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 152 | # checkin your Azure Web App publish settings, but sensitive information contained 153 | # in these scripts will be unencrypted 154 | PublishScripts/ 155 | 156 | # NuGet Packages 157 | *.nupkg 158 | # The packages folder can be ignored because of Package Restore 159 | **/packages/* 160 | # except build/, which is used as an MSBuild target. 161 | !**/packages/build/ 162 | # Uncomment if necessary however generally it will be regenerated when needed 163 | #!**/packages/repositories.config 164 | # NuGet v3's project.json files produces more ignoreable files 165 | *.nuget.props 166 | *.nuget.targets 167 | 168 | # Microsoft Azure Build Output 169 | csx/ 170 | *.build.csdef 171 | 172 | # Microsoft Azure Emulator 173 | ecf/ 174 | rcf/ 175 | 176 | # Windows Store app package directories and files 177 | AppPackages/ 178 | BundleArtifacts/ 179 | Package.StoreAssociation.xml 180 | _pkginfo.txt 181 | 182 | # Visual Studio cache files 183 | # files ending in .cache can be ignored 184 | *.[Cc]ache 185 | # but keep track of directories ending in .cache 186 | !*.[Cc]ache/ 187 | 188 | # Others 189 | ClientBin/ 190 | ~$* 191 | *~ 192 | *.dbmdl 193 | *.dbproj.schemaview 194 | *.pfx 195 | *.publishsettings 196 | node_modules/ 197 | orleans.codegen.cs 198 | 199 | # Since there are multiple workflows, uncomment next line to ignore bower_components 200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 201 | #bower_components/ 202 | 203 | # RIA/Silverlight projects 204 | Generated_Code/ 205 | 206 | # Backup & report files from converting an old project file 207 | # to a newer Visual Studio version. Backup files are not needed, 208 | # because we have git ;-) 209 | _UpgradeReport_Files/ 210 | Backup*/ 211 | UpgradeLog*.XML 212 | UpgradeLog*.htm 213 | 214 | # SQL Server files 215 | *.mdf 216 | *.ldf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | 232 | # Visual Studio 6 build log 233 | *.plg 234 | 235 | # Visual Studio 6 workspace options file 236 | *.opt 237 | 238 | # Visual Studio LightSwitch build output 239 | **/*.HTMLClient/GeneratedArtifacts 240 | **/*.DesktopClient/GeneratedArtifacts 241 | **/*.DesktopClient/ModelManifest.xml 242 | **/*.Server/GeneratedArtifacts 243 | **/*.Server/ModelManifest.xml 244 | _Pvt_Extensions 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | paket-files/ 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | # JetBrains Rider 254 | .idea/ 255 | *.sln.iml 256 | 257 | # ========================= 258 | # Operating System Files 259 | # ========================= 260 | 261 | # OSX 262 | # ========================= 263 | 264 | .DS_Store 265 | .AppleDouble 266 | .LSOverride 267 | 268 | # Thumbnails 269 | ._* 270 | 271 | # Files that might appear in the root of a volume 272 | .DocumentRevisions-V100 273 | .fseventsd 274 | .Spotlight-V100 275 | .TemporaryItems 276 | .Trashes 277 | .VolumeIcon.icns 278 | 279 | # Directories potentially created on remote AFP share 280 | .AppleDB 281 | .AppleDesktop 282 | Network Trash Folder 283 | Temporary Items 284 | .apdisk 285 | 286 | # Windows 287 | # ========================= 288 | 289 | # Windows image file caches 290 | Thumbs.db 291 | ehthumbs.db 292 | 293 | # Folder config file 294 | Desktop.ini 295 | 296 | # Recycle Bin used on file shares 297 | $RECYCLE.BIN/ 298 | 299 | # Windows Installer files 300 | *.cab 301 | *.msi 302 | *.msm 303 | *.msp 304 | 305 | # Windows shortcuts 306 | *.lnk 307 | src/Test/Big_Buck_Bunny_alt.webm 308 | src/Test/index.html 309 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Malte Rosenbjerg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## The RedHttp project is no longer maintained. See [Carter](https://github.com/CarterCommunity/Carter) for a similar low-ceremony experience. 2 | 3 | # RedHttpServer 4 | ### Low ceremony cross-platform http server framework with websocket support 5 | ![.NET Core](https://github.com/RedHttp/Red/workflows/CI/badge.svg) 6 | [![GitHub](https://img.shields.io/github/license/redhttp/red)](https://github.com/RedHttp/Red/blob/master/LICENSE.md) 7 | [![Nuget](https://img.shields.io/nuget/v/rhttpserver)](https://www.nuget.org/packages/RHttpServer/) 8 | [![Nuget](https://img.shields.io/nuget/dt/rhttpserver)](https://www.nuget.org/packages/RHttpServer/) 9 | ![Dependent repos (via libraries.io)](https://img.shields.io/librariesio/dependent-repos/nuget/rhttpserver) 10 | 11 | 12 | A .NET Standard web application framework built on ASP.NET Core w/ Kestrel and inspired by the simplicity of Express.js 13 | 14 | - [Homepage](https://redhttp.github.io/Red/) 15 | - [Documentation](https://redhttp.github.io/Red/doxygen/) 16 | 17 | ### Installation 18 | RedHttpServer can be installed from [NuGet](https://www.nuget.org/packages/RHttpServer/): `Install-Package RHttpServer` 19 | 20 | ## Middleware and plugins 21 | RedHttpServer is created to be easy to build on top of. 22 | The server supports both middleware modules and extension modules 23 | 24 | * JsonConverter - uses System.Text.Json 25 | * XmlConverter - uses System.Xml.Serialization 26 | * BodyParser - uses both the Json- and Xml converter to parse request body to an object, depending on content-type. 27 | 28 | More extensions and middleware 29 | - [CookieSessions](https://github.com/RedHttp/Red.CookieSessions) simple session management middleware that uses cookies with authentication tokens. 30 | - [JwtSessions](https://github.com/RedHttp/Red.JwtSessions) simple session management middleware that uses JWT tokens - uses [Jwt.Net](https://github.com/jwt-dotnet/jwt) 31 | - [Validation](https://github.com/RedHttp/Red.Validation) build validators for forms and queries using a fluent API 32 | - [EcsRenderer](https://github.com/RedHttp/Red.EcsRenderer) simple template rendering extension. See more info about the format by clicking the link. 33 | - [CommonMarkRenderer](https://github.com/RedHttp/Red.CommonMarkRenderer) simple CommonMark/Markdown renderer extension - uses [CommonMark.NET](https://github.com/Knagis/CommonMark.NET) 34 | - [HandlebarsRenderer](https://github.com/RedHttp/Red.HandlebarsRenderer) simple Handlebars renderer extension - uses [Handlebars.Net](https://github.com/rexm/Handlebars.Net) 35 | 36 | 37 | ### Example 38 | ```csharp 39 | var server = new RedHttpServer(5000, "public"); 40 | server.Use(new EcsRenderer()); 41 | server.Use(new CookieSessions(TimeSpan.FromDays(1))); 42 | 43 | // Authentication middleware 44 | async Task Auth(Request req, Response res) 45 | { 46 | if (req.GetData() != null) 47 | { 48 | return HandlerType.Continue; 49 | } 50 | 51 | await res.SendStatus(HttpStatusCode.Unauthorized); 52 | return HandlerType.Final; 53 | } 54 | 55 | var startTime = DateTime.UtcNow; 56 | 57 | // Url parameters 58 | server.Get("profile/:username", Auth, (req, res) => 59 | { 60 | var username = req.Context.ExtractUrlParameter("username"); 61 | // ... lookup username in database or similar and fetch profile ... 62 | var user = new { FirstName = "John", LastName = "Doe", Username = username }; 63 | return res.SendJson(user); 64 | }); 65 | 66 | // Using forms 67 | server.Post("/login", async (req, res) => 68 | { 69 | var form = await req.GetFormDataAsync(); 70 | // ... some validation and authentication ... 71 | await res.OpenSession(new MySession { Username = form["username"] }); 72 | return await res.SendStatus(HttpStatusCode.OK); 73 | }); 74 | 75 | server.Post("/logout", Auth, async (req, res) => 76 | { 77 | var session = req.GetData(); 78 | await res.CloseSession(session); 79 | return await res.SendStatus(HttpStatusCode.OK); 80 | }); 81 | 82 | // Simple redirects 83 | server.Get("/redirect", Auth, (req, res) => res.Redirect("/redirect/test/here")); 84 | 85 | // File uploads 86 | Directory.CreateDirectory("uploads"); 87 | server.Post("/upload", async (req, res) => 88 | { 89 | if (await req.SaveFiles("uploads")) 90 | return await res.SendString("OK"); 91 | else 92 | return await res.SendString("Error", status: HttpStatusCode.NotAcceptable); 93 | }); 94 | 95 | server.Get("/file", (req, res) => res.SendFile("somedirectory/animage.jpg")); 96 | 97 | // Using url queries 98 | server.Get("/search", Auth, (req, res) => 99 | { 100 | string searchQuery = req.Queries["query"]; 101 | string format = req.Queries["format"]; 102 | // ... perform search using searchQuery and return results ... 103 | var results = new[] { "Apple", "Pear" }; 104 | 105 | if (format == "xml") 106 | return res.SendXml(results); 107 | else 108 | return res.SendJson(results); 109 | }); 110 | 111 | // Markdown rendering 112 | server.Get("/markdown", (req, res) => res.RenderFile("markdown.md")); 113 | 114 | // Esc rendering 115 | server.Get("/serverstatus", async (req, res) => await res.RenderPage("pages/statuspage.ecs", 116 | new RenderParams 117 | { 118 | { "uptime", (int) DateTime.UtcNow.Subtract(startTime).TotalSeconds }, 119 | { "version", Red.RedHttpServer.Version } 120 | })); 121 | 122 | // Using websockets 123 | server.WebSocket("/echo", async (req, wsd) => 124 | { 125 | wsd.SendText("Welcome to the echo test server"); 126 | wsd.OnTextReceived += (sender, eventArgs) => { wsd.SendText("you sent: " + eventArgs.Text); }; 127 | return HandlerType.Final; 128 | }); 129 | 130 | // Keep the program running easily (async Program.Main - C# 7.1+) 131 | await server.RunAsync(); 132 | ``` 133 | 134 | ## Why? 135 | Because I like C# and .NET Core, but very often need a simple yet powerful web server for some project. Express.js is concise and simple to work with, so the API is inspired by that. 136 | 137 | ## License 138 | RedHttpServer is released under MIT license, so it is free to use, even in commercial projects. 139 | -------------------------------------------------------------------------------- /RedHttpServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2002 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D9A21FF4-1C0B-45B1-8442-D89F53C67E2B}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedHttpServer", "src\RedHttpServer\RedHttpServer.csproj", "{141EA45B-9148-4F09-A48B-17083BB96DB8}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleServer", "src\ExampleServer\ExampleServer.csproj", "{F63814F5-2D27-410B-95BD-01D0D286D6B3}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "src\Test\Test.csproj", "{C17662C3-07D5-46AD-8E18-209E713EE1E9}" 13 | EndProject 14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpTest", "src\FSharpTest\FSharpTest.fsproj", "{76F39769-1626-4658-A96F-533BEF078909}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedHttpServer.Tests", "src\RedHttpServer.Tests\RedHttpServer.Tests.csproj", "{C3595961-7D8A-4E46-96FF-E6F470550988}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {141EA45B-9148-4F09-A48B-17083BB96DB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {141EA45B-9148-4F09-A48B-17083BB96DB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {141EA45B-9148-4F09-A48B-17083BB96DB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {141EA45B-9148-4F09-A48B-17083BB96DB8}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {F63814F5-2D27-410B-95BD-01D0D286D6B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {F63814F5-2D27-410B-95BD-01D0D286D6B3}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {F63814F5-2D27-410B-95BD-01D0D286D6B3}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {F63814F5-2D27-410B-95BD-01D0D286D6B3}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {C17662C3-07D5-46AD-8E18-209E713EE1E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {C17662C3-07D5-46AD-8E18-209E713EE1E9}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {C17662C3-07D5-46AD-8E18-209E713EE1E9}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {C17662C3-07D5-46AD-8E18-209E713EE1E9}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {76F39769-1626-4658-A96F-533BEF078909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {76F39769-1626-4658-A96F-533BEF078909}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {76F39769-1626-4658-A96F-533BEF078909}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {76F39769-1626-4658-A96F-533BEF078909}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {C3595961-7D8A-4E46-96FF-E6F470550988}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {C3595961-7D8A-4E46-96FF-E6F470550988}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {C3595961-7D8A-4E46-96FF-E6F470550988}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {C3595961-7D8A-4E46-96FF-E6F470550988}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {141EA45B-9148-4F09-A48B-17083BB96DB8} = {D9A21FF4-1C0B-45B1-8442-D89F53C67E2B} 50 | {F63814F5-2D27-410B-95BD-01D0D286D6B3} = {D9A21FF4-1C0B-45B1-8442-D89F53C67E2B} 51 | {C17662C3-07D5-46AD-8E18-209E713EE1E9} = {D9A21FF4-1C0B-45B1-8442-D89F53C67E2B} 52 | {76F39769-1626-4658-A96F-533BEF078909} = {D9A21FF4-1C0B-45B1-8442-D89F53C67E2B} 53 | {C3595961-7D8A-4E46-96FF-E6F470550988} = {D9A21FF4-1C0B-45B1-8442-D89F53C67E2B} 54 | EndGlobalSection 55 | GlobalSection(ExtensibilityGlobals) = postSolution 56 | SolutionGuid = {DE192755-FA6B-4D74-86F1-2BD817ABD3BD} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | vectorstash-3dpolygon 17 | 19 | 21 | 25 | 29 | 30 | 32 | 36 | 40 | 41 | 43 | 47 | 51 | 52 | 61 | 62 | 64 | 65 | 67 | image/svg+xml 68 | 70 | vectorstash-3dpolygon 71 | 72 | 73 | free vector art graphics svg download 74 | 75 | 76 | 77 | 78 | vector 79 | 80 | 81 | 82 | 83 | 84 | 87 | 90 | 93 | 97 | 101 | 105 | 109 | 113 | 117 | 121 | 125 | 129 | 133 | 137 | 141 | 145 | 149 | 153 | 157 | 161 | 165 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /assets/red-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHttp/Red/3fbb6fc0d21ba1292db885954bbdd60bc8bbfb44/assets/red-large.png -------------------------------------------------------------------------------- /assets/red.doxy: -------------------------------------------------------------------------------- 1 | # Doxyfile 1.8.15 2 | 3 | # This file describes the settings to be used by the documentation system 4 | # doxygen (www.doxygen.org) for a project. 5 | # 6 | # All text after a double hash (##) is considered a comment and is placed in 7 | # front of the TAG it is preceding. 8 | # 9 | # All text after a single hash (#) is considered a comment and will be ignored. 10 | # The format is: 11 | # TAG = value [value, ...] 12 | # For lists, items can also be appended using: 13 | # TAG += value [value, ...] 14 | # Values that contain spaces should be placed between quotes (\" \"). 15 | 16 | #--------------------------------------------------------------------------- 17 | # Project related configuration options 18 | #--------------------------------------------------------------------------- 19 | 20 | DOXYFILE_ENCODING = UTF-8 21 | PROJECT_NAME = Red 22 | PROJECT_NUMBER = 4.0.0 23 | PROJECT_BRIEF = ".NET Standard web application framework built on ASP.NET Core w/ Kestrel and inspired by Express.js" 24 | PROJECT_LOGO = red.png 25 | OUTPUT_DIRECTORY = doxy_output 26 | CREATE_SUBDIRS = NO 27 | ALLOW_UNICODE_NAMES = YES 28 | OUTPUT_LANGUAGE = English 29 | OUTPUT_TEXT_DIRECTION = None 30 | BRIEF_MEMBER_DESC = YES 31 | REPEAT_BRIEF = YES 32 | ABBREVIATE_BRIEF = "The $name class" \ 33 | "The $name widget" \ 34 | "The $name file" \ 35 | is \ 36 | provides \ 37 | specifies \ 38 | contains \ 39 | represents \ 40 | a \ 41 | an \ 42 | the 43 | ALWAYS_DETAILED_SEC = NO 44 | INLINE_INHERITED_MEMB = NO 45 | FULL_PATH_NAMES = NO 46 | STRIP_FROM_PATH = 47 | STRIP_FROM_INC_PATH = 48 | SHORT_NAMES = NO 49 | JAVADOC_AUTOBRIEF = NO 50 | QT_AUTOBRIEF = NO 51 | MULTILINE_CPP_IS_BRIEF = NO 52 | INHERIT_DOCS = YES 53 | SEPARATE_MEMBER_PAGES = NO 54 | TAB_SIZE = 4 55 | ALIASES = 56 | TCL_SUBST = 57 | OPTIMIZE_OUTPUT_FOR_C = NO 58 | OPTIMIZE_OUTPUT_JAVA = YES 59 | OPTIMIZE_FOR_FORTRAN = NO 60 | OPTIMIZE_OUTPUT_VHDL = NO 61 | OPTIMIZE_OUTPUT_SLICE = NO 62 | EXTENSION_MAPPING = 63 | MARKDOWN_SUPPORT = YES 64 | TOC_INCLUDE_HEADINGS = 0 65 | AUTOLINK_SUPPORT = YES 66 | BUILTIN_STL_SUPPORT = NO 67 | CPP_CLI_SUPPORT = NO 68 | SIP_SUPPORT = NO 69 | IDL_PROPERTY_SUPPORT = YES 70 | DISTRIBUTE_GROUP_DOC = NO 71 | GROUP_NESTED_COMPOUNDS = NO 72 | SUBGROUPING = YES 73 | INLINE_GROUPED_CLASSES = NO 74 | INLINE_SIMPLE_STRUCTS = NO 75 | TYPEDEF_HIDES_STRUCT = NO 76 | LOOKUP_CACHE_SIZE = 0 77 | EXTRACT_ALL = NO 78 | EXTRACT_PRIVATE = NO 79 | EXTRACT_PACKAGE = NO 80 | EXTRACT_STATIC = NO 81 | EXTRACT_LOCAL_CLASSES = YES 82 | EXTRACT_LOCAL_METHODS = NO 83 | EXTRACT_ANON_NSPACES = NO 84 | HIDE_UNDOC_MEMBERS = NO 85 | HIDE_UNDOC_CLASSES = NO 86 | HIDE_FRIEND_COMPOUNDS = NO 87 | HIDE_IN_BODY_DOCS = NO 88 | INTERNAL_DOCS = NO 89 | CASE_SENSE_NAMES = NO 90 | HIDE_SCOPE_NAMES = NO 91 | HIDE_COMPOUND_REFERENCE= NO 92 | SHOW_INCLUDE_FILES = YES 93 | SHOW_GROUPED_MEMB_INC = NO 94 | FORCE_LOCAL_INCLUDES = NO 95 | INLINE_INFO = YES 96 | SORT_MEMBER_DOCS = YES 97 | SORT_BRIEF_DOCS = NO 98 | SORT_MEMBERS_CTORS_1ST = NO 99 | SORT_GROUP_NAMES = NO 100 | SORT_BY_SCOPE_NAME = NO 101 | STRICT_PROTO_MATCHING = NO 102 | GENERATE_TODOLIST = YES 103 | GENERATE_TESTLIST = YES 104 | GENERATE_BUGLIST = YES 105 | GENERATE_DEPRECATEDLIST= YES 106 | ENABLED_SECTIONS = 107 | MAX_INITIALIZER_LINES = 30 108 | SHOW_USED_FILES = YES 109 | SHOW_FILES = YES 110 | SHOW_NAMESPACES = YES 111 | FILE_VERSION_FILTER = 112 | LAYOUT_FILE = 113 | CITE_BIB_FILES = 114 | QUIET = NO 115 | WARNINGS = YES 116 | WARN_IF_UNDOCUMENTED = YES 117 | WARN_IF_DOC_ERROR = YES 118 | WARN_NO_PARAMDOC = NO 119 | WARN_AS_ERROR = NO 120 | WARN_FORMAT = "$file:$line: $text" 121 | WARN_LOGFILE = 122 | INPUT = ../src/RedHttpServer 123 | INPUT_ENCODING = UTF-8 124 | FILE_PATTERNS = *.c \ 125 | *.cc \ 126 | *.cxx \ 127 | *.cpp \ 128 | *.c++ \ 129 | *.java \ 130 | *.ii \ 131 | *.ixx \ 132 | *.ipp \ 133 | *.i++ \ 134 | *.inl \ 135 | *.idl \ 136 | *.ddl \ 137 | *.odl \ 138 | *.h \ 139 | *.hh \ 140 | *.hxx \ 141 | *.hpp \ 142 | *.h++ \ 143 | *.cs \ 144 | *.d \ 145 | *.php \ 146 | *.php4 \ 147 | *.php5 \ 148 | *.phtml \ 149 | *.inc \ 150 | *.m \ 151 | *.markdown \ 152 | *.md \ 153 | *.mm \ 154 | *.dox \ 155 | *.py \ 156 | *.pyw \ 157 | *.f90 \ 158 | *.f95 \ 159 | *.f03 \ 160 | *.f08 \ 161 | *.f \ 162 | *.for \ 163 | *.tcl \ 164 | *.vhd \ 165 | *.vhdl \ 166 | *.ucf \ 167 | *.qsf 168 | RECURSIVE = YES 169 | EXCLUDE = 170 | EXCLUDE_SYMLINKS = NO 171 | EXCLUDE_PATTERNS = 172 | EXCLUDE_SYMBOLS = 173 | EXAMPLE_PATH = 174 | EXAMPLE_PATTERNS = * 175 | EXAMPLE_RECURSIVE = NO 176 | IMAGE_PATH = 177 | INPUT_FILTER = 178 | FILTER_PATTERNS = 179 | FILTER_SOURCE_FILES = NO 180 | FILTER_SOURCE_PATTERNS = 181 | USE_MDFILE_AS_MAINPAGE = 182 | SOURCE_BROWSER = NO 183 | INLINE_SOURCES = NO 184 | STRIP_CODE_COMMENTS = YES 185 | REFERENCED_BY_RELATION = NO 186 | REFERENCES_RELATION = NO 187 | REFERENCES_LINK_SOURCE = YES 188 | SOURCE_TOOLTIPS = YES 189 | USE_HTAGS = NO 190 | VERBATIM_HEADERS = YES 191 | CLANG_ASSISTED_PARSING = NO 192 | CLANG_OPTIONS = 193 | CLANG_DATABASE_PATH = 194 | ALPHABETICAL_INDEX = YES 195 | COLS_IN_ALPHA_INDEX = 5 196 | IGNORE_PREFIX = 197 | GENERATE_HTML = YES 198 | HTML_OUTPUT = html 199 | HTML_FILE_EXTENSION = .html 200 | HTML_HEADER = 201 | HTML_FOOTER = 202 | HTML_STYLESHEET = 203 | HTML_EXTRA_STYLESHEET = 204 | HTML_EXTRA_FILES = 205 | HTML_COLORSTYLE_HUE = 356 206 | HTML_COLORSTYLE_SAT = 11 207 | HTML_COLORSTYLE_GAMMA = 106 208 | HTML_TIMESTAMP = NO 209 | HTML_DYNAMIC_MENUS = YES 210 | HTML_DYNAMIC_SECTIONS = NO 211 | HTML_INDEX_NUM_ENTRIES = 100 212 | GENERATE_DOCSET = NO 213 | DOCSET_FEEDNAME = "Doxygen generated docs" 214 | DOCSET_BUNDLE_ID = org.doxygen.Project 215 | DOCSET_PUBLISHER_ID = org.doxygen.Publisher 216 | DOCSET_PUBLISHER_NAME = Publisher 217 | GENERATE_HTMLHELP = NO 218 | CHM_FILE = 219 | HHC_LOCATION = 220 | GENERATE_CHI = NO 221 | CHM_INDEX_ENCODING = 222 | BINARY_TOC = NO 223 | TOC_EXPAND = NO 224 | GENERATE_QHP = NO 225 | QCH_FILE = 226 | QHP_NAMESPACE = org.doxygen.Project 227 | QHP_VIRTUAL_FOLDER = doc 228 | QHP_CUST_FILTER_NAME = 229 | QHP_CUST_FILTER_ATTRS = 230 | QHP_SECT_FILTER_ATTRS = 231 | QHG_LOCATION = 232 | GENERATE_ECLIPSEHELP = NO 233 | ECLIPSE_DOC_ID = org.doxygen.Project 234 | DISABLE_INDEX = NO 235 | GENERATE_TREEVIEW = YES 236 | ENUM_VALUES_PER_LINE = 4 237 | TREEVIEW_WIDTH = 250 238 | EXT_LINKS_IN_WINDOW = NO 239 | FORMULA_FONTSIZE = 10 240 | FORMULA_TRANSPARENT = YES 241 | USE_MATHJAX = NO 242 | MATHJAX_FORMAT = HTML-CSS 243 | MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest 244 | MATHJAX_EXTENSIONS = 245 | MATHJAX_CODEFILE = 246 | SEARCHENGINE = YES 247 | SERVER_BASED_SEARCH = NO 248 | EXTERNAL_SEARCH = NO 249 | SEARCHENGINE_URL = 250 | SEARCHDATA_FILE = searchdata.xml 251 | EXTERNAL_SEARCH_ID = 252 | EXTRA_SEARCH_MAPPINGS = 253 | GENERATE_LATEX = NO 254 | LATEX_OUTPUT = latex 255 | LATEX_CMD_NAME = latex 256 | MAKEINDEX_CMD_NAME = makeindex 257 | LATEX_MAKEINDEX_CMD = \makeindex 258 | COMPACT_LATEX = NO 259 | PAPER_TYPE = a4 260 | EXTRA_PACKAGES = 261 | LATEX_HEADER = 262 | LATEX_FOOTER = 263 | LATEX_EXTRA_STYLESHEET = 264 | LATEX_EXTRA_FILES = 265 | PDF_HYPERLINKS = YES 266 | USE_PDFLATEX = YES 267 | LATEX_BATCHMODE = NO 268 | LATEX_HIDE_INDICES = NO 269 | LATEX_SOURCE_CODE = NO 270 | LATEX_BIB_STYLE = plain 271 | LATEX_TIMESTAMP = NO 272 | LATEX_EMOJI_DIRECTORY = 273 | GENERATE_RTF = NO 274 | RTF_OUTPUT = rtf 275 | COMPACT_RTF = NO 276 | RTF_HYPERLINKS = NO 277 | RTF_STYLESHEET_FILE = 278 | RTF_EXTENSIONS_FILE = 279 | RTF_SOURCE_CODE = NO 280 | GENERATE_MAN = NO 281 | MAN_OUTPUT = man 282 | MAN_EXTENSION = .3 283 | MAN_SUBDIR = 284 | MAN_LINKS = NO 285 | GENERATE_XML = NO 286 | XML_OUTPUT = xml 287 | XML_PROGRAMLISTING = YES 288 | XML_NS_MEMB_FILE_SCOPE = NO 289 | GENERATE_DOCBOOK = NO 290 | DOCBOOK_OUTPUT = docbook 291 | DOCBOOK_PROGRAMLISTING = NO 292 | GENERATE_AUTOGEN_DEF = NO 293 | GENERATE_PERLMOD = NO 294 | PERLMOD_LATEX = NO 295 | PERLMOD_PRETTY = YES 296 | PERLMOD_MAKEVAR_PREFIX = 297 | ENABLE_PREPROCESSING = YES 298 | MACRO_EXPANSION = NO 299 | EXPAND_ONLY_PREDEF = NO 300 | SEARCH_INCLUDES = YES 301 | INCLUDE_PATH = 302 | INCLUDE_FILE_PATTERNS = 303 | PREDEFINED = 304 | EXPAND_AS_DEFINED = 305 | SKIP_FUNCTION_MACROS = YES 306 | TAGFILES = 307 | GENERATE_TAGFILE = 308 | ALLEXTERNALS = NO 309 | EXTERNAL_GROUPS = YES 310 | EXTERNAL_PAGES = YES 311 | PERL_PATH = /usr/bin/perl 312 | CLASS_DIAGRAMS = NO 313 | MSCGEN_PATH = 314 | DIA_PATH = 315 | HIDE_UNDOC_RELATIONS = YES 316 | HAVE_DOT = NO 317 | DOT_NUM_THREADS = 0 318 | DOT_FONTNAME = Helvetica 319 | DOT_FONTSIZE = 10 320 | DOT_FONTPATH = 321 | CLASS_GRAPH = YES 322 | COLLABORATION_GRAPH = YES 323 | GROUP_GRAPHS = YES 324 | UML_LOOK = NO 325 | UML_LIMIT_NUM_FIELDS = 10 326 | TEMPLATE_RELATIONS = NO 327 | INCLUDE_GRAPH = YES 328 | INCLUDED_BY_GRAPH = YES 329 | CALL_GRAPH = NO 330 | CALLER_GRAPH = NO 331 | GRAPHICAL_HIERARCHY = YES 332 | DIRECTORY_GRAPH = YES 333 | DOT_IMAGE_FORMAT = png 334 | INTERACTIVE_SVG = NO 335 | DOT_PATH = 336 | DOTFILE_DIRS = 337 | MSCFILE_DIRS = 338 | DIAFILE_DIRS = 339 | PLANTUML_JAR_PATH = 340 | PLANTUML_CFG_FILE = 341 | PLANTUML_INCLUDE_PATH = 342 | DOT_GRAPH_MAX_NODES = 50 343 | MAX_DOT_GRAPH_DEPTH = 0 344 | DOT_TRANSPARENT = NO 345 | DOT_MULTI_TARGETS = NO 346 | GENERATE_LEGEND = YES 347 | DOT_CLEANUP = YES 348 | -------------------------------------------------------------------------------- /assets/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHttp/Red/3fbb6fc0d21ba1292db885954bbdd60bc8bbfb44/assets/red.png -------------------------------------------------------------------------------- /src/ExampleServer/ExampleServer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | netcoreapp3.1 5 | 8 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ExampleServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using Red; 6 | using Red.CommonMarkRenderer; 7 | using Red.CookieSessions; 8 | using Red.EcsRenderer; 9 | using Red.JwtSessions; 10 | 11 | namespace ExampleServer 12 | { 13 | public class Program 14 | { 15 | class MySess : CookieSessionBase 16 | { 17 | public string Username; 18 | } 19 | 20 | public static async Task Main(string[] args) 21 | { 22 | // We create a server instance using port 5000 which serves static files, such as index.html from the 'public' directory 23 | var server = new RedHttpServer(5000, "public"); 24 | 25 | // We register the needed middleware: 26 | 27 | // We use the Red.EscRenderer plugin in this example 28 | server.Use(new EcsRenderer()); 29 | 30 | 31 | // We use Red.CookieSessions as authentication in this example 32 | server.Use(new CookieSessions(TimeSpan.FromDays(1)) 33 | { 34 | Secure = false // for development 35 | }); 36 | 37 | // Middleware function that closes requests that does not have a valid session associated 38 | async Task Auth(Request req, Response res) 39 | { 40 | if (req.GetData() != null) 41 | { 42 | return HandlerType.Continue; 43 | } 44 | await res.SendStatus(HttpStatusCode.Unauthorized); 45 | return HandlerType.Final; 46 | } 47 | 48 | var startTime = DateTime.UtcNow; 49 | 50 | // We register our endpoint handlers: 51 | 52 | // We can use url params, much like in express.js 53 | server.Get("/:param1/:paramtwo/:somethingthird", Auth, (req, res) => 54 | { 55 | var context = req.Context; 56 | return res.SendString( 57 | $"you entered:\n" + 58 | context.Params["param1"] + "\n" + 59 | context.Params["paramtwo"] + "\n" + 60 | context.Params["somethingthird"] + "\n"); 61 | }); 62 | 63 | // The clients can post to this endpoint to authenticate 64 | server.Post("/login", async (req, res) => 65 | { 66 | // To make it easy to test the session system only using the browser and no credentials 67 | await res.OpenSession(new MySess {Username = "benny"}); 68 | return await res.SendStatus(HttpStatusCode.OK); 69 | }); 70 | 71 | // The client can post to this endpoint to close their current session 72 | // Note that we require the client the be authenticated using the Auth-function we created above 73 | server.Get("/logout", Auth, async (req, res) => 74 | { 75 | await res.CloseSession(req.GetData()); 76 | return await res.SendStatus(HttpStatusCode.OK); 77 | }); 78 | 79 | 80 | // We redirect authenticated clients to some other page 81 | server.Get("/redirect", Auth, (req, res) => res.Redirect("/redirect/test/here")); 82 | 83 | // We save the files contained in the POST request from authenticated clients in a directory called 'uploads' 84 | Directory.CreateDirectory("uploads"); 85 | server.Post("/upload", async (req, res) => 86 | { 87 | if (await req.SaveFiles("uploads")) 88 | return await res.SendString("OK"); 89 | else 90 | return await res.SendString("Error", status: HttpStatusCode.NotAcceptable); 91 | }); 92 | 93 | // We can also serve files outside of the public directory, if we wish to 94 | // This can be handy when serving "dynamic files" - files which the client identify using an ID instead of the actual path on the server 95 | server.Get("/file", (req, res) => res.SendFile("testimg.jpeg")); 96 | 97 | // We can easily handle POST requests containing FormData 98 | server.Post("/formdata", async (req, res) => 99 | { 100 | var form = await req.GetFormDataAsync(); 101 | return await res.SendString("Hello " + form["firstname"]); 102 | }); 103 | 104 | // In a similar way, we can also handle url queries for a given request easily, in the example only for authenticated clients 105 | server.Get("/hello", Auth, async (req, res) => 106 | { 107 | var session = req.GetData(); 108 | var queries = req.Queries; 109 | return await res.SendString( 110 | $"Hello {queries["firstname"]} {queries["lastname"]}, you are logged in as {session.Username}"); 111 | }); 112 | 113 | // We can render Markdown/CommonMark using the Red.CommonMarkRenderer plugin 114 | server.Get("/markdown", (req, res) => res.RenderFile("markdown.md")); 115 | 116 | // We use the Red.EcsRenderer plugin to render a simple template 117 | server.Get("/serverstatus", async (req, res) => await res.RenderPage("pages/statuspage.ecs", new RenderParams 118 | { 119 | {"uptime", (int) DateTime.UtcNow.Subtract(startTime).TotalSeconds}, 120 | {"version", Red.RedHttpServer.Version} 121 | })); 122 | 123 | // We can also handle WebSocket requests, without any plugin needed 124 | // In this example we just have a simple WebSocket echo server 125 | server.WebSocket("/echo", async (req, res, wsd) => 126 | { 127 | wsd.SendText("Welcome to the echo test server"); 128 | wsd.OnTextReceived += (sender, eventArgs) => { wsd.SendText("you sent: " + eventArgs.Text); }; 129 | return HandlerType.Continue; 130 | }); 131 | 132 | // Then we start the server as an awaitable task 133 | // This is practical for C# 7.1 and up, since the Main-method of a program can be async and thus kept open by awaiting this call 134 | await server.RunAsync(); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /src/ExampleServer/markdown.md: -------------------------------------------------------------------------------- 1 | # RedHttpServer 2 | ### Cross-platform http server with websocket support 3 | 4 | 5 | A C# alternative to nodejs and similar server bundles 6 | 7 | Some of the use patterns has been inspired by nodejs and expressjs 8 | 9 | ### Documentation 10 | [Documentation for .NET Core version](https://rosenbjerg.dk/rhscore/docs/) 11 | 12 | [Documentation for .NET Framework version](https://rosenbjerg.dk/rhs/docs/) 13 | 14 | ### Installation 15 | RedHttpServer can be installed from [NuGet](https://www.nuget.org/packages/RHttpServer/): Install-Package RHttpServer 16 | 17 | ### Examples 18 | ```csharp 19 | // Serving static files, such as index.html, from the directory './public' and listens for requests on port 5000 20 | var server = new RedHttpServer(5000, "./public"); 21 | var startTime = DateTime.UtcNow; 22 | 23 | 24 | // Using url queries to generate an answer 25 | // Fx. the request "/hello?firstname=John&lastname=Doe" 26 | // will get the text reponse "Hello John Doe, have a nice day" 27 | server.Get("/hello", async (req, res) => 28 | { 29 | var queries = req.Queries; 30 | var firstname = queries["firstname"][0]; 31 | var lastname = queries["lastname"][0]; 32 | await res.SendString($"Hello {firstname} {lastname}, have a nice day"); 33 | }); 34 | 35 | 36 | // URL parameter demonstration 37 | server.Get("/:param1/:paramtwo/:somethingthird", async (req, res) => 38 | { 39 | await res.SendString($"URL: {req.Params["param1"]} / {req.Params["paramtwo"]} / {req.Params["somethingthird"]}"); 40 | }); 41 | 42 | 43 | // Redirect request to another page 44 | // In this example the client is redirected to 45 | // another site on the same domain 46 | server.Get("/redirect", async (req, res) => 47 | { 48 | await res.Redirect("/redirect/test/here"); 49 | }); 50 | 51 | 52 | // Handling data sent as forms (FormData) 53 | // This example shows how a simple user registration 54 | // with profile picture can be handled 55 | server.Post("/register", async (req, res) => 56 | { 57 | var form = await req.GetFormDataAsync(); 58 | 59 | var username = form["username"][0]; 60 | var password = form["password"][0]; 61 | var profilePicture = form.Files[0]; 62 | 63 | CreateUser(username, password); 64 | SaveUserImage(username, profilePicture); 65 | 66 | await res.SendString("User registered!"); 67 | }); 68 | 69 | 70 | // Save uploaded file from request body 71 | // In this example the file is saved in the './uploads' 72 | // directory using the filename it was uploaded with 73 | server.Post("/upload", async (req, res) => 74 | { 75 | if (await req.SaveBodyToFile("./uploads")) 76 | { 77 | await res.SendString("OK"); 78 | } 79 | else 80 | { 81 | await res.SendString("Error", status: 413); 82 | } 83 | }); 84 | 85 | 86 | // Save file uploaded in FormData object 87 | server.Post("/formupload", async (req, res) => 88 | { 89 | var form = await req.GetFormDataAsync(); 90 | var file = form.Files[0]; 91 | 92 | using (var outputfile = File.Create("./uploads/" + file.FileName)) 93 | { 94 | await file.CopyToAsync(outputfile); 95 | } 96 | await res.SendString("OK"); 97 | }); 98 | 99 | 100 | // Rendering a page with dynamic content 101 | // In this example we create RenderParams for a very simple 102 | // server status page, which only shows uptime in hours and 103 | // and which version of the server framework is used 104 | server.Get("/serverstatus", async (req, res) => 105 | { 106 | await res.RenderPage("./pages/statuspage.ecs", new RenderParams 107 | { 108 | { "uptime", DateTime.UtcNow.Subtract(startTime).TotalHours }, 109 | { "version", RedHttpServer.Version } 110 | }); 111 | }); 112 | 113 | 114 | // WebSocket echo server 115 | // Clients can connects to "/echo" using the WebSocket protocol 116 | // This example simply echoes any text message received from a 117 | // client back with "You sent: " prepended to the message 118 | server.WebSocket("/echo", (req, wsd) => 119 | { 120 | // We can also use the logger from the plugin collection 121 | wsd.ServerPlugins.Use().Log("WS", "Echo server visited"); 122 | 123 | wsd.SendText("Welcome to the echo test server"); 124 | wsd.OnTextReceived += (sender, eventArgs) => 125 | { 126 | wsd.SendText("You sent: " + eventArgs.Text); 127 | }; 128 | }); 129 | 130 | 131 | server.Start(); 132 | ``` 133 | ### Static files 134 | When serving static files, it not required to add a route action for every static file. 135 | If no route action is provided for the requested route, a lookup will be performed, determining whether the route matches a file in the public file directory specified when creating an instance of the RedHttpServer class. 136 | 137 | ## Plug-ins 138 | RedHttpServer is created to be easy to build on top of. 139 | The server supports plug-ins, and offer a method to easily add new functionality. 140 | The plugin system works by registering plug-ins before starting the server, so all plug-ins are ready when serving requests. 141 | Some of the default functionality is implemented through plug-ins, and can easily customized or changed entirely. 142 | The server comes with default handlers for json and xml (ServiceStack.Text), page renderering (ecs). 143 | You can easily replace the default plugins with your own, just implement the interface of the default plugin you want to replace, and 144 | register it before initializing default plugins and/or starting the server. 145 | 146 | ## The .ecs file format 147 | The .ecs file format is merely an extension used for html pages with ecs-tags. 148 | 149 | #### Tags 150 | - <% foo %> will get replaced with the text data in the RenderParams object passed to the renderer 151 | 152 | - <%= foo =%> will get replaced with a HTML encoded version of the text data in the RenderParams object passed to the renderer 153 | 154 | - <¤ files/style.css ¤> will get replaced with the content of the file with the specified path. Must be absolute or relative to the server executable. Only html, ecs, js, css and txt is supported for now, but if you have a good reason to include another filetype, please create an issue regarding that. 155 | 156 | 157 | The file extension is enforced by the default page renderer to avoid confusion with regular html files without tags. 158 | 159 | The format is inspired by the ejs format, though you cannot embed JavaScript or C# for that matter, in the pages. 160 | 161 | 162 | Embed your dynamic content using RenderParams instead of embedding the code for generation of the content in the html. 163 | 164 | ## Why? 165 | Because i like C#, the .NET framework and type-safety, but i also like the use-patterns of nodejs, with expressjs especially. 166 | 167 | ## License 168 | RedHttpServer is released under MIT license, so it is free to use, even in commercial projects. 169 | 170 | Buy me a beer? 171 | ``` 172 | 16B6bzSgvBBprQteahougoDpbRHf8PnHvD (BTC) 173 | 0x63761494aAf03141bDea42Fb1e519De0c01CcF10 (ETH) 174 | ``` 175 | -------------------------------------------------------------------------------- /src/ExampleServer/pages/statuspage.ecs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Status page 6 | 7 | 8 | Uptime in seconds: <% uptime %>
9 | Version: <% version %> 10 | 11 | -------------------------------------------------------------------------------- /src/ExampleServer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HEJ 5 | 35 | 36 | -------------------------------------------------------------------------------- /src/ExampleServer/public/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 |
10 | 11 | 12 |
13 | 14 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/FSharpTest/FSharpTest.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/FSharpTest/Program.fs: -------------------------------------------------------------------------------- 1 | open Red 2 | 3 | 4 | [] 5 | let main argv = 6 | 7 | 8 | let app = new RedHttpServer(5000) 9 | 10 | app.Get("/", (fun (req:Request) (res:Response) -> res.SendString("Hello from F#"))) 11 | 12 | app.Get("/:name", (fun (req:Request) (res:Response) -> 13 | let message = sprintf "Hello from F#, %s" (req.Context.Params.["name"]) 14 | res.SendString(message))) 15 | 16 | app.RunAsync() |> Async.AwaitTask |> Async.RunSynchronously 17 | 0 -------------------------------------------------------------------------------- /src/RedHttpServer.Tests/BodyParserTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | 6 | namespace RedHttpServer.Tests 7 | { 8 | public class BodyParserTests 9 | { 10 | private Red.RedHttpServer _server; 11 | private HttpClient _httpClient; 12 | 13 | private const int TestPort = 7592; 14 | private const string BaseUrl = "http://localhost:7592"; 15 | 16 | [SetUp] 17 | public void Setup() 18 | { 19 | _server = new Red.RedHttpServer(TestPort); 20 | _httpClient = new HttpClient(); 21 | } 22 | 23 | public class TestPayload 24 | { 25 | public string Name { get; set; } 26 | public int Number { get; set; } 27 | } 28 | 29 | [Test] 30 | public async Task JsonSerialization() 31 | { 32 | var obj = new TestPayload 33 | { 34 | Name = "test", 35 | Number = 42 36 | }; 37 | _server.Get("/json", (req, res) => res.SendJson(obj)); 38 | _server.Start(); 39 | 40 | var (status, content) = await _httpClient.GetContent(BaseUrl + "/json"); 41 | 42 | Assert.AreEqual(status, HttpStatusCode.OK); 43 | Assert.AreEqual(content, "{\"Name\":\"test\",\"Number\":42}"); 44 | 45 | await _server.StopAsync(); 46 | } 47 | [Test] 48 | public async Task XmlSerialization() 49 | { 50 | var obj = new TestPayload 51 | { 52 | Name = "test", 53 | Number = 42 54 | }; 55 | _server.Get("/xml", (req, res) => res.SendXml(obj)); 56 | _server.Start(); 57 | 58 | var (status, content) = await _httpClient.GetContent(BaseUrl + "/xml"); 59 | 60 | Assert.AreEqual(status, HttpStatusCode.OK); 61 | Assert.AreEqual(content, "test42"); 62 | 63 | await _server.StopAsync(); 64 | } 65 | 66 | 67 | [TearDown] 68 | public void Teardown() 69 | { 70 | _server.StopAsync().Wait(); 71 | _httpClient.Dispose(); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/RedHttpServer.Tests/RedHttpServer.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 8 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/RedHttpServer.Tests/RoutingIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | 7 | namespace RedHttpServer.Tests 8 | { 9 | public class RoutingIntegrationTests 10 | { 11 | private Red.RedHttpServer _server; 12 | private HttpClient _httpClient; 13 | 14 | private const int TestPort = 7592; 15 | private const string BaseUrl = "http://localhost:7592"; 16 | 17 | [SetUp] 18 | public void Setup() 19 | { 20 | _server = new Red.RedHttpServer(TestPort); 21 | _httpClient = new HttpClient(); 22 | } 23 | 24 | [Test] 25 | public async Task BasicRouting() 26 | { 27 | _server.Get("/", (req, res) => res.SendString("1")); 28 | _server.Get("/hello", (req, res) => res.SendString("2")); 29 | _server.Start(); 30 | 31 | var (status0, content0) = await _httpClient.GetContent(BaseUrl); 32 | var (status1, content1) = await _httpClient.GetContent(BaseUrl + "/"); 33 | var (status2, content2) = await _httpClient.GetContent(BaseUrl + "/hello"); 34 | 35 | Assert.AreEqual(HttpStatusCode.OK, status0); 36 | Assert.AreEqual(HttpStatusCode.OK, status1); 37 | Assert.AreEqual(HttpStatusCode.OK, status2); 38 | 39 | Assert.AreEqual("1", content0); 40 | Assert.AreEqual("1", content1); 41 | Assert.AreEqual("2", content2); 42 | 43 | await _server.StopAsync(); 44 | } 45 | [Test] 46 | public async Task WebsocketRouteOrdering() 47 | { 48 | _server.WebSocket("/websocket", (req, res, wsd) => res.SendString("1")); 49 | _server.Get("/*", (req, res) => res.SendString("2")); 50 | await _server.StartAsync(); 51 | 52 | var (status0, _) = await _httpClient.GetContent(BaseUrl + "/websocket"); 53 | var (status1, content1) = await _httpClient.GetContent(BaseUrl + "/askjldald"); 54 | 55 | Assert.AreEqual(HttpStatusCode.UpgradeRequired, status0); 56 | Assert.AreEqual(HttpStatusCode.OK, status1); 57 | 58 | Assert.AreEqual("2", content1); 59 | 60 | await _server.StopAsync(); 61 | } 62 | 63 | [Test] 64 | public async Task WildcardRoutingTest() 65 | { 66 | _server.Get("/hello", (req, res) => res.SendString("3")); 67 | _server.Get("/hello/*", (req, res) => res.SendString("2")); 68 | _server.Get("/*", (req, res) => res.SendString("1")); 69 | _server.Start(); 70 | 71 | var (status0, content0) = await _httpClient.GetContent(BaseUrl + "/blah"); 72 | var (status1, content1) = await _httpClient.GetContent(BaseUrl + "/hello/blah"); 73 | var (status2, content2) = await _httpClient.GetContent(BaseUrl + "/hello"); 74 | var (status3, content3) = await _httpClient.GetContent(BaseUrl + "/blah/blah"); 75 | 76 | Assert.AreEqual(HttpStatusCode.OK, status0); 77 | Assert.AreEqual(HttpStatusCode.OK, status1); 78 | Assert.AreEqual(HttpStatusCode.OK, status2); 79 | Assert.AreEqual(HttpStatusCode.OK, status3); 80 | 81 | Assert.AreEqual("1", content0); 82 | Assert.AreEqual("2", content1); 83 | Assert.AreEqual("3", content2); 84 | Assert.AreEqual("1", content3); 85 | 86 | await _server.StopAsync(); 87 | } 88 | 89 | [Test] 90 | public async Task ReverseWildcardOrderingRoutingTest() 91 | { 92 | // The ordering of catch-all wildcard matters as shown by this test 93 | _server.Get("/*", (req, res) => res.SendString("1")); 94 | _server.Get("/hello/*", (req, res) => res.SendString("2")); 95 | _server.Get("/hello/world", (req, res) => res.SendString("3")); 96 | _server.Start(); 97 | 98 | var (status0, content0) = await _httpClient.GetContent(BaseUrl + "/blah"); 99 | var (status1, content1) = await _httpClient.GetContent(BaseUrl + "/hello/world"); 100 | var (status2, content2) = await _httpClient.GetContent(BaseUrl + "/hello"); 101 | var (status3, content3) = await _httpClient.GetContent(BaseUrl + "/blah/blah"); 102 | 103 | Assert.AreEqual(HttpStatusCode.OK, status0); 104 | Assert.AreEqual(HttpStatusCode.OK, status1); 105 | Assert.AreEqual(HttpStatusCode.OK, status2); 106 | Assert.AreEqual(HttpStatusCode.OK, status3); 107 | 108 | Assert.AreEqual(content0, "1"); 109 | Assert.AreEqual(content1, "1"); 110 | Assert.AreEqual(content2, "1"); 111 | Assert.AreEqual(content3, "1"); 112 | 113 | await _server.StopAsync(); 114 | } 115 | [Test] 116 | public async Task WildcardOrderingRoutingTest() 117 | { 118 | // The ordering of catch-all wildcard matters as shown by this test 119 | _server.Get("/hello/world", (req, res) => res.SendString("3")); 120 | _server.Get("/hello/*", (req, res) => res.SendString("2")); 121 | _server.Get("/*", (req, res) => res.SendString("1")); 122 | _server.Start(); 123 | 124 | var (status0, content0) = await _httpClient.GetContent(BaseUrl + "/blah"); 125 | var (status1, content1) = await _httpClient.GetContent(BaseUrl + "/hello/world"); 126 | var (status2, content2) = await _httpClient.GetContent(BaseUrl + "/hello"); 127 | var (status3, content3) = await _httpClient.GetContent(BaseUrl + "/blah/blah"); 128 | 129 | Assert.AreEqual(HttpStatusCode.OK, status0); 130 | Assert.AreEqual(HttpStatusCode.OK, status1); 131 | Assert.AreEqual(HttpStatusCode.OK, status2); 132 | Assert.AreEqual(HttpStatusCode.OK, status3); 133 | 134 | Assert.AreEqual(content0, "1"); 135 | Assert.AreEqual(content1, "3"); 136 | Assert.AreEqual(content2, "2"); 137 | Assert.AreEqual(content3, "1"); 138 | 139 | await _server.StopAsync(); 140 | } 141 | 142 | [Test] 143 | public async Task ParametersRoutingTest() 144 | { 145 | _server.Get("/test", (req, res) => res.SendString("test1")); 146 | _server.Get("/:kind/test", (req, res) => res.SendString(req.Context.Params["kind"] + "2")); 147 | _server.Get("/:kind", (req, res) => res.SendString(req.Context.Params["kind"] + "3")); 148 | _server.Start(); 149 | 150 | var (status0, content0) = await _httpClient.GetContent(BaseUrl + "/test"); 151 | var (status1, content1) = await _httpClient.GetContent(BaseUrl + "/banana/test"); 152 | var (status2, content2) = await _httpClient.GetContent(BaseUrl + "/apple"); 153 | var (status3, content3) = await _httpClient.GetContent(BaseUrl + "/orange"); 154 | var (status4, content4) = await _httpClient.GetContent(BaseUrl + "/peach/test"); 155 | 156 | Assert.AreEqual(HttpStatusCode.OK, status0); 157 | Assert.AreEqual(HttpStatusCode.OK, status1); 158 | Assert.AreEqual(HttpStatusCode.OK, status2); 159 | Assert.AreEqual(HttpStatusCode.OK, status3); 160 | Assert.AreEqual(HttpStatusCode.OK, status4); 161 | 162 | Assert.AreEqual("test1", content0); 163 | Assert.AreEqual("banana2", content1); 164 | Assert.AreEqual("apple3", content2); 165 | Assert.AreEqual("orange3", content3); 166 | Assert.AreEqual("peach2", content4); 167 | 168 | await _server.StopAsync(); 169 | } 170 | 171 | 172 | [Test] 173 | public async Task NotFoundRoutingTest() 174 | { 175 | _server.Get("/test", (req, res) => res.SendString("test1")); 176 | _server.Get("/:kind/test", (req, res) => res.SendString(req.Context.Params["kind"] + "2")); 177 | _server.Start(); 178 | 179 | var (status0, _) = await _httpClient.GetContent(BaseUrl + "/test/blah"); 180 | var (status1, _) = await _httpClient.GetContent(BaseUrl + "/blah/blah"); 181 | var (status2, _) = await _httpClient.GetContent(BaseUrl + "/"); 182 | var (status3, _) = await _httpClient.GetContent(BaseUrl); 183 | var (status4, _) = await _httpClient.GetContent(BaseUrl + "/test1"); 184 | var (status5, content5) = await _httpClient.GetContent(BaseUrl + "/test"); 185 | 186 | Assert.AreEqual(HttpStatusCode.NotFound, status0); 187 | Assert.AreEqual(HttpStatusCode.NotFound, status1); 188 | Assert.AreEqual(HttpStatusCode.NotFound, status2); 189 | Assert.AreEqual(HttpStatusCode.NotFound, status3); 190 | Assert.AreEqual(HttpStatusCode.NotFound, status4); 191 | 192 | Assert.AreEqual(HttpStatusCode.OK, status5); 193 | Assert.AreEqual("test1", content5); 194 | 195 | await _server.StopAsync(); 196 | } 197 | 198 | [TearDown] 199 | public void Teardown() 200 | { 201 | _server.StopAsync().Wait(); 202 | _httpClient.Dispose(); 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /src/RedHttpServer.Tests/TestUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace RedHttpServer.Tests 6 | { 7 | public static class TestUtils 8 | { 9 | public static async Task<(HttpStatusCode, string)> GetContent(this HttpClient client, string url) 10 | { 11 | var response = await client.GetAsync(url); 12 | return (response.StatusCode, await response.Content.ReadAsStringAsync()); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Context.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Red 7 | { 8 | /// 9 | /// Class that encapsulates data relevant to both Requests and Responses, and the HttpContext 10 | /// 11 | public sealed class Context 12 | { 13 | private readonly Lazy> _data = new Lazy>(); 14 | private readonly Lazy> _strings = new Lazy>(); 15 | 16 | /// 17 | /// The ASP.NET HttpContext that is wrapped 18 | /// 19 | public readonly HttpContext AspNetContext; 20 | 21 | /// 22 | /// Represent the url parameters and theirs values, contained in the path of the current request. 23 | /// 24 | public readonly UrlParameters Params; 25 | 26 | /// 27 | /// The 28 | /// 29 | public readonly PluginCollection Plugins; 30 | 31 | internal Context(HttpContext aspNetContext, PluginCollection plugins) 32 | { 33 | Plugins = plugins; 34 | AspNetContext = aspNetContext; 35 | Params = new UrlParameters(aspNetContext); 36 | } 37 | 38 | /// 39 | /// Get data attached to request by middleware. The middleware should specify the type to lookup 40 | /// 41 | /// the data key 42 | public string? GetData(string key) 43 | { 44 | return _strings.Value.TryGetValue(key, out var value) ? value : default; 45 | } 46 | 47 | /// 48 | /// Get data attached to request by middleware. The middleware should specify the type to lookup 49 | /// 50 | /// the type key 51 | /// Object of specified type, registered to request. Otherwise default 52 | public TData? GetData() 53 | where TData : class 54 | { 55 | return _data.Value.TryGetValue(typeof(TData), out var value) ? (TData) value : default; 56 | } 57 | 58 | /// 59 | /// Function that middleware can use to attach data to the request, so the next handlers has access to the data 60 | /// 61 | /// the type of the data object (implicitly) 62 | /// the data object 63 | public void SetData(TData data) 64 | where TData : class 65 | { 66 | _data.Value[typeof(TData)] = data; 67 | } 68 | 69 | /// 70 | /// Function that middleware can use to attach string values to the request, so the next handlers has access to the 71 | /// data 72 | /// 73 | /// the data key 74 | /// the data value 75 | public void SetData(string key, string value) 76 | { 77 | _strings.Value[key] = value; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Extensions/BodyParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Red.Interfaces; 7 | 8 | namespace Red.Extensions 9 | { 10 | /// 11 | /// Extendable bodyparser 12 | /// 13 | internal sealed class BodyParser : IBodyParser, IRedExtension 14 | { 15 | private static readonly Dictionary ConverterMappings = new Dictionary 16 | { 17 | {"application/xml", typeof(IXmlConverter)}, 18 | {"text/xml", typeof(IXmlConverter)}, 19 | {"application/json", typeof(IJsonConverter)}, 20 | {"text/json", typeof(IJsonConverter)} 21 | }; 22 | 23 | /// 24 | public Task ReadAsync(Request request) 25 | { 26 | using var streamReader = new StreamReader(request.BodyStream); 27 | return streamReader.ReadToEndAsync(); 28 | } 29 | 30 | /// 31 | public async Task DeserializeAsync(Request request) 32 | where T : class 33 | { 34 | string contentType = request.Headers["Content-Type"]; 35 | if (!ConverterMappings.TryGetValue(contentType, out var converterType)) 36 | return default; 37 | 38 | var converter = request.Context.Plugins.Get(converterType); 39 | return await converter.DeserializeAsync(request.BodyStream, request.Aborted); 40 | } 41 | 42 | public void Initialize(RedHttpServer server) 43 | { 44 | server.Plugins.Register(this); 45 | } 46 | 47 | /// 48 | /// Set body converter for content-type 49 | /// 50 | public static void SetContentTypeConverter(string contentType) 51 | where TBodyConverter : IBodyConverter 52 | { 53 | ConverterMappings[contentType] = typeof(TBodyConverter); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Extensions/BodyParserExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Red.Interfaces; 3 | 4 | namespace Red.Extensions 5 | { 6 | /// 7 | /// Extension to RedRequests, to parse body to object of specified type 8 | /// 9 | public static class BodyParserExtension 10 | { 11 | /// 12 | /// Returns the body deserialized or parsed to specified type if possible, default if not 13 | /// 14 | public static Task ParseBodyAsync(this Request request) 15 | where T : class 16 | { 17 | var bodyParser = request.Context.Plugins.Get(); 18 | return bodyParser.DeserializeAsync(request); 19 | } 20 | 21 | /// 22 | /// Returns the body deserialized or parsed to specified type if possible, default if not 23 | /// 24 | public static Task ReadBodyAsync(this Request request) 25 | { 26 | var bodyParser = request.Context.Plugins.Get(); 27 | return bodyParser.ReadAsync(request); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Extensions/JsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text.Json; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Red.Interfaces; 6 | 7 | namespace Red.Extensions 8 | { 9 | /// 10 | /// Very simple JsonConverter plugin using System.Text.Json generic methods 11 | /// 12 | internal sealed class JsonConverter : IJsonConverter, IRedExtension 13 | { 14 | /// 15 | public string? Serialize(T obj) 16 | { 17 | try 18 | { 19 | return JsonSerializer.Serialize(obj); 20 | } 21 | catch (JsonException) 22 | { 23 | return default; 24 | } 25 | } 26 | 27 | /// 28 | public T? Deserialize(string jsonData) 29 | where T : class 30 | { 31 | try 32 | { 33 | return !string.IsNullOrEmpty(jsonData) 34 | ? JsonSerializer.Deserialize(jsonData) 35 | : default; 36 | } 37 | catch (JsonException) 38 | { 39 | return default; 40 | } 41 | } 42 | 43 | /// 44 | public async Task DeserializeAsync(Stream jsonStream, CancellationToken cancellationToken = default) 45 | where T : class 46 | { 47 | try 48 | { 49 | return await JsonSerializer.DeserializeAsync(jsonStream, cancellationToken: cancellationToken); 50 | } 51 | catch (JsonException) 52 | { 53 | return default; 54 | } 55 | } 56 | 57 | /// 58 | public async Task SerializeAsync(T obj, Stream jsonStream, CancellationToken cancellationToken = default) 59 | { 60 | try 61 | { 62 | await JsonSerializer.SerializeAsync(jsonStream, obj, cancellationToken: cancellationToken); 63 | } 64 | catch (JsonException) 65 | { 66 | } 67 | } 68 | 69 | public void Initialize(RedHttpServer server) 70 | { 71 | server.Plugins.Register(this); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Extensions/XmlConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Xml; 7 | using System.Xml.Serialization; 8 | using Red.Interfaces; 9 | 10 | namespace Red.Extensions 11 | { 12 | /// 13 | /// Very simple XmlConverter plugin using System.Xml.Serialization 14 | /// 15 | internal sealed class XmlConverter : IXmlConverter, IRedExtension 16 | { 17 | public void Initialize(RedHttpServer server) 18 | { 19 | server.Plugins.Register(this); 20 | } 21 | 22 | /// 23 | public string? Serialize(T obj) 24 | { 25 | try 26 | { 27 | using var stream = new MemoryStream(); 28 | using var xml = XmlWriter.Create(stream); 29 | var xs = new XmlSerializer(typeof(T)); 30 | xs.Serialize(xml, obj); 31 | var reader = new StreamReader(stream, Encoding.UTF8); 32 | return reader.ReadToEnd(); 33 | } 34 | catch (Exception) 35 | { 36 | return default; 37 | } 38 | } 39 | 40 | /// 41 | public T? Deserialize(string xmlData) 42 | where T : class 43 | { 44 | try 45 | { 46 | using var stringReader = new StringReader(xmlData); 47 | using var xml = XmlReader.Create(stringReader); 48 | return (T) xml.ReadContentAs(typeof(T), null); 49 | } 50 | catch (FormatException) 51 | { 52 | return default; 53 | } 54 | catch (InvalidCastException) 55 | { 56 | return default; 57 | } 58 | } 59 | 60 | /// 61 | public async Task DeserializeAsync(Stream xmlStream, CancellationToken cancellationToken = default) 62 | where T : class 63 | { 64 | try 65 | { 66 | using var xmlReader = XmlReader.Create(xmlStream); 67 | return (T) await xmlReader.ReadContentAsAsync(typeof(T), null); 68 | } 69 | catch (FormatException) 70 | { 71 | return default; 72 | } 73 | catch (InvalidCastException) 74 | { 75 | return default; 76 | } 77 | } 78 | 79 | /// 80 | public async Task SerializeAsync(T obj, Stream output, CancellationToken cancellationToken = default) 81 | { 82 | try 83 | { 84 | using var xmlWriter = XmlWriter.Create(output, new XmlWriterSettings 85 | { 86 | Async = true 87 | }); 88 | var xmlSerializer = new XmlSerializer(typeof(T)); 89 | xmlSerializer.Serialize(xmlWriter, obj); 90 | await xmlWriter.FlushAsync(); 91 | } 92 | catch (Exception) 93 | { 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/RedHttpServer/HandlerExceptionEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Red 4 | { 5 | /// 6 | /// 7 | public class HandlerExceptionEventArgs : EventArgs 8 | { 9 | /// 10 | /// The exception that occured 11 | /// 12 | public readonly Exception Exception; 13 | 14 | /// 15 | /// The endpoint path the exception occured on 16 | /// 17 | public readonly string Method; 18 | 19 | /// 20 | /// The endpoint path the exception occured on 21 | /// 22 | public readonly string Path; 23 | 24 | internal HandlerExceptionEventArgs(string method, string path, Exception exception) 25 | { 26 | Method = method; 27 | Path = path; 28 | Exception = exception; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/RedHttpServer/HandlerType.cs: -------------------------------------------------------------------------------- 1 | namespace Red 2 | { 3 | /// 4 | /// The type of handling that has been performed. 5 | /// 6 | public enum HandlerType 7 | { 8 | /// 9 | /// This handler has sent a final response. 10 | /// Do not invoke the rest of the handler-chain 11 | /// 12 | Final, 13 | 14 | /// 15 | /// This handler processed data. 16 | /// Continue invoking the handler-chain 17 | /// 18 | Continue, 19 | 20 | /// 21 | /// An error occured when handling the request. 22 | /// Stop invoking the rest of the handler chain 23 | /// 24 | Error 25 | } 26 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Handlers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | using Red.Extensions; 7 | 8 | namespace Red 9 | { 10 | /// 11 | /// Utilities 12 | /// 13 | public static class Handlers 14 | { 15 | internal static readonly Task CachedFinalHandlerTask = Task.FromResult(HandlerType.Final); 16 | internal static readonly Task CachedContinueHandlerTask = Task.FromResult(HandlerType.Continue); 17 | 18 | private static readonly IDictionary MimeTypes = 19 | new Dictionary(StringComparer.OrdinalIgnoreCase) 20 | { 21 | #region extension to MIME type list 22 | 23 | {".asf", "video/x-ms-asf"}, 24 | {".asx", "video/x-ms-asf"}, 25 | {".avi", "video/x-msvideo"}, 26 | {".bin", "application/octet-stream"}, 27 | {".cco", "application/x-cocoa"}, 28 | {".crt", "application/x-x509-ca-cert"}, 29 | {".css", "text/css"}, 30 | {".deb", "application/octet-stream"}, 31 | {".der", "application/x-x509-ca-cert"}, 32 | {".dll", "application/octet-stream"}, 33 | {".dmg", "application/octet-stream"}, 34 | {".ear", "application/java-archive"}, 35 | {".eot", "application/octet-stream"}, 36 | {".exe", "application/octet-stream"}, 37 | {".flv", "video/x-flv"}, 38 | {".gif", "image/gif"}, 39 | {".hqx", "application/mac-binhex40"}, 40 | {".htc", "text/x-component"}, 41 | {".htm", "text/html"}, 42 | {".html", "text/html"}, 43 | {".ico", "image/x-icon"}, 44 | {".img", "application/octet-stream"}, 45 | {".iso", "application/octet-stream"}, 46 | {".jar", "application/java-archive"}, 47 | {".jardiff", "application/x-java-archive-diff"}, 48 | {".jng", "image/x-jng"}, 49 | {".jnlp", "application/x-java-jnlp-file"}, 50 | {".jpeg", "image/jpeg"}, 51 | {".jpg", "image/jpeg"}, 52 | {".js", "application/x-javascript"}, 53 | {".json", "text/json"}, 54 | {".mml", "text/mathml"}, 55 | {".mng", "video/x-mng"}, 56 | {".mov", "video/quicktime"}, 57 | {".mp3", "audio/mpeg"}, 58 | {".mp4", "video/mp4"}, 59 | {".mpeg", "video/mpeg"}, 60 | {".mpg", "video/mpeg"}, 61 | {".msi", "application/octet-stream"}, 62 | {".msm", "application/octet-stream"}, 63 | {".msp", "application/octet-stream"}, 64 | {".pdb", "application/x-pilot"}, 65 | {".pdf", "application/pdf"}, 66 | {".pem", "application/x-x509-ca-cert"}, 67 | {".php", "text/x-php"}, 68 | {".pl", "application/x-perl"}, 69 | {".pm", "application/x-perl"}, 70 | {".png", "image/png"}, 71 | {".prc", "application/x-pilot"}, 72 | {".ra", "audio/x-realaudio"}, 73 | {".rar", "application/x-rar-compressed"}, 74 | {".rpm", "application/x-redhat-package-manager"}, 75 | {".rss", "text/xml"}, 76 | {".run", "application/x-makeself"}, 77 | {".sea", "application/x-sea"}, 78 | {".shtml", "text/html"}, 79 | {".sit", "application/x-stuffit"}, 80 | {".swf", "application/x-shockwave-flash"}, 81 | {".tcl", "application/x-tcl"}, 82 | {".tk", "application/x-tcl"}, 83 | {".txt", "text/plain"}, 84 | {".war", "application/java-archive"}, 85 | {".webm", "video/webm"}, 86 | {".wbmp", "image/vnd.wap.wbmp"}, 87 | {".wmv", "video/x-ms-wmv"}, 88 | {".xml", "text/xml"}, 89 | {".xpi", "application/x-xpinstall"}, 90 | {".zip", "application/zip"} 91 | 92 | #endregion 93 | }; 94 | 95 | /// 96 | /// Parsing middleware. 97 | /// Attempts to parse the body using ParseBodyAsync. 98 | /// If unable to parse the body, responds with Bad Request status. 99 | /// Otherwise saves the parsed object using SetData on the request, so it can be retrieved using GetData by a later 100 | /// handler. 101 | /// 102 | /// The request object 103 | /// The response object 104 | /// The type to parse the body to 105 | /// 106 | public static async Task CanParse(Request req, Response res) 107 | where T : class 108 | { 109 | var obj = await req.ParseBodyAsync(); 110 | if (obj == default) 111 | { 112 | await res.SendStatus(HttpStatusCode.BadRequest); 113 | return HandlerType.Error; 114 | } 115 | 116 | req.SetData(obj); 117 | return HandlerType.Continue; 118 | } 119 | 120 | /// 121 | /// Parsing middleware. 122 | /// Attempts to parse the body using ParseBodyAsync. 123 | /// If unable to parse the body, responds with Bad Request status. 124 | /// Otherwise saves the parsed object using SetData on the request, so it can be retrieved using GetData by a later 125 | /// handler. 126 | /// 127 | /// The request object 128 | /// The response object 129 | /// The websocket dialog (not modified) 130 | /// The type to parse the body to 131 | /// 132 | public static Task CanParse(Request req, Response res, WebSocketDialog _) 133 | where T : class 134 | { 135 | return CanParse(req, res); 136 | } 137 | 138 | 139 | /// 140 | /// Middleware for serving static files from a directory. 141 | /// Requires one wildcard (*) in the path(s) it is used in 142 | /// 143 | /// The path of the base directory the files are served from 144 | /// Whether to respond with 404 when file not found, or to continue to next handler 145 | /// 146 | public static Func> SendFiles(string basePath, bool send404NotFound = true) 147 | { 148 | var fullBasePath = Path.GetFullPath(basePath); 149 | return (req, res) => 150 | { 151 | var absoluteFilePath = Path.GetFullPath(Path.Combine(fullBasePath, req.Context.Params["any"])); 152 | if (!absoluteFilePath.StartsWith(fullBasePath) || !File.Exists(absoluteFilePath)) 153 | return send404NotFound 154 | ? res.SendStatus(HttpStatusCode.NotFound) 155 | : CachedContinueHandlerTask; 156 | 157 | return res.SendFile(absoluteFilePath); 158 | }; 159 | } 160 | 161 | 162 | internal static string GetMimeType(string? contentType, string? filePath, 163 | string defaultContentType = "application/octet-stream") 164 | { 165 | if (!string.IsNullOrEmpty(contentType)) 166 | return contentType; 167 | if (!string.IsNullOrEmpty(filePath) && MimeTypes.TryGetValue(Path.GetExtension(filePath), out var mimeType)) 168 | return mimeType; 169 | return defaultContentType; 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /src/RedHttpServer/InContext.cs: -------------------------------------------------------------------------------- 1 | namespace Red 2 | { 3 | /// 4 | /// Common base for Request and Response 5 | /// 6 | public abstract class InContext 7 | { 8 | /// 9 | /// The Red.Context this instance is in 10 | /// 11 | public readonly Context Context; 12 | 13 | /// 14 | /// Base constructor 15 | /// 16 | protected InContext(Context context) 17 | { 18 | Context = context; 19 | } 20 | 21 | 22 | /// 23 | /// Get data attached to request by middleware. The middleware should specify the type to lookup 24 | /// 25 | /// the type key 26 | /// Object of specified type, registered to request. Otherwise default 27 | public TData? GetData() where TData : class 28 | { 29 | return Context.GetData(); 30 | } 31 | 32 | /// 33 | /// Function that middleware can use to attach data to the request, so the next handlers has access to the data 34 | /// 35 | /// the type of the data object (implicitly) 36 | /// the data object 37 | public void SetData(TData data) where TData : class 38 | { 39 | Context.SetData(data); 40 | } 41 | 42 | /// 43 | /// Get data attached to request by middleware. The middleware should specify the type to lookup 44 | /// 45 | /// the data key 46 | public string? GetData(string key) 47 | { 48 | return Context.GetData(key); 49 | } 50 | 51 | /// 52 | /// Function that middleware can use to attach data to the request, so the next handlers has access to the data 53 | /// 54 | public void SetData(string key, string value) 55 | { 56 | Context.SetData(key, value); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Interfaces/IBodyConverter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Red.Interfaces 6 | { 7 | /// 8 | /// Interface for body converters 9 | /// 10 | public interface IBodyConverter 11 | { 12 | /// 13 | /// Serialize data to a string 14 | /// 15 | string? Serialize(T obj); 16 | 17 | /// 18 | /// Deserialize data to specified type 19 | /// 20 | T? Deserialize(string jsonData) 21 | where T : class; 22 | 23 | /// 24 | /// Deserialize data from a stream to specified type 25 | /// 26 | Task DeserializeAsync(Stream jsonStream, CancellationToken cancellationToken = default) 27 | where T : class; 28 | 29 | /// 30 | /// Serialize data to a stream 31 | /// 32 | Task SerializeAsync(T obj, Stream jsonStream, CancellationToken cancellationToken = default); 33 | } 34 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Interfaces/IBodyParser.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Red.Interfaces 4 | { 5 | /// 6 | /// Interface for classes used for parsing and deserializing body 7 | /// 8 | public interface IBodyParser 9 | { 10 | /// 11 | /// Parse the request body stream into a string 12 | /// 13 | /// 14 | /// 15 | Task ReadAsync(Request request); 16 | 17 | 18 | /// 19 | /// Parse the request body stream into an object of a given type 20 | /// 21 | /// 22 | /// 23 | /// 24 | Task DeserializeAsync(Request request) 25 | where T : class; 26 | } 27 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Interfaces/IJsonConverter.cs: -------------------------------------------------------------------------------- 1 | namespace Red.Interfaces 2 | { 3 | /// 4 | /// Interface for classes used for Json serialization and deserialization 5 | /// 6 | public interface IJsonConverter : IBodyConverter 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Interfaces/IRedExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Red.Interfaces 2 | { 3 | /// 4 | /// Interface for all extension modules for Red. 5 | /// 6 | public interface IRedExtension 7 | { 8 | /// 9 | /// Called on all registered plugins/middleware when the server is started. 10 | /// The module is handed a reference to the server, so it can do any needed registration 11 | /// 12 | /// A reference to the instance of the RedHttpServer 13 | void Initialize(RedHttpServer server); 14 | } 15 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Interfaces/IRedMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Red.Interfaces 4 | { 5 | /// 6 | /// 7 | /// 8 | public interface IRedMiddleware : IRedExtension 9 | { 10 | /// 11 | /// Method called for every get, post, put and delete request to the server 12 | /// 13 | /// 14 | /// 15 | Task Invoke(Request req, Response res); 16 | } 17 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Interfaces/IRedWebSocketMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Red.Interfaces 4 | { 5 | /// 6 | /// 7 | /// Interface for middleware that handle websocket middleware 8 | /// 9 | public interface IRedWebSocketMiddleware : IRedExtension 10 | { 11 | /// 12 | /// Method called for every websocket request to the server 13 | /// 14 | /// The request object 15 | /// The websocket dialog object 16 | /// The response object 17 | Task Invoke(Request req, Response res, WebSocketDialog wsd); 18 | } 19 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Interfaces/IRouter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Red.Interfaces 5 | { 6 | /// 7 | /// The interface for Routers 8 | /// 9 | public interface IRouter 10 | { 11 | /// 12 | /// Create a router and/or invoke a registration function 13 | /// 14 | /// 15 | /// 16 | /// 17 | IRouter CreateRouter(string routePrefix, Action? register = null); 18 | 19 | /// 20 | /// Add action to handle GET requests to a given route 21 | /// 22 | /// The route to respond to 23 | /// The handlers that wil respond to the request 24 | void Get(string route, params Func>[] handlers); 25 | 26 | /// 27 | /// Add action to handle POST requests to a given route 28 | /// 29 | /// The route to respond to 30 | /// The handlers that wil respond to the request 31 | void Post(string route, params Func>[] handlers); 32 | 33 | /// 34 | /// Add action to handle PUT requests to a given route. 35 | /// 36 | /// The route to respond to 37 | /// The handlers that wil respond to the request 38 | void Put(string route, params Func>[] handlers); 39 | 40 | /// 41 | /// Add action to handle DELETE requests to a given route. 42 | /// 43 | /// The route to respond to 44 | /// The handlers that wil respond to the request 45 | void Delete(string route, params Func>[] handlers); 46 | 47 | /// 48 | /// Add action to handle WEBSOCKET requests to a given route. 49 | /// 50 | /// 51 | /// The route to respond to 52 | /// The handlers that wil respond to the request 53 | void WebSocket(string route, params Func>[] handlers); 54 | } 55 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Interfaces/IXmlConverter.cs: -------------------------------------------------------------------------------- 1 | namespace Red.Interfaces 2 | { 3 | /// 4 | /// Interface for classes used for XML serialization and deserialization 5 | /// 6 | public interface IXmlConverter : IBodyConverter 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/RedHttpServer/PluginCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Red 5 | { 6 | /// 7 | /// Plugin manager that contains the registered plugins for the instance of the server. 8 | /// 9 | public sealed class PluginCollection 10 | { 11 | private readonly Dictionary _plugins = new Dictionary(); 12 | 13 | internal PluginCollection() 14 | { 15 | } 16 | 17 | /// 18 | /// Register a plugin to the collection. 19 | /// Preferably before starting the server 20 | /// 21 | /// s 22 | /// The type-key to register the plugin to 23 | /// 24 | /// The plugin to register 25 | /// Whether to overwrite the current plugin, if any 26 | public void Register(TValue plugin, bool overwrite = false) 27 | where TValue : class, TKey 28 | { 29 | var type = typeof(TKey); 30 | if (!overwrite && _plugins.ContainsKey(type)) 31 | throw new RedHttpServerException("You can only register one plugin to a plugin interface"); 32 | _plugins[type] = plugin; 33 | } 34 | 35 | /// 36 | /// Check whether a plugin is registered to the given type-key 37 | /// 38 | /// s 39 | /// The type-key to look-up 40 | /// Whether the any plugin is registered to TPluginInterface 41 | public bool IsRegistered() 42 | { 43 | return _plugins.ContainsKey(typeof(TKey)); 44 | } 45 | 46 | /// 47 | /// Returns the instance of the registered plugin 48 | /// 49 | /// The type-key to look-up 50 | /// Throws exception when trying to use a plugin that is not registered 51 | public TKey Get() 52 | { 53 | if (_plugins.TryGetValue(typeof(TKey), out var obj)) 54 | return (TKey) obj; 55 | throw new RedHttpServerException($"No plugin registered for '{typeof(TKey).Name}'"); 56 | } 57 | 58 | /// 59 | /// Returns the instance of the registered plugin 60 | /// 61 | /// The type-key to look-up 62 | /// Throws exception when trying to use a plugin that is not registered 63 | public TKey Get(Type typeKey) 64 | { 65 | if (_plugins.TryGetValue(typeKey, out var obj)) 66 | return (TKey) obj; 67 | throw new RedHttpServerException($"No plugin registered for '{typeKey.Name}'"); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("Rosenbjerg Softworks")] 10 | [assembly: AssemblyProduct("RedHttpServer")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | 17 | [assembly: ComVisible(false)] 18 | 19 | // The following GUID is for the ID of the typelib if this project is exposed to COM 20 | 21 | [assembly: Guid("331eb634-faf3-4d7a-83f1-1d3a780f0273")] -------------------------------------------------------------------------------- /src/RedHttpServer/RedHttpServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Routing.Template; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Red.Extensions; 10 | using Red.Interfaces; 11 | 12 | namespace Red 13 | { 14 | /// 15 | /// An Http server based on ASP.NET Core with Kestrel, with use-patterns inspired by express.js 16 | /// 17 | public partial class RedHttpServer : IRouter 18 | { 19 | /// 20 | /// The version of the library 21 | /// 22 | public static readonly string Version = typeof(RedHttpServer).GetTypeInfo().Assembly 23 | .GetCustomAttribute().Version; 24 | 25 | /// 26 | /// The plugin collection containing all plugins registered to this server instance. 27 | /// 28 | public readonly PluginCollection Plugins = new PluginCollection(); 29 | 30 | /// 31 | /// The port that the server is listening on 32 | /// 33 | public readonly int Port; 34 | 35 | private readonly List>> _middle = 36 | new List>>(); 37 | 38 | private readonly List>> _wsMiddle = 39 | new List>>(); 40 | 41 | /// 42 | /// Whether details about an exception should be sent together with the code 500 response. For debugging 43 | /// 44 | public bool RespondWithExceptionDetails = false; 45 | 46 | /// 47 | /// Constructs a server instance with given port and using the given path as public folder. 48 | /// Set path to null or empty string if none wanted 49 | /// 50 | /// The port that the server should listen on 51 | /// Path to use as public dir. Set to null or empty string if none wanted 52 | public RedHttpServer(int port = 5000, string publicDir = "") 53 | { 54 | Port = port; 55 | _publicRoot = publicDir; 56 | 57 | Use(new JsonConverter()); 58 | Use(new XmlConverter()); 59 | Use(new BodyParser()); 60 | } 61 | 62 | /// 63 | /// Method to register additional ASP.NET Core Services 64 | /// 65 | public Action? ConfigureServices { private get; set; } 66 | 67 | /// 68 | /// Method to configure the ASP.NET Core application additionally 69 | /// 70 | public Action? ConfigureApplication { private get; set; } 71 | 72 | /// 73 | public IRouter CreateRouter(string routePrefix, Action? register = null) 74 | { 75 | var router = new Router(routePrefix, this); 76 | register?.Invoke(router); 77 | return router; 78 | } 79 | 80 | /// 81 | public void Get(string route, params Func>[] handlers) 82 | { 83 | AddHandlers(route, GetMethod, handlers); 84 | } 85 | 86 | /// 87 | public void Post(string route, params Func>[] handlers) 88 | { 89 | AddHandlers(route, PostMethod, handlers); 90 | } 91 | 92 | /// 93 | public void Put(string route, params Func>[] handlers) 94 | { 95 | AddHandlers(route, PutMethod, handlers); 96 | } 97 | 98 | /// 99 | public void Delete(string route, params Func>[] handlers) 100 | { 101 | AddHandlers(route, DeleteMethod, handlers); 102 | } 103 | 104 | /// 105 | public void WebSocket(string route, 106 | params Func>[] handlers) 107 | { 108 | _useWebSockets = true; 109 | AddHandlers(route, handlers); 110 | } 111 | 112 | /// 113 | /// Event that is raised when an exception is thrown from a handler 114 | /// 115 | public event EventHandler? OnHandlerException; 116 | 117 | /// 118 | /// Starts the server. 119 | /// If no hostnames are provided, localhost will be used. 120 | /// 121 | /// 122 | /// The host names the server is handling requests for. Protocol and port will be added 123 | /// automatically 124 | /// 125 | public void Start(params string[] hostnames) 126 | { 127 | _host = Build(hostnames); 128 | _host.Start(); 129 | Console.WriteLine($"Red/{Version} running on port " + Port); 130 | } 131 | /// 132 | /// Runs the server, blocking the current thread until shutdown is requested. 133 | /// If no hostnames are provided, localhost will be used. 134 | /// 135 | /// 136 | /// The host names the server is handling requests for. Protocol and port will be added 137 | /// automatically 138 | /// 139 | public void Run(params string[] hostnames) 140 | { 141 | _host = Build(hostnames); 142 | Console.WriteLine($"Running Red/{Version} on port " + Port); 143 | _host.Run(); 144 | } 145 | 146 | /// 147 | /// Attempts to stop the running server using IWebHost.StopAsync 148 | /// 149 | public Task? StopAsync() 150 | { 151 | return _host?.StopAsync(); 152 | } 153 | 154 | /// 155 | /// Run the server using IWebHost.RunAsync and return the task so it can be awaited. 156 | /// If no hostnames are provided, localhost will be used. 157 | /// 158 | /// 159 | /// The host names the server is handling requests for. Protocol and port will be added 160 | /// automatically 161 | /// 162 | /// 163 | public Task RunAsync(params string[] hostnames) 164 | { 165 | _host = Build(hostnames); 166 | Console.WriteLine($"Running Red/{Version} on port " + Port); 167 | return _host.RunAsync(); 168 | } 169 | /// 170 | /// Start the server using IWebHost.StartAsync and return the task so it can be awaited. 171 | /// If no hostnames are provided, localhost will be used. 172 | /// 173 | /// 174 | /// The host names the server is handling requests for. Protocol and port will be added 175 | /// automatically 176 | /// 177 | /// 178 | public Task StartAsync(params string[] hostnames) 179 | { 180 | _host = Build(hostnames); 181 | Console.WriteLine($"Starting Red/{Version} on port " + Port); 182 | return _host.StartAsync(); 183 | } 184 | 185 | /// 186 | /// Register extension modules and middleware 187 | /// 188 | /// 189 | public void Use(IRedExtension extension) 190 | { 191 | if (extension is IRedWebSocketMiddleware wsMiddleware) 192 | _wsMiddle.Add(wsMiddleware.Invoke); 193 | if (extension is IRedMiddleware middleware) 194 | _middle.Add(middleware.Invoke); 195 | _plugins.Add(extension); 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /src/RedHttpServer/RedHttpServer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Library 4 | 5 | 6 | Library 7 | 8 | 9 | .NET Standard web application framework built on ASP.NET Core w/ Kestrel and inspired by Express.js 10 | Copyright © Rosenbjerg 2020 11 | RedHttpServer 12 | netstandard2.1 13 | true 14 | RedHttpServer 15 | RHttpServer 16 | false 17 | false 18 | false 19 | Red 20 | 21 | library 22 | 23 | 4.0.0 24 | red redhttp redhttpserver rhttpserver rhs http server web api expressjs http websocket middleware 25 | true 26 | false 27 | https://RedHttp.github.io/Red/ 28 | MIT 29 | https://redhttp.github.io/Red/assets/red-large.png 30 | https://github.com/RedHttp/Red 31 | GitHub 32 | Malte Rosenbjerg 33 | Fix contentType optional parameter 34 | Suppress OperationCanceledException in ws handler 35 | 5.0.0.0 36 | 5.0.0.0 37 | 8 38 | 5.2.1 39 | Red 40 | enable 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/RedHttpServer/RedHttpServerException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Red 4 | { 5 | /// 6 | /// 7 | /// Exception for errors in RedHttpServer 8 | /// 9 | public class RedHttpServerException : Exception 10 | { 11 | internal RedHttpServerException(string message) : base(message) 12 | { 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/RedHttpServer/RedHttpServerPrivate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Routing; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.FileProviders; 14 | using Red.Interfaces; 15 | 16 | namespace Red 17 | { 18 | public partial class RedHttpServer 19 | { 20 | private const string GetMethod = "GET"; 21 | private const string PostMethod = "POST"; 22 | private const string PutMethod = "PUT"; 23 | private const string DeleteMethod = "DELETE"; 24 | private static readonly Regex NamePathParameterRegex = new Regex(":[\\w-]+", RegexOptions.Compiled); 25 | 26 | private readonly List _plugins = new List(); 27 | private readonly string? _publicRoot; 28 | 29 | private readonly List> _routes = new List>(); 30 | 31 | private IWebHost? _host; 32 | private bool _useWebSockets; 33 | 34 | private IWebHost Build(IReadOnlyCollection hostnames) 35 | { 36 | if (_host != default) throw new RedHttpServerException("The server is already running"); 37 | Initialize(); 38 | var urls = hostnames.Count != 0 39 | ? hostnames.Select(url => $"http://{url}:{Port}").ToArray() 40 | : new[] {$"http://localhost:{Port}"}; 41 | return new WebHostBuilder() 42 | .UseKestrel() 43 | .ConfigureServices(services => 44 | { 45 | services.AddRouting(); 46 | ConfigureServices?.Invoke(services); 47 | }) 48 | .Configure(app => 49 | { 50 | if (!string.IsNullOrWhiteSpace(_publicRoot) && Directory.Exists(_publicRoot)) 51 | { 52 | var fullPublicPath = Path.GetFullPath(_publicRoot); 53 | app.UseFileServer(new FileServerOptions 54 | {FileProvider = new PhysicalFileProvider(fullPublicPath)}); 55 | Console.WriteLine($"Public files directory: {fullPublicPath}"); 56 | } 57 | 58 | if (_useWebSockets) 59 | app.UseWebSockets(); 60 | app.UseRouter(routeBuilder => 61 | { 62 | foreach (var route in _routes) 63 | route(routeBuilder); 64 | _routes.Clear(); 65 | }); 66 | ConfigureApplication?.Invoke(app); 67 | }) 68 | .UseUrls(urls) 69 | .Build(); 70 | } 71 | 72 | private void Initialize() 73 | { 74 | foreach (var plugin in _plugins) plugin.Initialize(this); 75 | } 76 | 77 | 78 | private async Task ExecuteHandler(HttpContext aspNetContext, 79 | IEnumerable>> handlers) 80 | { 81 | var context = new Context(aspNetContext, Plugins); 82 | var request = new Request(context); 83 | var response = new Response(context); 84 | 85 | var status = HandlerType.Continue; 86 | try 87 | { 88 | foreach (var middleware in _middle.Concat(handlers)) 89 | { 90 | status = await middleware(request, response); 91 | if (status != HandlerType.Continue) return status; 92 | } 93 | 94 | return status; 95 | } 96 | catch (OperationCanceledException) 97 | { 98 | return HandlerType.Final; 99 | } 100 | catch (Exception e) 101 | { 102 | return await HandleException(request, response, status, e); 103 | } 104 | } 105 | 106 | private async Task ExecuteHandler(HttpContext aspNetContext, 107 | IEnumerable>> handlers) 108 | { 109 | var context = new Context(aspNetContext, Plugins); 110 | var request = new Request(context); 111 | var response = new Response(context); 112 | 113 | var status = HandlerType.Continue; 114 | try 115 | { 116 | if (aspNetContext.WebSockets.IsWebSocketRequest) 117 | { 118 | var webSocket = await aspNetContext.WebSockets.AcceptWebSocketAsync(); 119 | var webSocketDialog = new WebSocketDialog(webSocket, request.Aborted); 120 | 121 | foreach (var middleware in _wsMiddle.Concat(handlers)) 122 | { 123 | status = await middleware(request, response, webSocketDialog); 124 | if (status != HandlerType.Continue) return status; 125 | } 126 | 127 | await webSocketDialog.ReadFromWebSocket(); 128 | return status; 129 | } 130 | 131 | response.Headers["Upgrade"] = "Websocket"; 132 | await response.SendStatus(HttpStatusCode.UpgradeRequired); 133 | return HandlerType.Error; 134 | } 135 | catch (OperationCanceledException) 136 | { 137 | return HandlerType.Final; 138 | } 139 | catch (Exception e) 140 | { 141 | return await HandleException(request, response, status, e); 142 | } 143 | } 144 | 145 | private async Task HandleException(Request request, Response response, HandlerType status, 146 | Exception e) 147 | { 148 | var path = request.AspNetRequest.Path.ToString(); 149 | var method = request.AspNetRequest.Method; 150 | OnHandlerException?.Invoke(this, new HandlerExceptionEventArgs(method, path, e)); 151 | 152 | if (status != HandlerType.Continue) 153 | return HandlerType.Error; 154 | 155 | if (RespondWithExceptionDetails) 156 | await response.SendString(e.ToString(), status: HttpStatusCode.InternalServerError); 157 | else 158 | await response.SendStatus(HttpStatusCode.InternalServerError); 159 | 160 | return HandlerType.Error; 161 | } 162 | 163 | private void AddHandlers(string route, string method, 164 | IReadOnlyCollection>> handlers) 165 | { 166 | if (handlers.Count == 0) 167 | throw new RedHttpServerException("A route requires at least one handler"); 168 | 169 | var path = ConvertPathParameters(route, NamePathParameterRegex); 170 | _routes.Add(routeBuilder => routeBuilder.MapVerb(method, path, ctx => ExecuteHandler(ctx, handlers))); 171 | } 172 | 173 | private void AddHandlers(string route, 174 | IReadOnlyCollection>> handlers) 175 | { 176 | if (handlers.Count == 0) 177 | throw new RedHttpServerException("A route requires at least one handler"); 178 | 179 | var path = ConvertPathParameters(route, NamePathParameterRegex); 180 | _routes.Add(routeBuilder => routeBuilder.MapGet(path, ctx => ExecuteHandler(ctx, handlers))); 181 | } 182 | 183 | private static string ConvertPathParameters(string parameter, Regex urlParam) 184 | { 185 | return urlParam 186 | .Replace(parameter, match => "{" + match.Value.TrimStart(':') + "}") 187 | .Replace("*", "{*any}") 188 | .Trim('/'); 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Request.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Http.Headers; 8 | 9 | namespace Red 10 | { 11 | /// 12 | /// Class representing a request from a client 13 | /// 14 | public sealed class Request : InContext 15 | { 16 | private readonly Lazy _typedHeaders; 17 | 18 | /// 19 | /// The ASP.NET HttpRequest that is wrapped 20 | /// 21 | public readonly HttpRequest AspNetRequest; 22 | 23 | private IFormCollection? _form; 24 | 25 | internal Request(Context context) : base(context) 26 | { 27 | AspNetRequest = context.AspNetContext.Request; 28 | _typedHeaders = new Lazy(AspNetRequest.GetTypedHeaders); 29 | } 30 | 31 | /// 32 | /// The query elements of the request 33 | /// 34 | public IQueryCollection Queries => AspNetRequest.Query; 35 | 36 | /// 37 | /// The cancellation token the request being aborted 38 | /// 39 | public CancellationToken Aborted => AspNetRequest.HttpContext.RequestAborted; 40 | 41 | /// 42 | /// The headers contained in the request 43 | /// 44 | public IHeaderDictionary Headers => AspNetRequest.Headers; 45 | 46 | /// 47 | /// The headers contained in the request 48 | /// 49 | public UrlParameters Params => Context.Params; 50 | 51 | 52 | /// 53 | /// Exposes the typed headers for the request 54 | /// 55 | public RequestHeaders TypedHeaders => _typedHeaders.Value; 56 | 57 | /// 58 | /// The cookies contained in the request 59 | /// 60 | public IRequestCookieCollection Cookies => AspNetRequest.Cookies; 61 | 62 | /// 63 | /// Returns the body stream of the request 64 | /// 65 | public Stream BodyStream => AspNetRequest.Body; 66 | 67 | /// 68 | /// Returns form-data from request, if any, null otherwise. 69 | /// 70 | public async Task GetFormDataAsync() 71 | { 72 | if (!AspNetRequest.HasFormContentType) 73 | return null; 74 | 75 | if (_form != null) 76 | return _form; 77 | 78 | _form = await AspNetRequest.ReadFormAsync(Aborted); 79 | return _form; 80 | } 81 | 82 | /// 83 | /// Save all files in requests to specified directory. 84 | /// 85 | /// The directory to place the file(s) in 86 | /// Function to rename the file(s) 87 | /// The max total filesize allowed 88 | /// Whether the file(s) was saved successfully 89 | public async Task SaveFiles(string saveDir, Func? fileRenamer = null, 90 | long maxSizeKb = 50000) 91 | { 92 | if (!AspNetRequest.HasFormContentType) return false; 93 | var form = await AspNetRequest.ReadFormAsync(Aborted); 94 | if (form.Files.Sum(file => file.Length) > maxSizeKb * 1000) 95 | return false; 96 | 97 | var fullSaveDir = Path.GetFullPath(saveDir); 98 | foreach (var formFile in form.Files) 99 | { 100 | var filename = fileRenamer == null ? formFile.FileName : fileRenamer(formFile.FileName); 101 | filename = Path.GetFileName(filename); 102 | if (string.IsNullOrWhiteSpace(filename)) continue; 103 | 104 | var filepath = Path.Combine(fullSaveDir, filename); 105 | await using var fileStream = File.Create(filepath); 106 | await formFile.CopyToAsync(fileStream, Aborted); 107 | } 108 | 109 | return true; 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Response.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Http; 8 | using Red.Interfaces; 9 | 10 | namespace Red 11 | { 12 | /// 13 | /// Class representing the response to a clients request 14 | /// All 15 | /// 16 | public sealed class Response : InContext 17 | { 18 | /// 19 | /// The ASP.NET HttpResponse that is wrapped 20 | /// 21 | public readonly HttpResponse AspNetResponse; 22 | 23 | internal Response(Context context) : base(context) 24 | { 25 | AspNetResponse = context.AspNetContext.Response; 26 | } 27 | 28 | /// 29 | /// The headers for the response 30 | /// 31 | public IHeaderDictionary Headers => AspNetResponse.Headers; 32 | 33 | /// 34 | /// The cancellation token the request being aborted 35 | /// 36 | public CancellationToken Aborted => AspNetResponse.HttpContext.RequestAborted; 37 | 38 | /// 39 | /// Obsolete. Please used Headers property instead. 40 | /// This method Will be removed in a later version 41 | /// 42 | /// The name of the header 43 | /// The value of the header 44 | [Obsolete] 45 | public void AddHeader(string headerName, string headerValue) 46 | { 47 | Headers[headerName] = headerValue; 48 | } 49 | 50 | /// 51 | /// Redirects the client to a given path or url 52 | /// 53 | /// The path or url to redirect to 54 | /// Whether to respond with a temporary or permanent redirect 55 | public Task Redirect(string redirectPath, bool permanent = false) 56 | { 57 | AspNetResponse.Redirect(redirectPath, permanent); 58 | return Handlers.CachedFinalHandlerTask; 59 | } 60 | 61 | /// 62 | /// Sends data as text 63 | /// 64 | /// The text data to send 65 | /// The mime type of the content 66 | /// If the data represents a file, the filename can be set through this 67 | /// Whether the file should be sent as attachment or inline 68 | /// The status code for the response 69 | public Task SendString(string data, string contentType = "text/plain", string fileName = "", 70 | bool attachment = false, HttpStatusCode status = HttpStatusCode.OK) 71 | { 72 | return SendString(AspNetResponse, data, contentType, fileName, attachment, status, Aborted); 73 | } 74 | 75 | /// 76 | /// Static helper for use in middleware 77 | /// 78 | public static async Task SendString(HttpResponse response, string data, string contentType, 79 | string fileName, bool attachment, HttpStatusCode status, CancellationToken cancellationToken) 80 | { 81 | response.StatusCode = (int) status; 82 | response.ContentType = contentType; 83 | if (!string.IsNullOrEmpty(fileName)) 84 | { 85 | var contentDisposition = $"{(attachment ? "attachment" : "inline")}; filename=\"{fileName}\""; 86 | response.Headers.Add("Content-disposition", contentDisposition); 87 | } 88 | 89 | await response.WriteAsync(data, cancellationToken); 90 | return HandlerType.Final; 91 | } 92 | 93 | /// 94 | /// Send a empty response with a status code 95 | /// 96 | /// The HttpResponse object 97 | /// The status code for the response 98 | public static Task SendStatus(HttpResponse response, HttpStatusCode status, CancellationToken cancellationToken) 99 | { 100 | return SendString(response, status.ToString(), "text/plain", "", false, status, cancellationToken); 101 | } 102 | 103 | /// 104 | /// Send a empty response with a status code 105 | /// 106 | /// The status code for the response 107 | public Task SendStatus(HttpStatusCode status) 108 | { 109 | return SendString(status.ToString(), status: status); 110 | } 111 | 112 | /// 113 | /// Sends object serialized to text using the current IJsonConverter plugin 114 | /// 115 | /// The object to be serialized and send 116 | /// The status code for the response 117 | public async Task SendJson(T data, HttpStatusCode status = HttpStatusCode.OK) 118 | { 119 | AspNetResponse.StatusCode = (int) status; 120 | AspNetResponse.ContentType = "application/json"; 121 | await Context.Plugins.Get().SerializeAsync(data, AspNetResponse.Body, Aborted); 122 | return HandlerType.Final; 123 | } 124 | 125 | /// 126 | /// Sends object serialized to text using the current IXmlConverter plugin 127 | /// 128 | /// The object to be serialized and send 129 | /// The status code for the response 130 | public async Task SendXml(T data, HttpStatusCode status = HttpStatusCode.OK) 131 | { 132 | AspNetResponse.StatusCode = (int) status; 133 | AspNetResponse.ContentType = "application/xml"; 134 | await Context.Plugins.Get().SerializeAsync(data, AspNetResponse.Body, Aborted); 135 | return HandlerType.Final; 136 | } 137 | 138 | /// 139 | /// Sends all data in stream to response 140 | /// 141 | /// The stream to copy data from 142 | /// The mime type of the data in the stream 143 | /// The filename that the browser should see the data as. Optional 144 | /// Whether the file should be sent as attachment or inline 145 | /// Whether to call dispose on stream when done sending 146 | /// The status code for the response 147 | public async Task SendStream(Stream dataStream, string contentType, string fileName = "", 148 | bool attachment = false, bool dispose = true, HttpStatusCode status = HttpStatusCode.OK) 149 | { 150 | AspNetResponse.StatusCode = (int) status; 151 | AspNetResponse.ContentType = contentType; 152 | if (!string.IsNullOrEmpty(fileName)) 153 | Headers["Content-disposition"] = $"{(attachment ? "attachment" : "inline")}; filename=\"{fileName}\""; 154 | 155 | await dataStream.CopyToAsync(AspNetResponse.Body, Aborted); 156 | if (dispose) dataStream.Dispose(); 157 | 158 | return HandlerType.Final; 159 | } 160 | 161 | /// 162 | /// Sends file as response and requests the data to be displayed in-browser if possible 163 | /// 164 | /// The local path of the file to send 165 | /// 166 | /// The mime type for the file, when set to null, the system will try to detect based on file 167 | /// extension 168 | /// 169 | /// Whether to enable handling of range-requests for the file(s) served 170 | /// Filename to show in header, instead of actual filename 171 | /// The status code for the response 172 | public async Task SendFile(string filePath, string? contentType = null, bool handleRanges = true, 173 | string? fileName = null, HttpStatusCode status = HttpStatusCode.OK) 174 | { 175 | if (handleRanges) Headers["Accept-Ranges"] = "bytes"; 176 | 177 | var fileSize = new FileInfo(filePath).Length; 178 | var range = Context.AspNetContext.Request.GetTypedHeaders().Range; 179 | var encodedFilename = WebUtility.UrlEncode(fileName ?? Path.GetFileName(filePath)); 180 | 181 | Headers["Content-disposition"] = $"inline; filename=\"{encodedFilename}\""; 182 | 183 | if (range != null && range.Ranges.Any()) 184 | { 185 | var firstRange = range.Ranges.First(); 186 | if (range.Unit != "bytes" || !firstRange.From.HasValue && !firstRange.To.HasValue) 187 | { 188 | await SendStatus(HttpStatusCode.BadRequest); 189 | return HandlerType.Error; 190 | } 191 | 192 | var offset = firstRange.From ?? fileSize - firstRange.To ?? 0; 193 | var length = firstRange.To.HasValue 194 | ? fileSize - offset - (fileSize - firstRange.To.Value) 195 | : fileSize - offset; 196 | 197 | AspNetResponse.StatusCode = (int) HttpStatusCode.PartialContent; 198 | AspNetResponse.ContentType = Handlers.GetMimeType(contentType, filePath); 199 | AspNetResponse.ContentLength = length; 200 | Headers["Content-Range"] = $"bytes {offset}-{offset + length - 1}/{fileSize}"; 201 | await AspNetResponse.SendFileAsync(filePath, offset, length, Aborted); 202 | } 203 | else 204 | { 205 | AspNetResponse.StatusCode = (int) status; 206 | AspNetResponse.ContentType = Handlers.GetMimeType(contentType, filePath); 207 | AspNetResponse.ContentLength = fileSize; 208 | await AspNetResponse.SendFileAsync(filePath, Aborted); 209 | } 210 | 211 | return HandlerType.Final; 212 | } 213 | 214 | /// 215 | /// Sends file as response and requests the data to be downloaded as an attachment 216 | /// 217 | /// The local path of the file to send 218 | /// The name filename the client receives the file with, defaults to using the actual filename 219 | /// 220 | /// The mime type for the file, when set to null, the system will try to detect based on file 221 | /// extension 222 | /// 223 | /// The status code for the response 224 | public async Task Download(string filePath, string? fileName = "", string? contentType = "", 225 | HttpStatusCode status = HttpStatusCode.OK) 226 | { 227 | AspNetResponse.StatusCode = (int) status; 228 | AspNetResponse.ContentType = Handlers.GetMimeType(contentType, filePath); 229 | var name = string.IsNullOrEmpty(fileName) ? Path.GetFileName(filePath) : fileName; 230 | Headers["Content-disposition"] = $"attachment; filename=\"{name}\""; 231 | await AspNetResponse.SendFileAsync(filePath, Aborted); 232 | return HandlerType.Final; 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /src/RedHttpServer/Router.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Red.Interfaces; 4 | 5 | namespace Red 6 | { 7 | /// 8 | /// A Router class that can be used to separate the handlers of the server in a more teamwork (git) friendly way 9 | /// 10 | public class Router : IRouter 11 | { 12 | private readonly string _baseRoute; 13 | private readonly IRouter _router; 14 | 15 | /// 16 | /// Creates a Router from a baseRoute, that will be prepended all handlers registered through it, 17 | /// and add them to the router 18 | /// 19 | /// The base route of the router 20 | /// The router (or server) to add handlers to 21 | public Router(string baseRoute, IRouter router) 22 | { 23 | _baseRoute = baseRoute.Trim('/'); 24 | _router = router; 25 | } 26 | 27 | /// 28 | public IRouter CreateRouter(string routePrefix, Action? register = null) 29 | { 30 | var router = new Router(routePrefix, this); 31 | register?.Invoke(router); 32 | return router; 33 | } 34 | 35 | /// 36 | public void Get(string route, params Func>[] handlers) 37 | { 38 | _router.Get(CombinePartialRoutes(_baseRoute, route), handlers); 39 | } 40 | 41 | /// 42 | public void Post(string route, params Func>[] handlers) 43 | { 44 | _router.Post(CombinePartialRoutes(_baseRoute, route), handlers); 45 | } 46 | 47 | /// 48 | public void Put(string route, params Func>[] handlers) 49 | { 50 | _router.Put(CombinePartialRoutes(_baseRoute, route), handlers); 51 | } 52 | 53 | /// 54 | public void Delete(string route, params Func>[] handlers) 55 | { 56 | _router.Delete(CombinePartialRoutes(_baseRoute, route), handlers); 57 | } 58 | 59 | /// 60 | public void WebSocket(string route, 61 | params Func>[] handlers) 62 | { 63 | _router.WebSocket(CombinePartialRoutes(_baseRoute, route), handlers); 64 | } 65 | 66 | private static string CombinePartialRoutes(string baseRoute, string route) 67 | { 68 | return $"{baseRoute}/{route.TrimStart('/')}"; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/RedHttpServer/UrlParameters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Routing; 6 | 7 | namespace Red 8 | { 9 | /// 10 | /// The url parameters in the path of the request 11 | /// 12 | public sealed class UrlParameters 13 | { 14 | private readonly Func _getRouteValue; 15 | private readonly Func _getRouteData; 16 | 17 | internal UrlParameters(HttpContext context) 18 | { 19 | _getRouteValue = context.GetRouteValue; 20 | _getRouteData = context.GetRouteData; 21 | } 22 | 23 | /// 24 | /// Returns the value of a given parameter id 25 | /// 26 | /// 27 | public string? this[string parameterId] => _getRouteValue(parameterId.TrimStart(':'))?.ToString(); 28 | 29 | /// 30 | /// Extract all url parameters as a dictionary of parameter ids and parameter values 31 | /// 32 | /// 33 | public Dictionary All() 34 | { 35 | return _getRouteData().Values.ToDictionary(x => x.Key, x => x.Value?.ToString()); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/RedHttpServer/WebSocketDialog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Red 10 | { 11 | /// 12 | /// Represents a websocket dialog between the server and a client. 13 | /// The WebSocketDialog will only start reading from the websocket after the handlers have been processed. 14 | /// 15 | public sealed class WebSocketDialog 16 | { 17 | /// 18 | /// The underlying WebSocket 19 | /// 20 | public readonly WebSocket WebSocket; 21 | 22 | private readonly CancellationToken _requestAborted; 23 | 24 | internal WebSocketDialog(WebSocket webSocket, CancellationToken requestAborted) 25 | { 26 | WebSocket = webSocket; 27 | _requestAborted = requestAborted; 28 | } 29 | 30 | 31 | /// 32 | /// Raised when binary WebSocket messages are received 33 | /// 34 | public event EventHandler? OnBinaryReceived; 35 | 36 | /// 37 | /// Raised when text WebSocket messages are received 38 | /// 39 | public event EventHandler? OnTextReceived; 40 | 41 | /// 42 | /// Raised when socket is closed 43 | /// 44 | public event EventHandler? OnClosed; 45 | 46 | /// 47 | /// Send text message using websocket 48 | /// 49 | /// 50 | /// 51 | public Task SendText(string text, bool endOfMessage = true) 52 | { 53 | return WebSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(text)), 54 | WebSocketMessageType.Text, endOfMessage, _requestAborted); 55 | } 56 | 57 | /// 58 | /// Send binary message using websocket 59 | /// 60 | /// 61 | /// 62 | public Task SendBytes(ArraySegment data, bool endOfMessage = true) 63 | { 64 | return WebSocket.SendAsync(data, WebSocketMessageType.Binary, endOfMessage, _requestAborted); 65 | } 66 | 67 | /// 68 | /// Closes the WebSocket connection 69 | /// 70 | /// 71 | /// 72 | /// 73 | public Task Close( 74 | WebSocketCloseStatus status = WebSocketCloseStatus.NormalClosure, 75 | string description = "") 76 | { 77 | return WebSocket.CloseAsync(status, description, _requestAborted); 78 | } 79 | 80 | internal async Task ReadFromWebSocket() 81 | { 82 | var buffer = ArrayPool.Shared.Rent(0x1000); 83 | try 84 | { 85 | var received = await WebSocket.ReceiveAsync(new ArraySegment(buffer), _requestAborted); 86 | while (!received.CloseStatus.HasValue) 87 | { 88 | switch (received.MessageType) 89 | { 90 | case WebSocketMessageType.Text: 91 | OnTextReceived?.Invoke(this, 92 | new TextMessageEventArgs(Encoding.UTF8.GetString(buffer, 0, received.Count), 93 | received.EndOfMessage)); 94 | break; 95 | case WebSocketMessageType.Binary: 96 | OnBinaryReceived?.Invoke(this, 97 | new BinaryMessageEventArgs(new ArraySegment(buffer, 0, received.Count), 98 | received.EndOfMessage)); 99 | break; 100 | case WebSocketMessageType.Close: 101 | await WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", _requestAborted); 102 | break; 103 | } 104 | 105 | received = await WebSocket.ReceiveAsync(new ArraySegment(buffer), _requestAborted); 106 | } 107 | } 108 | catch (WebSocketException) 109 | { 110 | } 111 | catch (OperationCanceledException) 112 | { 113 | } 114 | finally 115 | { 116 | try 117 | { 118 | if (!new [] { WebSocketState.Aborted, WebSocketState.Closed}.Contains(WebSocket.State)) 119 | { 120 | await WebSocket.CloseAsync(WebSocketCloseStatus.InternalServerError, "", _requestAborted); 121 | } 122 | } 123 | catch (WebSocketException) { } 124 | 125 | OnClosed?.Invoke(this, EventArgs.Empty); 126 | WebSocket.Dispose(); 127 | } 128 | } 129 | 130 | /// 131 | /// 132 | /// Represents a binary message received from websocket 133 | /// 134 | public sealed class BinaryMessageEventArgs : EventArgs 135 | { 136 | /// 137 | /// Whether this is a complete message or the end of one, or there is more to come. 138 | /// 139 | public readonly bool EndOfMessage; 140 | 141 | /// 142 | /// The binary content of the message 143 | /// 144 | public readonly ArraySegment Data; 145 | 146 | internal BinaryMessageEventArgs(ArraySegment data, bool endOfMessage) 147 | { 148 | Data = data; 149 | EndOfMessage = endOfMessage; 150 | } 151 | } 152 | 153 | /// 154 | /// 155 | /// Represents a UTF-8 encoded text message received from websocket 156 | /// 157 | public sealed class TextMessageEventArgs : EventArgs 158 | { 159 | /// 160 | /// Whether this is a complete message or the end of one, or there is more to come. 161 | /// 162 | public readonly bool EndOfMessage; 163 | 164 | /// 165 | /// The text content of the message 166 | /// 167 | public readonly string Text; 168 | 169 | internal TextMessageEventArgs(string text, bool endOfMessage) 170 | { 171 | Text = text; 172 | EndOfMessage = endOfMessage; 173 | } 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /src/Test/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Red; 7 | 8 | namespace Test 9 | { 10 | 11 | public class TestObject 12 | { 13 | public string Prop1 { get; set; } 14 | public int Prop2 { get; set; } 15 | } 16 | class MySess 17 | { 18 | public string Name { get; set; } 19 | } 20 | 21 | class Program 22 | { 23 | static async Task Auth(Request req, Response res) 24 | { 25 | return HandlerType.Final; 26 | } 27 | static async Task Auth(Request req, Response res, WebSocketDialog wsd) 28 | { 29 | return HandlerType.Final; 30 | } 31 | 32 | 33 | static async Task Main(string[] args) 34 | { 35 | var server = new RedHttpServer(5000); 36 | server.RespondWithExceptionDetails = true; 37 | server.Get("/exception", (req, res) => throw new Exception("oh no!")); 38 | server.Get("/index", Auth, (req, res) => res.SendFile("./index.html")); 39 | server.Get("/webm", (req, res) => res.SendFile("./Big_Buck_Bunny_alt.webm")); 40 | server.Get("/files/*", Handlers.SendFiles("public/files")); 41 | 42 | var testObj = new TestObject{Prop1 = "Hello", Prop2 = 42}; 43 | 44 | server.Get("/json", (req, res) => res.SendJson(testObj)); 45 | server.Get("/xml", (req, res) => res.SendXml(testObj)); 46 | 47 | server.CreateRouter("/test", TestRoutes.Register); 48 | 49 | server.OnHandlerException += (e, sender) => 50 | { 51 | Console.WriteLine(e); 52 | }; 53 | 54 | server.WebSocket("/echo", async (req, res, wsd) => 55 | { 56 | await wsd.SendText("Welcome to the echo test server"); 57 | wsd.OnTextReceived += (sender, eventArgs) => { wsd.SendText("you sent: " + eventArgs.Text); }; 58 | return HandlerType.Continue; 59 | }); 60 | await server.RunAsync(); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Test/Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | netcoreapp3.1 5 | 8 6 | enable 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Test/TestRoutes.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Red; 3 | using Red.Interfaces; 4 | 5 | namespace Test 6 | { 7 | public class TestRoutes 8 | { 9 | public static void Register(IRouter router) 10 | { 11 | router.Get("/1", (req, res) => res.SendString("Test1")); 12 | router.Get("/2", GetTest2); 13 | } 14 | 15 | private static Task GetTest2(Request arg1, Response arg2) 16 | { 17 | return arg2.SendString("Test2"); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Test/public/files/test.txt: -------------------------------------------------------------------------------- 1 | HIHIHIHIHIHIHIHIHIHIHIHIHI --------------------------------------------------------------------------------