%PDF- %PDF-
| Direktori : /backups/router/usr/local/opnsense/mvc/app/controllers/OPNsense/Base/ |
| Current File : //backups/router/usr/local/opnsense/mvc/app/controllers/OPNsense/Base/ApiControllerBase.php |
<?php
/*
* Copyright (C) 2015-2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Base;
use OPNsense\Auth\AuthenticationFactory;
use OPNsense\Core\ACL;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Mvc\Security;
/**
* Class ApiControllerBase, inherit this class to implement API calls
* @package OPNsense\Base
*/
class ApiControllerBase extends ControllerRoot
{
/***
* Recordset (array in array) search wrapper
* @param string $path path to search, relative to this model
* @param array $fields fieldnames to search through in result
* @param string|null $defaultSort default sort field name
* @param null|function $filter_funct additional filter callable
* @param int $sort_flags sorting behavior
* @param array|null $search_clauses optional overwrite to pass clauses to search instead of using searchPhrase
* @return array
*/
protected function searchRecordsetBase(
$records,
$fields = null,
$defaultSort = null,
$filter_funct = null,
$sort_flags = SORT_NATURAL | SORT_FLAG_CASE,
$search_clauses = null
) {
$records = is_array($records) ? $records : []; // safeguard input, we are only able to search arrays.
$itemsPerPage = intval($this->request->getPost('rowCount', 'int', 9999));
$itemsPerPage = $itemsPerPage == -1 ? count($records) : $itemsPerPage;
$currentPage = intval($this->request->getPost('current', 'int', 1));
$offset = ($currentPage - 1) * $itemsPerPage;
$entry_keys = array_keys($records);
if (!is_array($search_clauses)) {
/* default behavior, extract clauses to search from post */
$searchPhrase = (string)$this->request->getPost('searchPhrase', null, '');
$search_clauses = preg_split('/\s+/', $searchPhrase);
}
$sortOrder = SORT_ASC;
$sortKey = $defaultSort;
if (
$this->request->hasPost('sort') &&
is_array($this->request->getPost('sort')) &&
!empty($this->request->getPost('sort'))
) {
$keys = array_keys($this->request->getPost('sort'));
$sortOrder = $this->request->getPost('sort')[$keys[0]] == 'asc' ? SORT_ASC : SORT_DESC;
$sortKey = $keys[0];
}
if (!empty($sortKey) && !empty($records)) {
// make sure the sort key exists in the recordset to prevent "sizes are inconsistent"
foreach ($records as &$record) {
if (!isset($record[$sortKey])) {
$record[$sortKey] = null;
}
}
$keys = array_column($records, $sortKey);
array_multisort($keys, $sortOrder, $sort_flags, $records);
}
$entry_keys = array_filter($entry_keys, function ($key) use ($search_clauses, $filter_funct, $fields, &$records) {
if (is_callable($filter_funct) && !$filter_funct($records[$key])) {
// not applicable according to $filter_funct()
return false;
} elseif (!empty($search_clauses)) {
foreach ($search_clauses as $clause) {
$matches = false;
foreach ($records[$key] as $itemkey => $itemval) {
if (!empty($fields) && !in_array($itemkey, $fields)) {
continue;
}
if (is_array($itemval)) {
$tmp = [];
array_walk_recursive($itemval, function ($a) use (&$tmp) {
$tmp[] = $a;
});
$itemval = implode(' ', $tmp);
}
if (stripos((string)$itemval, $clause) !== false) {
$matches = true;
}
}
if (!$matches) {
return $matches;
}
}
return true;
} else {
return true;
}
});
$formatted = array_map(function ($value) use (&$records) {
foreach ($records[$value] as $ekey => $evalue) {
$item[$ekey] = $evalue;
}
return $item;
}, array_slice($entry_keys, $offset, $itemsPerPage));
return [
'total' => count($entry_keys),
'rowCount' => count($formatted),
'current' => $currentPage,
'rows' => $formatted,
];
}
/**
* passtru recordset (key value store) as csv output
* @param array $records dataset to export (e.g. [['field' => 'value'], ['field' => 'value']])
*/
protected function exportCsv(
$records,
$headers = [
'Content-Type: text/csv', 'Content-Transfer-Encoding: binary', 'Pragma: no-cache', 'Expires: 0'
]
) {
$records = is_array($records) ? $records : [];
$stream = fopen('php://temp', 'rw+');
if (isset($records[0])) {
fputcsv($stream, array_keys($records[0]));
}
foreach ($records as $record) {
fputcsv($stream, $record);
}
foreach ($headers as $header) {
$parts = explode(':', $header, 2);
$this->response->setHeader($parts[0], ltrim($parts[1]));
}
rewind($stream);
$this->response->setContent($stream);
}
/**
* passtru configd stream
* @param string $action configd action to perform
* @param array $params list of parameters
* @param array $headers http headers to send before pushing data
* @param int $poll_timeout poll timeout after connect
*/
protected function configdStream(
$action,
$params = [],
$headers = [
'Content-Type: application/json', 'Content-Transfer-Encoding: binary', 'Pragma: no-cache', 'Expires: 0'
],
$poll_timeout = 2
) {
$response = (new Backend())->configdpStream($action, $params, $poll_timeout);
foreach ($headers as $header) {
$parts = explode(':', $header, 2);
$this->response->setHeader($parts[0], ltrim($parts[1]));
}
$this->response->setContent($response);
}
/**
* parse raw json type content to POST data depending on content type
* (only for api calls)
* @return string
*/
private function parseJsonBodyData()
{
switch (strtolower(str_replace(' ', '', $this->request->getHeader('CONTENT_TYPE')))) {
case 'application/json':
case 'application/json;charset=utf-8':
$jsonRawBody = $this->request->getJsonRawBody();
if (empty($this->request->getRawBody()) && empty($jsonRawBody)) {
return "Invalid JSON syntax";
}
$_POST = is_array($jsonRawBody) ? $jsonRawBody : [];
foreach ($_POST as $key => $value) {
$_REQUEST[$key] = $value;
}
break;
case 'application/x-www-form-urlencoded':
case 'application/x-www-form-urlencoded;charset=utf-8':
// valid non parseable content
break;
default:
if (!empty($this->request->getRawBody())) {
$this->getLogger()->warning('unparsable Content-Type:' . $this->request->getHeader('CONTENT_TYPE') . ' received');
}
break;
}
return null;
}
/**
* is external client (other then session authenticated)
* @return bool
*/
protected function isExternalClient()
{
return !empty($this->request->getHeader('Authorization'));
}
/**
* before routing event.
* Handles authentication and authentication of user requests
* In case of API calls, also prevalidates if request can be executed to return a more readable response
* to the user.
* @param Dispatcher $dispatcher
* @return null|bool
*/
public function beforeExecuteRoute($dispatcher)
{
// handle authentication / authorization
if ($this->isExternalClient()) {
// Authorization header send, handle API request
$authHeader = explode(' ', $this->request->getHeader('Authorization'));
if (count($authHeader) > 1) {
$key_secret_hash = $authHeader[1];
$key_secret = explode(':', base64_decode($key_secret_hash));
if (count($key_secret) > 1) {
$apiKey = $key_secret[0];
$apiSecret = $key_secret[1];
$authFactory = new AuthenticationFactory();
$authenticator = $authFactory->get("Local API");
if ($authenticator->authenticate($apiKey, $apiSecret)) {
$authResult = $authenticator->getLastAuthProperties();
if (array_key_exists('username', $authResult)) {
// check ACL if user is returned by the Authenticator object
$acl = new ACL();
if (!$acl->isPageAccessible($authResult['username'], $_SERVER['REQUEST_URI'])) {
$this->getLogger()->error("uri " . $_SERVER['REQUEST_URI'] .
" not accessible for user " . $authResult['username'] . " using api key " .
$apiKey);
// not authenticated
$this->response->setStatusCode(403, "Forbidden");
$this->response->setContentType('application/json', 'UTF-8');
$this->response->setContent(['status' => 403,'message' => 'Forbidden']);
$this->response->send();
return false;
} else {
// link username on successful login
$this->logged_in_user = $authResult['username'];
// if body is send as json data, parse to $_POST first
$dispatchError = $this->parseJsonBodyData();
if ($dispatchError != null) {
$this->response->setStatusCode(400, "Bad Request");
$this->response->setContentType('application/json', 'UTF-8');
$this->response->setContent(['status' => 400, 'message' => $dispatchError]);
$this->response->send();
return false;
}
// pass revision context to config object
Config::getInstance()->setRevisionContext([
'username' => $authResult['username'],
'user_apitoken' => $apiKey
]);
return true;
}
}
} else {
$this->getLogger()->error("uri " . $_SERVER['REQUEST_URI'] .
" authentication failed for api key " . $apiKey);
}
}
}
// not authenticated
$this->response->setStatusCode(401, "Unauthorized");
$this->response->setContentType('application/json', 'UTF-8');
$this->response->setContent(['status' => 401, 'message' => 'Authentication Failed']);
$this->response->send();
return false;
} else {
// handle UI ajax requests
// use session data and ACL to validate request.
if (!$this->doAuth()) {
if (!$this->session->has("Username")) {
$this->response->setStatusCode(401, "Unauthorized");
} else {
$this->response->setStatusCode(403, "Forbidden");
}
return false;
}
// check for valid csrf on post requests
$csrf_valid = (new Security($this->session, $this->request))->checkToken(
null,
$this->request->getHeader('X_CSRFTOKEN')
);
if (
($this->request->isPost() ||
$this->request->isPut() ||
$this->request->isDelete()
) && !$csrf_valid
) {
// missing csrf, exit.
$this->getLogger()->error("no matching csrf found for request");
$this->response->setStatusCode(403, "Forbidden");
return false;
}
// when request is using a json body (based on content type), parse it first
$this->parseJsonBodyData();
// link username on successful login
$this->logged_in_user = $this->session->get("Username");
}
}
/**
* process API results, serialize return data to json.
* @param $dispatcher
* @return string json data
*/
public function afterExecuteRoute($dispatcher)
{
// process response, serialize to json object
$data = $dispatcher->getReturnedValue();
if (is_array($data)) {
$this->response->setContentType('application/json', 'UTF-8');
if ($this->isExternalClient()) {
$this->response->setContent(json_encode($data));
} else {
$this->response->setContent(htmlspecialchars(json_encode($data), ENT_NOQUOTES));
}
} elseif (is_string($data)) {
// XXX: fallback, controller returned data as string. a deprecation message might be an option here.
$this->response->setContent($data);
}
return $this->response->send();
}
}