Skip to content

Commit b34c326

Browse files
authored
Merge pull request #7 from carldebilly/dev/di-supported-config
Introduce DI support and customizable server options
2 parents e58c3b5 + 127bdf8 commit b34c326

File tree

19 files changed

+953
-406
lines changed

19 files changed

+953
-406
lines changed

README.md

Lines changed: 217 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Yllibed HttpServer
2-
This is a versatile http server designed to be used in mobile/UWP applications.
2+
3+
This is a versatile http server designed to be used in mobile/UWP applications and any applications which need to expose a simple web server.
34

45
## Packages and NuGet Statistics
56

@@ -17,9 +18,34 @@ This is a versatile http server designed to be used in mobile/UWP applications.
1718

1819
2. Register a server in your app:
1920
```csharp
20-
var myServer = new Server(8080); // create a web server on port 8080
21-
myServer.RegisterHandler(new StaticHandler("/", "text/plain", "Hello, world!")); // register a handler for the root
22-
var (uri4, uri6) = myServer.Start(); // start the server and get the URIs
21+
var myServer = new Server(); // Uses dynamic port (recommended)
22+
myServer.RegisterHandler(new StaticHandler("/", "text/plain", "Hello, world!"));
23+
var (uri4, uri6) = myServer.Start(); // Get the actual URIs with assigned ports
24+
25+
Console.WriteLine($"Server running on: {uri4}");
26+
```
27+
28+
Or specify a fixed port:
29+
```csharp
30+
var myServer = new Server(8080); // Fixed port
31+
myServer.RegisterHandler(new StaticHandler("/", "text/plain", "Hello, world!"));
32+
var (uri4, uri6) = myServer.Start();
33+
```
34+
35+
Or with dependency injection and configuration:
36+
```csharp
37+
var services = new ServiceCollection();
38+
services.AddYllibedHttpServer(opts =>
39+
{
40+
opts.Port = 0; // Dynamic port (recommended)
41+
opts.Hostname4 = "127.0.0.1";
42+
opts.Hostname6 = "::1";
43+
});
44+
45+
var sp = services.BuildServiceProvider();
46+
var server = sp.GetRequiredService<Server>();
47+
server.RegisterHandler(new StaticHandler("/", "text/plain", "Hello, world!"));
48+
var (uri4, uri6) = server.Start();
2349
```
2450

2551
## What it is
@@ -30,17 +56,158 @@ This is a versatile http server designed to be used in mobile/UWP applications.
3056
* This HTTP server is not designed for performance or high capacity
3157
* It's perfect for small applications, or small need, like to act as _return url_ for OAuth2 authentication using external browser.
3258

59+
## Features
60+
* Simple, lightweight, self-contained HTTP server
61+
* Supports IPv4 and IPv6
62+
* Supports HTTP 1.1 (limited: no keep-alive, no chunked encoding)
63+
* Supports GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, PATCH - even custom methods
64+
* Supports static files
65+
* Supports custom headers
66+
* Supports custom status codes
67+
* Supports custom content types
68+
* Supports custom content encodings
69+
* Supports dependency injection and configuration via `IOptions<ServerOptions>`
70+
* Configurable bind addresses and hostnames for IPv4/IPv6
71+
* Supports dynamic port assignment
72+
73+
## Common use cases
74+
* Return URL for OAuth2 authentication using external browser
75+
* Remote diagnostics/monitoring on your app
76+
* Building a headless Windows IoT app (for SSDP discovery or simply end-user configuration)
77+
* Any other use case where you need to expose a simple web server
78+
3379
## Limitations
3480
* There is no support for HTTP 2.0+ (yet) or WebSockets
3581
* There is no support for HTTPS (TLS)
3682

37-
## What you can do with it
38-
* Use it for building a headless Windows IoT app (for SSDP discovery or simply end-user configuration)
39-
* Use it for remote diagnostics/monitoring on your app
83+
## Security and Intended Use (No TLS)
84+
This server uses plain HTTP with no transport encryption. It is primarily intended for:
85+
- Localhost usage (loopback) during development or as an OAuth redirect target.
86+
- Internal communication on trusted networks, e.g., between Docker containers on the same host or in a private overlay network.
87+
- Embedded/local scenarios (IoT, diagnostics) where transport security is handled elsewhere.
88+
89+
If you need to expose it on a public or untrusted network:
90+
- Put it behind a reverse proxy that terminates TLS (e.g., Nginx, Traefik, Caddy, IIS/ASP.NET Core reverse proxy) and forward to this server over HTTP on a private interface.
91+
- Alternatively, use a secure tunnel (SSH, Cloudflare Tunnel, etc.).
92+
- Bind to loopback only (127.0.0.1 / ::1) when you want to ensure local-only access.
93+
94+
Note: Authentication/authorization is not built-in; implement it in your handlers or at the proxy layer as needed.
95+
96+
## Configuration and Dependency Injection
97+
98+
The server can now be configured via a `ServerOptions` POCO. You can construct `Server` directly with a `ServerOptions` instance, or register it with Microsoft.Extensions.DependencyInjection using `IOptions<ServerOptions>`.
99+
100+
### ServerOptions Properties
101+
102+
* `Port` - Port number to listen to (0 = dynamic)
103+
* `BindAddress4` - IPv4 address to bind the listener to (defaults to `IPAddress.Any`)
104+
* `BindAddress6` - IPv6 address to bind the listener to (defaults to `IPAddress.IPv6Any`)
105+
* `Hostname4` - Hostname used to compose the public IPv4 URI (defaults to "127.0.0.1")
106+
* `Hostname6` - Hostname used to compose the public IPv6 URI (defaults to "::1")
107+
108+
### Dynamic Port Assignment (Recommended)
109+
110+
Using port `0` enables **dynamic port assignment**, which is the **recommended approach** for most applications:
111+
112+
**Key Advantages:**
113+
- **Zero port conflicts**: The operating system automatically assigns an available port
114+
- **Perfect for testing**: Multiple test instances can run simultaneously without conflicts
115+
- **Microservices architecture**: Each service gets its own unique port automatically
116+
- **Team collaboration**: No need to coordinate port assignments between developers
117+
- **CI/CD friendly**: Parallel builds and tests work seamlessly
118+
119+
**Best Practices with Dynamic Ports:**
120+
121+
```csharp
122+
// ✅ Recommended: Use dynamic ports (default behavior)
123+
var server = new Server(); // Port 0 by default
124+
server.RegisterHandler(new StaticHandler("/", "text/plain", "Hello!"));
125+
var (uri4, uri6) = server.Start();
126+
127+
// Capture the actual assigned port for logging or service discovery
128+
var actualPort = new Uri(uri4).Port;
129+
Console.WriteLine($"Server started on port: {actualPort}");
130+
Console.WriteLine($"Access your service at: {uri4}");
131+
132+
// For service registration with discovery systems
133+
await RegisterWithServiceDiscovery(uri4);
134+
```
135+
136+
**In production with Dependency Injection:**
40137

41-
# Tips
138+
```csharp
139+
services.AddYllibedHttpServer(); // Uses port 0 by default - no conflicts!
42140
43-
## Opening port on Windows 10 IoT (typically on a Raspberry Pi)
141+
// Or be explicit for documentation purposes:
142+
services.AddYllibedHttpServer(opts =>
143+
{
144+
opts.Port = 0; // Dynamic port - recommended
145+
opts.BindAddress4 = IPAddress.Any; // Accept from all interfaces
146+
opts.Hostname4 = Environment.MachineName; // Use machine name in URIs
147+
});
148+
```
149+
150+
**When NOT to use dynamic ports:**
151+
- Public-facing web services requiring well-known ports (80, 443)
152+
- Legacy systems expecting fixed ports
153+
- Load balancer configurations requiring static endpoints
154+
- Development scenarios where you need predictable URLs
155+
156+
### Basic Configuration Example
157+
158+
```csharp
159+
var serverOptions = new ServerOptions
160+
{
161+
Port = 5000,
162+
Hostname4 = "192.168.1.100", // Custom hostname for callbacks
163+
BindAddress4 = IPAddress.Any // Listen on all interfaces
164+
};
165+
166+
var server = new Server(serverOptions);
167+
server.Start();
168+
```
169+
170+
### Dependency Injection Example
171+
172+
The cleanest approach using extension methods:
173+
174+
```csharp
175+
var services = new ServiceCollection();
176+
services.AddYllibedHttpServer(opts =>
177+
{
178+
opts.Port = 5000;
179+
opts.Hostname4 = "127.0.0.1";
180+
opts.Hostname6 = "::1";
181+
opts.BindAddress4 = IPAddress.Parse("0.0.0.0");
182+
});
183+
184+
var sp = services.BuildServiceProvider();
185+
var server = sp.GetRequiredService<Server>();
186+
server.Start();
187+
```
188+
189+
Alternative using `services.Configure<>()` and automatic constructor selection:
190+
191+
```csharp
192+
var services = new ServiceCollection();
193+
services.Configure<ServerOptions>(opts =>
194+
{
195+
opts.Port = 5000;
196+
opts.Hostname4 = "127.0.0.1";
197+
opts.Hostname6 = "::1";
198+
});
199+
services.AddSingleton<Server>(); // Uses IOptions<ServerOptions> constructor automatically
200+
201+
var sp = services.BuildServiceProvider();
202+
var server = sp.GetRequiredService<Server>();
203+
server.Start();
204+
```
205+
206+
This allows you to control the bind addresses and hostnames used to compose the public URIs that the server logs, which is especially useful for OAuth callbacks and REST API applications.
207+
208+
## Tips
209+
210+
### Opening port on Windows 10 IoT (typically on a Raspberry Pi)
44211
If you want to open "any" port on a Raspberry Pi running Windows 10 IoT, you may
45212
need to open a port.
46213

@@ -52,3 +219,44 @@ need to open a port.
52219
```shell
53220
netsh advfirewall firewall add rule name="My Application Webserver" dir=in action=allow protocol=TCP localport=8080
54221
```
222+
223+
### OAuth2 Callback Configuration
224+
When using this server for OAuth2 callbacks, dynamic ports are especially useful:
225+
226+
```csharp
227+
services.AddYllibedHttpServer(); // Dynamic port prevents conflicts
228+
229+
var server = serviceProvider.GetRequiredService<Server>();
230+
server.RegisterHandler(new MyOAuthCallbackHandler());
231+
var (uri4, uri6) = server.Start();
232+
233+
// Use the actual URI for OAuth redirect registration
234+
var redirectUri = $"{uri4}/oauth/callback";
235+
Console.WriteLine($"Register this redirect URI with your OAuth provider: {redirectUri}");
236+
237+
// No port conflicts even if multiple OAuth flows run simultaneously!
238+
```
239+
240+
For scenarios requiring fixed callback URLs:
241+
```csharp
242+
services.Configure<ServerOptions>(opts =>
243+
{
244+
opts.Port = 5001;
245+
opts.Hostname4 = "127.0.0.1"; // Match your OAuth redirect URI
246+
opts.BindAddress4 = IPAddress.Loopback; // Only accept local connections
247+
});
248+
```
249+
250+
### Listening on All Interfaces
251+
To accept connections from other machines on your network:
252+
253+
```csharp
254+
var serverOptions = new ServerOptions
255+
{
256+
Port = 8080,
257+
BindAddress4 = IPAddress.Any, // Listen on all IPv4 interfaces
258+
BindAddress6 = IPAddress.IPv6Any, // Listen on all IPv6 interfaces
259+
Hostname4 = "192.168.1.100", // Your actual IP for public URIs
260+
Hostname6 = "::1" // IPv6 loopback
261+
};
262+
```

Yllibed.HttpServer.Json.Tests/FixtureBase.cs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,22 @@
44
using System.Threading;
55
using Microsoft.VisualStudio.TestTools.UnitTesting;
66

7-
namespace Yllibed.HttpServer.Json.Tests
8-
{
9-
public class FixtureBase
10-
{
11-
protected CancellationToken CT;
12-
private CancellationTokenSource _ctSource;
7+
namespace Yllibed.HttpServer.Json.Tests;
138

14-
[TestInitialize]
15-
public void Initialize()
16-
{
17-
_ctSource = Debugger.IsAttached
18-
? new CancellationTokenSource()
19-
: new CancellationTokenSource(TimeSpan.FromSeconds(10));
20-
CT = _ctSource.Token;
21-
}
9+
public class FixtureBase
10+
{
11+
protected CancellationToken CT;
12+
private CancellationTokenSource _ctSource;
2213

23-
[TestCleanup]
24-
public void Terminate()
25-
{
26-
_ctSource.Dispose();
27-
}
14+
[TestInitialize]
15+
public void Initialize()
16+
{
17+
_ctSource = Debugger.IsAttached
18+
? new CancellationTokenSource()
19+
: new CancellationTokenSource(TimeSpan.FromSeconds(10));
20+
CT = _ctSource.Token;
2821
}
22+
23+
[TestCleanup]
24+
public void Terminate() => _ctSource.Dispose();
2925
}

Yllibed.HttpServer.Json.Tests/JsonHandlerBaseFixture.cs

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,59 +7,63 @@
77
using FluentAssertions;
88
using Microsoft.VisualStudio.TestTools.UnitTesting;
99
using Newtonsoft.Json;
10+
using Yllibed.HttpServer.Json;
1011

11-
namespace Yllibed.HttpServer.Json.Tests
12+
namespace Yllibed.HttpServer.Json.Tests;
13+
14+
[TestClass]
15+
public class JsonHandlerBaseFixture : FixtureBase
1216
{
13-
[TestClass]
14-
public class JsonHandlerBaseFixture : FixtureBase
17+
[TestMethod]
18+
public async Task TestSimpleGet_WithJsonHandler()
1519
{
16-
[TestMethod]
17-
public async Task TestSimpleGet_WithJsonHandler()
18-
{
19-
var server = new Server();
20+
var server = new Server();
2021

21-
var (uri4, uri6) = server.Start();
22-
var requestUri = uri4 + "abcd?a=1&b=%20%2020";
22+
var (uri4, uri6) = server.Start();
23+
var requestUri = uri4 + "abcd?a=1&b=%20%2020";
2324

24-
var sut = new MyJsonHandler("GET", "abcd");
25+
var sut = new MyJsonHandler("GET", "abcd");
2526

26-
using (server.RegisterHandler(sut))
27+
using (server.RegisterHandler(sut))
28+
{
29+
using (var client = new HttpClient())
2730
{
28-
using (var client = new HttpClient())
29-
{
30-
var response = await client.GetAsync(requestUri, CT);
31-
response.StatusCode.Should().Be(HttpStatusCode.OK);
31+
var response = await client.GetAsync(requestUri, CT);
32+
response.StatusCode.Should().Be(HttpStatusCode.OK);
3233

33-
var result = JsonConvert.DeserializeObject<MyResultPayload>(await response.Content.ReadAsStringAsync(CT));
34-
result.Should().NotBeNull();
35-
result.Should().BeEquivalentTo(new { A = "1", B = " 20" });
36-
}
34+
var result = JsonConvert.DeserializeObject<MyResultPayload>(await response.Content.ReadAsStringAsync(CT).ConfigureAwait(false));
35+
result.Should().NotBeNull();
36+
result.Should().BeEquivalentTo(new { A = "1", B = " 20" });
3737
}
3838
}
39+
}
3940

40-
public class MyResultPayload
41+
public class MyResultPayload
42+
{
43+
public string? A { get; set; }
44+
public string? B { get; set; }
45+
}
46+
47+
private sealed class MyJsonHandler : JsonHandlerBase<MyResultPayload>
48+
{
49+
public MyJsonHandler(string method, string path) : base(method, path)
4150
{
42-
public string? A { get; set; }
43-
public string? B { get; set; }
4451
}
4552

46-
private sealed class MyJsonHandler(string method, string path) : JsonHandlerBase<MyResultPayload>(method, path)
53+
protected override async Task<(MyResultPayload result, ushort statusCode)> ProcessRequest(CancellationToken ct, string relativePath, IDictionary<string, string[]> queryParameters)
4754
{
48-
protected override async Task<(MyResultPayload result, ushort statusCode)> ProcessRequest(CancellationToken ct, string relativePath, IDictionary<string, string[]> queryParameters)
49-
{
50-
queryParameters.TryGetValue("a", out var a);
51-
queryParameters.TryGetValue("b", out var b);
55+
queryParameters.TryGetValue("a", out var a);
56+
queryParameters.TryGetValue("b", out var b);
5257

53-
await Task.Yield();
58+
await Task.Yield();
5459

55-
var result = new MyResultPayload
56-
{
57-
A = a?.FirstOrDefault(),
58-
B = b?.FirstOrDefault(),
59-
};
60+
var result = new MyResultPayload
61+
{
62+
A = a?.FirstOrDefault(),
63+
B = b?.FirstOrDefault(),
64+
};
6065

61-
return (result, 200);
62-
}
66+
return (result, 200);
6367
}
6468
}
6569
}

0 commit comments

Comments
 (0)