Skip to content

Commit 6688a07

Browse files
Overhauled UI to be more pfSense-like and organized, added custom_headers and allow_options fields to API configuration, updated /api/v1/system/api endpoint to accomodate changes, updated documentation
1 parent 87f0806 commit 6688a07

7 files changed

Lines changed: 177 additions & 38 deletions

File tree

docs/documentation.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4356,15 +4356,15 @@
43564356
"header": [],
43574357
"body": {
43584358
"mode": "raw",
4359-
"raw": "{\n \"persist\": false, \n \"jwt_exp\": 86400, \n \"authmode\": \"token\",\n \"hashalgo\": \"sha512\", \n \"keybytes\": 64, \n \"allowed_interfaces\": [\"WAN\"]\n}",
4359+
"raw": "{\n \"persist\": false, \n \"jwt_exp\": 86400, \n \"authmode\": \"token\",\n \"hashalgo\": \"sha512\", \n \"keybytes\": 64, \n \"allowed_interfaces\": [\"WAN\"],\n \"custom_headers\": {\"custom-header-1\": \"Value1\", \"custom-header-2\": \"Value2\"},\n \"allow_options\": true\n}",
43604360
"options": {
43614361
"raw": {
43624362
"language": "json"
43634363
}
43644364
}
43654365
},
43664366
"url": {
4367-
"raw": "https://{{$hostname}}/api/v1/system/api?enable=boolean&persist=boolean&readonly=boolean&available_interfaces=array&authmode=string&jwt_exp=integer&keyhash=string&keybytes=integer",
4367+
"raw": "https://{{$hostname}}/api/v1/system/api?enable=boolean&persist=boolean&readonly=boolean&allow_options=boolean&available_interfaces=array&authmode=string&jwt_exp=integer&keyhash=string&keybytes=integer&custom_headers=array",
43684368
"protocol": "https",
43694369
"host": [
43704370
"{{$hostname}}"
@@ -4391,6 +4391,11 @@
43914391
"value": "boolean",
43924392
"description": "Enable read only mode. If set to `true`, the API will only answer read (GET) requests. This also means you will not be able to disable read only mode from the API. "
43934393
},
4394+
{
4395+
"key": "allow_options",
4396+
"value": "boolean",
4397+
"description": "Enable/disable the OPTIONS request method from API responses. If set to `true`, the API will answer OPTIONS requests. If set to `false`, the API will return a 405 Method Not Allowed response. (optional)"
4398+
},
43944399
{
43954400
"key": "available_interfaces",
43964401
"value": "array",
@@ -4415,6 +4420,11 @@
44154420
"key": "keybytes",
44164421
"value": "integer",
44174422
"description": "Update the key byte strength to use when generating API tokens. Choices are `16`, `32` and `64`. This Is only applicable when the `authmode` setting Is set to `token`. (optional)"
4423+
},
4424+
{
4425+
"key": "custom_headers",
4426+
"value": "array",
4427+
"description": "Update the custom response headers for the API to return in API responses. This must be an array of key-value pairs (e.g. `{\"custom-header\": \"custom-header-value}`. To revert custom headers to the default state, simply pass in an empty array. In most cases, custom headers are not necessary. An example use case for custom headers is setting CORS policy headers required by some frontend web applications. (optional)"
44184428
}
44194429
]
44204430
},

pfSense-pkg-API/files/etc/inc/api/framework/APIEndpoint.inc

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,42 @@ class APIEndpoint {
5353

5454
# Listen for HTTP requests and call the corresponding method
5555
public function listen() {
56+
$pkg_config = APITools\get_api_config()[1];
57+
58+
# Before responding, ensure the request method is allowed
5659
if ($_SERVER["REQUEST_METHOD"] === "GET") {
5760
$resp = (new APIQuery($this->get(), $this->query_excludes))->query();
58-
} elseif ($_SERVER["REQUEST_METHOD"] === "POST") {
61+
}
62+
elseif ($_SERVER["REQUEST_METHOD"] === "POST") {
5963
$resp = $this->post();
60-
} elseif ($_SERVER["REQUEST_METHOD"] === "PUT") {
64+
}
65+
elseif ($_SERVER["REQUEST_METHOD"] === "PUT") {
6166
$resp = $this->put();
62-
} elseif ($_SERVER["REQUEST_METHOD"] === "DELETE") {
67+
}
68+
elseif ($_SERVER["REQUEST_METHOD"] === "DELETE") {
6369
$resp = $this->delete();
64-
} else {
70+
}
71+
# Only allow OPTIONS requests if the allow options setting is checked
72+
elseif ($_SERVER["REQUEST_METHOD"] === "OPTIONS" and isset($pkg_config["allow_options"])) {
73+
$resp = APIResponse\get(0);
74+
}
75+
# Method is not allowed when no match, set error response
76+
else {
6577
$resp = APIResponse\get(2);
6678
}
6779

68-
# Format the HTTP response as JSON and set response code
80+
# Add custom response headers if configured
81+
if (!empty($pkg_config["custom_headers"])) {
82+
foreach ($pkg_config["custom_headers"] as $name=>$value) {
83+
header(strval($name).": ".strval($value));
84+
}
85+
}
86+
87+
# Add API required response headers, these will override any custom headers
6988
header("Content-Type: application/json", true);
7089
header("Referer: no-referrer");
90+
91+
# Format the HTTP response as JSON and set response code
7192
http_response_code($resp["code"]);
7293
echo json_encode($resp) . PHP_EOL;
7394
session_destroy();

pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,19 @@ function get($id, $data=[], $all=false) {
240240
"status" => "bad request",
241241
"code" => 400,
242242
"return" => $id,
243-
"message" => "Unsupport API token bytes count"
243+
"message" => "Unsupported API token bytes count"
244+
],
245+
1025 => [
246+
"status" => "bad request",
247+
"code" => 400,
248+
"return" => $id,
249+
"message" => "API custom headers must be an array containing key-value pairs"
250+
],
251+
1026 => [
252+
"status" => "bad request",
253+
"code" => 400,
254+
"return" => $id,
255+
"message" => "API custom header key-value pairs must be string types"
244256
],
245257

246258
// 2000-2999 reserved for /services API calls

pfSense-pkg-API/files/etc/inc/api/models/APISystemAPIUpdate.inc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ class APISystemAPIUpdate extends APIModel {
5959
}
6060
}
6161

62+
private function __validate_allow_options() {
63+
# Check for our optional 'allow_options' payload value
64+
if ($this->initial_data['allow_options'] === true) {
65+
$this->validated_data["allow_options"] = "";
66+
} elseif ($this->initial_data['allow_options'] === false) {
67+
unset($this->validated_data["allow_options"]);
68+
}
69+
}
70+
6271
private function __validate_allowed_interfaces() {
6372
# Local variables
6473
$non_config_ifs = array("any" => "Any", "localhost" => "Link-local");
@@ -85,6 +94,28 @@ class APISystemAPIUpdate extends APIModel {
8594
}
8695
}
8796

97+
private function __validate_custom_headers() {
98+
# Check for our optional 'custom_headers' payload value
99+
if (isset($this->initial_data["custom_headers"]) and !empty($this->initial_data["custom_headers"])) {
100+
if (is_array($this->initial_data["custom_headers"])) {
101+
# Loop through each requested header and ensure types are valid
102+
foreach ($this->initial_data["custom_headers"] as $key=>$value) {
103+
if (!is_string($key) or !is_string($value)) {
104+
$this->errors[] = APIResponse\get(1026);
105+
break;
106+
}
107+
}
108+
$this->validated_data["custom_headers"] = $this->initial_data["custom_headers"];
109+
} else {
110+
$this->errors[] = APIResponse\get(1025);
111+
}
112+
}
113+
# When the custom_headers was passed in but is empty, unset custom headers
114+
elseif (isset($this->initial_data["custom_headers"]) and empty($this->initial_data["custom_headers"])) {
115+
unset($this->validated_data["custom_headers"]);
116+
}
117+
}
118+
88119
private function __validate_authmode() {
89120
# Check for our option 'authmode' payload value
90121
if (isset($this->initial_data["authmode"])) {
@@ -137,10 +168,12 @@ class APISystemAPIUpdate extends APIModel {
137168
$this->__validate_enable();
138169
$this->__validate_persist();
139170
$this->__validate_readonly();
171+
$this->__validate_allow_options();
140172
$this->__validate_allowed_interfaces();
141173
$this->__validate_authmode();
142174
$this->__validate_jwt_exp();
143175
$this->__validate_keyhash();
144176
$this->__validate_keybytes();
177+
$this->__validate_custom_headers();
145178
}
146179
}

pfSense-pkg-API/files/usr/local/www/api/index.php

Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,35 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16-
# Imports and inits
1716
include_once("util.inc");
1817
include_once("guiconfig.inc");
1918
require_once("api/framework/APITools.inc");
2019

20+
# Initialize the pfSense UI page (note: $pgtitle must be defined before including head.inc)
2121
$pgtitle = array(gettext('System'), gettext('API'), gettext('Settings'));
2222
include('head.inc');
23+
echo "<link rel='stylesheet' href='/css/api.css'/>";
24+
echo "<script type='application/javascript' src='/js/api.js'></script>";
2325
$tab_array = [[gettext("Settings"), true, "/api/"], [gettext("Documentation"), false, "/api/documentation/"]];
2426
display_top_tabs($tab_array, true); # Ensure the tabs are written to the top of page
2527

26-
2728
# Variables
2829
global $config;
2930
$form = new Form(false);
3031
$general_section = new Form_Section('General Settings');
3132
$token_section = new Form_Section('API Token Settings');
3233
$jwt_section = new Form_Section('JWT Settings');
33-
$advanced_section = new Form_Section('Advanced Settings');
34+
$advanced_section = new Form_Section('Advanced Settings', 'api-advanced-settings');
3435
$pkg_index = APITools\get_api_config()[0];
3536
$pkg_config = APITools\get_api_config()[1];
3637

37-
# UPON POST
38+
# Generate new API token if requested
3839
if ($_POST["gen"] === "1") {
3940
$new_key = APITools\generate_token($_SESSION["Username"]);
4041
print_apply_result_box(0, "\nSave this API key somewhere safe, it cannot be viewed again: \n".$new_key);
4142
}
42-
# Rotate JWT server key requested
43+
44+
# Rotate JWT server key if requested
4345
if ($_POST["rotate_server_key"]) {
4446
$config["installedpackages"]["package"][$pkg_index]["conf"]["keys"] = [];
4547
APITools\create_jwt_server_key(true);
@@ -54,6 +56,8 @@
5456
write_config(sprintf(gettext($change_note)));
5557
print_apply_result_box(0);
5658
}
59+
60+
# Upon normal save, update changed values
5761
if (isset($_POST["save"])) {
5862
# Save enable value to config
5963
if (isset($_POST["enable"])) {
@@ -69,7 +73,7 @@
6973
if (isset($_POST["authmode"])) {
7074
$pkg_config["authmode"] = $_POST["authmode"];
7175
}
72-
# Save JWT expiration value to coonfig
76+
# Save JWT expiration value to config
7377
if (isset($_POST["jwt_exp"])) {
7478
$pkg_config["jwt_exp"] = $_POST["jwt_exp"];
7579
}
@@ -94,20 +98,53 @@
9498
} else {
9599
unset($pkg_config["readonly"]);
96100
}
97-
# Write and apply our changes, leave a session variable indicating save, then reload the page
98-
$config["installedpackages"]["package"][$pkg_index]["conf"] = $pkg_config;
99-
$change_note = " Updated API settings";
100-
write_config(sprintf(gettext($change_note)));
101-
APITools\create_jwt_server_key();
102-
print_apply_result_box(0);
101+
# Save our allow OPTIONS value
102+
if (isset($_POST["allow_options"])) {
103+
$pkg_config["allow_options"] = "";
104+
} else {
105+
unset($pkg_config["allow_options"]);
106+
}
107+
# Save any custom headers specified
108+
if (!empty($_POST["custom_headers"])) {
109+
# Decode the JSON string to ensure it is valid
110+
$headers = json_decode($_POST["custom_headers"], true);
111+
112+
# Only save the new value if it was a successfully decoded JSON string
113+
if (is_array($headers)) {
114+
# Loop through each requested header and ensure types are valid
115+
foreach ($headers as $key=>$value) {
116+
if (!is_string($key) or !is_string($value)) {
117+
print_input_errors(["Custom headers key-value pairs must be string types."]);
118+
$has_errors = true;
119+
break;
120+
}
121+
}
122+
$pkg_config["custom_headers"] = $headers;
123+
} else {
124+
print_input_errors(["Custom headers must be a JSON string containing key-value pairs."]);
125+
$has_errors = true;
126+
}
127+
} else {
128+
unset($pkg_config["custom_headers"]);
129+
}
130+
131+
# Only write changes if no errors occurred
132+
if (!$has_errors) {
133+
# Write and apply our changes, leave a session variable indicating save, then reload the page
134+
$config["installedpackages"]["package"][$pkg_index]["conf"] = $pkg_config;
135+
$change_note = " Updated API settings";
136+
write_config(sprintf(gettext($change_note)));
137+
APITools\create_jwt_server_key();
138+
print_apply_result_box(0);
139+
}
103140
}
104141

105142
# Backup our configuration is persist is enabled and the request is a POST request
106143
if(isset($pkg_config["persist"]) and $_SERVER["REQUEST_METHOD"] === "POST") {
107144
shell_exec("/usr/local/share/pfSense-pkg-API/manage.php backup");
108145
}
109146

110-
# POPULATE THE GENERAL SECTION OF THE UI
147+
# Populate the GENERAL section of the UI form
111148
$general_section->addInput(new Form_Checkbox(
112149
'enable',
113150
'Enable',
@@ -142,16 +179,26 @@
142179
$pkg_config["allowed_interfaces"],
143180
array_merge(["any" => "Any", "localhost" => "Link-local"], get_configured_interface_with_descr(true)),
144181
true
145-
));
182+
))->setHelp(
183+
"Select interfaces that are allowed to respond to API requests."
184+
);
146185

147186
$general_section->addInput(new Form_Select(
148187
'authmode',
149188
'Authentication Mode',
150189
$pkg_config["authmode"],
151190
["local" => "Local Database", "token" => "API Token", "jwt" => "JWT"]
152-
));
191+
))->setHelp(
192+
"Select the mode used to authenticate API requests See the <a href='/api/documentation/'>developer documentation</a>
193+
for more information on API authentication."
194+
);
195+
196+
# Add toggle button to show/hide the advanced settings
197+
$show_adv_btn = new Form_Button('display_advanced', 'Display Advanced', null, 'fa-cog');
198+
$show_adv_btn->setAttribute('type','button')->addClass('btn-info btn-sm')->setOnClick("toggle_advanced_settings()");
199+
$general_section->addInput(new Form_StaticText('Advanced Settings', $show_adv_btn));
153200

154-
# POPULATE THE API TOKEN SECTION OF THE UI
201+
### Populate the API TOKEN section of the UI form
155202
$token_section->addInput(new Form_Select(
156203
'keyhash',
157204
'Token Hash Algorithm',
@@ -160,6 +207,7 @@
160207
))->setHelp(
161208
"Hashing algorithm used when generating API tokens."
162209
);
210+
163211
$token_section->addInput(new Form_Select(
164212
'keybytes',
165213
'Token Bit Strength',
@@ -169,7 +217,7 @@
169217
"Bit strength used when generating API tokens."
170218
);
171219

172-
# POPULATE THE JWT SECTION OF THE UI
220+
### Populate the JWT section of the UI form
173221
$jwt_section->addInput(new Form_Input(
174222
'jwt_exp',
175223
'JWT Expiration',
@@ -181,43 +229,45 @@
181229
86400 seconds (1 day)."
182230
);
183231

184-
# POPULATE THE ADVANCED SECTION OF THE UI
232+
### Populate the ADVANCED section of the UI form
233+
$advanced_section->addClass("hide-api-advanced-settings");
185234
$advanced_section->addInput(new Form_Checkbox(
186235
'allow_options',
187236
'OPTIONS Method',
188237
'Allow OPTIONS Request Method',
189-
$pkg_config["allow_options"]
190-
))->setHelp("Allow API to answer OPTIONS requests. This is sometimes required for integration with frontend web applications.");
238+
isset($pkg_config["allow_options"])
239+
))->setHelp(
240+
"Allow API to answer OPTIONS requests. This is sometimes required for integration with frontend web applications."
241+
);
242+
191243
$advanced_section->addInput(new Form_Textarea(
192244
'custom_headers',
193-
'Custom Response Headers',
245+
'Custom Headers',
194246
(is_array($pkg_config["custom_headers"])) ? json_encode($pkg_config["custom_headers"]) : ""
195247
))->setHelp(
196248
'Specify custom response headers to return with API responses. This must be JSON encoded string containing key-value
197-
pairs (e.g. {"test-header-name": "test-header-value"}). This may be required by some HTTP clients and frameworks.
249+
pairs (e.g. <code>{"test-header-name": "test-header-value"}</code>). This may be required by some HTTP clients and frameworks.
198250
For example, this can be used to set CORS policy headers required by frontend web applications.'
199251
);
200252

201-
202-
# POPULATE OUR COMPLETE FORM
253+
# Populate the entire form
203254
$form->add($general_section);
204255
$form->add($advanced_section);
205-
206-
# Only display the Token or JWT sections if they are the selected auth mode
207256
($pkg_config["authmode"] === "token") ? $form->add($token_section) : null;
208257
($pkg_config["authmode"] === "jwt") ? $form->add($jwt_section) : null;
258+
259+
# Add buttons below the form
209260
$rotate_btn = new Form_Button('rotate_server_key', 'Rotate server key', null, 'fa-level-up');
210261
$rotate_btn->addClass('btn btn-sm btn-success');
211262
$rotate_btn->setOnclick("return confirm(\"Rotating the server key will void any existng API tokens and JWTs. Proceed?\");");
212-
$form->addGlobal($rotate_btn);
213263
$form->addGlobal(new Form_Button('save', 'Save', null, 'fa-save'))->addClass('btn btn-sm btn-primary api-save-btn');
264+
(in_array($pkg_config["authmode"], ["token", "jwt"])) ? $form->addGlobal($rotate_btn) : null;
214265
$form->addGlobal(new Form_Button('report', 'Report an Issue', 'https://github.com/jaredhendrickson13/pfsense-api/issues/new', ''))->addClass('fa fa-question-circle api-report');
215266

216-
# PRINT OUR FORM AND PFSENSE FOOTER
267+
# Display the populated configuration form
217268
print $form;
218-
//print "<a style=\"float: right;\" class=\"fa fa-question-circle\" href='https://github.com/jaredhendrickson13/pfsense-api/issues/new'> <span style=\"font-family: 'Helvetica'; font-size: 14px;\">Report an Issue</span></a>";
219269

220-
# POPULATE TOKEN TABLE
270+
# POPULATE TOKEN TABLE IF TOKEN AUTH MODE IS SET
221271
if ($pkg_config["authmode"] === "token") {
222272
# Pull credentials if configured
223273
$user_creds = APITools\get_existing_tokens($_SESSION["Username"]);

0 commit comments

Comments
 (0)