Sidecar: auth token for the website-facing API

config.rs loads all runtime settings from an external sidecar.toml (path via
$UOLINK_CONFIG), with env-var overrides (UOLINK_WEB_TOKEN, UOLINK_WEB_BIND,
UOLINK_SHARD_BIND, UOLINK_DB_PATH). Nothing is compiled into the binary. On first
run the file is generated with a random 24-byte auth token, so the sidecar is
secured out of the box and the operator just copies the token to the website.

An axum middleware rejects any request to a non-/health route that does not
present the token, as Authorization: Bearer, X-Api-Key, or ?token= (the last so
browser WebSocket clients, which cannot set handshake headers, can authenticate).
The comparison is constant-time. An empty token disables auth and is only
tolerated on a loopback bind; binding to 0.0.0.0 with no token logs a warning.

Verified: /health open (200); /history 401 without a token, 401 with a wrong one,
200 with the right one via either Bearer or X-Api-Key; an authed shard query
falls through to 503 when no shard is connected; WS rejected (401) with a bad
?token= and upgraded (101) with the right one.

sidecar.toml is gitignored (holds the secret); sidecar.toml.example is committed
as the reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:56:55 -05:00
parent 59675ccdb3
commit 8e400daf26

View File

@@ -0,0 +1,98 @@
using System;
using System.Reflection;
using Server.Accounting;
using Server.Items;
using Server.Mobiles;
namespace Server.Custom
{
/// <summary>
/// Spawns a reachable, houseless player vendor next to the `tester` character (wttest) and
/// gives tester gold, so the PlayerVendorSale core patch can be exercised by a real in-game
/// purchase. The buyer must be a non-GM — IsOwner() treats any GameMaster+ as the owner of
/// every player vendor, so an Owner-level character can never buy.
///
/// Owner is a seed_000 character, so buyer (wttest) and owner (seed_000) are different
/// accounts — a clean cheat-detection example.
///
/// Test scaffolding. Never deployed. Spawns a mobile and hands out gold; run only on the
/// throwaway seeded world.
/// </summary>
public static class BridgeVendorTestProbe
{
private static readonly MethodInfo SetVendorItem = typeof(PlayerVendor).GetMethod(
"SetVendorItem",
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[] { typeof(Item), typeof(int), typeof(string) },
null);
public static void Initialize()
{
if (Config.Get("Bridge.VendorTestOnStart", false))
EventSink.ServerStarted += () => Timer.DelayCall(TimeSpan.FromSeconds(3.0), Run);
}
private static void Run()
{
try
{
var ownerAcct = Accounting.Accounts.GetAccount("seed_000") as Account;
var owner = ownerAcct == null ? null : ownerAcct[0];
var buyerAcct = Accounting.Accounts.GetAccount("wttest") as Account;
var buyer = buyerAcct == null ? null : buyerAcct[0] as PlayerMobile;
if (owner == null || buyer == null)
{
Console.WriteLine("[VendorTest] need seed_000 owner and wttest/tester; not found");
return;
}
// Remove any prior test vendor (e.g. one orphaned on the Internal map).
if (PlayerVendor.PlayerVendors != null)
{
var doomed = new System.Collections.Generic.List<PlayerVendor>();
foreach (var v in PlayerVendor.PlayerVendors)
if (v != null && !v.Deleted && v.ShopName == "Bridge Test Shop")
doomed.Add(v);
foreach (var v in doomed)
v.Delete();
}
// A confirmed-walkable spot where tester was already standing this session, so the
// vendor is on solid ground and reachable. Then force tester's logout location right
// next to it, so tester logs in beside the vendor regardless of where it was.
var map = Map.Trammel;
var loc = new Point3D(3533, 2546, 20); // vendor
buyer.LogoutMap = map;
buyer.LogoutLocation = new Point3D(3532, 2546, 20); // tester appears here
var vendor = new PlayerVendor(owner, null)
{
Name = "test vendor",
ShopName = "Bridge Test Shop"
};
vendor.MoveToWorld(loc, map);
var blade = new Longsword();
vendor.Backpack.DropItem(blade);
if (SetVendorItem != null)
SetVendorItem.Invoke(vendor, new object[] { blade, 100, "a test blade" });
// Make sure tester can afford it.
if (buyer.Backpack != null)
buyer.Backpack.DropItem(new Gold(1000));
Console.WriteLine(
"[VendorTest] spawned '{0}' (owner {1}/seed_000) next to {2}/wttest at {3} on {4}; test blade = 100 gold; gave tester 1000 gold",
vendor.ShopName, owner.Name, buyer.Name, loc, map);
}
catch (Exception ex)
{
Console.WriteLine("[VendorTest] FAILED: " + ex);
}
}
}
}