import { logger, MemoryPersistence, ReasonCode } from "./deps.ts"; import { Context } from "./context.ts"; import type { Handlers } from "./context.ts"; import type { ConnNext, Persistence, ShareName, Topic } from "./deps.ts"; import { handlePacket } from "./handlers/handlePacket.ts"; import { createConfiguration } from "./config.ts"; import type { Configuration, ConfigurationInput } from "./config.ts"; /** * Default preconnect handler that unconditionally permits all connections. * @param _conn - The connection. * @returns Always returns true */ const defaultPreconnect = ( _conn: ConnNext, ) => (true) as const; /** * Default authentication handler that unconditionally permits all connections. * @param {Context} _ctx - The connection context. * @param {string} _clientId - The client identifier. * @param {string} _username - The username provided by the client. * @param {Uint8Array} _password - The password provided by the client. * @returns {IsAuthenticatedResult} Always returns ReasonCode.success. */ const defaultIsAuthenticated = ( _ctx: Context, _clientId: string, _username: string, _password: Uint8Array, ) => ({ reasonCode: ReasonCode.success }) as const; const defaultProcessAuth = ( _ctx: Context, _clientId: string, _authMethod: string, _authData: Uint8Array, ) => ({ reasonCode: ReasonCode.badAuthenticationMethod, reasonString: "Authentication method not supported", }) as const; /** * Default authorization handler that unconditionally permits all publish operations. * @param {Context} _ctx - The connection context. * @param {Topic} _topic - The topic being accessed. * @returns {boolean} Always returns true. */ const defaultIsAuthorizedToPublish = (_ctx: Context, _topic: Topic) => (true) as const; /** * Default authorization handler that unconditionally permits all subscribe operations. * @param {Context} _ctx - The connection context. * @param {Topic} _topic - The topic being accessed. * @param {ShareName} _shareName - The share name being accessed when using shared subscriptions * @returns {boolean} Always returns true. */ const defaultIsAuthorizedToSubscribe = ( _ctx: Context, _topic: Topic, _shareName: ShareName, ) => (true) as const; /** * Configuration options for creating an MqttServer instance. */ export type MqttServerOptions = { /** Optional persistence layer implementation. Defaults to MemoryPersistence. */ persistence?: Persistence; /** Optional custom handlers for authentication and authorization. */ handlers?: Handlers; /** Optional configuration data */ configuration?: ConfigurationInput; }; /** * The MqttServer class provides an MQTT server with configurable persistence and * authentication/authorization handlers. * * The default handlers are: * - isAuthenticated: always returns ok * - isAuthorizedToPublish: always returns true * - isAuthorizedToSubscribe: always returns true * * To customize the handlers, pass in a Handlers object. * To customize the persistence, pass in a Persistence object. */ export class MqttServer { /** The registered authentication and authorization handlers. */ handlers: Handlers; /** The persistence layer used for storing sessions and messages. */ persistence: Persistence; /** The persistence layer used for storing sessions and messages. */ configuration: Configuration; /** * Initializes a new instance of the MqttServer. * * @param {MqttServerOptions} options - The configuration options for the server. */ constructor({ persistence, handlers, configuration, }: MqttServerOptions) { this.persistence = persistence || new MemoryPersistence(); this.handlers = { preconnect: handlers?.preconnect || defaultPreconnect, isAuthenticated: handlers?.isAuthenticated || defaultIsAuthenticated, processAuth: handlers?.processAuth || defaultProcessAuth, isAuthorizedToPublish: handlers?.isAuthorizedToPublish || defaultIsAuthorizedToPublish, isAuthorizedToSubscribe: handlers?.isAuthorizedToSubscribe || defaultIsAuthorizedToSubscribe, }; this.configuration = createConfiguration(configuration); } /** * Serve a new client connection. * @param conn - The socket connection to serve. * @returns {Promise} A promise that resolves when the connection is closed. */ async serve(conn: ConnNext): Promise { if (this.handlers.preconnect) { if (!await this.handlers.preconnect(conn)) { conn.close(); return; } } const ctx = new Context( this.configuration, this.persistence, conn, this.handlers, ); if (conn.remoteAddr?.transport === "tcp") { logger.debug("socket connected from", conn.remoteAddr.hostname); } try { for await (const packet of ctx.mqttConn) { logger.debug("next packet"); await handlePacket(ctx, packet); } } catch (err) { logger.debug("Error while serving:", err); } finally { logger.debug("done serving for", ctx.clientId); await ctx.close(); } } /** * Close the server and all active client connections. * @param {boolean} cleanUp - If true, clean up client sessions on close. */ async close(cleanUp: boolean = false): Promise { logger.debug("stopping mqttServer"); for (const [clientid, ctx] of Context.clientList) { logger.debug("closing session for clientid:", clientid); await ctx.close(); if (cleanUp) { await ctx.clean(); } } } }