Skip to content

Commit 9929a62

Browse files
committed
Additional features
1 parent 86a72b2 commit 9929a62

78 files changed

Lines changed: 12658 additions & 46 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Directory.Packages.props‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@
3333
released independently. -->
3434
<PackageVersion Include="Shiny.Mediator" Version="6.8.0"/>
3535
<PackageVersion Include="ModelContextProtocol" Version="2.1.0"/>
36+
37+
<!-- Same arrangement as the mediator: this repo ships the HTTP-server side of DocumentDb and
38+
the engine is versioned and released on its own. 13.2.1 is the floor — it is the first
39+
release carrying the public Shiny.DocumentDb.Hosting surface (DocumentPredicate /
40+
DocumentFilter / DocumentStoreAccessor) that the endpoints evaluate a request scope with,
41+
so an earlier one would not compile. The SQLite provider is test-only and must stay in
42+
lockstep, because a provider compiled against a different engine build is a runtime
43+
mismatch rather than a build error. -->
44+
<PackageVersion Include="Shiny.DocumentDb" Version="13.2.1"/>
45+
<PackageVersion Include="Shiny.DocumentDb.Sqlite" Version="13.2.1"/>
46+
47+
<!-- Test-only. The gRPC tests call the server with the real gRPC client rather than a
48+
hand-rolled one, because "our framing agrees with itself" is not the claim being
49+
tested. Nothing that ships depends on it. -->
50+
<PackageVersion Include="Grpc.Net.Client" Version="2.83.0"/>
3651
</ItemGroup>
3752
<ItemGroup>
3853
<PackageVersion Include="xunit.v3" Version="3.2.2"/>

‎Shiny.Net.HttpServer.slnx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@
2323
</Folder>
2424

2525
<Project Path="src/Shiny.Net.HttpServer.AzureRelay/Shiny.Net.HttpServer.AzureRelay.csproj" />
26+
<Project Path="src/Shiny.Net.HttpServer.DocumentDb/Shiny.Net.HttpServer.DocumentDb.csproj" />
27+
<Project Path="src/Shiny.Net.HttpServer.Grpc/Shiny.Net.HttpServer.Grpc.csproj" />
2628
<Project Path="src/Shiny.Net.HttpServer.Jwt/Shiny.Net.HttpServer.Jwt.csproj" />
2729
<Project Path="src/Shiny.Net.HttpServer.Mcp/Shiny.Net.HttpServer.Mcp.csproj" />
30+
<Project Path="src/Shiny.Net.HttpServer.Mediator/Shiny.Net.HttpServer.Mediator.csproj" />
31+
<Project Path="src/Shiny.Net.HttpServer.Mediator.SourceGenerators/Shiny.Net.HttpServer.Mediator.SourceGenerators.csproj" />
2832
<Project Path="src/Shiny.Net.HttpServer.SourceGenerators/Shiny.Net.HttpServer.SourceGenerators.csproj" />
2933
<Project Path="src/Shiny.Net.HttpServer.Ssh/Shiny.Net.HttpServer.Ssh.csproj" />
34+
<Project Path="src/Shiny.Net.HttpServer.WebDav/Shiny.Net.HttpServer.WebDav.csproj" />
3035
<Project Path="src/Shiny.Net.HttpServer/Shiny.Net.HttpServer.csproj" />
3136
<Project Path="tests/Shiny.Net.HttpServer.Tests/Shiny.Net.HttpServer.Tests.csproj" />
3237

‎build.slnf‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
"src\\Shiny.Net.HttpServer\\Shiny.Net.HttpServer.csproj",
66
"src\\Shiny.Net.HttpServer.SourceGenerators\\Shiny.Net.HttpServer.SourceGenerators.csproj",
77
"src\\Shiny.Net.HttpServer.Jwt\\Shiny.Net.HttpServer.Jwt.csproj",
8+
"src\\Shiny.Net.HttpServer.Grpc\\Shiny.Net.HttpServer.Grpc.csproj",
89
"src\\Shiny.Net.HttpServer.AzureRelay\\Shiny.Net.HttpServer.AzureRelay.csproj",
910
"src\\Shiny.Net.HttpServer.Mcp\\Shiny.Net.HttpServer.Mcp.csproj",
10-
"src\\Shiny.Net.HttpServer.Ssh\\Shiny.Net.HttpServer.Ssh.csproj"
11+
"src\\Shiny.Net.HttpServer.Ssh\\Shiny.Net.HttpServer.Ssh.csproj",
12+
"src\\Shiny.Net.HttpServer.WebDav\\Shiny.Net.HttpServer.WebDav.csproj",
13+
"src\\Shiny.Net.HttpServer.DocumentDb\\Shiny.Net.HttpServer.DocumentDb.csproj",
14+
"src\\Shiny.Net.HttpServer.Mediator\\Shiny.Net.HttpServer.Mediator.csproj",
15+
"src\\Shiny.Net.HttpServer.Mediator.SourceGenerators\\Shiny.Net.HttpServer.Mediator.SourceGenerators.csproj"
1116
]
1217
}
1318
}

‎readme.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ JWT, OpenAPI, HPACK, QPACK — is built on what is in the box.
2121
| [Shiny.Net.HttpServer.Ssh](https://www.nuget.org/packages/Shiny.Net.HttpServer.Ssh) | SSH remote-forwarding tunnel provider, including zero-account quick tunnels |
2222
| [Shiny.Net.HttpServer.Mcp](https://www.nuget.org/packages/Shiny.Net.HttpServer.Mcp) | Model Context Protocol (Streamable HTTP) transport — host an MCP server without ASP.NET Core, including inside a MAUI app |
2323
| [Shiny.Net.HttpServer.Mediator](https://www.nuget.org/packages/Shiny.Net.HttpServer.Mediator) | Publishes Shiny.Mediator requests, commands and streams as endpoints generated at compile time. Generator included |
24+
| [Shiny.Net.HttpServer.DocumentDb](https://www.nuget.org/packages/Shiny.Net.HttpServer.DocumentDb) | Publishes a Shiny.DocumentDb type as a REST resource — list, by-id, count, CRUD, merge-patch and a live SSE tail |
25+
| [Shiny.Net.HttpServer.WebDav](https://www.nuget.org/packages/Shiny.Net.HttpServer.WebDav) | A WebDAV (RFC 4918) class 1 & 2 server over a directory — mount an app's storage in Finder, Windows Explorer or any WebDAV client |
26+
| [Shiny.Net.HttpServer.Grpc](https://www.nuget.org/packages/Shiny.Net.HttpServer.Grpc) | gRPC and gRPC-Web — unary, streaming and bidirectional methods over the same HTTP/2 stack, with serialization you supply |
2427

2528
## Getting Started
2629

@@ -76,14 +79,18 @@ is built on the one below and they compose in the same app.
7679

7780
| | |
7881
| --- | --- |
79-
| **Core** | Routing with constraints and runtime-mutable routes, ASP.NET-shaped middleware, a real `IServiceScope` per request, results in both `Results.*` and `IActionResult` spellings, content negotiation, RFC 9457 problem details and an exception-handler chain |
80-
| **Protocols** | HTTP/1.1, HTTP/2 (own HPACK), HTTP/3 (own QPACK), WebSockets, Server-Sent Events. Never guessed — ALPN over TLS, connection preface over cleartext |
82+
| **Core** | Routing with constraints and runtime-mutable routes, ASP.NET-shaped middleware, a real `IServiceScope` per request, results in both `Results.*` and `IActionResult` spellings, RFC 9457 problem details and an exception-handler chain |
83+
| **Formats** | Content negotiation in both directions — responses chosen from `Accept`, request bodies from `Content-Type`. JSON out of the box; XML, MessagePack and protobuf are one line each, and a format of your own is an `IOutputFormatter`/`IInputFormatter` pair. XML and MessagePack need no dependency and no attributes on your DTOs: they read the same `JsonTypeInfo` the JSON path reads, which is what keeps them AOT-clean where `XmlSerializer` cannot be |
84+
| **Protocols** | HTTP/1.1, HTTP/2 (own HPACK), HTTP/3 (own QPACK), WebSockets, Server-Sent Events, trailing headers on all three versions. Never guessed — ALPN over TLS, connection preface over cleartext |
8185
| **Content** | Static files from disk *or* embedded resources, a published Blazor WebAssembly app, streaming multipart uploads, downloads with byte ranges and conditional GETs, a file browser over a directory, and brotli/gzip/deflate compression |
8286
| **Security** | Authentication and authorization split ASP.NET-style, with Basic, API key, cookie and JWT schemes; policies, roles and claims; CORS, rate limiting and IP filtering, all with per-endpoint policies |
8387
| **TLS** | Several endpoints with per-endpoint TLS, self-signed certificates generated in managed code (iOS and Android included), client certificates, and SPKI pinning for the app's own `HttpClient` |
8488
| **OpenAPI** | An OpenAPI 3.0.3 document built entirely from compile-time metadata and your `JsonSerializerContext` — no reflection, no document object model |
8589
| **Tunnelling** | A pluggable `ITunnelProvider`, the reference relay (both ends), SSH remote forwarding, zero-account quick tunnels, and Azure Relay |
8690
| **Mediator** | Shiny.Mediator handlers published as endpoints — requests as JSON, commands as a status code, stream requests as Server-Sent Events, all bound at compile time |
91+
| **DocumentDb** | A document type as a complete HTTP resource, with filtering, cursor paging, sparse fieldsets, ETag/If-Match, RFC 7396 merge-patch, a live SSE tail, and server-side scopes enforced on both sides of a write |
92+
| **gRPC** | Unary, client-streaming, server-streaming and bidirectional methods, deadlines, per-message compression and status in trailers — plus gRPC-Web for browsers and anything on HTTP/1.1. Marshalling is yours, so nothing reflects over your messages |
93+
| **WebDAV** | RFC 4918 classes 1 and 2 over a directory — `PROPFIND`, `PROPPATCH`, `MKCOL`, `COPY`, `MOVE`, `LOCK`/`UNLOCK`, the `If` header and dead properties — so an app's storage mounts as a drive with no client to write |
8794
| **Lifecycle** | Start, stop and restart at runtime, serialized and idempotent, with an observable state — an embedded server gets toggled, not just booted |
8895

8996
Everything shipping targets `net10.0` with the trim, AOT and single-file analyzers enabled, so

‎samples/Sample.Api/Program.cs‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Shiny.Net.HttpServer.RateLimiting;
1010
using Shiny.Net.HttpServer.Security;
1111
using Shiny.Net.HttpServer.Tunneling;
12+
using Shiny.Net.HttpServer.WebDav;
1213

1314
// ---------------------------------------------------------------------------
1415
// The whole ramp, in one app. Every tier below is optional and they all compose:
@@ -179,6 +180,33 @@
179180

180181
app.OnGet("/boom", _ => throw new InvalidOperationException("deliberate failure"));
181182

183+
// --- A directory, as a drive ---
184+
//
185+
// One call maps the twenty-two routes of an RFC 4918 class 1 & 2 WebDAV mount. Point Finder (Go →
186+
// Connect to Server) or Explorer (Map network drive) at http://localhost:8080/dav and the folder
187+
// below opens as a drive — no client to write, and no client to install.
188+
//
189+
// A browser GET of the same URL shows a plain HTML index, which is the quickest way to see it
190+
// working without mounting anything.
191+
var davRoot = Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, "dav-root"));
192+
193+
if (!File.Exists(Path.Combine(davRoot.FullName, "readme.txt")))
194+
File.WriteAllText(Path.Combine(davRoot.FullName, "readme.txt"), "Edit me from your file manager.\n");
195+
196+
app.MapWebDav("/dav", o =>
197+
{
198+
o.RootPath = davRoot.FullName;
199+
o.AllowWrite = true;
200+
o.AllowDelete = true;
201+
o.DisplayName = "Sample";
202+
});
203+
204+
// Left open because this sample binds loopback and nothing else can reach it. A mount anyone else
205+
// can see needs both of these — a WebDAV client sends its password on every single request:
206+
//
207+
// app.MapWebDav("/dav", …).RequireAuthorization(); // or .RequireAuthorizationForChanges()
208+
// builder.Configure(o => o.Certificate = …); // see /httpserver/tls/
209+
182210
// --- Controlling the server from inside the app ---
183211
//
184212
// The server is an ordinary object: hold onto it and start, stop or restart it whenever. This

‎samples/Sample.Api/Sample.Api.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<ItemGroup>
1515
<ProjectReference Include="../../src/Shiny.Net.HttpServer/Shiny.Net.HttpServer.csproj"/>
1616
<ProjectReference Include="../../src/Shiny.Net.HttpServer.Jwt/Shiny.Net.HttpServer.Jwt.csproj"/>
17+
<ProjectReference Include="../../src/Shiny.Net.HttpServer.WebDav/Shiny.Net.HttpServer.WebDav.csproj"/>
1718
<ProjectReference Include="../../src/Shiny.Net.HttpServer.SourceGenerators/Shiny.Net.HttpServer.SourceGenerators.csproj"
1819
OutputItemType="Analyzer"
1920
ReferenceOutputAssembly="false"/>

‎samples/Sample.Maui/MauiProgram.cs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Shiny.Net.HttpServer.Security;
99
using Shiny.Net.HttpServer.Ssh;
1010
using Shiny.Net.HttpServer.StaticFiles;
11+
using Shiny.Net.HttpServer.WebDav;
1112

1213
namespace Sample.Maui;
1314

@@ -125,6 +126,23 @@ public static MauiApp CreateMauiApp()
125126
o.AllowWrite = true;
126127
o.AllowDelete = true;
127128
});
129+
130+
// The same directory again, over WebDAV — and the difference is who the client is.
131+
// /files is a JSON API for a script; this is the protocol a desktop already speaks,
132+
// so the phone's storage opens as a drive in Finder or Windows Explorer with nothing
133+
// installed on either end. Point them at http://<device>:<port>/dav and sign in with
134+
// the same account as everything else.
135+
//
136+
// Locking is left on, and that is not a detail: Finder and the Windows redirector
137+
// both check for DAV class 2 at mount time and mount read-only without it, however
138+
// much AllowWrite allows.
139+
server.MapWebDav("/dav", o =>
140+
{
141+
o.RootPath = FileSystem.AppDataDirectory;
142+
o.AllowWrite = true;
143+
o.AllowDelete = true;
144+
o.DisplayName = DeviceInfo.Name;
145+
});
128146
},
129147

130148
// Started by the UI instead, so the app does not open a port before anyone asked it to.

‎samples/Sample.Maui/Pages/ServerPage.xaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
<Label Text="{Binding McpUrl, TargetNullValue='Server not started'}"
5252
LineBreakMode="CharacterWrap" />
5353

54+
<!-- The WebDAV mount. Finder: Go → Connect to Server. Explorer: Map network
55+
drive. Same server, same password — the app's storage as a drive. -->
56+
<Label Text="WebDAV mount" TextColor="Gray" Margin="0,6,0,0" />
57+
<Label Text="{Binding WebDavUrl, TargetNullValue='Server not started'}"
58+
LineBreakMode="CharacterWrap" />
59+
5460
<HorizontalStackLayout Spacing="8" Margin="0,6,0,0">
5561
<Label Text="Requests served:" />
5662
<Label Text="{Binding RequestCount}" FontAttributes="Bold" />

‎samples/Sample.Maui/Sample.Maui.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
<ProjectReference Include="../../src/Shiny.Net.HttpServer/Shiny.Net.HttpServer.csproj" />
103103
<ProjectReference Include="../../src/Shiny.Net.HttpServer.Ssh/Shiny.Net.HttpServer.Ssh.csproj" />
104104
<ProjectReference Include="../../src/Shiny.Net.HttpServer.Mcp/Shiny.Net.HttpServer.Mcp.csproj" />
105+
<ProjectReference Include="../../src/Shiny.Net.HttpServer.WebDav/Shiny.Net.HttpServer.WebDav.csproj" />
105106
</ItemGroup>
106107

107108
<!-- The shared page is embedded rather than shipped as a MauiAsset, so one file serves from

‎samples/Sample.Maui/ViewModels/ServerViewModel.cs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@ IMainThread mainThread
4242
[NotifyPropertyChangedFor(nameof(HasPublicUrl))]
4343
[NotifyPropertyChangedFor(nameof(CanStopSharing))]
4444
[NotifyPropertyChangedFor(nameof(McpUrl))]
45+
[NotifyPropertyChangedFor(nameof(WebDavUrl))]
4546
string? publicUrl;
4647

4748
/// <summary>The address on this Wi-Fi network. Works with no internet at all.</summary>
4849
[ObservableProperty]
4950
[NotifyPropertyChangedFor(nameof(McpUrl))]
51+
[NotifyPropertyChangedFor(nameof(WebDavUrl))]
5052
string? localUrl;
5153

5254
[ObservableProperty]
@@ -74,6 +76,18 @@ IMainThread mainThread
7476
? $"{url.TrimEnd('/')}/mcp"
7577
: null;
7678

79+
/// <summary>
80+
/// What to type into Finder's "Connect to Server" or Explorer's "Map network drive".
81+
/// <para>
82+
/// The LAN address first, the opposite way round from <see cref="McpUrl"/>. A mounted drive is
83+
/// a long-lived connection that sends the password on every request, so the one that stays on
84+
/// the Wi-Fi is the one to offer — the tunnel is cleartext HTTP to a shared host.
85+
/// </para>
86+
/// </summary>
87+
public string? WebDavUrl => (this.LocalUrl ?? this.PublicUrl) is { Length: > 0 } url
88+
? $"{url.TrimEnd('/')}/dav"
89+
: null;
90+
7791
/// <summary>
7892
/// Whether there is anything to stop — which includes a share still being opened.
7993
/// <para>

0 commit comments

Comments
 (0)