diff --git a/.gitignore b/.gitignore index d19e84a3fe..6fa1c8cb93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ +.project +.vscode +/phpinfo.php resources/config.php secure/mailto.bat secure/*.db -secure/*.sqlite +secure/*.sqlite \ No newline at end of file diff --git a/app/tftp/app_config.php b/app/tftp/app_config.php new file mode 100644 index 0000000000..f6479954c0 --- /dev/null +++ b/app/tftp/app_config.php @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/app/tftp/app_defaults.php b/app/tftp/app_defaults.php new file mode 100644 index 0000000000..e37abc6d7f --- /dev/null +++ b/app/tftp/app_defaults.php @@ -0,0 +1,90 @@ + + Portions created by the Initial Developer are Copyright (C) 2016 + the Initial Developer. All Rights Reserved. + + Contributor(s): + Sebastian Krupinski +*/ + +//process this code online once + if ($domains_processed == 1) { + + //define array of settings + $x = 0; + $array[$x]['default_setting_category'] = 'provision'; + $array[$x]['default_setting_subcategory'] = 'tftp_service_address'; + $array[$x]['default_setting_name'] = 'text'; + $array[$x]['default_setting_value'] = '0.0.0.0'; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = 'the address for the TFTP service to listen for connection on'; + $x++; + $array[$x]['default_setting_category'] = 'provision'; + $array[$x]['default_setting_subcategory'] = 'tftp_service_port'; + $array[$x]['default_setting_name'] = 'numeric'; + $array[$x]['default_setting_value'] = '69'; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = 'the port for the TFTP service to listen for connection on'; + $x++; + $array[$x]['default_setting_category'] = 'provision'; + $array[$x]['default_setting_subcategory'] = 'tftp_service_fileslocation'; + $array[$x]['default_setting_name'] = 'numeric'; + $array[$x]['default_setting_value'] = '/tmp'; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = 'the location for static files e.g. firmware'; + + //get an array of the default settings + $sql = "SELECT * FROM v_default_settings "; + $sql .= "WHERE default_setting_category = 'provision' AND default_setting_subcategory = 'tftp_service_%'"; + $prep_statement = $db->prepare($sql); + $prep_statement->execute(); + $default_settings = $prep_statement->fetchAll(PDO::FETCH_NAMED); + unset ($prep_statement, $sql); + + //find the missing default settings + $x = 0; + foreach ($array as $setting) { + $found = false; + $missing[$x] = $setting; + foreach ($default_settings as $row) { + if (trim($row['default_setting_subcategory']) == trim($setting['default_setting_subcategory'])) { + $found = true; + //remove items from the array that were found + unset($missing[$x]); + } + } + $x++; + } + + //add the missing default settings + if (count($missing) > 0) foreach ($missing as $row) { + //add the default settings + $orm = new orm; + $orm->name('default_settings'); + $orm->save($row); + $message = $orm->message; + unset($orm); + //print_r($message); + } + unset($missing); + + } +?> \ No newline at end of file diff --git a/app/tftp/app_languages.php b/app/tftp/app_languages.php new file mode 100644 index 0000000000..15c5adc7fa --- /dev/null +++ b/app/tftp/app_languages.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/app/tftp/app_menu.php b/app/tftp/app_menu.php new file mode 100644 index 0000000000..15c5adc7fa --- /dev/null +++ b/app/tftp/app_menu.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/app/tftp/resources/dbhelper.php b/app/tftp/resources/dbhelper.php new file mode 100644 index 0000000000..9d2252ed8a --- /dev/null +++ b/app/tftp/resources/dbhelper.php @@ -0,0 +1,293 @@ + + Portions created by the Initial Developer are Copyright (C) 2016 + the Initial Developer. All Rights Reserved. + + Contributor(s): + Sebastian Krupinski +*/ + +class database { + + /** + * generate dsn statement + * @param string $type - sqlite, mysql, pgsql, etc + * @param string $host - fqdn or ip address + * @param string $port - port number + * @param string $name - name of database or filename when using sqlite + * @return object pdo + */ + private static function _dsn($type,$host=null,$port=null,$name) + { + switch ($type) { + case 'sqlite': + return "$type:$name;"; + break; + default: + return "$type:host=$host;port=$port;"; + break; + } + } + + /** + * connect to database + * @param string $type - sqlite, mysql, pgsql, etc + * @param string $host - fqdn or ip address + * @param string $port - port number + * @param string $name - name of database or filename when using sqlite + * @param string $username - authentication username + * @param string $password - authentication password + * @param array $options - array of database options + * @return object pdo + */ + public static function connect($type,$host=null,$port=null,$name,$username=null,$password=null,$options) + { + try + { + $db = new PDO(self::_dsn($type,$host,$port,$name), $username, $password, $options); + } + catch (PDOException $exception) + { + echo "Exception: ".$exception->getMessage(); + } + if ($db!==null) return $db; + else return false; + } + + /** + * disconnect from database + * @param pdo $db - database object as pdo type + */ + public static function disconnect($db) + { + try + { + $db=null; + return true; + } + catch (Exception $exception) + { + echo "Exception: ".$exception->getMessage(); + return false; + } + } + + /** + * begin a transaction. + * @param pdo $db - database object as pdo type + */ + public static function begin_transaction($db) + { + $db->setAttribute(PDO::ATTR_AUTOCOMMIT, 0); + $db->beginTransaction(); + } + + /** + * end the transaction. + * @param pdo $db - database object as pdo type + */ + public static function end_transaction($db) + { + $db->commit(); + $db->setAttribute(PDO::ATTR_AUTOCOMMIT, 1); + } + + /** + * revert the transaction. + * @param pdo $db - database object as pdo type + */ + public static function revert_transactions($db) + { + $db->rollBack(); + $db->setAttribute(PDO::ATTR_AUTOCOMMIT, 1); + } + + /** + * get last insert id + * @return int last insert id + */ + public static function get_lastid() + { + return $db->lastInsertId(); + } + + /** + * get only single row + * @param pdo $db - database object as pdo type + * @param string $table - table name + * @param string $filterc - filter column + * @param string $filterv - filter value + * @return array table row + */ + public static function get_row($db,$table,$filterc,$filterv) + { + $db->prepare("SELECT * FROM $table WHERE $filterc=?"); + $db->execute($filterv); + $data = $db->fetch(); + return $data; + } + + /** + * get only single column + * @param pdo $db - database object as pdo type + * @param string $table - table name + * @param string $column - column to return value from + * @param array $filter - ["filter column",">","filter value"] + * @return array table row + */ + public static function get_col($db,$table,$column,$filter) + { + $db->prepare("SELECT $column FROM $table WHERE $filter[0] $filter[1] ?"); + $db->execute($filter[2]); + $data = $cmd->fetchAll(); + return $data; + } + + /** + * get only single value + * @param pdo $db - database object as pdo type + * @param string $table - table name + * @param string $column - column to return value from + * @param string $filterc - filter column + * @param string $filterv - filter value + * @return mixed data in field + */ + public static function get_value($db,$table,$column,$filterc,$filterv) + { + $cmd = $db->prepare("SELECT $column FROM $table WHERE $filterc=?"); + $cmd->bindValue(1, $filterv); + $cmd->execute(); + $data = $cmd->fetchColumn(0); + return $data; + } + + /** + * get count of rows + * @param pdo $db - database object as pdo type + * @param string $table - table name + * @param array $filter - ["filter column",">","filter value"] + * @return mixed data in field + */ + public static function get_count($db,$table,$filter) + { + + $cmd = $db->prepare("SELECT COUNT(0) FROM $table WHERE $filter[0] $filter[1] ?"); + $cmd->bindValue(1, $filter[2]); + $cmd->execute(); + $data = $cmd->fetchColumn(0); + } + + /** + * get specific columns and rows + * @param pdo $db - database object as pdo type + * @param string $table - table name + * @param array $columns - specific columns to return + * @param array $filter - ["filter column",">","filter value"] + * @return array selected tables and rows + */ + public static function get_table($db,$table,$columns,$filter) + { + if($columns === null) $columns=array("*"); + $cmd = $db->prepare("SELECT ".implode(',', $columns)." FROM $table WHERE $filter[0] $filter[1] ?"); + $cmd->bindValue(1, $filter[2]); + $cmd->execute(); + $data = $cmd->fetchAll(); + return $data; + } + + /** + * get data with custom sql statment + * @param pdo $db - database object as pdo type + * @param string $sql - custom sql statment + * @return mixed - any returned data + */ + public static function execute($db,$sql) + { + if($sql === null) return null; + $cmd = $db->prepare($sql); + $cmd->execute(); + $data = $cmd->fetchAll(); + return $data; + } + + /** + * set single row + * @param pdo $db - database object as pdo type + * @param string $table - table name + * @param array $data - associative array 'col'=>'val' + * @param string $filterc - primary key column name or any other columns name + * @param string $filterv - value to match the condition field to + * @param int $val key value + */ + public static function set_row($db,$table,$data,$filterc,$filterv) { + + if($data === null) + exit; + elseif ($filterc !== null&&$filterv !== null) + { + // get values + $v = array_values($data); + // add condition value + array_push($v,$filterv); + // get keys + $c=array(); + foreach (array_keys($data) as $k) { + $c[]=$k."=?"; + } + // phrase command + $cmd=$db->prepare("UPDATE $table SET ".implode(', ', $c)." WHERE $filterc=?;"); + $cmd->execute($v); + } + else + { + // get values + $v = array_values($data); + // get keys + $c=implode(', ', array_keys($data)); + // phrase command + $cmd=$db->prepare("INSERT INTO $table ($c) values (".str_repeat("?,",count($c)-1)."?)"); + $cmd->execute($v); + } + } + + /** + * delete row + * @param string $table table name + * @param string $where column name for condition (commonly primay key column name) + * @param int $id key value + */ + public static function delete_row($db,$table,$filterc,$filterv) { + $cmd=$db->prepare("DELETE FROM $table WHERE $filterc=?"); + $cmd->execute($filterv); + } + + /** + * delete rows + * @param string $table table name + * @param string $where column name for condition (commonly primay key column name) + * @param int $id key value + */ + public static function delete_rows($db,$table,$filterc,$filterv) { + $cmd=$db->prepare("DELETE FROM $table WHERE $filterc=?"); + $cmd->execute($filterv); + } +} +?> \ No newline at end of file diff --git a/app/tftp/resources/systemd.service.template b/app/tftp/resources/systemd.service.template new file mode 100644 index 0000000000..ebe6459958 --- /dev/null +++ b/app/tftp/resources/systemd.service.template @@ -0,0 +1,19 @@ +[Unit] +Description={$name} +After=syslog.target network.target {$database} +Requires={$database} + +[Service] +User=www-data +Group=www-data +WorkingDirectory={$scriptfolder} +Type=simple +StandardOutput=null +StandardError=syslog +ExecStart=/usr/bin/php {$scriptname} +PrivateTmp=true +InaccessibleDirectories=/home /root /boot /opt /mnt /media /etc /usr +ReadOnlyDirectories= + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/app/tftp/resources/tftpserver.class.php b/app/tftp/resources/tftpserver.class.php new file mode 100644 index 0000000000..768ac69ef7 --- /dev/null +++ b/app/tftp/resources/tftpserver.class.php @@ -0,0 +1,873 @@ + + * + * 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? + * + */ + +/* 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); + } +} + +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); + $write = null; + $excpt = 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/app/tftp/resources/tftpservice.class.php b/app/tftp/resources/tftpservice.class.php new file mode 100644 index 0000000000..9e5f925172 --- /dev/null +++ b/app/tftp/resources/tftpservice.class.php @@ -0,0 +1,174 @@ + + Portions created by the Initial Developer are Copyright (C) 2016 + the Initial Developer. All Rights Reserved. + + Contributor(s): + Sebastian Krupinski +*/ + +// load required files +require_once 'tftpserver.class.php'; + +class tftpservice extends TFTPServer +{ + private $_debug=true; + private $_dbtype; + private $_dbhost; + private $_dbport; + private $_dbname; + private $_dbusername; + private $_dbpassword; + private $_fileslocation; + + function __construct($server_url, $config) + { + parent::__construct($server_url); + if (isset($config['debug'])) $this->_debug=$config['debug']; + if (isset($config['db_type'])) $this->_dbtype=$config['db_type']; + if (isset($config['db_host'])) $this->_dbhost=$config['db_host']; + if (isset($config['db_port'])) $this->_dbport=$config['db_port']; + if (isset($config['db_name'])) $this->_dbname=$config['db_name']; + if (isset($config['db_username'])) $this->_dbusername=$config['db_username']; + if (isset($config['db_password'])) $this->_dbpassword=$config['db_password']; + if (isset($config['files_location'])) $this->_fileslocation=$config['files_location']; + + if (!file_exists($_fileslocation)) { + $_fileslocation = (strpos(PHP_OS,"WIN") !== false) ? $_SERVER["TMP"] : "/tmp"; + } + } + + private function log($client, $level, $message) { + if($level!='D'||$this->_debug) + echo + date("H:i:s") . " " . + $level . " " . + $client . " " . + $message . "\n"; + } + + public function get($client, $filepath, $mode) + { + $this->log($client,"N", "Requested File ".$filepath); + + try { + $regex_filter='/provision\/(?\b(?:(?-)[A-Za-z0-9-\_]{1,63}(?-)\.)+[A-Za-z]{1,63}\b|\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b)\/(?\b(?:[0-9a-fA-F]{2}(?:\-|\:)?){6}\b)/'; + + preg_match($regex_filter,$filepath,$regex_matches); + + // check if filepath is in a specific format and respond acordingly + if ($regex_matches['domain']&&$regex_matches['mac']) + { + // generate file from db + $filedata = $this->generate_file($client,$regex_matches['domain'],$regex_matches['mac']); + } + else + { + // retrieve file from disk + $filedata = $this->retrieve_file($client,$filepath); + } + + if($filedata !== false) + { + $this->log($client,"N", "Transmitting File ".$filepath); + + return $filedata; + } + else + { + return false; + } + } + catch (Exception $exception) + { + $this->log($client,"E", "Exception: ".$exception->getMessage()); + return false; + } + } + + public function generate_file($client, $domain, $mac) + { + // load required files + require_once __DIR__.'/dbhelper.php'; + require_once __DIR__.'/../../../resources/functions.php'; + require_once __DIR__.'/../../../resources/classes/template.php'; + require_once __DIR__.'/../../provision/resources/classes/provision.php'; + + $this->log($client,"D", "Generating File ".$domain." ".$mac); + + // connect to database + $db = database::connect($this->_dbtype,$this->_dbhost,$this->_dbport,$this->_dbname,$this->_dbusername,$this->_dbpassword); + + // get domain uuid + $domain_uuid = database::get_value($db,'v_domains','domain_uuid','domain_name',$domain); + + // set temporary folder for template engine + $_SESSION['server']['temp']['dir'] = (strpos(PHP_OS,"WIN") !== false) ? $_SERVER["TMP"] : "/tmp"; + + // update device provisioned status + $data=array('device_provisioned_date'=>date("Y-m-d H:i:s"),'device_provisioned_method'=>'tftp','device_provisioned_ip'=>$client); + database::set_row($db,'v_devices',$data,'device_mac_address',$mac); + + // generate file + $prov = new provision; + $prov->db = $db; + $prov->domain_uuid = $domain_uuid; + $prov->mac = $mac; + $data = $prov->render(); + + // return data or false + if($data === false) + { + $this->log($client,"W", "Generating File Failed ".$domain." ".$mac); + return false; + } + else + { + return $data; + } + } + + public function retrieve_file($client, $path){ + + $this->log($client,"D", "Retrieve File ".$path); + // check for reletive path directive + if(strstr($path, "../") != false || strstr($path, "/..") != false) return false; + // combine base and path + $path = rtrim($this->_fileslocation,'/').'/'.ltrim($path,'/'); + if(substr($path, 0, strlen($this->_fileslocation)) != $this->_fileslocation) return false; + // read contents + if($this->_debug) $this->log($client,"D", "Reading File ".$path); + $data = @file_get_contents($path); + // return data or false + if($data === false) + { + $this->log($client,"W", "Retrieving File Failed ".$path); + return false; + } + else + { + return $data; + } + + } +} + +?> \ No newline at end of file diff --git a/app/tftp/tftpservice.php b/app/tftp/tftpservice.php new file mode 100644 index 0000000000..aef50f03f9 --- /dev/null +++ b/app/tftp/tftpservice.php @@ -0,0 +1,209 @@ +#!/usr/bin/env php + + Portions created by the Initial Developer are Copyright (C) 2016 + the Initial Developer. All Rights Reserved. + + Contributor(s): + Sebastian Krupinski +*/ + +// define variables and constants +$appname = "fusionpbx-tftp"; +$appdesc = "FusionPBX TFTP Service"; +$pid=null; +$pidfile = (strpos(PHP_OS,"WIN") !== false) ? $_SERVER["TMP"]."\\$appname.pid" : "/var/run/$appname.pid"; +$tftpservice_address="0.0.0.0"; +$tftpservice_port=69; +$tftpservice_fileslocation=(strpos(PHP_OS,"WIN") !== false) ? $_SERVER["TMP"] : "/tmp"; + +function Service_Install() +{ + global $appname; + global $appdesc; + + // install for specific os + if (strpos(PHP_OS,"WIN") !== false) + { + // check if we found the executable binary + if (file_exists(PHP_BINARY)) + { + exec('sc create '.$appname.' type=own binPath="'.PHP_BINARY.' '.$_SERVER["SCRIPT_FILENAME"].'" DisplayName="'.$appdesc.'" start=auto'); + die($appdesc.' was successfully installed.\n'); + } + else + { + die($appdesc.' could not be installed because the php executable was not found.\n'); + } + } + else + { + require_once __DIR__.'/../../resources/config.php'; + + // read template file + $template=file_get_contents('resources/systemd.service.template'); + + // service name + $template=str_replace('\{\$name\}',$appdesc,$template); + // service dependencies + switch ($dbtype) { + case 'pgsql': + $template=str_replace('\{\$database\}','postgresql.service',$template); + break; + case 'mysql': + $template=str_replace('\{\$database\}','mariadb.service',$template); + break; + default: + $template=str_replace('\{\$database\}','',$template); + break; + } + // script folder + $template=str_replace('\{\$scriptfolder\}',dirname(__FILE__),$template); + // script name + $template=str_replace('\{\$scriptname\}',basename(__FILE__),$template); + + // write service file + file_put_contents('/lib/systemd/system/'.$appname.'.service'); + + die($appdesc.' was successfully installed.\n'); + } +} + +function Service_Uninstall() +{ + global $appname; + global $appdesc; + + // uninstall for specific os + if (strpos(PHP_OS,"WIN") !== false) + { + exec('sc delete "'.$appname.'"'); + die($appdesc.' was successfully uninstalled.\n'); + } + else + { + unlink('/lib/systemd/system/'.$appname.'.service'); + die($appdesc.' was successfully uninstalled.\n'); + } +} + +function Run() +{ + global $appname; + global $appdesc; + global $pid; + global $pidfile; + global $tftpservice_address; + global $tftpservice_port; + global $tftpservice_fileslocation; + + // required for php 4.3.0 + /* + declare(ticks = 1); + + function _process_term() { exit(0);} + function _process_output($buffer) { } + */ + + // check for existing process + if (file_exists($pidfile)) { + $pid = file_get_contents($pidfile); + if (is_numeric($pid)) { + if (strpos(PHP_OS,"WIN") !== false) + { + exec('tasklist -NH -FO TABLE -FI "PID eq '.$pid.'" 2>NUL', $data); + foreach($data as $line) + { + if (strpos($line,$pid) !== false) die($appdesc.' already running with process id '.$pid); + } + } + else + { + if (file_exists('/proc/'.$pid)) die($appdesc.' already running with process id'.$pid); + } + } + } + + /* + // fork process + $pid = pcntl_fork(); + if ($pid < 0) + die("fusionpbx-tftpservice process fork failed\n"); + else if ($pid) // parent + die("fusionpbx-tftpservice process fork failed\n"); + + posix_setsid(); + pcntl_signal(SIGTERM, "_process_term"); + pcntl_signal(SIGHUP, SIG_IGN); + // redirect normal output to null function + ob_start("_process_output"); + */ + + // write pid file + file_put_contents($pidfile, getmypid()); + + // load required files + require_once __DIR__.'/../../resources/config.php'; + require_once 'resources/tftpservice.class.php'; + require_once 'resources/dbhelper.php'; + + // get service settings from database + // connect to database + $db = database::connect($db_type,$db_host,$db_port,$db_name,$db_username,$db_password); + // get settings + $s = database::get_table($db,'v_default_settings',array('default_setting_subcategory','default_setting_value'),array('default_setting_subcategory','LIKE','tftp_service_%')); + // set local variables + foreach ($s as $i) { + switch ($i[0]) { + case 'tftp_service_address': + $tftpservice_address=$i[1]; + break; + case 'tftp_service_port': + $tftpservice_port=$i[1]; + break; + case 'tftp_service_fileslocation': + $tftpservice_fileslocation=$i[1]; + break; + } + } + // disconnect from database + unset($db); + // destroy data + unset($s); + + // start service + $server = new tftpservice("udp://$tftpservice_address:$tftpservice_port", array('db_type'=>$db_type,'db_host'=>$db_host, "db_port"=>$db_port, "db_name"=>$db_name, "db_username"=>$db_username, "db_password"=>$db_password, "files_location"=>$tftpservice_fileslocation)); + echo $appdesc.' has started.'; + if(!$server->loop($error, $user)) die("$error\n"); + echo $appdesc.' has stopped.'; +} + +// Install System Service +if(isset($_SERVER["argv"][1])&&$_SERVER["argv"][1]=="--InstallService") + Service_Install(); +// Uninstall System Service +elseif(isset($_SERVER["argv"][1])&&$_SERVER["argv"][1]=="--UninstallService") + Service_Uninstall(); +// Run Service +else Run(); + + +?>