From c295ac677e26c9e2288b5eef997d72d5e56fea77 Mon Sep 17 00:00:00 2001 From: c43721 Date: Thu, 23 May 2024 16:50:35 -0500 Subject: [PATCH] feat: add signal support, forward socket events, add additional documentation --- .github/workflows/publish.yml | 19 ++++++++ LICENSE | 2 +- README.md | 34 ++++++-------- examples/abort-controller.mjs | 17 +++++++ examples/write-to-console.mjs | 2 +- jsr.json | 5 +- src/index.ts | 1 + src/logReceiver.ts | 88 +++++++++++++++++++++++++++++------ src/parser.ts | 12 +---- src/types.ts | 24 ++++------ 10 files changed, 141 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 examples/abort-controller.mjs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..ba4135a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,19 @@ +name: Publish Package +on: + push: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Publish package + run: npx jsr publish diff --git a/LICENSE b/LICENSE index 4eefc0e..ffc89c2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 rcon.tf +Copyright (c) 2024 rcon.tf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8580f68..a0f5303 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,23 @@ # srcds-logs -Wrapper around srcds udp log streams for node +Wrapper around [Source Dedicated Server](https://developer.valvesoftware.com/wiki/Source_Dedicated_Server) UDP log streams for node. -# Usage +## Set Up -You can define a receiver and listen to messages published to the UDP stream +Your server must have logs enabled. To do so, run `log on` in your server console or through RCON. In order to receieve messages, you must also add a log address using `logaddress_add` command to add the URL or IP of your server's receiever. To delete a destination, use `logaddress_del`. -```ts -import { LogReceiver } from "srcds"; +You can always list existing addresses using `logaddress_list`. -const receiver = new LogReceiver({ - address: "0.0.0.0", - port: 9871, -}); +# Installation -console.log("Log receiver running.. "); +See https://jsr.io/@c43721/srcds-log-receiver for more details. -receiver.on("event", (message) => console.log(message)); -``` +# Usage -In order to check for passwords (in cases where it's important to not have people publish unwanted packets to your server) you can check the password property on the object. +You can define a receiver and listen to messages published to the UDP stream. ```ts -import { LogReceiver } from "srcds"; +import { LogReceiver } from "@c43721/srcds-log-receiver"; const receiver = new LogReceiver({ address: "0.0.0.0", @@ -31,12 +26,11 @@ const receiver = new LogReceiver({ console.log("Log receiver running.. "); -receiver.on("event", (message) => { - if (message.password === "mysupersecretpassword") { - // do something awesome - } -}); +receiver.on("event", (message) => console.log(message)); ``` +For more examples, see documentation or the examples folder. + # Contributing -If there's a feature or bug, please raise a github issue first alongside your PR (if you're kind enough to make a PR) + +If there's a feature or bug, please raise a github issue first alongside your PR (if you're kind enough to make a PR.) diff --git a/examples/abort-controller.mjs b/examples/abort-controller.mjs new file mode 100644 index 0000000..178004c --- /dev/null +++ b/examples/abort-controller.mjs @@ -0,0 +1,17 @@ +import { LogReceiver } from "@c43721/srcds-log-receiver"; + +const controller = new AbortController(); +const { signal } = controller; + +const receiver = new LogReceiver({ + address: "0.0.0.0", + port: 9871, + signal, +}); + +console.log("Log receiver running.. "); + +receiver.on("event", (message) => console.log(message)); +receiver.on("close", () => console.log("Closed the socket")); + +controller.abort(); diff --git a/examples/write-to-console.mjs b/examples/write-to-console.mjs index b4d90a0..957d492 100644 --- a/examples/write-to-console.mjs +++ b/examples/write-to-console.mjs @@ -1,4 +1,4 @@ -import { LogReceiver } from "srcds"; +import { LogReceiver } from "@c43721/srcds-log-receiver"; const receiver = new LogReceiver({ address: "0.0.0.0", diff --git a/jsr.json b/jsr.json index 01c1c2f..509ea5d 100644 --- a/jsr.json +++ b/jsr.json @@ -1,5 +1,8 @@ { "name": "@c43721/srcds-log-receiver", "version": "1.0.1", - "exports": "./src/index.ts" + "exports": "./src/index.ts", + "publish": { + "exclude": ["./examples"] + } } diff --git a/src/index.ts b/src/index.ts index 33bcdb5..7cc54d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export { LogReceiver, type LogReceiverOptions } from "./logReceiver"; +export { EventData } from "./types"; diff --git a/src/logReceiver.ts b/src/logReceiver.ts index 56355c8..d7066ff 100644 --- a/src/logReceiver.ts +++ b/src/logReceiver.ts @@ -2,35 +2,75 @@ import { Buffer } from "node:buffer"; import { EventEmitter } from "node:events"; import { type Socket, createSocket, type RemoteInfo } from "node:dgram"; import { parsePacket } from "./parser"; +import { EventData } from "./types"; /** * The socket options for the UDP socket */ export interface LogReceiverOptions { /** - * The IP address to bind to + * The address to listen on + * + * @default "0.0.0.0" */ address?: string; /** * The port to use + * + * @default 9871 */ port?: number; + + /** + * The abort signal to use + * + * When calling {@link AbortSignal.abort}, it will automatically close the underlying socket + */ + signal?: AbortSignal; } /** * An event emitter that will emit a message event when a valid UDP log is created on the server - * + * + * When a log is sent, there will be a `message` event published that you can extract the data from. This is a valid payload. It is important to check the IP or password of the content to ensure it came from a trusted source + * + * You can listen on socket events such as `error` and `close` by listening to those events. All default events of the {@link Socket} will be forwarded. If you need to close the socket at any time, provide an {@link AbortSignal}. Aborting will automatically call {@link Socket.close} for you on the socket and emits a `close` event + * + * @example Simple receiver that logs to console every message * ```ts -import { LogReceiver } from "srcds"; - -const receiver = new LogReceiver({ - address: "0.0.0.0", - port: 9871, -}); - -receiver.on("event", (message) => console.log(message)); -``` + * import { LogReceiver} from "@c43721/srcds-log-receiver"; + * + * const receiver = new LogReceiver({ + * address: "0.0.0.0", + * port: 9871, + * }); + * + * receiver.on("event", (message) => console.log(message)); + * ``` + * + * On every log generated by the srcds server, it will log that message out to console + * + * @example Passing in an abort controller to close the socket + * ```ts + * import { LogReceiver} from "@c43721/srcds-log-receiver"; + * + * const controller = new AbortController(); + * const { signal } = controller; + * + * const receiver = new LogReceiver({ + * address: "0.0.0.0", + * port: 9871, + * }); + * + * receiver.on("event", (message) => console.log(message)); + * receiver.on("close", () => console.log("Closed the socket")); + * + * controller.abort(); + * ``` + * + * While not required, there may be times where you have short-lived receivers. Since each instance creates a socket, closing those sockets becomes a problem. Abort controllers help with that by providing a way to explicitly close the resources for you + * */ export class LogReceiver extends EventEmitter { #socket: Socket; @@ -40,20 +80,38 @@ export class LogReceiver extends EventEmitter { * @param options The log reciever options to use */ constructor( - { address, port }: LogReceiverOptions = { address: "0.0.0.0", port: 9871 } + options: LogReceiverOptions = { + address: "0.0.0.0", + port: 9871, + } ) { + const { address, port, signal } = options; + super(); - this.#socket = createSocket("udp4", this.#handleMessage.bind(this)); + + this.#socket = createSocket({ + type: "udp4", + signal, + }); + this.#socket.bind(port, address); + + this.#socket.on("close", () => this.emit("close")); + this.#socket.on("connect", () => this.emit("connect")); + this.#socket.on("error", (error) => this.emit("error", error)); + this.#socket.on("listening", () => this.emit("listening")); + this.#socket.on("message", (buffer, serverInfo) => this.#handleMessage(buffer, serverInfo)); } #handleMessage(buffer: Buffer, serverInfo: RemoteInfo) { - const response = parsePacket(buffer, serverInfo); + const response = parsePacket(buffer); if (response == null) { return; } - this.emit("event", response); + const eventData = { ...response, socket: serverInfo } as EventData; + + this.emit("event", eventData); } } diff --git a/src/parser.ts b/src/parser.ts index c83a53c..ab49f87 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,18 +1,14 @@ import { Buffer } from "node:buffer"; -import type { RemoteInfo } from "node:dgram"; import { logMessageEndChar, packetHeader, passwordFlag } from "./constants"; import { ParsedLogMessage } from "./types"; /** * Parses and validates the incoming buffer + * @private * @param message The message from the socket - * @param serverInfo The socket information of the connecting resource * @returns An object that contains the log message, the password used, and the socket information or null if no message was parsed */ -export function parsePacket( - message: Buffer, - serverInfo: RemoteInfo -): ParsedLogMessage | null { +export function parsePacket(message: Buffer): ParsedLogMessage | null { if (message.length < 16) { return null; } @@ -47,9 +43,5 @@ export function parsePacket( return { password, message: fullLogMessage, - socket: { - ip: serverInfo.address, - port: serverInfo.port, - }, }; } diff --git a/src/types.ts b/src/types.ts index 9fad2b5..957ece3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,29 +1,23 @@ +import { type RemoteInfo } from "node:dgram"; + /** - * Object holding the parsed information from the UDP log + * Represents the parsed information from the message data */ export interface ParsedLogMessage { /** - * The password sent by the server + * The password sent by the server, if present */ password: string | null; /** - * The log the server sent + * The message contained in the data */ message: string; +} +export interface EventData extends ParsedLogMessage { /** - * The socket information of the server + * The remote address information that sent the packet */ - socket: { - /** - * The source IP of the message - */ - ip: string; - - /** - * The port of the server - */ - port: number; - }; + socket: RemoteInfo; }