Initial checkin of resolver.php

- use \\ instead of just \ in FileName
- Use file_put_contents instead of open/write
- Use file_exist instead of stat
- Added /lib directory
- Moved /tftpboot/index.cfg -> /config.ini
- Moved /tftpboot/resolver.php -> /lib/resolver.php
- Added /lib/config.php
  - include /lib/config.php in resolver.php and index.php
- Changed $config array
- Remove print_r($config['main']['base_path']) from config.php
- Add isValidRequest() function
- Use Boolean in tree_base data
- Simplify config['subdirs'] substitution
- Add lib/utils.php file
- Added simple shell/utf/html escape checking
- Added a collection of test cases (we need some more escape checking ones)
- Added lib/logger.php (copied from tftpserver.php, so that it can be reused for that).
- Clarify config.ini logformat
- Update logger implementation
- Replaced index.php with version that uses lib/resolver.php
- Replaced ../etc/nginx/sites-available/tftpboot Example file
This commit is contained in:
Diederik de Groot
2020-02-15 19:30:00 +01:00
parent d5bdcd4c30
commit a4ebaee776
9 changed files with 454 additions and 570 deletions

80
lib/config.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
include_once("logger.php");
$base_path = !empty($_SERVER['DOCUMENT_ROOT']) ? realpath($_SERVER['DOCUMENT_ROOT'] . "/../"): realpath(getcwd()."/../");
$base_config = Array(
'main' => Array(
'debug' => 1,
'default_language' => 'English_United_States',
'log_type' => "NULL",
'log_level' => LOG_EMERG
),
'subdirs' => Array(
'tftproot' => 'tftpboot',
'firmware' => 'firmware',
'settings' => 'settings',
'wallpapers' => 'wallpapers',
'ringtones' => 'ringtones',
'locales' => 'locales',
'countries' => 'countries',
'languages' => 'languages',
)
);
$tree_base = Array(
'settings' => array('path' => 'tftproot', "strip" => TRUE),
'wallpapers' => array('path' => 'tftproot', "strip" => FALSE),
'ringtones' => array('path' => 'tftproot', "strip" => TRUE),
'locales' => array('path' => 'tftproot', "strip" => TRUE),
'firmware' => array('path' => 'tftproot', "strip" => TRUE),
'languages' => array('path' => 'locales', "strip" => FALSE),
'countries' => array('path' => 'locales', "strip" => FALSE),
'default_language' => array('path' => 'locales', "strip" => TRUE),
);
# Merge config
$ini_array = parse_ini_file('../config.ini', TRUE, INI_SCANNER_TYPED);
if (!empty($ini_array)) {
$config = array_merge($base_config, $ini_array);
}
# build new config['subdirs'] paths substituting bases from tree_base
foreach ($tree_base as $key => $value) {
$tmp = $config;
if (!empty($tmp['subdirs'][$key])) {
if (substr($tmp['subdirs'][$key], 0, 1) !== "/") {
if (is_array($tmp['subdirs'][$value['path']])) {
$path = $tmp['subdirs'][$value['path']]['path'].'/'.$tmp['subdirs'][$key];
} else {
$path = $tmp['subdirs'][$value['path']].'/'.$tmp['subdirs'][$key];
}
}
$config['subdirs'][$key] = array('path' => $path, 'strip' => $value['strip']);
}
}
$config['main']['base_path'] = $base_path;
$config['main']['tftproot'] = (!empty($config['main']['tftproot'])) ? $base_path . "tftpboot" : '/tftpboot';
switch($config['main']['log_type']) {
case 'SYSLOG':
$logger = new Logger_Syslog($config['main']['log_level']);
break;
case 'FILE':
if (!isempty($config['main']['log_file'])) {
$logger = new Logger_Filename($config['main']['log_level'], $config['main']['log_file']);
}
break;
case 'STDOUT':
$logger = new Logger_Stdout($config['main']['log_level']);
break;
case 'STDERR':
$logger = new Logger_Stderr($config['main']['log_level']);
break;
default:
$logger = new Logger_Null($config['main']['log_level']);
}
# Fixup debug
$print_debug = (!empty($config['main']['debug'])) ? $config['main']['debug'] : 'off';
$print_debug = ($print_debug == 1) ? 'on' : $print_debug;
?>

94
lib/logger.php Normal file
View File

@@ -0,0 +1,94 @@
<?php
/* Note about the Logger class:
* The "priority" and "minimum should be one of the constants used for syslog.
* See: http://php.net/manual/en/function.syslog.php
* They are: LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE,
* LOG_INFO, LOG_DEBUG
* Note that LOG_EMERG, LOG_ALERT, and LOG_CRIT are not really relevant to a
* tftp server - these represent instability in the entire operating system.
* Note that the number they are represented by are in reverse order -
* LOG_EMERG is the lowest, LOG_DEBUG the highest.
*/
abstract class Logger
{
function __construct($minimum)
{
$this->minimum = $minimum;
}
function shouldlog($priority)
{
// Note: this looks reversed, but is correct
// the priority must be AT LEAST the minimum,
// because higher priorities represent lower numbers.
return $priority <= $this->minimum;
}
abstract function log($priority, $message);
}
class Logger_Null extends Logger
{
function log($priority, $message)
{
}
}
class Logger_Syslog extends Logger
{
function log($priority, $message)
{
if($this->shouldlog($priority))
syslog($priority,$message);
}
}
class Logger_Filehandle extends Logger
{
private $priority_map = array(
LOG_DEBUG => "D",
LOG_INFO => "I",
LOG_NOTICE => "N",
LOG_WARNING => "W",
LOG_ERR => "E",
LOG_CRIT => "C",
LOG_ALERT => "A",
LOG_EMERG => "!"
);
function __construct($minimum, $filehandle, $dateformat = "r")
{
$this->filehandle = $filehandle;
$this->dateformat = $dateformat;
return parent::__construct($minimum);
}
function log($priority, $message)
{
if($this->shouldlog($priority))
fwrite($this->filehandle, date($this->dateformat) . ": " . $this->priority_map[$priority] . " $message\n");
}
}
class Logger_Filename extends Logger_Filehandle
{
function __construct($minimum, $filename, $dateformat = "r")
{
return parent::__construct($minimum, fopen($filename, "a"), $dateformat);
}
}
class Logger_Stderr extends Logger_Filehandle
{
function __construct($minimum, $dateformat = "r")
{
return parent::__construct($minimum, STDERR, $dateformat);
}
}
class Logger_Stdout extends Logger_Filehandle
{
function __construct($minimum, $dateformat = "r")
{
return parent::__construct($minimum, STDOUT, $dateformat);
}
}
?>

168
lib/resolver.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
include_once("config.php");
include_once("utils.php");
/* Todo:
✔️ setup logging
✔️ read config.file
✔? improve error handling
✔️? secure urlencoding/urldecoding
✔️? don't allow browsing
- check source ip-range
- check HTTPHeader for known BrowserTypes
- Could use some more test-cases, especially error ones
*/
class Resolver {
private $isDirty = FALSE;
private $cache = array();
private $config;
//private $logger;
function __construct($config) {
//global $logger;
$this->config = $config;
//$this->logger = $logger;
if(file_exists($this->config['main']['cache_filename'])) {
$this->cache = unserialize(file_get_contents($config['main']['cache_filename']));
} else {
$this->buildCleanCache();
}
}
function __destruct() {
// $this->printCache()
if ($this->isDirty) {
if (!file_put_contents($this->config['main']['cache_filename'], serialize($this->cache))) {
$this->log_error_and_throw("Could not write to file '".$this->config['cache_filename']."' at Resolver::destruct");
}
}
}
function log_error_and_throw($message) {
global $logger;
$logger->log('LOG_ERROR', $message);
throw new Exception($message);
}
function log_debug($message) {
global $logger;
$logger->log('LOG_DEBUG', $message);
}
function searchForFile($filename) {
foreach($this->config['subdirs'] as $key => $value) {
if ($key === "firmware" || $key === "tftproot" ) {
continue;
}
$path = realpath($this->config['main']['base_path'] . "/" . $value['path'] . "/$filename");
if (file_exists($path)) {
$this-> addFile($filename, $path);
return $path;
}
}
$this->log_error_and_throw("File '$filename' does not exist");
}
function buildCleanCache() {
foreach($this->config['subdirs'] as $key =>$value) {
if ($key === "tftproot") {
continue;
}
$path = $this->config['main']['base_path'] . "/" . $value['path'] . "/";
$dir_iterator = new RecursiveDirectoryIterator($path);
$iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
if ($file->isFile()) {
if ($value['strip']) {
$this->addFile($file->getFileName(), $file->getPathname());
} else {
$subdir = basename(dirname($file->getPathname()));
$this->addFile('$subpath/'.$file->getFileName(), $file->getPathname());
}
}
}
}
$this->isDirty = TRUE;
}
function addFile($requestpath, $truepath) {
//$this->logger->log('LOG_DEBUG', "Adding $requestpath");
$this->log_debug("Adding $requestpath");
$this->cache[$requestpath] = $truepath;
$this->isDirty =TRUE;
}
function removeFile($requestpath) {
$this->log_debug("Removing $hash");
unset($this->cache[$requestpath]);
$this->isDirty = TRUE;
}
function validateRequest($request) {
/* make sure request does not startwith or contain: "/", "../" or "/./" */
/* make sure request only starts with filename or one of $config[$subdir]['locale'] or $config[$subdir]['wallpaper'] */
/* check uri/url decode */
if (!is_string($request)) {
$this->log_error_and_throw("Request is not a string");
}
$this->log_debug($request . ":" . escapeshellarg($request) . ":" . utf8_urldecode($request) . "\n");
$escaped_request = escapeshellarg(utf8_urldecode($request));
if ($escaped_request !== "'" . $request . "'") {
$this->log_error_and_throw("Request '$request' contains invalid characters");
}
if (strstr($escaped_request, "..")) {
$this->log_error_and_throw("Request '$request' containst '..'");
}
}
function resolve($request) /* canthrow */ {
$this->validateRequest($request);
$path = '';
if (array_key_exists($request, $this->cache)) {
if ($path = $this->cache[$request]) {
if (!file_exists($path)) {
$this->removeFile($request);
$this->log_error_and_throw("File '$request' does not exist on FS");
}
return $path;
}
}
if ($this->searchForFile($request)) {
return $this->cache[$request];
}
return $path;
}
/* temporary */
function printCache() {
print_r($this->cache);
}
}
// Testing
if(defined('STDIN') ) {
$resolver = new Resolver($config);
$test_cases = Array(
Array('request' => 'jar70sccp.9-4-2ES26.sbn', 'expected' => '/tftpboot/firmware/7970/jar70sccp.9-4-2ES26.sbn', 'throws' => FALSE),
Array('request' => 'Russian_Russian_Federation/be-sccp.jar', 'expected' => '/tftpboot/locales/languages/Russian_Russian_Federation/be-sccp.jar', 'throws' => FALSE),
Array('request' => 'Spain/g3-tones.xml', 'expected' => '/tftpboot/locales/countries/Spain/g3-tones.xml', 'throws' => FALSE),
Array('request' => '320x196x4/Chan-SCCP-b.png', 'expected' => '/tftpboot/wallpapers/320x196x4/Chan-SCCP-b.png', 'throws' => FALSE),
Array('request' => 'XMLDefault.cnf.xml', 'expected' => '/tftpboot/settings/bak/XMLDefault.cnf.xml', 'throws' => FALSE),
Array('request' => '../XMLDefault.cnf.xml', 'expected' => '', 'throws' => TRUE),
Array('request' => 'XMLDefault.cnf.xml/../../text.xml', 'expected' => '', 'throws' => TRUE),
);
foreach($test_cases as $test) {
try {
$result = $resolver->resolve($test['request']);
if ($result !== $base_path . $test['expected']) {
print("Error: expected result does not match what we got\n");
print("request:'".$test['request']."', result:'" . $base_path . $test['expected'] . "'\n");
} else {
print("'" . $test['request'] . "' => '" . $result . "'\n");
}
} catch (Exception $e) {
if (!$test['throws']) {
print("Error: request was expected to throw: $e\n");
} else {
print("'" . $test['request'] . "' => throws error as expected\n");
}
}
}
unset($resolver);
#unlink($CACHEFILE_NAME);
}
?>

10
lib/utils.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
//
// Helper functions
//
function utf8_urldecode($str) {
$str = preg_replace("/%u([0-9a-f]{3,4})/i","&#x\\1;",urldecode($str));
return html_entity_decode($str,null,'UTF-8');;
}
?>