diff --git a/README.md b/README.md index bbc2247..793192e 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ services: | `reload-filter` | Reload the filters on your pfSense® firewall | `GET` | [Reload Filter Endpoint](#reload-filter-endpoint) | | `update-dyndns` | Update the Dynamic DNS settings on your pfSense® firewall | `GET` | [Update Dynamic DNS Endpoint](#update-dynamic-dns-endpoint) | | `wol` | Send a Wake-on-LAN request to a connected device | `POST` | [Wake-on-LAN Endpoint](#wake-on-lan-endpoint) | +| `wol-check` | Send a Wake-on-LAN check request to a connected device | `POST` | [Wake-on-LAN Check Endpoint](#wake-on-lan-check-endpoint) | ## Authorizing Your Requests When accessing the controller, ensure that you have the `Authorization` header properly set to the `API_KEY` you specified during image creation. For example: @@ -101,11 +102,13 @@ This section describes how to interact with the available API endpoints for mana GET http://localhost:9898/reload-filter Authorization: Bearer [YOUR_API_KEY] ``` + #### Update Dynamic DNS Endpoint: ```http request GET http://localhost:9898/update-dyndns Authorization: Bearer [YOUR_API_KEY] ``` + #### Wake-on-LAN Endpoint: ```http request POST http://localhost:9898/wol @@ -118,6 +121,17 @@ Content-Type: application/json } ``` +#### Wake-on-LAN Check Endpoint: +```http request +POST http://localhost:9898/wol-check +Authorization: Bearer [YOUR_API_KEY] +Content-Type: application/json + +{ + "ipAddress": "[YOUR_IP_ADDRESS]", +} +``` + ## Retrieving the Broadcast IP Address In order to send a Wake-on-LAN request, a broadcast address is required since magic packets cannot be sent across subnets. Follow the instructions below: diff --git a/package.json b/package.json index e43f4eb..e1d16d5 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pfsense-actions", "displayName": "pfSense® Actions", - "version": "1.0.0", + "version": "1.1.0", "description": "A controller that allows a network administrator to run simple actions such as Wake-on-LAN and reload firewall filters", "main": "./build/index.js", "exports": "./build/index.js", diff --git a/src/index.ts b/src/index.ts index 1a96d5c..e6dfd00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import express from 'express'; import { Pfsense } from '@/lib/api.js'; -import { wakeOnLan } from '@/lib/schema.js'; +import { wakeOnLan, wakeOnLanCheck } from '@/lib/schema.js'; import { getEnvironmentVariables, isValidApiKey } from '@/lib/utility.js'; import type { ServerAddMiddlewareReturns, @@ -21,6 +21,9 @@ import type { ServerRouteUpdateDyndnsRequest, ServerRouteUpdateDyndnsResponse, ServerRouteUpdateDyndnsReturns, + ServerRouteWolCheckRequest, + ServerRouteWolCheckResponse, + ServerRouteWolCheckReturns, ServerRouteWolRequest, ServerRouteWolResponse, ServerRouteWolReturns, @@ -99,6 +102,7 @@ class Server { this.#app.get('/reload-filter', this.routeReloadFilter.bind(this)); this.#app.get('/update-dyndns', this.routeUpdateDyndns.bind(this)); this.#app.post('/wol', this.routeWol.bind(this)); + this.#app.post('/wol-check', this.routeWolCheck.bind(this)); } /** @@ -156,9 +160,9 @@ class Server { try { const instance = new Pfsense(this.#env); - console.log(await instance.login()); - console.log(await instance.systemInformation()); - console.log(await instance.logout()); + await instance.login(); + console.log(JSON.stringify(await instance.systemInformation())); + await instance.logout(); response.sendStatus(200); } catch (error) { @@ -184,9 +188,9 @@ class Server { try { const instance = new Pfsense(this.#env); - console.log(await instance.login()); - console.log(await instance.reloadFilter()); - console.log(await instance.logout()); + await instance.login(); + console.log(JSON.stringify(await instance.reloadFilter())); + await instance.logout(); response.sendStatus(200); } catch (error) { @@ -212,9 +216,9 @@ class Server { try { const instance = new Pfsense(this.#env); - console.log(await instance.login()); - console.log(await instance.updateDyndns()); - console.log(await instance.logout()); + await instance.login(); + console.log(JSON.stringify(await instance.updateDyndns())); + await instance.logout(); response.sendStatus(200); } catch (error) { @@ -249,9 +253,9 @@ class Server { const { broadcastAddress, macAddress } = responseBody.data; - console.log(await instance.login()); - console.log(await instance.wakeOnLan(broadcastAddress, macAddress)); - console.log(await instance.logout()); + await instance.login(); + console.log(JSON.stringify(await instance.wakeOnLan(broadcastAddress, macAddress))); + await instance.logout(); response.sendStatus(200); } catch (error) { @@ -260,6 +264,59 @@ class Server { response.sendStatus(500); } } + + /** + * Server - Route wol check. + * + * @param {ServerRouteWolCheckRequest} request - Request. + * @param {ServerRouteWolCheckResponse} response - Response. + * + * @private + * + * @returns {ServerRouteWolCheckReturns} + * + * @since 1.0.0 + */ + private async routeWolCheck(request: ServerRouteWolCheckRequest, response: ServerRouteWolCheckResponse): ServerRouteWolCheckReturns { + try { + const instance = new Pfsense(this.#env); + const responseBody = wakeOnLanCheck.safeParse(request.body); + + if (!responseBody.success) { + response.sendStatus(400); + + return; + } + + const { ipAddress } = responseBody.data; + + await instance.login(); + + // Pings the device 3 times (5 tries each) until it gives up. + for (let i = 1; i <= 3; i += 1) { + const pingResponse = await instance.ping(ipAddress, 5); + + console.log(JSON.stringify(pingResponse)); + + // If command does not respond with "100.0% packet loss", it means the device is now online. + if (pingResponse.success && !pingResponse.info.stdout.includes('100.0% packet loss')) { + await instance.logout(); + + response.sendStatus(200); + + return; + } + } + + await instance.logout(); + + response.sendStatus(503); + } catch (error) { + console.error(error); + + response.sendStatus(500); + } + } } new Server(); diff --git a/src/lib/api.ts b/src/lib/api.ts index 92c08e9..f56fa5a 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -7,6 +7,9 @@ import type { PfsenseIsAuthenticatedReturns, PfsenseLoginReturns, PfsenseLogoutReturns, + PfsensePingCount, + PfsensePingIpAddress, + PfsensePingReturns, PfsenseReloadFilterReturns, PfsenseSession, PfsenseSystemInformationReturns, @@ -152,6 +155,51 @@ export class Pfsense { }; } + /** + * Pfsense - Ping. + * + * @param {PfsensePingIpAddress} ipAddress - Ip address. + * @param {PfsensePingCount} count - Count. + * + * @returns {PfsensePingReturns} + * + * @since 1.0.0 + */ + public async ping(ipAddress: PfsensePingIpAddress, count: PfsensePingCount): PfsensePingReturns { + let errorObject; + + try { + // Check if this session is not authenticated. + if (!await this.isAuthenticated()) { + return { + action: 'PING', + success: false, + info: { + message: 'Failed to authenticate to pfSense via SSH', + }, + }; + } + + const response = await this.#session.sshClient.execCommand(`ping -c ${count} ${ipAddress}`); + + return { + action: 'PING', + success: true, + info: response, + }; + } catch (error) { + errorObject = serializeError(error); + } + + return { + action: 'PING', + success: false, + info: { + error: errorObject, + }, + }; + } + /** * Pfsense - Reload filter. * diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 4145858..5c9d1dc 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -45,3 +45,12 @@ export const wakeOnLan = z.object({ }, ), }); + +/** + * Wake on lan check. + * + * @since 1.0.0 + */ +export const wakeOnLanCheck = z.object({ + ipAddress: z.string().ip(), +}); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index c6fd447..18cdef0 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -60,6 +60,19 @@ export type PfsenseLogoutReturnsInfo = null; export type PfsenseLogoutReturns = Promise>; +/** + * Pfsense - Ping. + * + * @since 1.0.0 + */ +export type PfsensePingIpAddress = string; + +export type PfsensePingCount = number; + +export type PfsensePingReturnsInfo = SSHExecCommandResponse; + +export type PfsensePingReturns = Promise>; + /** * Pfsense - Reload filter. * @@ -199,6 +212,17 @@ export type ServerRouteWolResponse = express.Response; export type ServerRouteWolReturns = Promise; +/** + * Server - Route wol check. + * + * @since 1.0.0 + */ +export type ServerRouteWolCheckRequest = express.Request; + +export type ServerRouteWolCheckResponse = express.Response; + +export type ServerRouteWolCheckReturns = Promise; + /** * Server - Start server. * diff --git a/src/types/shared.d.ts b/src/types/shared.d.ts index 00f5696..81d1b00 100644 --- a/src/types/shared.d.ts +++ b/src/types/shared.d.ts @@ -8,6 +8,7 @@ import type { ErrorObject } from 'serialize-error'; export type ApiResponseAction = 'LOGIN' | 'LOGOUT' + | 'PING' | 'RELOAD_FILTER' | 'SYSTEM_INFORMATION' | 'UPDATE_DYNDNS'