Files
2026-06-01 17:11:09 +10:00

156 lines
6.4 KiB
C#

using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SNote.Server.Data;
using SNote.Server.Endpoints;
using SNote.Server.Security;
namespace SNote.Server;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add Configuration & DB Context (SQLite)
var dbPath = Path.Combine(AppContext.BaseDirectory, "snote_server.db");
builder.Services.AddDbContext<ServerDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}"));
// Register custom security managers & HTTP utilities
builder.Services.AddSingleton<CertificateManager>();
builder.Services.AddSingleton<RsaKeyManager>();
builder.Services.AddSingleton<PeerCache>();
builder.Services.AddSingleton<HttpClient>();
// Register OpenAPI explorer
builder.Services.AddOpenApi();
var app = builder.Build();
Console.WriteLine($"[SNote Server] Started with dynamic Server GUID: {SyncEndpoints.ServerGuid}");
// 1. Initialize SQLite Database Schema
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
db.Database.EnsureCreated();
}
// Configure Development Tools
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
// 2. Map Modular API Endpoint Classes
app.MapAuthEndpoints();
app.MapNodeEndpoints();
app.MapSyncEndpoints();
// Welcome / Healthcheck root
app.MapGet("/", () => Results.Ok(new
{
service = "SNote Server",
status = "Online",
time = DateTime.UtcNow
}));
// 3. Start Background Sync Runner
var hostApplicationLifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
var services = app.Services;
var config = app.Configuration;
// Dynamic Handshake & Bootstrapping: Connect to target destination server on startup, and optionally bootstrap
var destUrl = config["Sync:DestinationServerUrl"];
var localUrl = config["Sync:LocalServerUrl"];
if (!string.IsNullOrEmpty(destUrl) && !string.IsNullOrEmpty(localUrl))
{
Task.Run(async () =>
{
// Wait briefly for server startup
await Task.Delay(2000);
using var scope = services.CreateScope();
var rsaKeyManager = scope.ServiceProvider.GetRequiredService<RsaKeyManager>();
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var client = scope.ServiceProvider.GetRequiredService<HttpClient>();
// 1. Perform handshake
await SyncEndpoints.RegisterPeerWithDestinationAsync(destUrl, localUrl, rsaKeyManager, configuration, client);
// 2. Check if we have successfully handshaked and obtained a token
if (!string.IsNullOrEmpty(SyncEndpoints.UpstreamSessionToken))
{
var bootstrapMode = configuration["Sync:BootstrapFromUpstream"] ?? "Never";
bool doBootstrap = false;
if (string.Equals(bootstrapMode, "Always", StringComparison.OrdinalIgnoreCase))
{
doBootstrap = true;
}
else if (string.Equals(bootstrapMode, "FirstTime", StringComparison.OrdinalIgnoreCase))
{
var flagPath = Path.Combine(AppContext.BaseDirectory, "snote_bootstrapped.flag");
if (!File.Exists(flagPath))
{
doBootstrap = true;
}
else
{
Console.WriteLine("[Bootstrap] Skipped because BootstrapFromUpstream is set to FirstTime and server has bootstrapped before.");
}
}
if (doBootstrap)
{
var db = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
await SyncEndpoints.BootstrapFromPeerServerAsync(destUrl, SyncEndpoints.ServerGuid, SyncEndpoints.UpstreamSessionToken, db, client);
// If it's FirstTime, write the flag file
if (string.Equals(bootstrapMode, "FirstTime", StringComparison.OrdinalIgnoreCase))
{
try
{
var flagPath = Path.Combine(AppContext.BaseDirectory, "snote_bootstrapped.flag");
await File.WriteAllTextAsync(flagPath, DateTime.UtcNow.ToString("o"));
Console.WriteLine("[Bootstrap] Wrote bootstrap flag file.");
}
catch (Exception ex)
{
Console.WriteLine($"[Bootstrap] Warning: Could not write bootstrap flag file: {ex.Message}");
}
}
}
}
else
{
Console.WriteLine("[Bootstrap] Skipped because handshake failed (no token obtained).");
}
});
}
// Heartbeat Loop: Periodic Downstream Heartbeat pinger pings downstream peers every 30 seconds
Task.Run(async () =>
{
var token = hostApplicationLifetime.ApplicationStopping;
using var scope = services.CreateScope();
var peerCache = scope.ServiceProvider.GetRequiredService<PeerCache>();
var rsaKeyManager = scope.ServiceProvider.GetRequiredService<RsaKeyManager>();
var client = scope.ServiceProvider.GetRequiredService<HttpClient>();
await SyncEndpoints.StartHeartbeatPingerAsync(peerCache, rsaKeyManager, client, token);
});
app.Run();
}
}