Commit Graph
59 Commits
Author SHA1 Message Date
bbassie 97bab72eb9 Merge remote-tracking branch 'upstream/master' into fix/host-save-details-ownership 2026-09-13 17:10:00 +00:00
bbassieandClaude Opus 5 70c823ae9f Correct misaligned instance names on forwarded log messages
The instance name on a log line is whatever the host sent, so a host can
attribute a line for one of its own instances to the wrong name. Compare
it with the name the controller has on record and replace it when they
differ.

Host connections can live for days, so the warning suppression expires
after five minutes instead of latching for the life of the connection,
and the bogus instance and misaligned name warnings share it so the two
together cannot spam the log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 17:09:53 +00:00
bbassie daa39fd7ec Merge remote-tracking branch 'upstream/master' into fix/instance-assign-failure 2026-09-13 14:43:43 +00:00
Cooldude2606 c355fc6888 Merge branch 'master' into fix/terminate-deleted-user-sessions 2026-09-13 15:18:25 +01:00
d32d857eb3 Re-apply debug dump ignore flag on control connection resume (#1035)
handleDebugDumpWsRequest sets clusterio_ignore_dump on the current
socket so the WsServer send wrapper does not echo the dumper's own
frames back into debugEvents. The flag was only set on the connector
"connect" event, but WsServerConnector.continue() installs a fresh
socket and emits "resume", not "connect", so a resumed session lost
the flag.

After a resume the next outgoing frame was echoed to debugEvents,
ws_dumper sent a DebugWsMessageEvent, the wrapper echoed that too,
and so on: infinite synchronous recursion ending in a RangeError:
Invalid string length from JSON.stringify in link.ts sendEvent. That
uncaught error made handleUnhandledErrors exit the controller.

Repro: admin sends DebugDumpWsRequest, the control connection drops
and resumes (e.g. a network blip), controller crashes on resume.

Set the flag on "resume" as well as "connect".

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-09-13 15:13:24 +01:00
bbassieandClaude Opus 5 cf4e85c6ee Guard forwarded log messages by instance assignment
Log messages share the root cause with the save details updates fixed in
the previous commit: the controller trusted host supplied instance
fields. LogMessageEvent only declares level and message in its schema,
but the Ajv instance is built without removeAdditional and the schema
does not set additionalProperties, so instance_id and instance_name sent
by a host survive validation and pass through fromJSON untouched. A host
could tag a log line with an instance assigned to another host and have
the controller store and serve it as that instance's output.

Lift the assignment check out of handleInstanceSaveDetailsUpdatesEvent
into a private method and use it from both handlers.

Strip the instance fields instead of dropping the line. A host racing a
reassignment does emit instance log lines for an instance the controller
has already moved, and dropping those loses real output exactly when a
migration is going wrong. The line is still genuine output from the host,
so it is recorded as a host level message, which grants the host nothing
it could not already log directly. Warn once per connection so a
misbehaving host cannot flood the log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 14:11:23 +00:00
bbassieandClaude Opus 5 1da7c1da67 Terminate deleted user sessions immediately
Drop the special case that deferred terminating the connection issuing
the delete, so deleting yourself behaves the same as revoke-token and
every session of the deleted user is closed before the handler returns.

Set tokenValidAfter to the current time in createUser instead of 0.
Deleting a user and recreating it under the same name left tokens issued
before the deletion valid, as the check is iat < tokenValidAfter. Doing
it in createUser covers every path that makes a user. A token signed in
the same second as creation is still accepted, so create-admin and the
other production paths sign after the user exists and are unaffected.

The integration harness signed the control token at import time, before
bootstrap create-admin created the user. Sign it in the root before hook
instead and reuse it in link.js.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 13:31:09 +00:00
bbassieandClaude Opus 5 fb71ee7282 Terminate control connections of deleted users
ControlConnection keeps the User object it authenticated with and checks
permissions against it. Deleting the user removed the record but left
its sessions open, so a deleted user kept all its permissions until the
WebSocket closed.

Repro: create "victim" with role 0 and connect as it. Another admin
sends UserDeleteRequest("victim"). Victim can still send
UserCreateRequest("mallory") and UserUpdateRolesRequest("mallory", [0]).

Terminate the deleted user's connections like revoke-token does. When
a user deletes itself, defer terminating the requesting connection so
the response is sent first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 13:30:45 +00:00
Cooldude2606 3c692c4321 Merge branch 'master' into fix/instance-assign-failure 2026-09-13 14:04:45 +01:00
BastiaanandClaude Opus 5 5b63fd27fe Fix listener leaks when a control connection closes (#1027)
The close handler removed the log subscription transport from the global
lib logger, but updateLogSubscriptions adds it to the controller's
clusterLogger. Every control connection that subscribed to logs stayed
attached to clusterLogger after closing, keeping the connection alive and
running every later cluster log line through its filter.

handleDebugDumpWsRequest added a new debugEvents listener on each call
and overwrote ws_dumper, so the close handler only removed the last one.
Repeated requests stacked listeners, triggered MaxListenersExceededWarning
after 11 calls, and left the earlier listeners behind. Ignore the request
if the connection is already dumping.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 13:44:18 +01:00
bbassieandClaude Opus 4.8 5b769adcc5 Reject host save updates for foreign instances
handleInstanceSaveDetailsUpdatesEvent trusted the instanceId in the
event and in each SaveDetails without checking the instance was assigned
to the sending host. InstanceSaveDetailsUpdatesEvent has host in its src
list and validateIngress only checks the source host id, not the
payload, so any authenticated host could corrupt another instance's
controller-side save records.

Repro: a host owning nothing sends the event with a SaveDetails for
instance 1002 (on another host) to inject a bogus save, or sends empty
updates with instanceId 1002 to make the delete-missing sweep wipe that
instance's save list.

Guard both paths: bail if event.instanceId names an instance not
assigned to this host, and drop the whole event if any update is for an
instance not assigned here. Matches handleInstanceStatusChangedEvent's
bogus-update warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 02:39:12 +00:00
bbassieandClaude Opus 5 fbdd647168 Keep instances assignable after a failed assign or create
assignInstance sets instance.assigned_host to the new host before
sending InstanceAssignInternalRequest. If the host fails that request
the controller kept the new host id while the host never added the
instance. Assigning to the same host again then returned early as a
no-op. Repro: make the host's instances directory read-only, assign
instance 3001 to host 10 (EACCES on mkdir), make it writable, assign
again (ok, nothing happens), start (instance does not exist on host).
Now a failed send leaves the instance unassigned and rethrows.

InstanceCreateRequest applied instance.assigned_host from the request
config, creating an instance assigned to a host that never heard of
it. Repro: InstanceCreateRequest({"instance.id": 4001, "instance.name":
"c1", "instance.assigned_host": 10}). Reject a non-null value with the
same error the config set requests use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 01:36:54 +00:00
bbassieandClaude Fable 5.1 87aa7b3b4c Require a token to scrape /metrics
The metrics endpoint fans out a gather to every connected host, so
anyone who could reach the controller could read cluster statistics and
put load on all hosts at will. Scraping now needs a user token holding
the new core.controller.metrics permission. The token is accepted via
Authorization: Bearer as well as X-Access-Token because Prometheus can
only send the former. Concurrent scrapes share one gather so hosts see
at most one metrics request at a time.

The three connection gauges in controller.ts now no-op before the
controller exists, like the other gauges there, so the endpoint can be
tested against the mock controller.

Closes #1009

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 22:28:43 +00:00
bbassie db84455c34 Merge remote-tracking branch 'upstream/master' into refactor/plugin-hooks-v2
# Conflicts:
#	packages/lib/src/data/messages_plugin.ts
#	packages/web_ui/src/components/PluginViewPage.tsx
#	test/lib/plugin.js
2026-09-12 18:07:12 +00:00
BastiaanandClaude Fable 5.1 1a10b38743 Only report a missing plugin web build when one is expected (#1013)
Plugins with only host, instance or ctl code have no reason to ship a
webpack build, yet the plugin list showed "Missing dist/web/manifest.json"
as an error for them and the controller logged a warning on every start.

Decide whether a build is expected from the plugin declaration, using the
same rule the create tool uses to decide when to generate one: a web or
controller entrypoint, or any config fields. Plugins outside that rule get
no bundle path and no error, the web UI skips loading them, and the plugin
pages show them as having no web module instead of an error or a prompt to
reload.

Fixes #1004

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:59:16 +01:00
7122cbccf8 Add typed hook system for plugins
Replace invokeHook and the per plugin on* methods with AsyncHook
collections on the controller, host, instance, clusterioctl and web
control objects. Plugin entrypoints export a default function that
receives a load context and attaches handlers to the hooks it needs.
Class exports still load through fromContext with a deprecation
warning. The create templates and docs use the entrypoint function.

This is clusterio/clusterio#921 recreated on top of master after the
ESM migration and the PluginDeclaration permissions change.

Closes #730

Co-Authored-By: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 22:49:09 +00:00
Hornwitser 5dcaca4d0b Migrate to ECMAScript Modules
Migrate the usage of CommonJS specific code and imports to ESM.  Roughly
summarised this:
- Changes all require() calls to import statement or expressions.
- Changes usage of module.exports to export statements.
- Replaces require.resolve with import.meta.resolve.
- Replaces conditional imports with conditional subpath imports.
- Replaces module === require.main with import.meta.main.
- Adds the full filename with extention to all relative imports.
- Changes all usage of __dirname and __filename to their import.meta
  equivalents.
- Sets "type": "module" in package.json files.
- Removes usage of "use strict".
- Adds .ts an an alias of .js for the webpack build.

Due to differences in how require and import resolves files the plugin
entrypoints will have to include the filename and extension after this
migration in order to work.
2026-09-11 22:43:57 +02:00
Hornwitser 511b8b7d97 Remove global_chat dependency in restart test
Use a fictional restart_test mock plugin added as a before step instead
of global_chat for the .checkRestartRequired() test.  Prevents the test
from failing if the integration tests are not also run.

Also fixes the fixture not accounting for setting load_plugin causing
restartRequired to be set to true due to the integration tests having
already set it to the same value.
2026-09-11 19:51:16 +02:00
Hornwitser 30083b96c1 Add packagePath to plugin info
Provide the absolute path to the package.json file in the resolved
plugin info to simplify the logic around loading files from it.
2026-09-11 19:51:15 +02:00
bbassieandClaude Fable 5 930223019e Add serveWeb test for webRoot and staticRoot substitution
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 10:54:25 +00:00
bbassieandClaude Fable 5 4c252a0763 Add tests for stripStaticPrefix and /api/plugins
The codecov/patch check flagged the new routes code as uncovered. The
unit tests never hit /api/plugins, so cover the static/ stripping and the
manifest error paths directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 10:48:54 +00:00
Loi NguyenandLoi Nguyen 08651b5917 fix: prevent restart downgrades (#951)
Co-authored-by: Loi Nguyen <1948922+lntutor@users.noreply.github.com>
2026-07-26 12:20:22 +01:00
Hornwitser 2e48b728dd Remove fs-extra and klaw (#882)
These packages are obsoleted by the promises API to the built-in Node fs
module.  The fs-extra package is also causing problems with broken APIs
that don't behave the same as Node's fs module, and throwing errors
without code properties on them.
2026-04-09 22:53:13 +02:00
Hornwitser 66c066616b Add test for Controller.factorioVersions failure
Check that errors from fetchFactorioVersions is correctly handled in the
fetch method provided by the Controller class.
2026-03-10 02:14:44 +01:00
Hornwitser b0595a6502 Fix identical updatedAtMs values in datastore
The assumption with the subscription system is that new updates will
always increment the updatedAtMs value to something greater than what
has been seen before.  This might not happen if the system clock is
adjusted, or updates are sent so fast that Date.now() returns the same
value.

Fix these edge cases so that the timestamp is always incremented.
2026-03-09 17:41:11 +01:00
Hornwitser 05b6bf8c04 Replace remaining usage of phin (#862)
Replace the remaining places in the codebase using phin with fetch.
With the exception of HttpCloser which uses the http/https request
method instead.
2026-03-06 15:27:12 +01:00
Cooldude2606andHornwitser 2235294c59 Refactor user and instance manager (#837)
* Refactor UserManager to use Datastore

* Rename lib.User to UserDetails also adds to/fromUserDetails

* Fix existing tests

* Add user class tests

* Add tests for finaliseUsers

* Move HostDetails to own file with tests

* Move InstanceDetails to its own file with tests

* Rename HostInfo and add tests

* Rename InstanceInfo and add tests

- Also changes the names of the host properties related to instances
- Also removes the need to specify the location of InstanceInfo

* Make json round trip test validate schema

* Add InstanceManager

* Fix lint errors

* Extra tests

* Add final test coverage

* Add one last test to InstanceManager

* Response to comments

* Use generator rather than return anonumous generator

Co-authored-by: Hornwitser <github@hornwitser.no>

* Fix lint issue with generator

---------

Co-authored-by: Hornwitser <github@hornwitser.no>
2026-03-01 23:04:37 +00:00
Cooldude2606 567a27f0c4 Fix migration tests 2026-02-01 22:23:00 +00:00
Cooldude2606 6b0dd4e9cf Add tests for new api route 2026-01-21 23:47:47 +00:00
Cooldude2606 d52931a326 Better handling of mod dependencies (#801)
* Add ModDependency and version types

* Add warnings for mod dependencies

* Add warning for wrong factorio version

* Add fix issues button

* Add automatic dependency resolution and download

* Add abilty to check for updates

* Add extra test coverage

* One extra test

* Fix copy paste error
2025-11-07 19:22:17 +00:00
Cooldude2606 73cc019d2f Sets restart required when a plugin is updated (#794)
* Template type for load plugin config

* Add plugin update checking

* Add restart icon to instance list

* Add tests

* Log package name when failing to read package json
2025-11-01 16:11:11 +00:00
Cooldude2606 df1edec623 Add role subscriptions and data model 2025-06-14 00:28:03 +01:00
Cooldude2606 a5bf8d92d9 Separate roles to their own file 2025-06-13 22:24:56 +01:00
Cooldude2606 63e30dd0ee Add missing migrations for system info and tests (#766)
* Add missing migration for SystemInfo

* Add missing migration tests
2025-05-21 17:07:24 +01:00
Cooldude2606 17db53403a Move filepath into config class 2025-05-12 21:30:48 +01:00
Cooldude2606 a9def876e8 Remove phin without introducing new dependencies (#755) 2025-04-15 19:09:25 +01:00
Cooldude2606 d80385c947 Add test for user merging 2024-11-22 04:06:34 -08:00
Cooldude2606 bdf02a5ea2 Made user ids case insensitive 2024-11-22 04:06:34 -08:00
Hornwitser 34da073800 Fix setting instance config fields not being saved
If only the config of an instance is updated the dirty flag of the
instance is not set, which results in changes being discarded when the
controller shuts down afterwards.

Fix by always triggering the update logic when an instance field is
changed.
2024-10-28 19:30:03 +01:00
Cooldude2606 2c2521a688 Add error tests for sendEvent and sendRequest (#638) 2024-06-07 15:46:49 +01:00
Hornwitser 8865509011 Delete player statistics when deleting instances
Clear the statistics stored per user for the instance when an instance
is deleted.
2024-02-08 19:06:21 +01:00
Hornwitser 8c4d2db192 Move TypeScript Node.js build to dist/node
Use a separate folder for the TypeScript build for Node.js in the dist
folder.  This makes each type of build consistently have its own output
folder instead of being interleaved with the Node.js build.
2024-02-08 03:20:26 +01:00
Hornwitser c53781d16c Add time unit suffixes to variables (#561)
Add suffixes like Ms and Ns to variables whos time unit is not in
seconds.  This is aligning with a new code style of putting units
on variables that are not in SI base units.
2024-01-20 13:26:55 +01:00
Hornwitser 5f25aad273 Use X-Forwarded-For header on trusted proxies
If the controller is behind a proxy then the socket.remoteAddress will
be that of the proxy instead of the end client that connected.  In this
case the X-Forwarded-For header will contain the address of the end
client, but this should only be relied upon if the party connecting is a
trusted proxy, otherwise a malicious user could spoof the IP address
that is logged by providing a forged X-Forwarded-For header.

Implement a trusted proxy list consisting of a configured list of IP
addresses and/or CIDR blocks and use the last address[1] in the
X-Forwarded-For header if the remoteAddress address of the connecting
party is in the trusted proxy list.

[1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
2024-01-16 06:21:48 +01:00
Hornwitser e74065a493 Remove config groups
Refactor away the config group concept and simplyfy the way config
fields are defined.  Fields are defined directly as properties of the
static field fieldDefinitions on the class itself with a separate
interface to declare the types of fields.

This makes it possible to easily make accessing the config fields type
checked in a way that lets plugins extend the interfaces with their own
config options by adding additional fields to the interface via
declation merging.
2024-01-09 02:19:07 +01:00
Hornwitser 001db4befe Refactor config to JSON data class pattern
Refactor the Config and ConfigGroup classes to match the JSON data class
pattern with toJSON, toRemote and fromJSON methods replacing the load,
init, and serialize methods.  Since fromJSON requires extra information
the messages structures passes around the plain JSON representation of
the configs instead of serialising instances of the config classes
automatically.
2024-01-06 19:53:32 +01:00
Hornwitser 27af142a87 Remove silly support for async initial_value
This complicates the handling of configs for no good reason.
2024-01-06 19:49:35 +01:00
Hornwitser ede150d80f Add roundtrip test for InstanceInfo 2024-01-06 19:32:42 +01:00
Hornwitser e453a80155 Refactor User and Role into data classes (#531)
Use the JSON data class pattern for User, Role, and PlayerStats and move
them into lib/src/data.  This means that User is purely data now and
can't reference role objects directly.  To make it possible easily check
permissions attached to roles and store data only visible on the
controller on user objects a ControllerUser type is introduced which is
linked with the UserManager and can therefore reference role objects
directly.

Resolves #525.
2023-12-02 01:41:43 +01:00
Hornwitser 808eda0513 Move base plugins to their respective packages
Move BaseControllerPlugin to controller/src/BaseControllerPlugin.ts
Move BaseInstancePlugin to host/src/BaseInstancePlugin.ts
Move BaseControlPlugin to ctl/src/BaseCtlPlugin.ts
Move BaseWebPlugin to web_ui/src/BaseWebPlugin.ts

This solves the circular package dependency problem of the base plugins
needing types that are available only in the dependencies to lib.
2023-11-25 18:41:43 +01:00