@octopwngmbh/comms
v0.0.2
Published
OctoPwn browser comms library (main-thread WebSocket + Worker protocol client)
Maintainers
Readme
OctoPwn Javascript Comms Library
Technical Debt
- The comms library functions are not named in a conventional way (eg. some functions that would trigger a change don't start with
onXXX) - The protobuf message types are not stored as enum but string comparison is used, this comes with performance penalty
- Since this protocol was never refactored, there are functions with overlapping utility (eg. sending a command to be executed on the server can be done with 4 different functions)
- Documentation needs drastic improvement
Communications
The frontend communicates with the backend (OctoPwn Core) over a websocket connection. The message exchange is based on protobuf messages.
- The communication MUST always start with authentication. In this repo/demo,
NONEauth (empty user/pass) is the primary supported/tested flow. Seeauthenticate(...). - After authentication the server will send a massive data packet (onRestoreSession funct, OctoSession packet) containin the descriptors of all current sessions, what clients / scanners / utils etc allowed, all credentials and targets and current sessions
All messages are encapsulated in OctoMessage protobuf message type. This is processed by the onIncomingRawMessage function, which just a small wrapper that deserializes the incoming message then dispatches it to onIncomingMessage function.
Architecture
On a high level the octopwn framework consists of the following components:
- Core
- Screen handler
- UI
Core Components
The Core is built of the following components:
- Targets: Targets are basically server addresses and some accompaning metadata that helps octopwn's non-ocre features to connect to the target hosts.
- Credentials: Credentials hold authentication material that is used by the authentication protocol. They are also used to hold user secrets obtained by the modules which cannot be directly used for authentication, but can be cracked or otherwise show an attack's success.
- Proxies: Basically targets but not to reach hosts rather to reach proxies to reach the hosts using those proxies. In this group we technically differentiate between a Proxy and a Proxychain, latter is merely a list of Proxy IDs that will be connected to when reaching the Target.
- Clients: Basically protocol clients which have commands that perform certain operations using the given protocol, like invoking the "list" command on an SMB client will list all files/folders on the current share. Clients can be used for the manual pentesting both for attacks, and information gathering, changing configuration on the servers the client connected to. A client is created using a Target and a Credential and optionally a Proxy. Of course depending on the client type eg. DNS credential can be omitted. IMPORTANT: There is one special client session, with sessionID of "0" (named MAIN). This is NOT a protocol client but the octopwn core. Anything that's directly octopwn related is done on this client, for example: if you want to create a new credential / target / proxy / client / scanner etc you'd need to invoke the appropriate create command on this session.
- Scanners: A scanner performs a simple task on a given protocol depending on the scanner type but does it on multiple targets in parallel. A scanner can be created without any parameters, only the scanner's name. Upon creation the UI will get a descriptor that shows all scanner parameters and their types. Scanners have a minimal set of commands that the UI can (or should) invoke, "scan" starts the scan "stop" stops the scan, "options" lists all scanner parameters with their current values "set" can be used to change a parameter. All parameters are string-serialized on the protocol level, lists are comma separated values. When a scan is started, the core contantly will send messages to the UI reporting progress/finish and streaming scannerdata.
- Servers: Similar to scanners in operation logic, server sessions can be created without parameters. Parameters can be set after creation, same commands just like scanners, except the "scan" command is replaced by the "serve" command.
- Attacks: Same as scanners in logic, but performs a given attack.
- Utils: These are special sessions, most of them are in developement. They can be created without parameters, they have a parameters table just like scanners but it's not always used (highly dependent on what the util does). Example: We have a util that can parse LSASS files.
Screen Handler
This is the communications handler between the UI and the Core. Every time the Core wants to send message to the UI, the screenhandler's appropriate function is called, and the othwer way any time the UI wants to send a command to the Core, the UI's own screenhandler implementation is called, which usually serializes the message and passes it to the Core's screen handler. Exception is if the UI is bundled in to the Core then there is only one screenhandler used, currently there is only one implementation of that which is the terminal-based CLI.
UI
The user interface component is independent from the Core but has its own screen handler to interface with it. While Octopwn comes with a UI, the UI is completely independent from octopwn, this way developers can write their own UI/screenhandler implementations.
Events that MUST be supported by any UI:
- Generic events:
- createNewSessionWindow
- newTargetAdded
- newTargetsAdded
- refreshTargets
- newCredentialAdded
- newCredentialsAdded
- refreshCredentials
- newProxyAdded
- newProxyAdded (of type chain)
- changeFavorite
- showServerMessage
- updateSyncModal
- stopSyncModal
- startSyncModal
- newTargetPortAdded
- paramsChanged
- Session-specific events:
- onConsoleMessage
- tabMessage
Command execution: Invoking a command from the frontend will result in a single data packet sent to the server, reply to the command can vary depending on what you invoked. All commands MUST specify a sessionID where the command will be dispatched to. Eg. if you create an SMB client, it will have a unique sessionID, if you want to invoke a command on that session, you'll have to pass the sessionID. Clarification: Every message type has a token field that can be used to track the invocations and singnal success/error/more data expected occurrences.
Fire and forget: Command invoked, the server will send back a packet either "OK" "ERR" "CONTINUE" type, but frontend don't care, these will be discarded.
Invoke and wait until response: Command invoked, server will will send back a packet either "OK" "ERR" "CONTINUE" type, but this time the javascript function doesn't return until the response has been received. If the response is "ERR" an exception will be thrown with the message the server sent in the "ERR" message. If the response type is "OK", the server MIGHT send additinal data (result) in the SAME PACKET regardles, both ERR and OK signals that there will be no more data sent regarding this invocation. "CONTINUE" packet is not expected in this case, but sometimes I mess up the code, it can be treated as an "OK" without data.
Invoke and communicate: Same as previous one, but if a "CONTINUE" is sent from the server, it means that the command has been acknowledged by the server and depending on the command either the server or the client (javascript) can send additional data using the "TOKEN" that's inside of the CONTINUE message.
commands can be invoked with
- runSimpleCommand -> Invoke and wait until response.
- sendMainSessionCommand -> older version of runSimpleCommand
- runCommand -> this should not be used, it's old. originally it was used for fire and forget type invocations for terminal console commands
- sendGenericCommand -> this is the "raw" remote command invocation method, it will return a tokenID and a queue where you can monitor for responses from the serer for that specific command
Framework-friendly (Worker) integration
If you don't want to implement a callback-heavy "UI object" (or deal with protobuf globals), use the Worker-based client:
// In an app, prefer the npm package import:
// import WorkerClient from "@octopwngmbh/comms";
// In this repo (local dev), you can import from src:
import WorkerClient from "./src/WorkerClient.mjs";
const client = new WorkerClient({
logger: (level, ...args) => console.log(level, ...args),
});
client.on("targetsAdded", (targets) => console.log("targetsAdded", targets));
client.on("sessionWindowCreated", (s) => console.log("sessionWindowCreated", s));
client.on("windowMessage", (m) => console.log("windowMessage", m));
await client.connect("ws://localhost:8080"); // WebSocket is on the main thread
await client.authenticate("NONE", "user", "");
// MAIN session id = 0
const res = await client.runSimpleCommand(0, "createscanner", ["NMAP", "debug"]);
console.log("result", res);
// Convenience helpers (protobuf messages, not CLI commands)
await client.addTarget("10.10.10.10", { hostname: "dc01.test.lab", dcip: "10.10.10.10", realm: "TEST.LAB" });
await client.addCredential("TEST", "Administrator", "Passw0rd!", "password");
await client.addProxy("SOCKS5", "127.0.0.1", 1080, { description: "local proxy" });- WebSocket on main thread:
WorkerClientowns the WS connection. - Protocol/protobuf in a background Worker:
CommsManagerruns insrc/WorkerCommsManager.mjs. - No UI callbacks required: you subscribe to events with
client.on(type, handler).
For an LLM-friendly, contract-first description of the protocol and UI event model, see LLM_PROTOCOL_GUIDE.md.
Stable event contract (public API)
Event names are stable and safe for frontend apps to depend on.
- Server
serverInfo:{ version, motd, allowedServers, allowedClients, allowedScanners, allowedUtils, allowedAttacks, screensettings, clientDescriptors, scannerDescriptors }- The Worker parses the descriptor blobs automatically and emits objectified fields:
screensettings: parsed object (plusscreensettingsRaw)clientDescriptors: parsed object (plusclientDescriptorsRaw)scannerDescriptors: parsed object (plusscannerDescriptorsRaw)
- If parsing ever fails (unexpected), the Worker emits
workerErrorand still emitsserverInfowith safe default objects. clientDescriptorscontains per-client-type creator metadata (used for building UI forms):MANDATORY: keys that must be present in the dictionary/object passed when creating a new clientATYPES: for a given auth type, which secret types are acceptableDEFAULTS: default values the server will assume if the key is omitted/left empty (e.g.PID=0,TIMEOUT=5,PORT=389)
- The Worker parses the descriptor blobs automatically and emits objectified fields:
Example: Creating a client using serverInfo.clientDescriptors
High-level flow:
- Pick a client type from
Object.keys(serverInfo.clientDescriptors)(e.g."SMB","LDAP","RDP", ...). - Build a dictionary/object that includes the keys listed in
descriptor.MANDATORY. - Fill missing fields with
descriptor.DEFAULTS(optional, but recommended for UI convenience). - Call
createclient_jsonon the MAIN session (cid=0):
/**
* @param {import("./src/WorkerClient.mjs").default} client
* @param {import("./src/WorkerClient.d.ts").CreateClientParams} cparams
*/
async function createClient(client, cparams) {
return await client.runSimpleCommand(0, "createclient_json", [JSON.stringify(cparams)]);
}
client.on("serverInfo", async (info) => {
const desc = info.clientDescriptors["LDAP"];
const defaults = desc.DEFAULTS || {};
/** @type {import("./src/WorkerClient.d.ts").CreateClientParams} */
const cparams = {
CTYPE: "LDAP",
ATYPE: "NTLM",
TID: 123,
CID: 456,
PORT: typeof defaults.PORT === "number" ? defaults.PORT : 389,
TIMEOUT: typeof defaults.TIMEOUT === "number" ? defaults.TIMEOUT : 5,
PID: typeof defaults.PID === "number" ? defaults.PID : 0,
SETTINGS: JSON.stringify({ /* client-specific settings */ }),
};
// UI should validate: all keys in desc.MANDATORY are present in cparams (non-null)
const res = await createClient(client, cparams);
console.log("createclient_json result", res);
});- Sessions
sessionWindowCreated:{ cid, cname, config, allMessagesSynced, startHidden }- The Worker parses these automatically and emits an objectified config:
config.params: parsed params arrayconfig.commandDescriptor: parsed command descriptor object
- The original JSON strings are preserved for debugging:
config.paramsRawconfig.commandDescriptorRaw
config.commandsis the list of documented commands for this session, intended for terminal autocomplete.config.ctype/atype/targetId/credentialId/proxyIdexist mainly for legacy CLIENT sessions and are not generally relied on anymore.
- The Worker parses these automatically and emits an objectified config:
Tip: you can still use src/sessionwindow.mjs helpers if you consume raw config from elsewhere, but normal Worker events already include the parsed fields.
Example: ParamManager built from sessionWindowCreated.config.params
config.params is already parsed (no JSON.parse needed). You can build a simple parameter helper like this:
class ParamManager {
/**
* @param {import("./src/WorkerClient.mjs").default} client
* @param {number} cid
* @param {Array<{Parameter:string, Value:string}>} params
*/
constructor(client, cid, params) {
this.client = client;
this.cid = cid;
this.params = params || [];
}
onParamsChanged(msg) {
// server sent a full update of params
this.params = msg;
}
get(name) {
const p = this.params.find(x => x.Parameter === name);
return p ? p.Value : undefined;
}
async set(name, value) {
// Protocol expects string-serialized values
await this.client.runSimpleCommand(this.cid, "setparam", [String(name), String(value), "false"]);
}
async clear(name) {
await this.client.runSimpleCommand(this.cid, "clearparam", [String(name)]);
}
getInfo(defaultValue = null) {
const p = this.params.find(x => x.Parameter === "__info");
return p && p.Description ? p.Description : defaultValue;
}
}
// Usage
client.on("sessionWindowCreated", ({ cid, config }) => {
const pm = new ParamManager(client, cid, config.params);
console.log("timeout:", pm.get("timeout"));
});
client.on("paramsChanged", ({ cid, msg }) => {
// If you keep a map cid -> ParamManager, call pm.onParamsChanged(msg)
console.log("paramsChanged", cid, msg);
});