View on GitHub

Yllibed.HttpServer

Simple versatile web service for mobile applications

Yllibed HttpServer

Yllibed logo

A small, self-contained HTTP server for desktop, mobile, and embedded apps that need to expose a simple web endpoint.


Packages and NuGet

Package Downloads Stable Pre-release
Yllibed.HttpServer Downloads Stable Pre-release
Yllibed.HttpServer.Json Downloads Stable Pre-release

Quick start

  1. First install nuget package:
         PM> Install-Package Yllibed.HttpServer 
    
  2. Register a server in your app:
     var myServer = new Server(); // Uses dynamic port (recommended)
     myServer.RegisterHandler(new StaticHandler("/", "text/plain", "Hello, world!")); 
     var (uri4, uri6) = myServer.Start(); // Get the actual URIs with assigned ports
        
     Console.WriteLine($"Server running on: {uri4}");
    

    Or specify a fixed port:

     var myServer = new Server(8080); // Fixed port
     myServer.RegisterHandler(new StaticHandler("/", "text/plain", "Hello, world!"));
     var (uri4, uri6) = myServer.Start();
    

    Or with dependency injection and configuration:

     var services = new ServiceCollection();
     services.AddYllibedHttpServer(opts =>
     {
         opts.Port = 0; // Dynamic port (recommended)
         opts.Hostname4 = "127.0.0.1";
         opts.Hostname6 = "::1";
     });
        
     var sp = services.BuildServiceProvider();
     var server = sp.GetRequiredService<Server>();
     server.RegisterHandler(new StaticHandler("/", "text/plain", "Hello, world!"));
     var (uri4, uri6) = server.Start();
    

What it is

What it is not

Features

Common use cases

Limitations

Security and Intended Use (No TLS)

This server uses plain HTTP with no transport encryption. It is primarily intended for:

If you need to expose it on a public or untrusted network:

Note: Authentication/authorization is not built-in; implement it in your handlers or at the proxy layer as needed.

GuardHandler (basic request filtering)

GuardHandler provides best-effort filtering of incoming requests, rejecting obviously problematic or oversized requests early. This is lightweight filtering against unsophisticated attacks, not comprehensive security.

What it enforces (configurable):

Basic usage:

var server = new Server();
server.RegisterHandler(new GuardHandler(
    maxUrlLength: 2048,
    maxHeadersCount: 100,
    maxHeadersTotalSize: 32 * 1024,
    maxBodyBytes: 10 * 1024 * 1024,
    allowedMethods: new[] { "GET", "POST" },
    allowedHosts: null, // any
    requireHostHeader: true));
server.RegisterHandler(new StaticHandler("/", "text/plain", "OK"));
server.Start();

Wrapping another handler (next handler pattern):

var hello = new StaticHandler("/", "text/plain", "Hello");
var guard = new GuardHandler(allowedMethods: new[] { "GET" }, inner: hello);
server.RegisterHandler(guard); // guard calls hello only if checks pass

Using Microsoft DI elegantly:

var services = new ServiceCollection();
services.AddYllibedHttpServer();
services.AddGuardHandler(opts =>
{
    opts.MaxUrlLength = 2048;
    opts.MaxHeadersCount = 100;
    opts.AllowedMethods = new[] { "GET", "POST" };
    opts.RequireHostHeader = true;
    opts.AllowedHosts = new[] { "127.0.0.1", "localhost" };
});

// Easiest: auto-register into Server without manual resolution
services.AddGuardHandlerAndRegister(opts =>
{
    opts.MaxUrlLength = 2048;
    opts.MaxHeadersCount = 100;
    opts.AllowedMethods = new[] { "GET", "POST" };
    opts.RequireHostHeader = true;
    opts.AllowedHosts = new[] { "127.0.0.1", "localhost" };
});

var sp = services.BuildServiceProvider();
var server = sp.GetRequiredService<Server>();
server.RegisterHandler(new StaticHandler("/", "text/plain", "OK"));
server.Start();

Notes:

Configuration and Dependency Injection

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>.

ServerOptions Properties

Using port 0 enables dynamic port assignment, which is the recommended approach for most applications:

Key Advantages:

Best Practices with Dynamic Ports:

// ✅ Recommended: Use dynamic ports (default behavior)
var server = new Server(); // Port 0 by default
server.RegisterHandler(new StaticHandler("/", "text/plain", "Hello!"));
var (uri4, uri6) = server.Start();

// Capture the actual assigned port for logging or service discovery
var actualPort = new Uri(uri4).Port;
Console.WriteLine($"Server started on port: {actualPort}");
Console.WriteLine($"Access your service at: {uri4}");

// For service registration with discovery systems
await RegisterWithServiceDiscovery(uri4);

In production with Dependency Injection:

services.AddYllibedHttpServer(); // Uses port 0 by default - no conflicts!

// Or be explicit for documentation purposes:
services.AddYllibedHttpServer(opts =>
{
    opts.Port = 0; // Dynamic port - recommended
    opts.BindAddress4 = IPAddress.Any; // Accept from all interfaces
    opts.Hostname4 = Environment.MachineName; // Use machine name in URIs
});

When NOT to use dynamic ports:

Basic Configuration Example

var serverOptions = new ServerOptions
{
    Port = 5000,
    Hostname4 = "192.168.1.100", // Custom hostname for callbacks
    BindAddress4 = IPAddress.Any  // Listen on all interfaces
};

var server = new Server(serverOptions);
server.Start();

Dependency Injection Example

The cleanest approach using extension methods:

var services = new ServiceCollection();
services.AddYllibedHttpServer(opts =>
{
    opts.Port = 5000;
    opts.Hostname4 = "127.0.0.1";
    opts.Hostname6 = "::1";
    opts.BindAddress4 = IPAddress.Parse("0.0.0.0");
});

var sp = services.BuildServiceProvider();
var server = sp.GetRequiredService<Server>();
server.Start();

Alternative using services.Configure<>() and automatic constructor selection:

var services = new ServiceCollection();
services.Configure<ServerOptions>(opts =>
{
    opts.Port = 5000;
    opts.Hostname4 = "127.0.0.1";
    opts.Hostname6 = "::1";
});
services.AddSingleton<Server>(); // Uses IOptions<ServerOptions> constructor automatically

var sp = services.BuildServiceProvider();
var server = sp.GetRequiredService<Server>();
server.Start();

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.

Tips

Opening port on Windows 10 IoT (typically on a Raspberry Pi)

If you want to open “any” port on a Raspberry Pi running Windows 10 IoT, you may need to open a port.

  1. First, connect to your device using powershell:
       Enter-PsSession -ComputerName <device name or ip> -Credential .\Administrator
    
  2. Add a rule in the firewall to authorize inbound traffic to your application: (example for port 8080)
       netsh advfirewall firewall add rule name="My Application Webserver" dir=in action=allow protocol=TCP localport=8080
    

OAuth2 Callback Configuration

When using this server for OAuth2 callbacks, dynamic ports are especially useful:

services.AddYllibedHttpServer(); // Dynamic port prevents conflicts

var server = serviceProvider.GetRequiredService<Server>();
server.RegisterHandler(new MyOAuthCallbackHandler());
var (uri4, uri6) = server.Start();

// Use the actual URI for OAuth redirect registration
var redirectUri = $"{uri4}/oauth/callback";
Console.WriteLine($"Register this redirect URI with your OAuth provider: {redirectUri}");

// No port conflicts even if multiple OAuth flows run simultaneously!

For scenarios requiring fixed callback URLs:

services.Configure<ServerOptions>(opts =>
{
    opts.Port = 5001;
    opts.Hostname4 = "127.0.0.1"; // Match your OAuth redirect URI
    opts.BindAddress4 = IPAddress.Loopback; // Only accept local connections
});

Listening on All Interfaces

To accept connections from other machines on your network:

var serverOptions = new ServerOptions
{
    Port = 8080,
    BindAddress4 = IPAddress.Any,        // Listen on all IPv4 interfaces
    BindAddress6 = IPAddress.IPv6Any,    // Listen on all IPv6 interfaces
    Hostname4 = "192.168.1.100",        // Your actual IP for public URIs
    Hostname6 = "::1"                   // IPv6 loopback
};

Server-Sent Events (SSE)

SSE lets your server push a continuous stream of text events over a single HTTP response. This project now provides a minimal SSE path without chunked encoding: headers are sent, then the connection stays open while your code writes events; closing the connection ends the stream.

Quick example (application code):

// Register a handler for /sse (very basic example)
public sealed class SseDemoHandler : IHttpHandler
{
    public Task HandleRequest(CancellationToken ct, IHttpServerRequest request, string relativePath)
    {
        if (!string.Equals(request.Method, "GET", StringComparison.OrdinalIgnoreCase)) return Task.CompletedTask;
        if (!string.Equals(relativePath, "/sse", StringComparison.Ordinal)) return Task.CompletedTask;

        request.StartSseSession(RunSseSession,
            headers: new Dictionary<string, IReadOnlyCollection<string>>
            {
                ["Access-Control-Allow-Origin"] = new[] { "*" } // if you need CORS
            },
            options: new SseOptions
            {
                HeartbeatInterval = TimeSpan.FromSeconds(30),
                HeartbeatComment = "keepalive",
                AutoFlush = true
            });
        return Task.CompletedTask;
    }

    private async Task RunSseSession(ISseSession sse, CancellationToken ct)
    {
        // Optional: initial comment
        await sse.SendCommentAsync("start", ct);

        var i = 0;
        while (!ct.IsCancellationRequested && i < 10)
        {
            // Write an event every second
            await sse.SendEventAsync($"{DateTimeOffset.UtcNow:O}", eventName: "tick", id: i.ToString(), ct: ct);
            await Task.Delay(TimeSpan.FromSeconds(1), ct);
            i++;
        }
    }
}

// Usage during startup
var server = new Server();
var ssePath = new RelativePathHandler("/updates");
ssePath.RegisterHandler(new SseDemoHandler());
server.RegisterHandler(ssePath);
var (uri4, _) = server.Start();
Console.WriteLine($"SSE endpoint: {uri4}/updates/sse");

SseHandler convenience base class:

public sealed class MySseHandler : SseHandler
{
    protected override bool ShouldHandle(IHttpServerRequest request, string relativePath)
        => base.ShouldHandle(request, relativePath) && relativePath is "/sse";

    protected override Task HandleSseSession(ISseSession sse, CancellationToken ct)
        => RunSseSession(sse, ct); // Reuse the same private method as above
}

// Registration
var server = new Server();
var ssePath = new RelativePathHandler("/updates");
ssePath.RegisterHandler(new MySseHandler());
server.RegisterHandler(ssePath);

Client-side (browser):

<script>
  const es = new EventSource('/updates/sse');
  es.addEventListener('tick', e => console.log('tick', e.data));
  es.onmessage = e => console.log('message', e.data);
  es.onerror = e => console.warn('SSE error', e);
</script>

Notes:

SSE Spec and Interop Notes