TFTP Service v1.0.3 (#1858)

* Added functionality to show when a devices was last provisioned

Added functionality to show when a devices was last provisioned

Need addition to database: v_devices table:
device_provisioned_on -> datetime
device_provisioned_by -> char(10)

* Revert "Added functionality to show when a devices was last provisioned"

This reverts commit c3e40d68fa.

* Revert "Revert "Added functionality to show when a devices was last provisioned""

This reverts commit 8c27a46565.

* Changed field names as requested

Changed field names as requested for last provisioned data

* Added database fields for device provisoned functionality

Added provisioned_date, provisioned_method, provisioned_ip

* Added ability to search device provisioned info

* Added ip tracking to device provisoned functionality

Added ip tracking to device provisoned functionality and moved the code
to before rendering to register the contact even on unseccessful render

* Added IP address to status column

* TFTP Service v1

TFTP Service  v1

* Update app_config.php

* TFTP Service v1.0.1

TFTP Service v1.0.1

* TFTP Service v1.0.2

* TFTP Service v1.0.2-1

Renamed file

* TFTP Service 1.0.3

Bug fixes

* Modified to ignore IDE files
This commit is contained in:
minotaur01 2016-08-26 15:04:49 -04:00 committed by FusionPBX
parent 57c8d4db6e
commit 94b7b98f8e
10 changed files with 1691 additions and 1 deletions

5
.gitignore vendored
View File

@ -1,4 +1,7 @@
.project
.vscode
/phpinfo.php
resources/config.php resources/config.php
secure/mailto.bat secure/mailto.bat
secure/*.db secure/*.db
secure/*.sqlite secure/*.sqlite

23
app/tftp/app_config.php Normal file
View File

@ -0,0 +1,23 @@
<?php
//application details
$apps[$x]['name'] = "TFTP Service";
$apps[$x]['uuid'] = "4b99ccfb-cb98-40e1-a5e5-aaa89e14a388";
$apps[$x]['category'] = "";;
$apps[$x]['subcategory'] = "";
$apps[$x]['version'] = "";
$apps[$x]['license'] = "Mozilla Public License 1.1";
$apps[$x]['url'] = "http://www.fusionpbx.com";
$apps[$x]['description']['en-us'] = "TFTP Service";
$apps[$x]['description']['es-cl'] = "";
$apps[$x]['description']['es-mx'] = "";
$apps[$x]['description']['de-de'] = "";
$apps[$x]['description']['de-ch'] = "";
$apps[$x]['description']['de-at'] = "";
$apps[$x]['description']['fr-fr'] = "";
$apps[$x]['description']['fr-ca'] = "";
$apps[$x]['description']['fr-ch'] = "";
$apps[$x]['description']['pt-pt'] = "";
$apps[$x]['description']['pt-br'] = "";
?>

90
app/tftp/app_defaults.php Normal file
View File

@ -0,0 +1,90 @@
<?php
/*
FusionPBX
Version: MPL 1.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
The Original Code is FusionPBX
The Initial Developer of the Original Code is
Sebastian Krupinski <sebastian@ksacorp.com>
Portions created by the Initial Developer are Copyright (C) 2016
the Initial Developer. All Rights Reserved.
Contributor(s):
Sebastian Krupinski <sebastian@ksacorp.com>
*/
//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);
}
?>

View File

@ -0,0 +1,3 @@
<?php
?>

3
app/tftp/app_menu.php Normal file
View File

@ -0,0 +1,3 @@
<?php
?>

View File

@ -0,0 +1,293 @@
<?php
/*
FusionPBX
Version: MPL 1.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
The Original Code is FusionPBX
The Initial Developer of the Original Code is
Sebastian Krupinski <sebastian@ksacorp.com>
Portions created by the Initial Developer are Copyright (C) 2016
the Initial Developer. All Rights Reserved.
Contributor(s):
Sebastian Krupinski <sebastian@ksacorp.com>
*/
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);
}
}
?>

View File

@ -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

View File

@ -0,0 +1,873 @@
<?php
/*
* PHP TFTP Server
*
* Copyright (c) 2011 <mattias.wadman@gmail.com>
*
* 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;
}
}
?>

View File

@ -0,0 +1,174 @@
<?php
/*
FusionPBX
Version: MPL 1.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
The Original Code is FusionPBX
The Initial Developer of the Original Code is
Sebastian Krupinski <sebastian@ksacorp.com>
Portions created by the Initial Developer are Copyright (C) 2016
the Initial Developer. All Rights Reserved.
Contributor(s):
Sebastian Krupinski <sebastian@ksacorp.com>
*/
// 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\/(?<domain>\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)\/(?<mac>\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;
}
}
}
?>

209
app/tftp/tftpservice.php Normal file
View File

@ -0,0 +1,209 @@
#!/usr/bin/env php
<?php
/*
FusionPBX
Version: MPL 1.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
The Original Code is FusionPBX
The Initial Developer of the Original Code is
Sebastian Krupinski <sebastian@ksacorp.com>
Portions created by the Initial Developer are Copyright (C) 2016
the Initial Developer. All Rights Reserved.
Contributor(s):
Sebastian Krupinski <sebastian@ksacorp.com>
*/
// 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();
?>