Files
fastway-plugin-webui/fw_plugin_api.pas
Ken Johnson dcff1f5dd2
All checks were successful
Build & Release Plugin / build (push) Successful in 14s
v0.1.6: Rebuild with updated fw_plugin_api.pas (SessionSetNodeStatus)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 20:18:42 -07:00

491 lines
20 KiB
ObjectPascal

unit fw_plugin_api;
{
Fastway BBS Plugin API
BACKWARD COMPATIBILITY POLICY:
The server accepts plugins compiled against API versions from
FW_MIN_PLUGIN_API_VERSION through FW_PLUGIN_API_VERSION (currently v1).
This gives plugin authors a 2-version grace period to update.
When the API version is bumped:
- New interfaces/methods are ADDED, never removed or changed
- Plugins compiled against older versions continue to work — they simply
don't see the new features
- The server logs an info message when loading an older-version plugin
- FW_MIN_PLUGIN_API_VERSION = 1 (pre-production, all versions reset)
VERSION HISTORY:
v1 — Initial plugin API (Phase 2)
v2 — Added IFWMenuPlugin, IFWConfigPlugin, IFWScheduledPlugin (Phase 3)
v3 — Added IFWServicePlugin, ACL methods, WebSocket methods (Phase 7-9)
v4 — Added IFWProtocolDetector, IMailerIO, detection signatures (Phase 6)
}
{$mode objfpc}{$H+}
{$INTERFACES CORBA}
interface
uses
Classes, SysUtils, fpjson;
const
FW_PLUGIN_API_VERSION = 1;
FW_MIN_PLUGIN_API_VERSION = 1; { Oldest API version we still accept (N-2 backward compat) }
{ Protocol detector result codes (returned by HandleInbound) }
PDR_COMPLETED = 0; { Mailer session completed — transport should disconnect }
PDR_IEMSI = 1; { IEMSI auto-login — transport should BBS with user data in AResultJSON }
PDR_ABORT_TO_BBS = 2; { User hit ESC — transport should go to normal BBS login }
PDR_FAILED = 3; { Session failed — transport should disconnect }
type
{ Plugin permission flags }
TFWPermission = (
fpNetworkListen, // Open TCP ports
fpNetworkConnect, // Make outbound connections
fpDatabaseRead, // Read from database
fpDatabaseWrite, // Write to database
fpDatabaseCreateTables, // Create new tables
fpFilesystemRead, // Read files
fpFilesystemWrite, // Write files
fpSystemExec, // Execute system commands (CRITICAL)
fpEventsSubscribe, // Listen to events
fpEventsFire, // Emit events
fpRoutesRegister, // Add HTTP routes
fpAdminMenu, // Add admin menu items
fpConfigRead, // Read configuration
fpConfigWrite, // Modify configuration
fpUsersRead, // Read user data
fpUsersWrite, // Modify user data
fpSessionsRead, // Read session data
fpSessionsWrite, // Modify sessions (kick users, etc.)
fpPluginCall // Call other plugins
);
TFWPermissionSet = set of TFWPermission;
{ Event callback type }
TFWEventCallback = procedure(const AEventType: string; AData: TJSONObject) of object;
{ Route handler type }
TFWRouteHandler = procedure(const AMethod, APath: string; ARequestBody: TJSONObject;
out AResponseCode: Integer; out AResponseBody: TJSONObject) of object;
{ Admin menu item record }
TFWAdminMenuItem = record
Title: string;
URL: string;
Icon: string;
Position: Integer;
PluginName: string; { Group name for sidebar section }
OwnerPlugin: string; { Actual plugin name for removal }
end;
{ Scheduled task info }
TFWScheduledTask = record
Name: string;
IntervalSeconds: Integer;
LastRun: TDateTime;
end;
{ Plugin target environment }
TFWPluginTarget = (
ptServer, // Runs on primary server only
ptClient, // Runs on thin client only
ptBoth // Runs on either
);
{ Plugin capability flags - used to query sub-interfaces }
TFWPluginCapability = (
pcProtocol, // IFWProtocolPlugin — telnet, SSH, modem listeners
pcRoutes, // IFWRoutePlugin — REST API endpoints
pcAdmin, // IFWAdminPlugin — web admin pages/menus
pcDatabase, // IFWDatabasePlugin — plugin DB tables
pcEvents, // IFWEventPlugin — event subscriptions
pcMenu, // IFWMenuPlugin — BBS menu commands
pcConfig, // IFWConfigPlugin — configuration management
pcScheduled, // IFWScheduledPlugin — periodic tasks
pcService, // IFWServicePlugin — inter-plugin callable functions
pcProtocolDetector // IFWProtocolDetector — registers detection signatures with transports
);
TFWPluginCapabilities = set of TFWPluginCapability;
{ ---- IMailerIO — Transport-agnostic I/O interface ----
Provides byte-level I/O for mailer protocols (EMSI, Wazoo, ZModem, etc.).
Concrete implementations wrap serial ports (TSerialMailerIO in modem.pp)
or TCP sockets (TSocketMailerIO in telnet.pp). Protocol code works with
either transport through this interface. }
IMailerIO = interface
['{9A5F7E3D-B1C4-4D2E-8F6A-1C3D5E7F9A0B}']
function SendBytes(const ABuf: PByte; ALen: Integer): Boolean;
function SendString(const S: string): Boolean;
function RecvByte(ATimeoutMs: Integer; out B: Byte): Boolean;
function RecvBytes(ATimeoutMs: Integer; ABuf: PByte; ALen: Integer): Integer;
function RecvString(ATimeoutMs: Integer; AMaxLen: Integer): string;
function IsConnected: Boolean;
procedure Flush;
procedure PurgeInput;
end;
{ Forward declarations }
IFWPluginHost = interface;
IFWPlugin = interface;
IFWProtocolPlugin = interface;
IFWRoutePlugin = interface;
IFWAdminPlugin = interface;
IFWDatabasePlugin = interface;
IFWEventPlugin = interface;
IFWServicePlugin = interface;
IFWProtocolDetector = interface;
IFWScheduledPlugin = interface;
{ IFWPlugin - base interface every plugin must implement }
IFWPlugin = interface
['{E1A2B3C4-D5E6-F7A8-B9C0-D1E2F3A4B5C6}']
function GetName: string;
function GetVersion: string;
function GetDescription: string;
function GetAuthor: string;
function GetDependencies: TStringArray;
function GetPermissions: TFWPermissionSet;
function GetCapabilities: TFWPluginCapabilities;
function GetTarget: TFWPluginTarget;
function AsProtocol: IFWProtocolPlugin;
function AsRoute: IFWRoutePlugin;
function AsAdmin: IFWAdminPlugin;
function AsDBPlugin: IFWDatabasePlugin;
function AsEvent: IFWEventPlugin;
function AsService: IFWServicePlugin;
function AsProtocolDetector: IFWProtocolDetector;
function AsScheduled: IFWScheduledPlugin;
function Initialize(AHost: IFWPluginHost): Boolean;
procedure Finalize;
end;
{ IFWPluginHost - what the server provides to every plugin }
IFWPluginHost = interface
['{F2B3C4D5-E6F7-A8B9-C0D1-E2F3A4B5C6D7}']
// Logging (prefixed with [plugin:name])
procedure LogInfo(const AMsg: string);
procedure LogWarning(const AMsg: string);
procedure LogError(const AMsg: string);
procedure LogDebug(const AMsg: string);
// Structured logging to system_logs (visible in web admin)
procedure SystemLog(const ALevel, ACategory, AMessage: string);
procedure SystemLog(const ALevel, ACategory, AMessage, ADetails: string);
// Database (plugin gets prefixed tables: p_<name>_*)
function DBQuery(const ASQL: string): TJSONArray;
function DBExec(const ASQL: string): Boolean;
function DBExecParams(const ASQL: string; AParams: TJSONArray): Boolean;
procedure DBCreateTable(const ASQL: string);
// Configuration
function ConfigGet(const AKey: string; const ADefault: string = ''): string;
procedure ConfigSet(const AKey, AValue: string);
function ConfigGetSection(const ASection: string): TJSONObject;
// Events
procedure EventSubscribe(const AEventType: string; ACallback: TFWEventCallback);
procedure EventUnsubscribeCallback(ACallback: TFWEventCallback);
procedure EventFire(const AEventType: string; AData: TJSONObject);
// Routes
procedure RouteRegister(const AMethod, APath: string; AHandler: TFWRouteHandler);
// Admin
// AGroup: sidebar section name to group under (default = plugin name)
procedure AdminAddMenu(const ATitle, AURL, AIcon: string; APosition: Integer; const AGroup: string);
procedure AdminRegisterPage(const APath, AHTMLFile: string);
// Sessions (read-only for most plugins)
function SessionGetAll: TJSONArray;
function SessionGetByNode(ANode: Integer): TJSONObject;
function SessionGetCount: Integer;
// Inter-plugin
function PluginCall(const APluginName, AFunctionName: string; AArgs: TJSONObject): TJSONObject;
function PluginGetService(const APluginName: string): IFWPlugin;
// Security
function GetCurrentPermissions: TFWPermissionSet;
function HasPermission(APerm: TFWPermission): Boolean;
// User data access (permission-gated)
function UserGet(AUserID: Integer): TJSONObject;
function UserAuthenticate(const AUsername, APassword: string): TJSONObject;
function UserCreate(const AUsername, APassword, ARealName, AEmail: string): Integer;
// ACL — flag-based access control
function ACLGetUserFlags(AUserID: Integer): string;
function ACLCheckAccess(AUserID: Integer; AResourceSecurity: Integer;
const ARequiredFlags: string): Boolean;
function ACLCheckAccessByFlags(const AUserFlags: string;
AUserIsAdmin: Boolean; AUserSecurityLevel: Integer;
AResourceSecurity: Integer; const ARequiredFlags: string): Boolean;
procedure ACLRegisterFlag(const AFlagName, ADescription, ACategory: string);
// Server info
function GetServerVersion: string;
function GetServerName: string;
function GetUptime: Int64;
// Session management (for protocol plugins)
function SessionAllocateNode(const AProtocol: string): Integer;
procedure SessionReleaseNode(ANodeID: Integer);
procedure SessionSetNodeUser(ANodeID: Integer; const AUsername: string; AUserID: Integer);
procedure SessionSetNodeAddress(ANodeID: Integer; const AAddress: string);
procedure SessionSetNodeStatus(ANodeID: Integer; const AStatus: string);
// Protocol detection (thin client only — stubs on primary server)
// Transport plugins call these to discover and dispatch to protocol detectors.
function GetDetectionSignatureCount: Integer;
procedure GetDetectionSignature(AIndex: Integer; out AName: string;
out APattern: string; out AProbeData: string;
out AProbeIntervalMs: Integer; out APriority: Integer);
function DispatchInboundProtocol(AIO: IMailerIO; const ASignatureName: string;
const ABufferedData: string; out AResultJSON: string): Integer;
function DispatchOutboundProtocol(AIO: IMailerIO; const AProtocol: string;
const AParamsJSON: string): Boolean;
// WebSocket command push (primary server → thin clients)
// Pushes a command JSON to the first connected thin client that supports
// the given protocol. No-op if WebSocket server is not running or no
// clients support the protocol. Thin client stub does nothing.
procedure WSPushCommand(const AProtocol: string; ACommand: TJSONObject);
// WebSocket state — check if WS client is connected to primary.
// Used by protocol plugins to skip REST polling when WS is active.
// Returns True on thin client when WSClient.Connected, False on primary.
function IsWSConnected: Boolean;
// WebSocket client ID — the integer ID assigned by the primary server.
// Used by protocol plugins to pass client_id when starting BBS sessions
// so door I/O can be routed back to the correct thin client.
// Returns 0 on primary server (not applicable).
function GetWSClientID: Integer;
// WebSocket targeted delivery — send a JSON command to a specific client by ID.
// Used for door session routing (primary → specific thin client).
// No-op on thin client.
procedure WSSendToClient(AClientID: Integer; ACommand: TJSONObject);
// WebSocket client discovery — get list of connected clients supporting a protocol.
// Returns JSON array of [{id, client_name, active_nodes, max_nodes}].
// Caller owns the returned array. Returns empty array on thin client.
function WSGetClientsByProtocol(const AProtocol: string): TJSONArray;
end;
{ IFWProtocolPlugin - for telnet, SSH, modem, etc. }
IFWProtocolPlugin = interface
['{A3B4C5D6-E7F8-A9B0-C1D2-E3F4A5B6C7D8}']
function GetProtocolName: string;
function GetListenPort: Integer;
function GetListenAddress: string;
function Start: Boolean;
procedure Stop;
function GetConnectionCount: Integer;
function IsRunning: Boolean;
end;
{ IFWRoutePlugin - registers REST API routes }
IFWRoutePlugin = interface
['{B4C5D6E7-F8A9-B0C1-D2E3-F4A5B6C7D8E9}']
procedure RegisterRoutes;
procedure UnregisterRoutes;
end;
{ IFWAdminPlugin - adds web admin pages/menu items }
IFWAdminPlugin = interface
['{C5D6E7F8-A9B0-C1D2-E3F4-A5B6C7D8E9F0}']
procedure RegisterAdminPages;
function GetAdminMenuItems: TJSONArray;
end;
{ IFWDatabasePlugin - creates/migrates plugin DB tables }
IFWDatabasePlugin = interface
['{D6E7F8A9-B0C1-D2E3-F4A5-B6C7D8E9F0A1}']
function GetSchemaVersion: Integer;
procedure CreateSchema;
procedure MigrateSchema(AFromVersion: Integer);
end;
{ IFWEventPlugin - subscribes to and fires events }
IFWEventPlugin = interface
['{E7F8A9B0-C1D2-E3F4-A5B6-C7D8E9F0A1B2}']
procedure RegisterEvents;
function GetSubscribedEvents: TStringArray;
end;
{ IFWMenuPlugin - adds BBS menu commands }
IFWMenuPlugin = interface
['{F8A9B0C1-D2E3-F4A5-B6C7-D8E9F0A1B2C3}']
function GetMenuCommands: TJSONArray;
end;
{ IFWConfigPlugin - registers config sections }
IFWConfigPlugin = interface
['{A9B0C1D2-E3F4-A5B6-C7D8-E9F0A1B2C3D4}']
function GetConfigDefaults: TJSONObject;
procedure OnConfigChanged(const AKey, AValue: string);
end;
{ IFWScheduledPlugin - registers periodic tasks }
IFWScheduledPlugin = interface
['{B0C1D2E3-F4A5-B6C7-D8E9-F0A1B2C3D4E5}']
function GetScheduledTasks: TJSONArray;
procedure RunTask(const ATaskName: string);
end;
{ IFWServicePlugin - exports callable services for other plugins }
IFWServicePlugin = interface
['{C1D2E3F4-A5B6-C7D8-E9F0-A1B2C3D4E5F6}']
function GetExportedFunctions: TStringArray;
function CallFunction(const AName: string; AArgs: TJSONObject): TJSONObject;
end;
{ IFWProtocolDetector - registers detection signatures with transport plugins.
Session protocol plugins (EMSI, Wazoo, etc.) implement this interface
so transports (modem, telnet) can discover what to watch for and
dispatch matching connections to the right handler.
Detection signatures specify:
Name — unique identifier (e.g., 'emsi_inq', 'yoohoo')
Pattern — byte sequence to watch for (may contain binary)
ProbeData — data to send periodically during detection (empty = passive)
ProbeInterval — ms between probe sends (0 = don't send)
Priority — higher = check first
HandleInbound returns a PDR_* result code:
PDR_COMPLETED — mailer session done, transport disconnects
PDR_IEMSI — IEMSI auto-login, AResultJSON has user data
PDR_ABORT_TO_BBS — user hit ESC, transport goes to normal BBS login
PDR_FAILED — session failed }
IFWProtocolDetector = interface
['{D2E3F4A5-B6C7-D8E9-F0A1-B2C3D4E5F6A7}']
function GetDetectionSignatureCount: Integer;
procedure GetDetectionSignature(AIndex: Integer; out AName: string;
out APattern: string; out AProbeData: string;
out AProbeIntervalMs: Integer; out APriority: Integer);
function HandleInbound(AIO: IMailerIO; const AMatchedName: string;
const ABufferedData: string; out AResultJSON: string): Integer;
function HandleOutbound(AIO: IMailerIO; const AProtocol: string;
const AParamsJSON: string): Boolean;
end;
{ Native plugin library exports }
TFWPluginAPIVersionFunc = function: Integer; cdecl;
TFWPluginCreateFunc = function(AHost: IFWPluginHost): IFWPlugin; cdecl;
TFWPluginDestroyProc = procedure(APlugin: IFWPlugin); cdecl;
const
{ Export function names for native plugins }
FW_EXPORT_API_VERSION = 'FWPluginAPIVersion';
FW_EXPORT_CREATE = 'FWPluginCreate';
FW_EXPORT_DESTROY = 'FWPluginDestroy';
{ Standard event types }
FW_EVENT_SERVER_START = 'server.start';
FW_EVENT_SERVER_STOP = 'server.stop';
FW_EVENT_USER_LOGIN = 'user.login';
FW_EVENT_USER_LOGOUT = 'user.logout';
FW_EVENT_USER_CREATE = 'user.create';
FW_EVENT_SESSION_START = 'session.start';
FW_EVENT_SESSION_END = 'session.end';
FW_EVENT_CONNECTION_NEW = 'connection.new';
FW_EVENT_CONNECTION_CLOSE = 'connection.close';
FW_EVENT_PLUGIN_LOADED = 'plugin.loaded';
FW_EVENT_PLUGIN_UNLOADED = 'plugin.unloaded';
FW_EVENT_CONFIG_CHANGED = 'config.changed';
{ Permission helper functions }
function PermissionToString(APerm: TFWPermission): string;
function StringToPermission(const AStr: string; out APerm: TFWPermission): Boolean;
function PermissionSetToJSON(APerms: TFWPermissionSet): TJSONArray;
function JSONToPermissionSet(AArr: TJSONArray): TFWPermissionSet;
function PermissionIsCritical(APerm: TFWPermission): Boolean;
implementation
function PermissionToString(APerm: TFWPermission): string;
begin
case APerm of
fpNetworkListen: Result := 'network.listen';
fpNetworkConnect: Result := 'network.connect';
fpDatabaseRead: Result := 'database.read';
fpDatabaseWrite: Result := 'database.write';
fpDatabaseCreateTables: Result := 'database.create_tables';
fpFilesystemRead: Result := 'filesystem.read';
fpFilesystemWrite: Result := 'filesystem.write';
fpSystemExec: Result := 'system.exec';
fpEventsSubscribe: Result := 'events.subscribe';
fpEventsFire: Result := 'events.fire';
fpRoutesRegister: Result := 'routes.register';
fpAdminMenu: Result := 'admin.menu';
fpConfigRead: Result := 'config.read';
fpConfigWrite: Result := 'config.write';
fpUsersRead: Result := 'users.read';
fpUsersWrite: Result := 'users.write';
fpSessionsRead: Result := 'sessions.read';
fpSessionsWrite: Result := 'sessions.write';
fpPluginCall: Result := 'plugin.call';
else
Result := 'unknown';
end;
end;
function StringToPermission(const AStr: string; out APerm: TFWPermission): Boolean;
begin
Result := True;
if AStr = 'network.listen' then APerm := fpNetworkListen
else if AStr = 'network.connect' then APerm := fpNetworkConnect
else if AStr = 'database.read' then APerm := fpDatabaseRead
else if AStr = 'database.write' then APerm := fpDatabaseWrite
else if AStr = 'database.create_tables' then APerm := fpDatabaseCreateTables
else if AStr = 'filesystem.read' then APerm := fpFilesystemRead
else if AStr = 'filesystem.write' then APerm := fpFilesystemWrite
else if AStr = 'system.exec' then APerm := fpSystemExec
else if AStr = 'events.subscribe' then APerm := fpEventsSubscribe
else if AStr = 'events.fire' then APerm := fpEventsFire
else if AStr = 'routes.register' then APerm := fpRoutesRegister
else if AStr = 'admin.menu' then APerm := fpAdminMenu
else if AStr = 'config.read' then APerm := fpConfigRead
else if AStr = 'config.write' then APerm := fpConfigWrite
else if AStr = 'users.read' then APerm := fpUsersRead
else if AStr = 'users.write' then APerm := fpUsersWrite
else if AStr = 'sessions.read' then APerm := fpSessionsRead
else if AStr = 'sessions.write' then APerm := fpSessionsWrite
else if AStr = 'plugin.call' then APerm := fpPluginCall
else Result := False;
end;
function PermissionSetToJSON(APerms: TFWPermissionSet): TJSONArray;
var
P: TFWPermission;
begin
Result := TJSONArray.Create;
for P := Low(TFWPermission) to High(TFWPermission) do
if P in APerms then
Result.Add(PermissionToString(P));
end;
function JSONToPermissionSet(AArr: TJSONArray): TFWPermissionSet;
var
I: Integer;
P: TFWPermission;
begin
Result := [];
if AArr = nil then Exit;
for I := 0 to AArr.Count - 1 do
if StringToPermission(AArr.Strings[I], P) then
Include(Result, P);
end;
function PermissionIsCritical(APerm: TFWPermission): Boolean;
begin
Result := APerm in [fpSystemExec, fpFilesystemWrite, fpUsersWrite, fpSessionsWrite];
end;
end.