Sindbad~EG File Manager
<?php
/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2022 Teclib' and contributors.
* @copyright 2003-2014 by the INDEPNET Development Team.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/
namespace Glpi\Agent\Communication;
use DOMDocument;
use DOMElement;
use Glpi\Agent\Communication\Headers\Common;
use Glpi\Application\ErrorHandler;
use Toolbox;
/**
* Handle agent requests
* Both XML (legacy) and JSON inventory formats are supported.
*
* @see https://github.com/glpi-project/inventory_format/blob/master/inventory.schema.json
*/
abstract class AbstractRequest
{
const DEFAULT_FREQUENCY = 24;
const XML_MODE = 0;
const JSON_MODE = 1;
//FusionInventory agent
const PROLOG_QUERY = 'prolog';
const INVENT_QUERY = 'inventory';
const SNMP_QUERY = 'snmp';
const OLD_SNMP_QUERY = 'snmpquery';
//GLPI AGENT ACTION
const CONTACT_ACTION = 'contact';
const REGISTER_ACTION = 'register';
const CONFIG_ACTION = 'configuration';
const INVENT_ACTION = 'inventory';
const NETDISCOVERY_ACTION = 'netdiscovery';
const NETINV_ACTION = 'netinventory';
const ESX_ACTION = 'esx';
const COLLECT_ACTION = 'collect';
const DEPLOY_ACTION = 'deploy';
const WOL_ACTION = 'wakeonlan';
const GET_PARAMS = 'get_params';
//GLPI AGENT TASK
const INVENT_TASK = 'inventory';
const NETDISCOVERY_TASK = 'netdiscovery';
const NETINV_TASK = 'netinventory';
const ESX_TASK = 'esx';
const COLLECT_TASK = 'collect';
const DEPLOY_TASK = 'deploy';
const WOL_TASK = 'wakeonlan';
const REMOTEINV_TASK = 'remoteinventory';
const COMPRESS_NONE = 0;
const COMPRESS_ZLIB = 1;
const COMPRESS_GZIP = 2;
const COMPRESS_BR = 3;
const COMPRESS_DEFLATE = 4;
/** @var integer */
protected $mode;
/** @var string */
private $deviceid;
/** @var DOMDocument */
private $response;
/** @var integer */
private $compression;
/** @var boolean */
private $error = false;
/** @var boolean */
protected $test_rules = false;
/** @var Glpi\Agent\Communication\Headers\Common */
protected $headers;
/** @var int */
private $http_response_code = 200;
/** @var string */
protected $query;
public function __construct()
{
$this->headers = $this->initHeaders();
$this->handleContentType($_SERVER['CONTENT_TYPE'] ?? false);
}
abstract protected function initHeaders(): Common;
/**
* Set mode and initialize response
*
* @param integer $mode Expected mode. One of *_MODE constants
*
* @return void
*
* @throw RuntimeException
*/
protected function setMode($mode)
{
$this->mode = $mode;
switch ($mode) {
case self::XML_MODE:
$this->response = new DOMDocument();
$this->response->appendChild(
$this->response->createElement('REPLY')
);
break;
case self::JSON_MODE:
$this->response = [];
break;
default:
throw new \RuntimeException("Unknown mode $mode");
}
$this->prepareHeaders();
}
/**
* Guess import mode
*
* @return void
*/
private function guessMode($contents): void
{
// In the case handleContentType() didn't set mode, just check $contents first char
if ($contents[0] === '{') {
$this->setMode(self::JSON_MODE);
} else {
//defaults to XML; whose validity is checked later.
$this->setMode(self::XML_MODE);
}
}
/**
* Handle request headers
*
* @param $data
*/
public function handleHeaders()
{
$req_headers = [];
if (!function_exists('getallheaders')) {
foreach ($_SERVER as $name => $value) {
/* RFC2616 (HTTP/1.1) defines header fields as case-insensitive entities. */
if (strtolower(substr($name, 0, 5)) == 'http_') {
$req_headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
} else {
$req_headers = getallheaders();
}
$this->headers->setHeaders($req_headers);
}
/**
* Handle agent request
*
* @param mixed $data Sent data
*
* @return boolean
*/
public function handleRequest($data): bool
{
// Some network inventories may request may contains lots of information.
// e.g. a Huawei S5720-52X-LI-AC inventory file may weigh 20MB,
// and GLPI will consume about 500MB of memory to handle it,
// and may take up to 2 minutes on server that has low performances.
//
// Setting limits to 1GB / 5 minutes should permit to handle any inventories request.
$memory_limit = (int)Toolbox::getMemoryLimit();
$max_execution_time = ini_get('max_execution_time');
if ($memory_limit > 0 && $memory_limit < (1024 * 1024 * 1024)) {
ini_set('memory_limit', '1024M');
}
if ($max_execution_time > 0 && $max_execution_time < 300) {
ini_set('max_execution_time', '300');
}
if ($this->compression !== self::COMPRESS_NONE) {
switch ($this->compression) {
case self::COMPRESS_ZLIB:
$data = gzuncompress($data);
break;
case self::COMPRESS_GZIP:
$data = gzdecode($data);
break;
case self::COMPRESS_BR:
$data = brotli_uncompress($data);
break;
case self::COMPRESS_DEFLATE:
$data = gzinflate($data);
break;
default:
throw new \UnexpectedValueException("Unknown compression mode" . $this->compression);
}
}
if ($this->mode === null) {
$this->guessMode($data);
}
//load and check data
switch ($this->mode) {
case self::XML_MODE:
return $this->handleXMLRequest($data);
case self::JSON_MODE:
return $this->handleJSONRequest($data);
}
return false;
}
/**
* Handle Query
*
* @param string $action Action (one of self::*_ACTION)
* @param mixed $content Contents, optional
*
* @return boolean
*/
abstract protected function handleAction($action, $content = null): bool;
/**
* Handle Task
*
* @param string $task Task (one of self::*_TASK)
*
* @return array
*/
abstract protected function handleTask($task): array;
/**
* Handle XML request
*
* @param string $data Sent XML
*
* @return boolean
*/
public function handleXMLRequest($data): bool
{
libxml_use_internal_errors(true);
if (mb_detect_encoding($data, 'UTF-8', true) === false) {
$data = utf8_encode($data);
}
$xml = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA);
if (!$xml) {
$xml_errors = libxml_get_errors();
/* @var \LibXMLError $xml_error */
foreach ($xml_errors as $xml_error) {
ErrorHandler::getInstance()->handleError(
E_USER_WARNING,
$xml_error->message,
$xml_error->file,
$xml_error->line
);
}
$this->addError('XML not well formed!', 400);
return false;
}
$this->deviceid = (string)$xml->DEVICEID;
//query is not mandatory. Defaults to inventory
$action = self::INVENT_QUERY;
if (property_exists($xml, 'QUERY')) {
$action = strtolower((string)$xml->QUERY);
}
return $this->handleAction($action, $xml);
}
/**
* Handle JSON request
*
* @param string $data Sent JSON
*
* @return boolean
*/
public function handleJSONRequest($data): bool
{
$jdata = json_decode($data);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->addError('JSON not well formed!', 400);
return false;
}
$this->deviceid = $jdata->deviceid ?? null;
$action = self::INVENT_ACTION;
if (property_exists($jdata, 'action')) {
$action = $jdata->action;
} else if (property_exists($jdata, 'query')) {
$action = $jdata->query;
}
return $this->handleAction($action, $jdata);
}
/**
* Get request mode
*
* @return null|integer One of self::*_MODE
*/
public function getMode()
{
return $this->mode;
}
/**
* Adds an error
*
* @param string $message Error message
* @param integer $code HTTP response code
*
* @return void
*/
public function addError($message, $code = 500)
{
if ($code >= 400) {
$this->error = true;
}
$this->http_response_code = $code;
if (!empty($message)) {
if ($this->mode === self::JSON_MODE) {
$this->addToResponse([
'status' => 'error',
'message' => \Html::resume_text($message, 250),
'expiration' => self::DEFAULT_FREQUENCY
]);
} else {
$this->addToResponse(['ERROR' => \Html::resume_text($message, 250)]);
}
}
}
/**
* Add elements to response
*
* @param array $entries Array of key => values entries
*
* @return void
*/
public function addToResponse(array $entries)
{
if ($this->mode === self::XML_MODE) {
$root = $this->response->documentElement;
foreach ($entries as $name => $content) {
$this->addNode($root, $name, $content);
}
} else {
foreach ($entries as $name => $content) {
if ($name == "message" && isset($this->response[$name])) {
$this->response[$name] .= ";$content";
} else if ($name == "disabled") {
$this->response[$name][] = $content;
} else {
$this->response[$name] = $content;
}
}
}
}
/**
* Add node to response for XML_MODE
*
* @param DOMElement $parent Parent element
* @param string $name Element name to create
* @param string|array|null $content Element contents, if any
*
* @return void
*/
private function addNode(DOMElement $parent, $name, $content)
{
if (is_array($content) && !isset($content['content']) && !isset($content['attributes'])) {
$node = is_string($name)
? $parent->appendChild($this->response->createElement($name))
: $parent;
foreach ($content as $sname => $scontent) {
$this->addNode($node, $sname, $scontent);
}
} else {
$attributes = [];
if (is_array($content) && isset($content['content']) && isset($content['attributes'])) {
$attributes = $content['attributes'];
$content = $content['content'];
}
$new_node = $this->response->createElement(
$name,
$content
);
if (count($attributes)) {
foreach ($attributes as $aname => $avalue) {
$attr = $this->response->createAttribute($aname);
$attr->value = $avalue;
$new_node->appendChild($attr);
}
}
$parent->appendChild($new_node);
}
}
/**
* Get content-type
*
* @return string
*/
public function getContentType(): string
{
if ($this->mode === null) {
throw new \RuntimeException("Mode has not been set");
}
if ($this->compression !== null) {
switch (strtolower($this->compression)) {
case self::COMPRESS_ZLIB:
return 'application/x-compress-zlib';
case self::COMPRESS_GZIP:
return 'application/x-compress-gzip';
case self::COMPRESS_BR:
return 'application/x-br';
case self::COMPRESS_DEFLATE:
return 'application/x-compress-deflate';
}
}
switch ($this->mode) {
case self::XML_MODE:
return 'application/xml';
case self::JSON_MODE:
return 'application/json';
default:
throw new \RuntimeException("Unknown mode " . $this->mode);
}
}
/**
* Get response
*
* @return string
*/
public function getResponse(): string
{
// Default to return empty response on no response set
$data = "";
if ($this->response !== null) {
if ($this->mode === null) {
throw new \RuntimeException("Mode has not been set");
}
switch ($this->mode) {
case self::XML_MODE:
$data = $this->response->saveXML();
break;
case self::JSON_MODE:
$data = json_encode($this->response);
break;
default:
throw new \UnexpectedValueException("Unknown mode " . $this->mode);
}
if ($this->compression === null) {
throw new \RuntimeException("Compression has not been set");
}
if ($this->compression !== self::COMPRESS_NONE) {
switch ($this->compression) {
case self::COMPRESS_ZLIB:
$data = gzcompress($data);
break;
case self::COMPRESS_GZIP:
$data = gzencode($data);
break;
case self::COMPRESS_BR:
$data = brotli_compress($data);
break;
case self::COMPRESS_DEFLATE:
$data = gzdeflate($data);
break;
default:
throw new \UnexpectedValueException("Unknown compression mode" . $this->compression);
}
}
}
return $data;
}
/**
* Handle Content-Type header
*
* @param string $type Content type
*
* @return void
*/
public function handleContentType($type)
{
switch (strtolower($type)) {
case 'application/x-zlib':
case 'application/x-compress-zlib':
$this->compression = self::COMPRESS_ZLIB;
break;
case 'application/x-gzip':
case 'application/x-compress-gzip':
$this->compression = self::COMPRESS_GZIP;
break;
case 'application/x-br':
case 'application/x-compress-br':
if (!function_exists('brotli_compress')) {
$this->addError('Brotli PHP extension is missing!', 415);
} else {
$this->compression = self::COMPRESS_BR;
}
break;
case 'application/x-deflate':
case 'application/x-compress-deflate':
$this->compression = self::COMPRESS_DEFLATE;
break;
case 'application/xml':
$this->compression = self::COMPRESS_NONE;
$this->setMode(self::XML_MODE);
break;
case 'application/json':
$this->setMode(self::JSON_MODE);
$this->compression = self::COMPRESS_NONE;
break;
case 'text/plain': //probably JSON
default:
$this->compression = self::COMPRESS_NONE;
break;
}
}
/**
* Is current request in error?
*
* @return boolean
*/
public function inError()
{
return $this->error;
}
public function testRules(): self
{
$this->test_rules = true;
return $this;
}
/**
* Accepted encodings
*
* @return string[]
*/
public function acceptedEncodings(): array
{
$encodings = [
'gzip',
'deflate'
];
if (!function_exists('brotli_compress')) {
$encodings[] = 'br';
}
return $encodings;
}
/**
* Prepare HTTP headers
*
* @return void
*/
private function prepareHeaders()
{
$headers = [
'Content-Type' => $this->getContentType(),
];
$this->headers->setHeaders($headers);
}
/**
* Get HTTP headers
*
* @param boolean $legacy Set to true to shunt required headers checks
*
* @return array
*/
public function getHeaders($legacy = true): array
{
return $this->headers->getHeaders($legacy);
}
public function getHttpResponseCode(): int
{
return $this->http_response_code;
}
public function getQuery(): ?string
{
return $this->query;
}
public function getDeviceID(): string
{
return $this->deviceid;
}
}
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists