From be42b09309b98a660dc435d1ea59a60200bb66b3 Mon Sep 17 00:00:00 2001
From: wuarmin <armin@wurzweb.com>
Date: Wed, 25 May 2022 14:23:41 +0200
Subject: [PATCH 1/3] Added /api/v1/services/wol/send/ endpoint to be able to
 send a WOL packet to a host

---
 .../inc/api/endpoints/APIServicesWOLSend.inc  | 26 +++++++
 .../etc/inc/api/framework/APIResponse.inc     | 30 ++++++++
 .../api/models/APIServicesWOLSendCreate.inc   | 76 +++++++++++++++++++
 .../local/www/api/documentation/openapi.yml   | 29 +++++++
 tests/test_api_v1_services_wol_send.py        | 63 +++++++++++++++
 5 files changed, 224 insertions(+)
 create mode 100644 pfSense-pkg-API/files/etc/inc/api/endpoints/APIServicesWOLSend.inc
 create mode 100644 pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc
 create mode 100644 tests/test_api_v1_services_wol_send.py

diff --git a/pfSense-pkg-API/files/etc/inc/api/endpoints/APIServicesWOLSend.inc b/pfSense-pkg-API/files/etc/inc/api/endpoints/APIServicesWOLSend.inc
new file mode 100644
index 000000000..ae0201426
--- /dev/null
+++ b/pfSense-pkg-API/files/etc/inc/api/endpoints/APIServicesWOLSend.inc
@@ -0,0 +1,26 @@
+<?php
+//   Copyright 2022 Jared Hendrickson
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+
+require_once("api/framework/APIEndpoint.inc");
+
+class APIServicesWOLSend extends APIEndpoint {
+    public function __construct() {
+        $this->url = "/api/v1/services/wol/send";
+    }
+
+    protected function post() {
+        return (new APIServicesWOLSendCreate())->call();
+    }
+}
diff --git a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
index eb5beb19c..532285b50 100644
--- a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
@@ -1193,6 +1193,36 @@ function get($id, $data=[], $all=false) {
             "return" => $id,
             "message" => "Host overrides must be contained within an array"
         ],
+        2099 => [
+            "status" => "bad request",
+            "code" => 400,
+            "return" => $id,
+            "message" => "No interface specified"
+        ],
+        2100 => [
+            "status" => "bad request",
+            "code" => 400,
+            "return" => $id,
+            "message" => "Could not obtain IP address for interface"
+        ],
+        2101 => [
+            "status" => "bad request",
+            "code" => 400,
+            "return" => $id,
+            "message" => "No MAC address specified"
+        ],
+        2102 => [
+            "status" => "bad request",
+            "code" => 400,
+            "return" => $id,
+            "message" => "Invalid MAC address specified"
+        ],
+        2103 => [
+            "status" => "server error",
+            "code" => 500,
+            "return" => $id,
+            "message" => "Please check the system_logs, the WOL command did not complete successfully"
+        ],
         2999 => [
             "status" => "bad request",
             "code" => 400,
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc
new file mode 100644
index 000000000..a94636d6a
--- /dev/null
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc
@@ -0,0 +1,76 @@
+<?php
+//   Copyright 2022 Jared Hendrickson
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+
+require_once("api/framework/APIModel.inc");
+require_once("api/framework/APIResponse.inc");
+
+class APIServicesWOLSendCreate extends APIModel {
+    # Create our method constructor
+    public function __construct() {
+        parent::__construct();
+        $this->privileges = ["page-all", "page-services-wakeonlan"];
+        $this->change_note = "Sent magic WOL packet via API";
+    }
+
+    public function action() {
+        $if = $this->validated_data["interface"];
+        $if_ip = $this->validated_data["interface_ip"];
+        $mac = $this->validated_data["mac"];
+
+        $bc_ip = $this->__get_broadcast_ip($if, $if_ip);
+        $response_data = array_merge($this->validated_data, ["broadcast_ip" => $bc_ip]);
+
+        if (!mwexec("/usr/local/bin/wol -i {$bc_ip} {$mac}")) {
+            return APIResponse\get(0, $response_data);
+        } else {
+            return APIResponse\get(2103, $response_data);
+        }
+    }
+
+    private function __get_broadcast_ip($if, $if_ip) {
+        return gen_subnet_max($if_ip, get_interface_subnet($if));
+    }
+
+    public function validate_payload() {
+        $this->__validate_interface();
+        $this->__validate_mac();
+    }
+
+    private function __validate_interface() {
+        if (isset($this->initial_data["interface"])) {
+            $interface_ip = get_interface_ip($this->initial_data["interface"]);
+            if (is_ipaddr($interface_ip)) {
+                $this->validated_data["interface"] = $this->initial_data["interface"];
+                $this->validated_data["interface_ip"] = $interface_ip;
+            } else {
+                $this->errors[] = APIResponse\get(2100, ["interface" => $this->initial_data["interface"], "interface_ip" => $interface_ip]);
+            }
+        } else {
+            $this->errors[] = APIResponse\get(2099);
+        }
+    }
+
+    private function __validate_mac() {
+        if (isset($this->initial_data["mac"])) {
+            if (is_macaddr($this->initial_data["mac"])) {
+                $this->validated_data["mac"] = $this->initial_data["mac"];
+            } else {
+                $this->errors[] = APIResponse\get(2102);
+            }
+        } else {
+            $this->errors[] = APIResponse\get(2101);
+        }
+    }
+}
diff --git a/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml b/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
index 086e1505b..0c1c11cdc 100644
--- a/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
+++ b/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
@@ -7500,6 +7500,34 @@ paths:
       summary: Stop DNS Resolver (Unbound) service
       tags:
         - Services > Unbound
+  /api/v1/services/wol/send:
+    post:
+      description: 'Send a magic WOL packet to a host.<br><br>
+
+        _Requires at least one of the following privileges:_ [`page-all`, `page-services-wakeonlan`]'
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                interface:
+                  description: Interface to which the host to be woken is connected.
+                  type: string
+                mac:
+                  description: MAC address of the host to be woken.
+                  type: string
+              required:
+                - interface
+                - mac
+              type: object
+      responses:
+        200:
+          $ref: '#/components/responses/Success'
+        401:
+          $ref: '#/components/responses/AuthenticationFailed'
+      summary: Send a magic WOL packet to a host
+      tags:
+        - Services > WOL
   /api/v1/status/carp:
     get:
       description: 'Read the CARP (failover) status.<br><br>
@@ -9580,5 +9608,6 @@ tags:
   - name: Services > SSHD
   - name: Services > SYSLOGD
   - name: Services > DDNS
+  - name: Services > WOL
   - name: Diagnostics > Command Prompt
 
diff --git a/tests/test_api_v1_services_wol_send.py b/tests/test_api_v1_services_wol_send.py
new file mode 100644
index 000000000..1848cb62a
--- /dev/null
+++ b/tests/test_api_v1_services_wol_send.py
@@ -0,0 +1,63 @@
+# Copyright 2022 Jared Hendrickson
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import e2e_test_framework
+
+class APIE2ETestServicesWOLSend(e2e_test_framework.APIE2ETest):
+    uri = "/api/v1/services/wol/send/"
+    post_tests = [
+        {
+            "name": "Send magic WOL packet",
+            "payload": {
+                "interface": "lan",
+                "mac": "2C:54:91:88:C9:E3"
+            }
+        },
+        {
+            "name": "Send magic WOL packet with missing interface",
+            "payload": {
+                "mac": "2C:54:91:88:C9:E3",
+            },
+            "status": 400,
+            "return": 2099
+        },
+        {
+            "name": "Send magic WOL packet with invalid interface",
+            "payload": {
+                "interface": "INVALID",
+                "mac": "2C:54:91:88:C9:E3"
+            },
+            "status": 400,
+            "return": 2100
+        },
+        {
+            "name": "Send magic WOL packet with missing mac",
+            "payload": {
+                "interface": "lan"
+            },
+            "status": 400,
+            "return": 2101
+        },
+        {
+            "name": "Send magic WOL packet with invalid mac",
+            "payload": {
+                "interface": "lan",
+                "mac": "INVALID"
+            },
+            "status": 400,
+            "return": 2102
+        },
+    ]
+
+APIE2ETestServicesWOLSend()

From dc1db2d55eb208bf2625c2189f3bd1f2d7eebd50 Mon Sep 17 00:00:00 2001
From: wuarmin <armin@wurzweb.com>
Date: Wed, 25 May 2022 20:20:18 +0200
Subject: [PATCH 2/3] Removed unnecessary response data from Error-APIResponse

---
 .../files/etc/inc/api/models/APIServicesWOLSendCreate.inc       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc
index a94636d6a..11c7e4683 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc
@@ -55,7 +55,7 @@ class APIServicesWOLSendCreate extends APIModel {
                 $this->validated_data["interface"] = $this->initial_data["interface"];
                 $this->validated_data["interface_ip"] = $interface_ip;
             } else {
-                $this->errors[] = APIResponse\get(2100, ["interface" => $this->initial_data["interface"], "interface_ip" => $interface_ip]);
+                $this->errors[] = APIResponse\get(2100);
             }
         } else {
             $this->errors[] = APIResponse\get(2099);

From 88b311d51405ae8745c314a2e56e03fead5e2309 Mon Sep 17 00:00:00 2001
From: wuarmin <armin@wurzweb.com>
Date: Mon, 30 May 2022 09:39:06 +0200
Subject: [PATCH 3/3] Improved interface lookup at
 /api/v1/services/wol/send-endpoint

---
 .../files/etc/inc/api/models/APIServicesWOLSendCreate.inc    | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc
index 11c7e4683..79686f309 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesWOLSendCreate.inc
@@ -50,9 +50,10 @@ class APIServicesWOLSendCreate extends APIModel {
 
     private function __validate_interface() {
         if (isset($this->initial_data["interface"])) {
-            $interface_ip = get_interface_ip($this->initial_data["interface"]);
+            $interface = APITools\get_pfsense_if_id($this->initial_data['interface']);
+            $interface_ip = get_interface_ip($interface);
             if (is_ipaddr($interface_ip)) {
-                $this->validated_data["interface"] = $this->initial_data["interface"];
+                $this->validated_data["interface"] = $interface;
                 $this->validated_data["interface_ip"] = $interface_ip;
             } else {
                 $this->errors[] = APIResponse\get(2100);