2026-06-01 05:09:20 +10:00
|
|
|
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;
|
2026-05-28 02:54:17 +10:00
|
|
|
|
|
|
|
|
namespace SNote.Server;
|
|
|
|
|
|
|
|
|
|
public class Program
|
|
|
|
|
{
|
|
|
|
|
public static void Main(string[] args)
|
|
|
|
|
{
|
2026-06-01 05:09:20 +10:00
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
2026-05-28 02:54:17 +10:00
|
|
|
|
2026-06-01 05:09:20 +10:00
|
|
|
// 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>();
|
2026-06-01 05:49:08 +10:00
|
|
|
builder.Services.AddSingleton<RsaKeyManager>();
|
2026-06-01 05:09:20 +10:00
|
|
|
builder.Services.AddSingleton<PeerCache>();
|
|
|
|
|
builder.Services.AddSingleton<HttpClient>();
|
2026-05-28 02:54:17 +10:00
|
|
|
|
2026-06-01 05:09:20 +10:00
|
|
|
// Register OpenAPI explorer
|
2026-05-28 02:54:17 +10:00
|
|
|
builder.Services.AddOpenApi();
|
|
|
|
|
|
|
|
|
|
var app = builder.Build();
|
|
|
|
|
|
2026-06-01 05:09:20 +10:00
|
|
|
// 1. Initialize SQLite Database Schema
|
|
|
|
|
using (var scope = app.Services.CreateScope())
|
|
|
|
|
{
|
|
|
|
|
var db = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
|
|
|
|
|
db.Database.EnsureCreated();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configure Development Tools
|
2026-05-28 02:54:17 +10:00
|
|
|
if (app.Environment.IsDevelopment())
|
|
|
|
|
{
|
|
|
|
|
app.MapOpenApi();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 05:09:20 +10:00
|
|
|
// 2. Map Modular API Endpoint Classes
|
|
|
|
|
app.MapAuthEndpoints();
|
|
|
|
|
app.MapNodeEndpoints();
|
|
|
|
|
app.MapSyncEndpoints();
|
2026-05-28 02:54:17 +10:00
|
|
|
|
2026-06-01 05:09:20 +10:00
|
|
|
// Welcome / Healthcheck root
|
|
|
|
|
app.MapGet("/", () => Results.Ok(new
|
|
|
|
|
{
|
|
|
|
|
service = "SNote Server",
|
|
|
|
|
status = "Online",
|
|
|
|
|
time = DateTime.UtcNow
|
|
|
|
|
}));
|
2026-05-28 02:54:17 +10:00
|
|
|
|
2026-06-01 05:09:20 +10:00
|
|
|
// 3. Start Background Sync Runner
|
|
|
|
|
var hostApplicationLifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
|
|
|
|
|
var services = app.Services;
|
|
|
|
|
|
|
|
|
|
var config = app.Configuration;
|
2026-05-28 02:54:17 +10:00
|
|
|
|
2026-06-01 05:49:08 +10:00
|
|
|
// Dynamic Handshake & Bootstrapping: Connect to target destination server on startup, and optionally bootstrap
|
2026-06-01 05:09:20 +10:00
|
|
|
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();
|
2026-06-01 05:49:08 +10:00
|
|
|
var rsaKeyManager = scope.ServiceProvider.GetRequiredService<RsaKeyManager>();
|
|
|
|
|
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
2026-06-01 05:09:20 +10:00
|
|
|
var client = scope.ServiceProvider.GetRequiredService<HttpClient>();
|
2026-06-01 05:49:08 +10:00
|
|
|
|
|
|
|
|
// 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, localUrl, 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).");
|
|
|
|
|
}
|
2026-06-01 05:09:20 +10:00
|
|
|
});
|
|
|
|
|
}
|
2026-05-28 02:54:17 +10:00
|
|
|
|
2026-06-01 05:09:20 +10:00
|
|
|
// 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>();
|
2026-06-01 05:49:08 +10:00
|
|
|
var rsaKeyManager = scope.ServiceProvider.GetRequiredService<RsaKeyManager>();
|
2026-06-01 05:09:20 +10:00
|
|
|
var client = scope.ServiceProvider.GetRequiredService<HttpClient>();
|
2026-06-01 05:49:08 +10:00
|
|
|
await SyncEndpoints.StartHeartbeatPingerAsync(peerCache, rsaKeyManager, client, token);
|
2026-06-01 05:09:20 +10:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.Run();
|
|
|
|
|
}
|
2026-05-28 02:54:17 +10:00
|
|
|
}
|