From 5064cb3e36a8caf56bf21603ccbe49b96d096015 Mon Sep 17 00:00:00 2001 From: Diederik de Groot Date: Sun, 22 Mar 2020 10:15:31 +0100 Subject: [PATCH] Adopted composer autoload way Split up classes into separate way, to allow autoloader to do it's work Signed-off-by: Diederik de Groot --- .gitattributes | 1 + .gitignore | 3 + composer.json | 5 +- lib/{config.php => ConfigParser.php} | 24 +- lib/Crypto/Certificate.php | 15 + lib/Crypto/Crypto.php | 12 + lib/Crypto/HashAlgo.php | 11 + lib/Crypto/Signer.php | 8 + lib/{device.php => Device.php} | 9 +- lib/Logger/Filehandle.php | 54 ++ lib/Logger/Filename.php | 12 + lib/Logger/Logger.php | 32 + lib/Logger/Null.php | 11 + lib/Logger/Stderr.php | 12 + lib/Logger/Stdout.php | 12 + lib/Logger/Syslog.php | 13 + lib/{resolve.php => Resolve.php} | 33 +- .../FileCache.php} | 28 +- lib/ResolveCache/ResolveCache.php | 24 + lib/TFTP/Error.php | 44 + lib/TFTP/Opcode.php | 56 ++ lib/TFTP/ReadTransfer.php | 161 ++++ lib/TFTP/Server.php | 361 ++++++++ lib/TFTP/Transfer.php | 150 ++++ lib/TFTP/TransferState.php | 39 + lib/TFTP/WriteTransfer.php | 158 ++++ lib/Utils.php | 33 + lib/{xml.php => XML.php} | 4 +- lib/crypto.php | 31 - lib/logger.php | 97 --- lib/tftp.php | 784 ------------------ lib/utils.php | 31 - srv/tftp/tftp_provisioner | 26 +- srv/web/index.php | 21 +- 34 files changed, 1292 insertions(+), 1023 deletions(-) create mode 100644 .gitattributes rename lib/{config.php => ConfigParser.php} (88%) create mode 100644 lib/Crypto/Certificate.php create mode 100644 lib/Crypto/Crypto.php create mode 100644 lib/Crypto/HashAlgo.php create mode 100644 lib/Crypto/Signer.php rename lib/{device.php => Device.php} (98%) create mode 100644 lib/Logger/Filehandle.php create mode 100644 lib/Logger/Filename.php create mode 100644 lib/Logger/Logger.php create mode 100644 lib/Logger/Null.php create mode 100644 lib/Logger/Stderr.php create mode 100644 lib/Logger/Stdout.php create mode 100644 lib/Logger/Syslog.php rename lib/{resolve.php => Resolve.php} (82%) rename lib/{resolveCache.php => ResolveCache/FileCache.php} (62%) create mode 100644 lib/ResolveCache/ResolveCache.php create mode 100644 lib/TFTP/Error.php create mode 100644 lib/TFTP/Opcode.php create mode 100644 lib/TFTP/ReadTransfer.php create mode 100644 lib/TFTP/Server.php create mode 100644 lib/TFTP/Transfer.php create mode 100644 lib/TFTP/TransferState.php create mode 100644 lib/TFTP/WriteTransfer.php create mode 100644 lib/Utils.php rename lib/{xml.php => XML.php} (95%) delete mode 100644 lib/crypto.php delete mode 100644 lib/logger.php delete mode 100644 lib/tftp.php delete mode 100644 lib/utils.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2a0e9d2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +tests/ export-ignore diff --git a/.gitignore b/.gitignore index 59d0f7f..7f2ec3c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ data/settings/*.tlv data/settings/authorized_keys data/settings/*.jar data/settings/*.json +vendor/ +bak/ +build/ diff --git a/composer.json b/composer.json index 1c64463..0c0ec05 100644 --- a/composer.json +++ b/composer.json @@ -26,12 +26,13 @@ }, "autoload": { "psr-4": { - "provision\\": "lib/" + "PROVISION\\": "lib/" } }, "require": { "php": ">=5.6.0", - "psr/log": ">= 1.0.0" + "psr/log": ">= 1.0.0", + "myclabs/php-enum": ">= 1.7.6" }, "require-dev": { "phing/phing": "*", diff --git a/lib/config.php b/lib/ConfigParser.php similarity index 88% rename from lib/config.php rename to lib/ConfigParser.php index 89185c5..2b8d17a 100644 --- a/lib/config.php +++ b/lib/ConfigParser.php @@ -1,10 +1,9 @@ config['main']['log_type']) { case 'SYSLOG': - $logger = new \SCCP\Logger\Logger_Syslog($this->config['main']['log_level']); + $logger = new Logger\Syslog($this->config['main']['log_level']); break; case 'FILE': if (!isempty($config['main']['log_file'])) { - $logger = new Logger_Filename($this->config['main']['log_level'], $this->config['main']['log_file']); + $logger = new Logger\Filename($this->config['main']['log_level'], $this->config['main']['log_file']); } break; case 'STDOUT': - $logger = new Logger_Stdout($this->config['main']['log_level']); + $logger = new Logger\Stdout($this->config['main']['log_level']); break; case 'STDERR': - $logger = new Logger_Stderr($this->config['main']['log_level']); + $logger = new Logger\Stderr($this->config['main']['log_level']); break; default: - $logger = new Logger_Null($this->config['main']['log_level']); + $logger = new Logger\Null($this->config['main']['log_level']); } $this->config['main']['logger'] = $logger; } @@ -200,11 +199,4 @@ class ConfigParser { return $this->config; } } - -$configParser = new ConfigParser($base_path, "config.ini"); -$config = $configParser->getConfiguration(); - -# Fixup debug -//$print_debug = (!empty($config['main']['debug'])) ? $config['main']['debug'] : 'off'; -//$print_debug = ($print_debug == 1) ? 'on' : $print_debug; ?> diff --git a/lib/Crypto/Certificate.php b/lib/Crypto/Certificate.php new file mode 100644 index 0000000..53ca2ef --- /dev/null +++ b/lib/Crypto/Certificate.php @@ -0,0 +1,15 @@ +file = $file; + } + + public function hashAsBase64(int $hashAlg) : string { + return ""; + } +} +?> \ No newline at end of file diff --git a/lib/Crypto/Crypto.php b/lib/Crypto/Crypto.php new file mode 100644 index 0000000..1278192 --- /dev/null +++ b/lib/Crypto/Crypto.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/lib/Crypto/Signer.php b/lib/Crypto/Signer.php new file mode 100644 index 0000000..5b37564 --- /dev/null +++ b/lib/Crypto/Signer.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/lib/device.php b/lib/Device.php similarity index 98% rename from lib/device.php rename to lib/Device.php index a81f87b..9b8128c 100644 --- a/lib/device.php +++ b/lib/Device.php @@ -1,11 +1,10 @@ "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 Filename extends Filehandle +{ + function __construct($minimum, $filename, $dateformat = "r") + { + return parent::__construct($minimum, fopen($filename, "a"), $dateformat); + } +} + +class Stderr extends Filehandle +{ + function __construct($minimum, $dateformat = "r") + { + return parent::__construct($minimum, STDERR, $dateformat); + } +} +class Stdout extends Filehandle +{ + function __construct($minimum, $dateformat = "r") + { + return parent::__construct($minimum, STDOUT, $dateformat); + } +} + +?> \ No newline at end of file diff --git a/lib/Logger/Filename.php b/lib/Logger/Filename.php new file mode 100644 index 0000000..3d6952e --- /dev/null +++ b/lib/Logger/Filename.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/lib/Logger/Logger.php b/lib/Logger/Logger.php new file mode 100644 index 0000000..c634d51 --- /dev/null +++ b/lib/Logger/Logger.php @@ -0,0 +1,32 @@ +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); +} +?> \ No newline at end of file diff --git a/lib/Logger/Null.php b/lib/Logger/Null.php new file mode 100644 index 0000000..6e2725d --- /dev/null +++ b/lib/Logger/Null.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/lib/Logger/Stderr.php b/lib/Logger/Stderr.php new file mode 100644 index 0000000..8785980 --- /dev/null +++ b/lib/Logger/Stderr.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/lib/Logger/Stdout.php b/lib/Logger/Stdout.php new file mode 100644 index 0000000..9f00021 --- /dev/null +++ b/lib/Logger/Stdout.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/lib/Logger/Syslog.php b/lib/Logger/Syslog.php new file mode 100644 index 0000000..0b8cb12 --- /dev/null +++ b/lib/Logger/Syslog.php @@ -0,0 +1,13 @@ +shouldlog($priority)) + syslog($priority,$message); + } +} +?> \ No newline at end of file diff --git a/lib/resolve.php b/lib/Resolve.php similarity index 82% rename from lib/resolve.php rename to lib/Resolve.php index c7e4d6d..fd9dee1 100644 --- a/lib/resolve.php +++ b/lib/Resolve.php @@ -1,13 +1,10 @@ config = $config; - $this->cache = new ResolveCache\fileCache($this->config['main']['cache_filename']); + $this->cache = new ResolveCache\FileCache($this->config['main']['cache_filename']); if ($this->cache->isDirty()) { $this->rebuildCache(); } @@ -60,12 +57,12 @@ class Resolve { $this->cache->addFile($filename, $path); return $path; } - Utils\log_error("File '$filename' does not exist"); + Utils::log_error("File '$filename' does not exist"); return ResolveResult::FileNotFound; } public function rebuildCache() { - Utils\log_debug("Rebuilding Cache, standby..."); + Utils::log_debug("Rebuilding Cache, standby..."); foreach($this->config['subdirs'] as $key =>$value) { if ($key === "data" || $key === "etc") { continue; @@ -103,21 +100,21 @@ class Resolve { /* make sure request only starts with filename or one of $config[$subdir]['locale'] or $config[$subdir]['wallpaper'] */ /* check uri/url decode */ if (!$request || empty($request)) { - Utils\log_error("Request is empty"); + Utils::log_error("Request is empty"); return ResolveResult::EmptyRequest; } if (!is_string($request)) { - Utils\log_error("Request is not a string"); + Utils::log_error("Request is not a string"); return ResolveResult::RequestNotAString; } - Utils\log_debug($request . ":" . escapeshellarg($request) . ":" . Utils\utf8_urldecode($request) . "\n"); - $escaped_request = escapeshellarg(Utils\utf8_urldecode($request)); + Utils::log_debug($request . ":" . escapeshellarg($request) . ":" . Utils::utf8_urldecode($request) . "\n"); + $escaped_request = escapeshellarg(Utils::utf8_urldecode($request)); if ($escaped_request !== "'" . $request . "'") { - Utils\log_error("Request '$request' contains invalid characters"); + Utils::log_error("Request '$request' contains invalid characters"); return ResolveResult::RequestContainsInvalidChar; } if (strstr($escaped_request, "..")) { - Utils\log_error("Request '$request' contains '..'"); + Utils::log_error("Request '$request' contains '..'"); return ResolveResult::RequestContainsPathWalk; } return ResolveResult::Ok; @@ -132,7 +129,7 @@ class Resolve { if (($path = $this->cache->getPath($request))) { if (!file_exists($path)) { $this->cache->removeFile($request); - Utils\log_error("File '$request' does not exist on FS"); + Utils::log_error("File '$request' does not exist on FS"); return ResolveResult::FileNotFound; } return $path; diff --git a/lib/resolveCache.php b/lib/ResolveCache/FileCache.php similarity index 62% rename from lib/resolveCache.php rename to lib/ResolveCache/FileCache.php index d5dfd1d..cd9d523 100644 --- a/lib/resolveCache.php +++ b/lib/ResolveCache/FileCache.php @@ -1,15 +1,9 @@ _cache_file."' at Resolver::destruct"); }*/ if (!file_put_contents($this->_cache_file, serialize($this->_cache))) { - Utils\log_error_and_throw("Could not write to file '".$this->_cache_file."' at Resolver::destruct"); + Utils::log_error_and_throw("Could not write to file '".$this->_cache_file."' at Resolver::destruct"); } } } @@ -45,7 +39,7 @@ class fileCache extends resolveCache { public function addFile($filename, $realpath) { if ($this->check($filename)) - Utils\log_error("Duplicate file:$filename"); /* should we prevent this ? */ + Utils::log_error("Duplicate file:$filename"); /* should we prevent this ? */ $this->_cache[$filename] = $realpath; $this->_isDirty =true; } @@ -69,16 +63,4 @@ class fileCache extends resolveCache { } } -/* -class sqliteCache extends resolveCache { - function __construct() { - } - function __destruct() { - } - public function addFile($filename, $realpath); - public function removeFile($filename); - public function check($filename); - public function getPath($filename); -} -*/ ?> \ No newline at end of file diff --git a/lib/ResolveCache/ResolveCache.php b/lib/ResolveCache/ResolveCache.php new file mode 100644 index 0000000..96d9b35 --- /dev/null +++ b/lib/ResolveCache/ResolveCache.php @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/lib/TFTP/Error.php b/lib/TFTP/Error.php new file mode 100644 index 0000000..d7b6d5a --- /dev/null +++ b/lib/TFTP/Error.php @@ -0,0 +1,44 @@ + + * + * MIT License: + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +declare(strict_types=1); +namespace PROVISION\TFTP; + +use PROVISION\Logger as Logger; + +class Error +{ + const NOT_DEFINED = 0; // see error message instead of error code + const FILE_NOT_FOUND = 1; + const ACCESS_VIOLATION = 2; + const DISK_FULL = 3; + const ILLEGAL_OPERATION = 4; + const UNKNOWN_TID = 5; // unknown transfer (id is ip:port pair) + const FILE_ALREADY_EXISTS = 6; + const NO_SUCH_USER = 7; + const OACK_FAILURE = 8; +} +?> diff --git a/lib/TFTP/Opcode.php b/lib/TFTP/Opcode.php new file mode 100644 index 0000000..a0f3e84 --- /dev/null +++ b/lib/TFTP/Opcode.php @@ -0,0 +1,56 @@ + + * + * MIT License: + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +declare(strict_types=1); +namespace PROVISION\TFTP; + +use PROVISION\Logger as Logger; + +class Opcode +{ + public static function name($v) + { + static $names = array(TFTPOpcode::RRQ => "RRQ", + TFTPOpcode::WRQ => "WRQ", + TFTPOpcode::DATA => "DATA", + TFTPOpcode::ACK => "ACK", + TFTPOpcode::ERROR => "ERROR", + TFTPOpcode::OACK => "OACK"); + if(isset($names[$v])) + return $names[$v]; + else + return "UNKNOWN"; + } + + const RRQ = 1; // read request + const WRQ = 2; // write request + const DATA = 3; // send data + const ACK = 4; // ack data + const ERROR = 5; + const OACK = 6; // option ack, instead of first ACK/DATA +} +?> diff --git a/lib/TFTP/ReadTransfer.php b/lib/TFTP/ReadTransfer.php new file mode 100644 index 0000000..4037208 --- /dev/null +++ b/lib/TFTP/ReadTransfer.php @@ -0,0 +1,161 @@ + + * + * MIT License: + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +declare(strict_types=1); +namespace PROVISION\TFTP; + +use PROVISION\Logger as Logger; +use PROVISION\TFTP as TFTP; + +class ReadTransfer extends TFTP\Transfer { + private $_last_recv_ack; + private $_last_sent_data; + private $_buffer; + private $_block; + private $_last_block; + + function __construct($server, $peer, $extensions) + { + parent::__construct($server, $peer, $extensions); + $this->_last_recv_ack = time(); + $this->_last_sent_data = $this->_last_recv_ack; + $this->_buffer = false; + $this->_block = 1; + $this->_last_block = 1; + + $this->log_debug("new read transfer"); + } + + private function current_block() + { + return substr($this->_buffer, + ($this->_block - 1) * $this->block_size, + $this->block_size); + } + + private function packet_data_current() + { + $this->_last_sent_data = time(); + + if($this->state == TFTPTransferState::SENDING_WAIT_OACK) + return $this->packet_oack(); + else + return TFTPServer::packet_data($this->_block, $this->current_block()); + } + + public function rrq($filename, $mode) + { + $this->log_debug("RRQ: filename $filename in $mode mode"); + + if($this->state != TFTPTransferState::READY) + return $this->illegal_operation("RRQ", "Not in ready state"); + + if(!$this->_server->exists($this->peer, $filename)) + return $this->terminal_info(TFTPError::FILE_NOT_FOUND, + "File $filename does not exist"); + + if(!$this->_server->readable($this->peer, $filename)) + return $this->terminal_info(TFTPError::ACCESS_VIOLATION, + "File $filename is not readable"); + + $this->_buffer = $this->_server->get($this->peer, $filename, $mode); + if($this->_buffer === false) + return $this->terminal_info(TFTPError::FILE_NOT_FOUND, + "Failed to read $filename"); + + $this->log_info("Reading $filename (" . + strlen($this->_buffer) . " bytes)"); + + if($this->use_extensions()) + $this->state = TFTPTransferState::SENDING_WAIT_OACK; + else + $this->state = TFTPTransferState::SENDING; + $this->_last_block = floor(strlen($this->_buffer) / + $this->block_size) + 1; + + $this->log_debug("RRQ: send first block or OACK"); + return $this->packet_data_current(); + } + + public function ack($block) + { + if($this->state == TFTPTransferState::SENDING_WAIT_OACK) { + if($block != 0) { + $this->log_debug("ACK: waiting OACK ACK got block $block"); + return false; + } + + $this->state = TFTPTransferState::SENDING; + $this->log_debug("ACK: got OACK ACK, send first block"); + return $this->packet_data_current(); + } + + if($this->state != TFTPTransferState::SENDING) + return $this->illegal_operation("ACK", "Not in sending state"); + + $this->log_debug("ACK: block $block"); + $this->_last_recv_ack = time(); + + if($block < $this->_block) { + $this->log_debug("ACK: duplicate block $block"); + // just ignore it + return false; + } + + if($block > $this->_last_block) + return $this->illegal_operation("ACK", + "Block $block outside " . + "range 1-{$this->_last_block}"); + + if($block == $this->_last_block) { + $this->log_debug("ACK: last block, done"); + $this->state = TFTPTransferState::TERMINATING; + return false; + } + + // move to next block + $this->_block = $block + 1; + + $this->log_debug("ACK: sending block {$this->_block}"); + return $this->packet_data_current(); + } + + public function retransmit($now) + { + if($now - $this->_last_recv_ack > $this->_server->timeout) { + $this->log_debug("retransmit: timeout"); + $this->state = TFTPTransferState::TERMINATING; + return false; + } + + if($now - $this->_last_sent_data > $this->retransmit_timeout) { + $this->log_debug("retransmit: resending block {$this->_block} or OACK"); + return $this->packet_data_current(); + } + + return false; + } +} +?> diff --git a/lib/TFTP/Server.php b/lib/TFTP/Server.php new file mode 100644 index 0000000..46c5008 --- /dev/null +++ b/lib/TFTP/Server.php @@ -0,0 +1,361 @@ + + * + * MIT License: + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * + * Extend TFTPServer class and then call loop method with UDP URL. + * Possible methods to override: + * exists($peer, $filename) + * Check if file exist, default always true. + * readable($peer, $filename) + * Check if file is readable, default always true. + * get($peer, $filename, $mode) + * Return content of file, default always false. + * Only called if both exists and readable returns true. + * writable($peer, $filename) + * Check if file is writable, default always false. + * put($peer, $filename, $mode, $content) + * Write content to file. + * Only falled if both exists and writable returns true. + * + * $peer is $ip:$port, source ip and port of client + * $filename is filename specified by client + * $mode is probably "octet" or "netascii" + * $content is file content + * + * The server support multiple concurrent read and writes, but the method calls + * are serialized, so make sure to return quickly. + * + * TODO: + * select must handle EINTR, how? + * multiple recv per select? + * + */ +declare(strict_types=1); +namespace PROVISION\TFTP; + +use PROVISION\Logger as Logger; + +class Server { + public $block_size = 512; + public $max_block_size = 65464; // max block size from rfc2348 + public $timeout = 10; + public $retransmit_timeout = 1; + public $max_put_size = 10485760; // 10 Mibi + private $_socket_url; + private $_socket; + private $_transfers = array(); + private $_logger = NULL; + + function __construct($socket_url, $logger = NULL) + { + $this->_socket_url = $socket_url; + $this->_logger = $logger; + } + + public function exists($peer, $filename) + { + return true; + } + + public function readable($peer, $filename) + { + return true; + } + + public function get($peer, $filename, $mode) + { + return false; + } + + public function writable($peer, $filename) + { + return false; + } + + public function put($peer, $filename, $mode, $content) + { + } + + public function logger_log($priority, $message) { + if($this->_logger === NULL) + return; + + $this->_logger->log($priority, $message); + } + + public function log_debug($peer, $message) + { + $this->logger_log(LOG_DEBUG, "$peer $message"); + } + + public function log_info($peer, $message) + { + $this->logger_log(LOG_INFO, "$peer $message"); + } + + public function log_warning($peer, $message) + { + $this->logger_log(LOG_WARNING, "$peer $message"); + } + + public function log_error($peer, $message) + { + $this->logger_log(LOG_ERR, "$peer $message"); + } + + public static function packet_ack($block) + { + return pack("nn", TFTPOpcode::ACK, $block); + } + + public static function packet_data($block, $data) + { + return pack("nn", TFTPOpcode::DATA, $block) . $data; + } + + public static function packet_error($code, $message = "") + { + return pack("nn", TFTPOpcode::ERROR, $code) . $message . "\0"; + } + + public static function packet_oack($options) + { + $data = ""; + foreach($options as $key => $value) + $data .= "$key\0$value\0"; + return pack("n", TFTPOpcode::OACK) . $data; + } + + public static function escape_string($str) + { + $b = ""; + $l = strlen($str); + for($i = 0; $i < $l; $i++) { + $c = $str[$i]; + if(ctype_print($c)) + $b .= $c; + else + $b .= sprintf("\\x%'02x", ord($c)); + } + + return $b; + } + + public function loop(&$error = false, $user = null) + { + $this->_socket = + stream_socket_server($this->_socket_url, $errno, $errstr, + STREAM_SERVER_BIND); + if(!$this->_socket) { + if($error !== false) + $error = "$errno: $errstr"; + return false; + } + + if($user != null) { + posix_seteuid($user["uid"]); + posix_setegid($user["gid"]); + } + + stream_set_blocking($this->_socket, false); + + return $this->loop_ex(); + } + + private function loop_ex() + { + $now = $last = time(); + + while(true) { + $read = array($this->_socket); + //$r = stream_select($read, $write = null, $excpt = null, 1); + $write = null; + $except = null; + $r = stream_select($read, $write, $excpt, 1); + + if($r === false) { + $this->log_error("server", "select returned false"); + continue; + } + + if(count($read) > 0) { + $packet = stream_socket_recvfrom($this->_socket, + 65535, // max udp packet size + 0, // no flags + $peer); + // ipv6 hack, convert to [host]:port format + if(strpos($peer, ".") === false) { + $portpos = strrpos($peer, ":"); + $host = substr($peer, 0, $portpos); + $port = substr($peer, $portpos + 1); + $peer = "[$host]:$port"; + } + $this->log_debug($peer, "request: " . strlen($packet). " bytes"); + $this->log_debug($peer, "request: " . + TFTPServer::escape_string($packet)); + $reply = $this->request($peer, $packet); + if($reply !== false) { + $this->log_debug($peer, "reply: " . + TFTPServer::escape_string($reply)); + stream_socket_sendto($this->_socket, $reply, 0, $peer); + } + } + + $now = time(); + if($now != $last) { + $last = $now; + $this->retransmit($now); + } + } + } + + private function retransmit($now) + { + foreach($this->_transfers as $peer => $transfer) { + $reply = $transfer->retransmit($now); + if($reply !== false) { + $this->log_debug($peer, "resend: " . + TFTPServer::escape_string($reply)); + stream_socket_sendto($this->_socket, $reply, 0, $peer); + } + + if($transfer->state == TFTPTransferState::TERMINATING) + unset($this->_transfers[$peer]); + } + } + + private function request($peer, $packet) + { + if(strlen($packet) < 4) { + $this->log_debug($peer, "request: short packet"); + return false; + } + + $reply = false; + $transfer = false; + if(isset($this->_transfers[$peer])) { + $this->log_debug($peer, "request: existing transfer"); + $transfer = $this->_transfers[$peer]; + } + + $fields = unpack("n", $packet); + $op = $fields[1]; + $this->log_debug($peer, "request: opcode " . + TFTPOpcode::name($op) . " ($op)"); + switch($op) { + case TFTPOpcode::WRQ: + case TFTPOpcode::RRQ: + $a = explode("\0", substr($packet, 2)); + if(count($a) < 3 || $a[count($a) - 1] != "") { + $this->log_warning($peer, "request: malformed " . + TFTPOpcode::name($op)); + return false; + } + + $rawexts = array_slice($a, 2, -1); + + // Cisco IP Phone 7941 (and possibly others) return an extra null + // at the end; a breach of RFC rfc2347. This is a workaround. + // If odd count strip last and continue if empty, else warn and ignore + if(count($rawexts) % 2 != 0) { + if(array_pop($rawexts)!="") { + $this->log_warning($peer, "request: malformed extension " . + "key/value pairs " . TFTPOpcode::name($op)); + return false; + } + } + + $extensions = array(); + foreach(array_chunk($rawexts, 2) as $pair) + $extensions[strtolower($pair[0])] = $pair[1]; + + if($transfer === false) { + if($op == TFTPOpcode::RRQ) + $transfer = new TFTPReadTransfer($this, $peer, $extensions); + else + $transfer = new TFTPWriteTransfer($this, $peer, $extensions); + + $this->_transfers[$peer] = $transfer; + } + + if($op == TFTPOpcode::RRQ) + $reply = $transfer->rrq($a[0], $a[1]); + else + $reply = $transfer->wrq($a[0], $a[1]); + + break; + case TFTPOpcode::ACK: + if(strlen($packet) != 4) { + $this->log_warning($peer, "request: malformed ACK"); + return false; + } + + $a = unpack("n", substr($packet, 2)); + if($transfer === false) { + // do not warn, some clients like BSD tftp sends ack on read error + $this->log_debug($peer, "request: ack from unknwon peer"); + } else + $reply = $transfer->ack($a[1]); + break; + case TFTPOpcode::DATA: + if(strlen($packet) < 4) { + $this->log_warning($peer, "request: malformed DATA"); + return false; + } + + $a = unpack("n", substr($packet, 2)); + $data = substr($packet, 4, strlen($packet) - 4); + if($transfer === false) { + $this->log_warning($peer, "request: data from unknwon peer"); + $reply = TFTPServer::packet_error(TFTPError::UNKNOWN_TID, + "Unknown TID for DATA"); + } else + $reply = $transfer->data($a[1], $data); + break; + case TFTPOpcode::ERROR: + $a = unpack("n", substr($packet, 2, 2)); + $message = substr($packet, 4, strlen($packet) - 5); + + if($transfer === false) + $this->log_warning($peer, "request: error from unknwon peer, " . + "{$a[1]}:$message"); + else + $transfer->error($a[1], $message); + break; + default: + break; + } + + if($transfer !== false && + $transfer->state == TFTPTransferState::TERMINATING) { + $this->log_debug($peer, "request: terminating"); + unset($this->_transfers[$transfer->peer]); + } + + return $reply; + } +} +?> diff --git a/lib/TFTP/Transfer.php b/lib/TFTP/Transfer.php new file mode 100644 index 0000000..69d740d --- /dev/null +++ b/lib/TFTP/Transfer.php @@ -0,0 +1,150 @@ + + * + * MIT License: + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +declare(strict_types=1); +namespace PROVISION\TFTP; + +use PROVISION\Logger as Logger; + +abstract class Transfer { + public $state; + public $peer; + public $retransmit_timeout; + public $block_size; + public $tsize; + protected $_server; // TFTPServer reference + + function __construct($server, $peer, $extensions) + { + $this->state = TFTPTransferState::READY; + $this->peer = $peer; + $this->retransmit_timeout = $server->retransmit_timeout; + $this->block_size = $server->block_size; + $this->tsize = 0; + $this->_server = $server; + + if(isset($extensions["timeout"])) { + $timeout = (int)$extensions["timeout"]; + if($timeout > 0 && $timeout < 256) + $this->retransmit_timeout = $timeout; + } + + if(isset($extensions["blksize"])) { + $blksize = (int)$extensions["blksize"]; + if($blksize > 0 && $blksize <= $server->max_block_size) + $this->block_size = $blksize; + } + + // tsize is only checked for in write transfers + } + + protected function log_debug($message) + { + $this->_server->log_debug($this->peer, $message); + } + + protected function log_info($message) + { + $this->_server->log_info($this->peer, $message); + } + + protected function log_warning($message) + { + $this->_server->log_warning($this->peer, $message); + } + + protected function log_error($message) + { + $this->_server->log_error($this->peer, $message); + } + + protected function terminal_info($error, $message) + { + $this->log_info($message); + $this->state = TFTPTransferState::TERMINATING; + return TFTPServer::packet_error($error, $message); + } + + protected function terminal_error($op, $error, $message) + { + $this->log_debug("$op: $message"); + $this->state = TFTPTransferState::TERMINATING; + return TFTPServer::packet_error($error, $message); + } + + protected function illegal_operation($op, $message = "Illegal operation") + { + return $this->terminal_error($op, TFTPError::ILLEGAL_OPERATION, $message); + } + + public function rrq($filename, $mode) + { + return $this->illegal_operation("RRQ"); + } + + public function wrq($filename, $mode) + { + return $this->illegal_operation("WRQ"); + } + + public function data($block, $data) + { + return $this->illegal_operation("DATA"); + } + + public function ack($block) + { + return $this->illegal_operation("ACK"); + } + + public function error($error, $message) + { + $this->log_debug("ERROR: $error: $message"); + $this->state = TFTPTransferState::TERMINATING; + } + + protected function use_extensions() { + return + $this->retransmit_timeout != $this->_server->retransmit_timeout || + $this->block_size != $this->_server->block_size || + $this->tsize != 0; + } + + protected function packet_oack() { + $options = array(); + + if($this->retransmit_timeout != $this->_server->retransmit_timeout) + $options["timeout"] = (string)$this->retransmit_timeout; + + if($this->block_size != $this->_server->block_size) + $options["blksize"] = (string)$this->block_size; + + if($this->tsize != 0) + $options["tsize"] = (string)$this->tsize; + + return TFTPServer::packet_oack($options); + } +} +?> diff --git a/lib/TFTP/TransferState.php b/lib/TFTP/TransferState.php new file mode 100644 index 0000000..48d0f8d --- /dev/null +++ b/lib/TFTP/TransferState.php @@ -0,0 +1,39 @@ + + * + * MIT License: + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +declare(strict_types=1); +namespace PROVISION\TFTP; + +use PROVISION\Logger as Logger; + +class TFTPTransferState +{ + const READY = 1; + const SENDING_WAIT_OACK = 2; + const SENDING = 3; + const RECEIVING = 4; + const TERMINATING = 5; +} +?> diff --git a/lib/TFTP/WriteTransfer.php b/lib/TFTP/WriteTransfer.php new file mode 100644 index 0000000..a6b992c --- /dev/null +++ b/lib/TFTP/WriteTransfer.php @@ -0,0 +1,158 @@ + + * + * MIT License: + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +declare(strict_types=1); +namespace PROVISION\TFTP; + +use PROVISION\Logger as Logger; +use PROVISION\TFTP as TFTP; + +class TFTPWriteTransfer extends TFTP\Transfer { + private $_last_sent_ack; + private $_last_recv_data; + private $_buffer; + private $_buffer_size; + private $_next_block; + private $_filename; + private $_mode; + + function __construct($server, $peer, $extensions) + { + parent::__construct($server, $peer, $extensions); + $this->_last_sent_ack = time(); + $this->_last_recv_data = $this->_last_sent_ack; + $this->_buffer = array(); + $this->_buffer_size = 0; + $this->_last_recv_block = 0; + $this->_filename = false; + $this->_mode = false; + + if(isset($extensions["tsize"])) + $this->tsize = (int)$extensions["tsize"]; + + $this->log_debug("new write transfer"); + } + + private function packet_ack_current() + { + $this->_last_sent_ack = time(); + + if($this->_last_recv_block == 0 && $this->use_extensions()) + return $this->packet_oack(); + else + return TFTPServer::packet_ack($this->_last_recv_block); + } + + public function wrq($filename, $mode) + { + $this->log_debug("WRQ: filename $filename in $mode mode"); + + if($this->state != TFTPTransferState::READY) + return $this->illegal_operation("WRQ", "Not in ready state"); + + if(!$this->_server->writable($this->peer, $filename)) + return $this->terminal_info(TFTPError::ACCESS_VIOLATION, + "File $filename is not writable"); + + if($this->tsize != 0 && $this->tsize > $this->_server->max_put_size) + return $this->terminal_info(TFTPError::DISK_FULL, + "File too big, " . + $this->tsize . "(tsize) > " . + $this->_server->max_put_size); + + $this->state = TFTPTransferState::RECEIVING; + $this->_filename = $filename; + $this->_mode = $mode; + $this->_last_sent_ack = time(); + + $this->log_debug("WRQ: ack request"); + if($this->use_extensions()) + return $this->packet_oack(); + else + return TFTPServer::packet_ack(0); + } + + public function data($block, $data) + { + if($this->state != TFTPTransferState::RECEIVING) + return $this->illegal_operation("DATA", "Not in receiving state"); + + $this->log_debug("DATA: block $block"); + $this->last_recv_data = time(); + + if($block <= $this->_last_recv_block) { + $this->log_debug("DATA: duplicate block $block"); + // just ignore it + return false; + } + + if($block != $this->_last_recv_block + 1) + return $this->illegal_operation("DATA", + "Expected block " . + ($this->_last_recv_block + 1) . + " got $block"); + + $this->_last_recv_block = $block; + $this->_last_recv_data = time(); + array_push($this->_buffer, $data); + $this->_buffer_size += strlen($data); + + if($this->_buffer_size > $this->_server->max_put_size) + return $this->terminal_info(TFTPError::DISK_FULL, + "File too big, " . + $this->_buffer_size . " > " . + $this->_server->max_put_size); + + if(strlen($data) < $this->block_size) { + $this->log_debug("DATA: last, done"); + $this->state = TFTPTransferState::TERMINATING; + $this->log_info("Writing {$this->_filename} " . + "({$this->_buffer_size} bytes)"); + $this->_server->put($this->peer, $this->_filename, $this->_mode, + implode("", $this->_buffer)); + return $this->packet_ack_current(); + } + + $this->log_debug("DATA: ack block $block"); + return $this->packet_ack_current(); + } + + public function retransmit($now) + { + if($now - $this->_last_recv_data > $this->_server->timeout) { + $this->log_debug("retransmit: timeout"); + $this->state = TFTPTransferState::TERMINATING; + return false; + } + + if($now - $this->_last_sent_ack > $this->retransmit_timeout) { + $this->log_debug("retransmit: reack block {$this->_last_recv_block}"); + return $this->packet_ack_current(); + } + + return false; + } +} +?> diff --git a/lib/Utils.php b/lib/Utils.php new file mode 100644 index 0000000..0420630 --- /dev/null +++ b/lib/Utils.php @@ -0,0 +1,33 @@ +log('LOG_DEBUG', $message); + } + } + + static function log_error($message) { + global $logger; + if ($logger) { + $logger->log('LOG_ERROR', $message); + } + } + + static function log_error_and_throw($message) { + log_error($message); + throw new Exception($message); + } +} +?> diff --git a/lib/xml.php b/lib/XML.php similarity index 95% rename from lib/xml.php rename to lib/XML.php index 29ce5e0..b1fd604 100644 --- a/lib/xml.php +++ b/lib/XML.php @@ -1,7 +1,7 @@ file = $file; - } - - public function hashAsBase64(int $hashAlg) : string { - return ""; - } -} - -class Crypto { - - public static function certificateHashAsBase64(string $certificateFile, int $hashAlg) : string { - return new Certificate($certificateFile) - .hashAsBase64($hashAlg); - } -} - -class Signer { -} diff --git a/lib/logger.php b/lib/logger.php deleted file mode 100644 index 2ad173b..0000000 --- a/lib/logger.php +++ /dev/null @@ -1,97 +0,0 @@ -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); - } -} -?> \ No newline at end of file diff --git a/lib/tftp.php b/lib/tftp.php deleted file mode 100644 index 88459c1..0000000 --- a/lib/tftp.php +++ /dev/null @@ -1,784 +0,0 @@ - - * - * MIT License: - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * - * Extend TFTPServer class and then call loop method with UDP URL. - * Possible methods to override: - * exists($peer, $filename) - * Check if file exist, default always true. - * readable($peer, $filename) - * Check if file is readable, default always true. - * get($peer, $filename, $mode) - * Return content of file, default always false. - * Only called if both exists and readable returns true. - * writable($peer, $filename) - * Check if file is writable, default always false. - * put($peer, $filename, $mode, $content) - * Write content to file. - * Only falled if both exists and writable returns true. - * - * $peer is $ip:$port, source ip and port of client - * $filename is filename specified by client - * $mode is probably "octet" or "netascii" - * $content is file content - * - * The server support multiple concurrent read and writes, but the method calls - * are serialized, so make sure to return quickly. - * - * TODO: - * select must handle EINTR, how? - * multiple recv per select? - * - */ - -// origin: https://github.com/tm1000/tftpserver -include_once("logger.php"); - -class TFTPOpcode -{ - public static function name($v) - { - static $names = array(TFTPOpcode::RRQ => "RRQ", - TFTPOpcode::WRQ => "WRQ", - TFTPOpcode::DATA => "DATA", - TFTPOpcode::ACK => "ACK", - TFTPOpcode::ERROR => "ERROR", - TFTPOpcode::OACK => "OACK"); - if(isset($names[$v])) - return $names[$v]; - else - return "UNKNOWN"; - } - - const RRQ = 1; // read request - const WRQ = 2; // write request - const DATA = 3; // send data - const ACK = 4; // ack data - const ERROR = 5; - const OACK = 6; // option ack, instead of first ACK/DATA -} - -class TFTPError -{ - const NOT_DEFINED = 0; // see error message instead of error code - const FILE_NOT_FOUND = 1; - const ACCESS_VIOLATION = 2; - const DISK_FULL = 3; - const ILLEGAL_OPERATION = 4; - const UNKNOWN_TID = 5; // unknown transfer (id is ip:port pair) - const FILE_ALREADY_EXISTS = 6; - const NO_SUCH_USER = 7; - const OACK_FAILURE = 8; -} - -class TFTPTransferState -{ - const READY = 1; - const SENDING_WAIT_OACK = 2; - const SENDING = 3; - const RECEIVING = 4; - const TERMINATING = 5; -} - -abstract class TFTPTransfer { - public $state; - public $peer; - public $retransmit_timeout; - public $block_size; - public $tsize; - protected $_server; // TFTPServer reference - - function __construct($server, $peer, $extensions) - { - $this->state = TFTPTransferState::READY; - $this->peer = $peer; - $this->retransmit_timeout = $server->retransmit_timeout; - $this->block_size = $server->block_size; - $this->tsize = 0; - $this->_server = $server; - - if(isset($extensions["timeout"])) { - $timeout = (int)$extensions["timeout"]; - if($timeout > 0 && $timeout < 256) - $this->retransmit_timeout = $timeout; - } - - if(isset($extensions["blksize"])) { - $blksize = (int)$extensions["blksize"]; - if($blksize > 0 && $blksize <= $server->max_block_size) - $this->block_size = $blksize; - } - - // tsize is only checked for in write transfers - } - - protected function log_debug($message) - { - $this->_server->log_debug($this->peer, $message); - } - - protected function log_info($message) - { - $this->_server->log_info($this->peer, $message); - } - - protected function log_warning($message) - { - $this->_server->log_warning($this->peer, $message); - } - - protected function log_error($message) - { - $this->_server->log_error($this->peer, $message); - } - - protected function terminal_info($error, $message) - { - $this->log_info($message); - $this->state = TFTPTransferState::TERMINATING; - return TFTPServer::packet_error($error, $message); - } - - protected function terminal_error($op, $error, $message) - { - $this->log_debug("$op: $message"); - $this->state = TFTPTransferState::TERMINATING; - return TFTPServer::packet_error($error, $message); - } - - protected function illegal_operation($op, $message = "Illegal operation") - { - return $this->terminal_error($op, TFTPError::ILLEGAL_OPERATION, $message); - } - - public function rrq($filename, $mode) - { - return $this->illegal_operation("RRQ"); - } - - public function wrq($filename, $mode) - { - return $this->illegal_operation("WRQ"); - } - - public function data($block, $data) - { - return $this->illegal_operation("DATA"); - } - - public function ack($block) - { - return $this->illegal_operation("ACK"); - } - - public function error($error, $message) - { - $this->log_debug("ERROR: $error: $message"); - $this->state = TFTPTransferState::TERMINATING; - } - - protected function use_extensions() { - return - $this->retransmit_timeout != $this->_server->retransmit_timeout || - $this->block_size != $this->_server->block_size || - $this->tsize != 0; - } - - protected function packet_oack() { - $options = array(); - - if($this->retransmit_timeout != $this->_server->retransmit_timeout) - $options["timeout"] = (string)$this->retransmit_timeout; - - if($this->block_size != $this->_server->block_size) - $options["blksize"] = (string)$this->block_size; - - if($this->tsize != 0) - $options["tsize"] = (string)$this->tsize; - - return TFTPServer::packet_oack($options); - } -} - -class TFTPReadTransfer extends TFTPTransfer { - private $_last_recv_ack; - private $_last_sent_data; - private $_buffer; - private $_block; - private $_last_block; - - function __construct($server, $peer, $extensions) - { - parent::__construct($server, $peer, $extensions); - $this->_last_recv_ack = time(); - $this->_last_sent_data = $this->_last_recv_ack; - $this->_buffer = false; - $this->_block = 1; - $this->_last_block = 1; - - $this->log_debug("new read transfer"); - } - - private function current_block() - { - return substr($this->_buffer, - ($this->_block - 1) * $this->block_size, - $this->block_size); - } - - private function packet_data_current() - { - $this->_last_sent_data = time(); - - if($this->state == TFTPTransferState::SENDING_WAIT_OACK) - return $this->packet_oack(); - else - return TFTPServer::packet_data($this->_block, $this->current_block()); - } - - public function rrq($filename, $mode) - { - $this->log_debug("RRQ: filename $filename in $mode mode"); - - if($this->state != TFTPTransferState::READY) - return $this->illegal_operation("RRQ", "Not in ready state"); - - if(!$this->_server->exists($this->peer, $filename)) - return $this->terminal_info(TFTPError::FILE_NOT_FOUND, - "File $filename does not exist"); - - if(!$this->_server->readable($this->peer, $filename)) - return $this->terminal_info(TFTPError::ACCESS_VIOLATION, - "File $filename is not readable"); - - $this->_buffer = $this->_server->get($this->peer, $filename, $mode); - if($this->_buffer === false) - return $this->terminal_info(TFTPError::FILE_NOT_FOUND, - "Failed to read $filename"); - - $this->log_info("Reading $filename (" . - strlen($this->_buffer) . " bytes)"); - - if($this->use_extensions()) - $this->state = TFTPTransferState::SENDING_WAIT_OACK; - else - $this->state = TFTPTransferState::SENDING; - $this->_last_block = floor(strlen($this->_buffer) / - $this->block_size) + 1; - - $this->log_debug("RRQ: send first block or OACK"); - return $this->packet_data_current(); - } - - public function ack($block) - { - if($this->state == TFTPTransferState::SENDING_WAIT_OACK) { - if($block != 0) { - $this->log_debug("ACK: waiting OACK ACK got block $block"); - return false; - } - - $this->state = TFTPTransferState::SENDING; - $this->log_debug("ACK: got OACK ACK, send first block"); - return $this->packet_data_current(); - } - - if($this->state != TFTPTransferState::SENDING) - return $this->illegal_operation("ACK", "Not in sending state"); - - $this->log_debug("ACK: block $block"); - $this->_last_recv_ack = time(); - - if($block < $this->_block) { - $this->log_debug("ACK: duplicate block $block"); - // just ignore it - return false; - } - - if($block > $this->_last_block) - return $this->illegal_operation("ACK", - "Block $block outside " . - "range 1-{$this->_last_block}"); - - if($block == $this->_last_block) { - $this->log_debug("ACK: last block, done"); - $this->state = TFTPTransferState::TERMINATING; - return false; - } - - // move to next block - $this->_block = $block + 1; - - $this->log_debug("ACK: sending block {$this->_block}"); - return $this->packet_data_current(); - } - - public function retransmit($now) - { - if($now - $this->_last_recv_ack > $this->_server->timeout) { - $this->log_debug("retransmit: timeout"); - $this->state = TFTPTransferState::TERMINATING; - return false; - } - - if($now - $this->_last_sent_data > $this->retransmit_timeout) { - $this->log_debug("retransmit: resending block {$this->_block} or OACK"); - return $this->packet_data_current(); - } - - return false; - } -} - -class TFTPWriteTransfer extends TFTPTransfer { - private $_last_sent_ack; - private $_last_recv_data; - private $_buffer; - private $_buffer_size; - private $_next_block; - private $_filename; - private $_mode; - - function __construct($server, $peer, $extensions) - { - parent::__construct($server, $peer, $extensions); - $this->_last_sent_ack = time(); - $this->_last_recv_data = $this->_last_sent_ack; - $this->_buffer = array(); - $this->_buffer_size = 0; - $this->_last_recv_block = 0; - $this->_filename = false; - $this->_mode = false; - - if(isset($extensions["tsize"])) - $this->tsize = (int)$extensions["tsize"]; - - $this->log_debug("new write transfer"); - } - - private function packet_ack_current() - { - $this->_last_sent_ack = time(); - - if($this->_last_recv_block == 0 && $this->use_extensions()) - return $this->packet_oack(); - else - return TFTPServer::packet_ack($this->_last_recv_block); - } - - public function wrq($filename, $mode) - { - $this->log_debug("WRQ: filename $filename in $mode mode"); - - if($this->state != TFTPTransferState::READY) - return $this->illegal_operation("WRQ", "Not in ready state"); - - if(!$this->_server->writable($this->peer, $filename)) - return $this->terminal_info(TFTPError::ACCESS_VIOLATION, - "File $filename is not writable"); - - if($this->tsize != 0 && $this->tsize > $this->_server->max_put_size) - return $this->terminal_info(TFTPError::DISK_FULL, - "File too big, " . - $this->tsize . "(tsize) > " . - $this->_server->max_put_size); - - $this->state = TFTPTransferState::RECEIVING; - $this->_filename = $filename; - $this->_mode = $mode; - $this->_last_sent_ack = time(); - - $this->log_debug("WRQ: ack request"); - if($this->use_extensions()) - return $this->packet_oack(); - else - return TFTPServer::packet_ack(0); - } - - public function data($block, $data) - { - if($this->state != TFTPTransferState::RECEIVING) - return $this->illegal_operation("DATA", "Not in receiving state"); - - $this->log_debug("DATA: block $block"); - $this->last_recv_data = time(); - - if($block <= $this->_last_recv_block) { - $this->log_debug("DATA: duplicate block $block"); - // just ignore it - return false; - } - - if($block != $this->_last_recv_block + 1) - return $this->illegal_operation("DATA", - "Expected block " . - ($this->_last_recv_block + 1) . - " got $block"); - - $this->_last_recv_block = $block; - $this->_last_recv_data = time(); - array_push($this->_buffer, $data); - $this->_buffer_size += strlen($data); - - if($this->_buffer_size > $this->_server->max_put_size) - return $this->terminal_info(TFTPError::DISK_FULL, - "File too big, " . - $this->_buffer_size . " > " . - $this->_server->max_put_size); - - if(strlen($data) < $this->block_size) { - $this->log_debug("DATA: last, done"); - $this->state = TFTPTransferState::TERMINATING; - $this->log_info("Writing {$this->_filename} " . - "({$this->_buffer_size} bytes)"); - $this->_server->put($this->peer, $this->_filename, $this->_mode, - implode("", $this->_buffer)); - return $this->packet_ack_current(); - } - - $this->log_debug("DATA: ack block $block"); - return $this->packet_ack_current(); - } - - public function retransmit($now) - { - if($now - $this->_last_recv_data > $this->_server->timeout) { - $this->log_debug("retransmit: timeout"); - $this->state = TFTPTransferState::TERMINATING; - return false; - } - - if($now - $this->_last_sent_ack > $this->retransmit_timeout) { - $this->log_debug("retransmit: reack block {$this->_last_recv_block}"); - return $this->packet_ack_current(); - } - - return false; - } -} - -class TFTPServer { - public $block_size = 512; - public $max_block_size = 65464; // max block size from rfc2348 - public $timeout = 10; - public $retransmit_timeout = 1; - public $max_put_size = 10485760; // 10 Mibi - private $_socket_url; - private $_socket; - private $_transfers = array(); - private $_logger = NULL; - - function __construct($socket_url, $logger = NULL) - { - $this->_socket_url = $socket_url; - $this->_logger = $logger; - } - - public function exists($peer, $filename) - { - return true; - } - - public function readable($peer, $filename) - { - return true; - } - - public function get($peer, $filename, $mode) - { - return false; - } - - public function writable($peer, $filename) - { - return false; - } - - public function put($peer, $filename, $mode, $content) - { - } - - public function logger_log($priority, $message) { - if($this->_logger === NULL) - return; - - $this->_logger->log($priority, $message); - } - - public function log_debug($peer, $message) - { - $this->logger_log(LOG_DEBUG, "$peer $message"); - } - - public function log_info($peer, $message) - { - $this->logger_log(LOG_INFO, "$peer $message"); - } - - public function log_warning($peer, $message) - { - $this->logger_log(LOG_WARNING, "$peer $message"); - } - - public function log_error($peer, $message) - { - $this->logger_log(LOG_ERR, "$peer $message"); - } - - public static function packet_ack($block) - { - return pack("nn", TFTPOpcode::ACK, $block); - } - - public static function packet_data($block, $data) - { - return pack("nn", TFTPOpcode::DATA, $block) . $data; - } - - public static function packet_error($code, $message = "") - { - return pack("nn", TFTPOpcode::ERROR, $code) . $message . "\0"; - } - - public static function packet_oack($options) - { - $data = ""; - foreach($options as $key => $value) - $data .= "$key\0$value\0"; - return pack("n", TFTPOpcode::OACK) . $data; - } - - public static function escape_string($str) - { - $b = ""; - $l = strlen($str); - for($i = 0; $i < $l; $i++) { - $c = $str[$i]; - if(ctype_print($c)) - $b .= $c; - else - $b .= sprintf("\\x%'02x", ord($c)); - } - - return $b; - } - - public function loop(&$error = false, $user = null) - { - $this->_socket = - stream_socket_server($this->_socket_url, $errno, $errstr, - STREAM_SERVER_BIND); - if(!$this->_socket) { - if($error !== false) - $error = "$errno: $errstr"; - return false; - } - - if($user != null) { - posix_seteuid($user["uid"]); - posix_setegid($user["gid"]); - } - - stream_set_blocking($this->_socket, false); - - return $this->loop_ex(); - } - - private function loop_ex() - { - $now = $last = time(); - - while(true) { - $read = array($this->_socket); - //$r = stream_select($read, $write = null, $excpt = null, 1); - $write = null; - $except = null; - $r = stream_select($read, $write, $excpt, 1); - - if($r === false) { - $this->log_error("server", "select returned false"); - continue; - } - - if(count($read) > 0) { - $packet = stream_socket_recvfrom($this->_socket, - 65535, // max udp packet size - 0, // no flags - $peer); - // ipv6 hack, convert to [host]:port format - if(strpos($peer, ".") === false) { - $portpos = strrpos($peer, ":"); - $host = substr($peer, 0, $portpos); - $port = substr($peer, $portpos + 1); - $peer = "[$host]:$port"; - } - $this->log_debug($peer, "request: " . strlen($packet). " bytes"); - $this->log_debug($peer, "request: " . - TFTPServer::escape_string($packet)); - $reply = $this->request($peer, $packet); - if($reply !== false) { - $this->log_debug($peer, "reply: " . - TFTPServer::escape_string($reply)); - stream_socket_sendto($this->_socket, $reply, 0, $peer); - } - } - - $now = time(); - if($now != $last) { - $last = $now; - $this->retransmit($now); - } - } - } - - private function retransmit($now) - { - foreach($this->_transfers as $peer => $transfer) { - $reply = $transfer->retransmit($now); - if($reply !== false) { - $this->log_debug($peer, "resend: " . - TFTPServer::escape_string($reply)); - stream_socket_sendto($this->_socket, $reply, 0, $peer); - } - - if($transfer->state == TFTPTransferState::TERMINATING) - unset($this->_transfers[$peer]); - } - } - - private function request($peer, $packet) - { - if(strlen($packet) < 4) { - $this->log_debug($peer, "request: short packet"); - return false; - } - - $reply = false; - $transfer = false; - if(isset($this->_transfers[$peer])) { - $this->log_debug($peer, "request: existing transfer"); - $transfer = $this->_transfers[$peer]; - } - - $fields = unpack("n", $packet); - $op = $fields[1]; - $this->log_debug($peer, "request: opcode " . - TFTPOpcode::name($op) . " ($op)"); - switch($op) { - case TFTPOpcode::WRQ: - case TFTPOpcode::RRQ: - $a = explode("\0", substr($packet, 2)); - if(count($a) < 3 || $a[count($a) - 1] != "") { - $this->log_warning($peer, "request: malformed " . - TFTPOpcode::name($op)); - return false; - } - - $rawexts = array_slice($a, 2, -1); - - // Cisco IP Phone 7941 (and possibly others) return an extra null - // at the end; a breach of RFC rfc2347. This is a workaround. - // If odd count strip last and continue if empty, else warn and ignore - if(count($rawexts) % 2 != 0) { - if(array_pop($rawexts)!="") { - $this->log_warning($peer, "request: malformed extension " . - "key/value pairs " . TFTPOpcode::name($op)); - return false; - } - } - - $extensions = array(); - foreach(array_chunk($rawexts, 2) as $pair) - $extensions[strtolower($pair[0])] = $pair[1]; - - if($transfer === false) { - if($op == TFTPOpcode::RRQ) - $transfer = new TFTPReadTransfer($this, $peer, $extensions); - else - $transfer = new TFTPWriteTransfer($this, $peer, $extensions); - - $this->_transfers[$peer] = $transfer; - } - - if($op == TFTPOpcode::RRQ) - $reply = $transfer->rrq($a[0], $a[1]); - else - $reply = $transfer->wrq($a[0], $a[1]); - - break; - case TFTPOpcode::ACK: - if(strlen($packet) != 4) { - $this->log_warning($peer, "request: malformed ACK"); - return false; - } - - $a = unpack("n", substr($packet, 2)); - if($transfer === false) { - // do not warn, some clients like BSD tftp sends ack on read error - $this->log_debug($peer, "request: ack from unknwon peer"); - } else - $reply = $transfer->ack($a[1]); - break; - case TFTPOpcode::DATA: - if(strlen($packet) < 4) { - $this->log_warning($peer, "request: malformed DATA"); - return false; - } - - $a = unpack("n", substr($packet, 2)); - $data = substr($packet, 4, strlen($packet) - 4); - if($transfer === false) { - $this->log_warning($peer, "request: data from unknwon peer"); - $reply = TFTPServer::packet_error(TFTPError::UNKNOWN_TID, - "Unknown TID for DATA"); - } else - $reply = $transfer->data($a[1], $data); - break; - case TFTPOpcode::ERROR: - $a = unpack("n", substr($packet, 2, 2)); - $message = substr($packet, 4, strlen($packet) - 5); - - if($transfer === false) - $this->log_warning($peer, "request: error from unknwon peer, " . - "{$a[1]}:$message"); - else - $transfer->error($a[1], $message); - break; - default: - break; - } - - if($transfer !== false && - $transfer->state == TFTPTransferState::TERMINATING) { - $this->log_debug($peer, "request: terminating"); - unset($this->_transfers[$transfer->peer]); - } - - return $reply; - } -} -?> diff --git a/lib/utils.php b/lib/utils.php deleted file mode 100644 index 20f9c2a..0000000 --- a/lib/utils.php +++ /dev/null @@ -1,31 +0,0 @@ -log('LOG_DEBUG', $message); - } -} - -function log_error($message) { - global $logger; - if ($logger) { - $logger->log('LOG_ERROR', $message); - } -} - -function log_error_and_throw($message) { - log_error($message); - throw new Exception($message); -} -?> diff --git a/srv/tftp/tftp_provisioner b/srv/tftp/tftp_provisioner index a7b9141..981b636 100755 --- a/srv/tftp/tftp_provisioner +++ b/srv/tftp/tftp_provisioner @@ -2,13 +2,21 @@ _debug = $debug; $this->max_put_size = 60000000; - $this->_resolve = new Resolve\Resolve($config); + $this->_resolve = new Resolve($config); $this->_settings_path = $this->_config['main']['base_path'] . DIRECTORY_SEPARATOR . $this->_config['subdirs']['settings']['path'] . DIRECTORY_SEPARATOR; } @@ -83,13 +91,13 @@ class TFTPProvisioner extends TFTP\TFTPServer } -global $base_path; $host = "127.0.0.1"; $port = 10069; $url = "udp://$host:$port"; +$base_path = realpath(__DIR__ . DIRECTORY_SEPARATOR . "../.."); echo "\nStarting TFTP Provisioner...\n"; -$configParser = new SCCP\Config\ConfigParser($base_path, "config.ini"); +$configParser = new ConfigParser($base_path, "config.ini"); $server = new TFTPProvisioner($url, $configParser->getConfiguration(), $logger); if(!$server->loop($error)) die("$error\n"); diff --git a/srv/web/index.php b/srv/web/index.php index 5f92f93..a2a53c9 100644 --- a/srv/web/index.php +++ b/srv/web/index.php @@ -1,9 +1,19 @@ getConfiguration()); + $configParser = new ConfigParser($base_path, "config.ini"); + $resolve = new Resolve($configParser->getConfiguration()); if (($filename = $resolve->resolve($req_filename))) { sendfile($filename); }