* @version 0.0.0 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2015 Hub Developer Team * @license GNU GPL 3.0 or any newer version * @link http://www.shipsimu.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ class ChunkHandler extends BaseHandler implements HandleableChunks, Registerable { /** * Stacker for chunks with final EOP */ const STACKER_NAME_CHUNKS_WITH_FINAL_EOP = 'final_chunks'; const STACKER_NAME_CHUNKS_WITHOUT_FINAL = 'pending_chunks'; const STACKER_NAME_ASSEMBLED_RAW_DATA = 'chunk_raw_data'; /** * Chunk splits: * 0 = Hash * 1 = Serial number * 2 = Raw data */ const CHUNK_SPLITS_INDEX_HASH = 0; const CHUNK_SPLITS_INDEX_SERIAL = 1; const CHUNK_SPLITS_INDEX_RAW_DATA = 2; /** * The final array for assembling the original package back together */ private $finalPackageChunks = array(); /** * Array of chunk hashes */ private $chunkHashes = array(); /** * Raw EOP chunk data in an array: * * 0 = Final hash, * 1 = Hash of last chunk */ private $eopChunk = array(); /** * Raw package data */ private $rawPackageData = ''; /** * Fragmenter instance, needs to be set here again */ private $fragmenterInstance = NULL; /** * Protected constructor * * @return void */ protected function __construct () { // Call parent constructor parent::__construct(__CLASS__); // Set handler name $this->setHandlerName('chunk'); // Initialize handler $this->initHandler(); // Get a fragmenter instance for later verification of serial numbers (e.g. if all are received) $fragmenterInstance = FragmenterFactory::createFragmenterInstance('package'); // Set it in this handler $this->fragmenterInstance = $fragmenterInstance; } /** * Creates an instance of this class * * @return $handlerInstance An instance of a chunk Handler class */ public final static function createChunkHandler () { // Get new instance $handlerInstance = new ChunkHandler(); // Get a FIFO stacker $stackInstance = ObjectFactory::createObjectByConfiguredName('chunk_handler_stacker_class'); // Init all stacker $stackInstance->initStacks(array( self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP, self::STACKER_NAME_CHUNKS_WITHOUT_FINAL, self::STACKER_NAME_ASSEMBLED_RAW_DATA )); // Set the stacker in this handler $handlerInstance->setStackInstance($stackInstance); // Get a crypto instance ... $cryptoInstance = ObjectFactory::createObjectByConfiguredName('crypto_class'); // ... and set it in this handler $handlerInstance->setCryptoInstance($cryptoInstance); // Return the prepared instance return $handlerInstance; } /** * Initializes the handler * * @return void */ private function initHandler () { // Noisy debug line: //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: Initializing handler ...'); // Init finalPackageChunks $this->finalPackageChunks = array( // Array for package content 'content' => array(), // ... and for the hashes 'hashes' => array(), // ... marker for that the final array is complete for assembling all chunks 'is_complete' => FALSE, // ... steps done to assemble all chunks 'assemble_steps' => 0, ); // ... chunkHashes: $this->chunkHashes = array(); // ... eopChunk: $this->eopChunk = array( 0 => 'INVALID', 1 => 'INVALID', ); } /** * Checks whether the hash generated from package content is the same ("valid") as given * * @param $chunkSplits An array from a splitted chunk * @return $isValid Whether the hash is "valid" */ private function isChunkHashValid (array $chunkSplits) { // Noisy debug line: //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: chunkSplits=' . print_r($chunkSplits, TRUE)); // Assert on some elements assert(isset($chunkSplits[self::CHUNK_SPLITS_INDEX_RAW_DATA])); assert(isset($chunkSplits[self::CHUNK_SPLITS_INDEX_HASH])); // Now hash the raw data again $chunkHash = $this->getCryptoInstance()->hashString($chunkSplits[self::CHUNK_SPLITS_INDEX_RAW_DATA], $chunkSplits[self::CHUNK_SPLITS_INDEX_HASH], FALSE); // Check it $isValid = ($chunkSplits[self::CHUNK_SPLITS_INDEX_HASH] === $chunkHash); // Debug output //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: chunkHash=' . $chunkHash . ',isValid=' . intval($isValid)); // ... and return it return $isValid; } /** * Checks whether the given serial number is valid * * @param $serialNumber A serial number from a chunk * @return $isValid Whether the serial number is valid */ private function isSerialNumberValid ($serialNumber) { // Check it $isValid = ((strlen($serialNumber) == PackageFragmenter::MAX_SERIAL_LENGTH) && ($this->hexval($serialNumber, FALSE) === $serialNumber)); // Return result return $isValid; } /** * Adds the chunk to the final array which will be used for the final step * which will be to assemble all chunks back to the original package content * and for the final hash check. * * This method may throw an exception if a chunk with the same serial number * has already been added to avoid mixing chunks from different packages. * * @param $chunkSplits An array from a splitted chunk * @return void */ private function addChunkToFinalArray (array $chunkSplits) { // Is the serial number (index 1) already been added? if (isset($this->finalPackageChunks[$chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL]])) { // Then throw an exception throw new ChunkAlreadyAssembledException(array($this, $chunkSplits), self::EXCEPTION_CHUNK_ALREADY_ASSEMBLED); } // END - if // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: serialNumber=' . $chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL] . ',hash=' . $chunkSplits[self::CHUNK_SPLITS_INDEX_HASH]); // Add the chunk data (index 2) to the final array and use the serial number as index $this->finalPackageChunks['content'][$chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL]] = $chunkSplits[self::CHUNK_SPLITS_INDEX_RAW_DATA]; // ... and the hash as well $this->finalPackageChunks['hashes'][$chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL]] = $chunkSplits[self::CHUNK_SPLITS_INDEX_HASH]; } /** * Marks the final array as completed, do only this if you really have all * chunks together including EOP and "hash chunk". * * @return void */ private function markFinalArrayAsCompleted () { /* * As for now, just set the array element. If any further steps are * being added, this should always be the last step. */ $this->finalPackageChunks['is_complete'] = TRUE; } /** * Sorts the chunks array by using the serial number as a sorting key. In * most situations a call of ksort() is enough to accomblish this. So this * method may only call ksort() on the chunks array. * * This method sorts 'content' and 'hashes' so both must have used the * serial numbers as array indexes. * * @return void */ private function sortChunksArray () { // Sort 'content' first ksort($this->finalPackageChunks['content']); // ... then 'hashes' ksort($this->finalPackageChunks['hashes']); } /** * Prepares the package assemble by removing last chunks (last shall be * hash chunk, pre-last shall be EOP chunk) and verify that all serial * numbers are valid (same as PackageFragmenter class would generate). * * @return void */ private function preparePackageAssmble () { // Make sure both arrays have same count (this however should always be TRUE) assert(count($this->finalPackageChunks['hashes']) == count($this->finalPackageChunks['content'])); //* DIE: */ exit(__METHOD__ . ':finalPackageChunks='.print_r($this->finalPackageChunks['content'], TRUE)); /* * Remove last element (hash chunk) from 'hashes'. This hash will never * be needed, so ignore it. */ array_pop($this->finalPackageChunks['hashes']); // ... and from 'content' as well but save it for later use $this->chunkHashes = explode(PackageFragmenter::CHUNK_HASH_SEPARATOR, substr(array_pop($this->finalPackageChunks['content']), strlen(PackageFragmenter::HASH_CHUNK_IDENTIFIER))); // Remove EOP chunk and keep a copy of it array_pop($this->finalPackageChunks['hashes']); $this->eopChunk = explode(PackageFragmenter::CHUNK_HASH_SEPARATOR, substr(array_pop($this->finalPackageChunks['content']), strlen(PackageFragmenter::END_OF_PACKAGE_IDENTIFIER))); // Verify all serial numbers $this->verifyChunkSerialNumbers(); } /** * Verifies all chunk serial numbers by using a freshly initialized * fragmenter instance. Do ALWAYS sort the array and array_pop() the hash * chunk before calling this method to avoid re-requests of many chunks. * * @return void */ private function verifyChunkSerialNumbers () { // Debug message //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: finalPackageChunks=' . print_r($this->finalPackageChunks, TRUE)); // Get final hash $finalHash = $this->generateFinalHash(implode('', $this->finalPackageChunks['content'])); // Reset the serial number generator $this->fragmenterInstance->resetSerialNumber($finalHash); // "Walk" through all (content) chunks foreach ($this->finalPackageChunks['content'] as $serialNumber => $content) { // Get next serial number $nextSerial = $this->fragmenterInstance->getNextHexSerialNumber($finalHash); // Debug output //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: serialNumber=' . $serialNumber . ',nextSerial=' . $nextSerial); // Is it not the same? Then re-request it if ($serialNumber != $nextSerial) { // This is invalid, so remove it unset($this->finalPackageChunks['content'][$serialNumber]); unset($this->finalPackageChunks['hashes'][$serialNumber]); // And re-request it with valid serial number (and hash chunk) $this->rerequestChunkBySerialNumber($nextSerial); } // END - if } // END - foreach } /** * Assembles and verifies ("final check") chunks back together to the * original package (raw data for the start). This method should only be * called AFTER the EOP and final-chunk chunk have been removed. * * @return void */ private function assembleAllChunksToPackage () { // If chunkHashes is not filled, don't continue assert(count($this->chunkHashes) > 0); // Init raw package data string $this->rawPackageData = ''; // That went well, so start assembling all chunks //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: Handling ' . count($this->finalPackageChunks['content']) . ' entries ...'); foreach ($this->finalPackageChunks['content'] as $serialNumber => $content) { // Assert on 'hash' entry (must always be set) assert(isset($this->finalPackageChunks['hashes'][$serialNumber])); // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: serialNumber=' . $serialNumber . ',hashes=' . $this->finalPackageChunks['hashes'][$serialNumber] . ' - validating ...'); //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('finalPackageChunks=' . print_r($this->finalPackageChunks, TRUE) . 'chunkHashes=' . print_r($this->chunkHashes, TRUE)); // Is this chunk valid? This should be the case assert($this->isChunkHashValid(array( self::CHUNK_SPLITS_INDEX_HASH => $this->finalPackageChunks['hashes'][$serialNumber], self::CHUNK_SPLITS_INDEX_RAW_DATA => $content ))); // ... and is also in the hash chunk? assert(in_array($this->finalPackageChunks['hashes'][$serialNumber], $this->chunkHashes)); // Verification okay, add it to the raw data //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: Adding ' . strlen($content) . ' bytes as raw package data ...'); $this->rawPackageData .= $content; } // END - foreach // Debug output //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: eopChunk[1]=' . $this->eopChunk[1] . ',index=' . (count($this->chunkHashes) - 2) . ',chunkHashes='.print_r($this->chunkHashes, TRUE)); // The last chunk hash must match with the one from eopChunk[1] assert($this->eopChunk[1] == $this->chunkHashes[count($this->chunkHashes) - 2]); } /** * Generate final hash if EOP chunk is found, else an assert will happen. * * @param $rawPackageData Raw package data * @return $finalHash Final hash if EOP chunk is found */ private function generateFinalHash ($rawPackageData) { // Make sure the raw package data is given assert((is_string($rawPackageData)) && (!empty($rawPackageData))); // Make sure the EOP chunk is set assert((isset($this->eopChunk[0])) && (isset($this->eopChunk[1]))); assert((is_string($this->eopChunk[0])) && (!empty($this->eopChunk[0]))); // Hash the raw data $finalHash = $this->getCryptoInstance()->hashString($rawPackageData, $this->eopChunk[0], FALSE); // Return it return $finalHash; } /** * Verifies the finally assembled raw package data by comparing it against * the final hash. * * @return void */ private function verifyRawPackageData () { // Generate final hash $finalHash = $this->generateFinalHash($this->rawPackageData); // Is it the same? //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: eopChunk[1]=' . $this->eopChunk[1] . ',finalHash=' . $finalHash); assert($finalHash == $this->eopChunk[0]); } /** * Checks whether the final (last) chunk is valid * * @param $chunks An array with chunks and (hopefully) a valid final chunk * @return $isValid Whether the final (last) chunk is valid */ private function isValidFinalChunk (array $chunks) { // Default is all fine $isValid = TRUE; // Split the (possible) EOP chunk $chunkSplits = explode(PackageFragmenter::CHUNK_DATA_HASH_SEPARATOR, $chunks[count($chunks) - 1]); // Make sure chunks with only 3 elements are parsed (for details see ChunkHandler) //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('eopChunk=' . $chunks[count($chunks) - 1] . ',chunkSplits=' . print_r($chunkSplits, TRUE)); assert(count($chunkSplits) == 3); // Validate final chunk if (substr($chunkSplits[ChunkHandler::CHUNK_SPLITS_INDEX_RAW_DATA], 0, strlen(PackageFragmenter::END_OF_PACKAGE_IDENTIFIER)) != PackageFragmenter::END_OF_PACKAGE_IDENTIFIER) { // Not fine $isValid = FALSE; } elseif (substr_count($chunkSplits[ChunkHandler::CHUNK_SPLITS_INDEX_RAW_DATA], PackageFragmenter::CHUNK_HASH_SEPARATOR) != 1) { // CHUNK_HASH_SEPARATOR shall only be found once $isValid = FALSE; } // Return status return $isValid; } /** * Adds all chunks if the last one verifies as a 'final chunk'. * * @param $chunks An array with chunks, the last one should be a 'final' * @return void * @throws FinalChunkVerificationException If the final chunk does not start with 'EOP:' */ public function addAllChunksWithFinal (array $chunks) { // Try to validate the final chunk try { // Validate final chunk $this->isValidFinalChunk($chunks); } catch (AssertionException $e) { // Last chunk is not valid throw new FinalChunkVerificationException(array($this, $chunks, $e), BaseListener::EXCEPTION_FINAL_CHUNK_VERIFICATION); } // Do we have some pending chunks (no final)? while (!$this->getStackInstance()->isStackEmpty(self::STACKER_NAME_CHUNKS_WITHOUT_FINAL)) { // Then get it first and add it before the EOP chunks array_unshift($chunks, $this->getStackInstance()->popNamed(self::STACKER_NAME_CHUNKS_WITHOUT_FINAL)); } // END - while // Add all chunks to the FIFO stacker foreach ($chunks as $chunk) { // Add the chunk $this->getStackInstance()->pushNamed(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP, $chunk); } // END - foreach } /** * Adds all chunks and wait for more (e.g. incomplete transmission) * * @param $chunks An array with chunks, the last one should be a 'final' * @return void */ public function addAllChunksWait (array $chunks) { // Add all chunks to the FIFO stacker foreach ($chunks as $chunk) { // Add the chunk $this->getStackInstance()->pushNamed(self::STACKER_NAME_CHUNKS_WITHOUT_FINAL, $chunk); } // END - foreach } /** * Checks whether unhandled chunks are available * * @return $unhandledChunks Whether unhandled chunks are left */ public function ifUnhandledChunksWithFinalAvailable () { // Simply check if the stacker is not empty $unhandledChunks = $this->getStackInstance()->isStackEmpty(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP) === FALSE; // Return result return $unhandledChunks; } /** * Handles available chunks by processing one-by-one (not all together, * this would slow-down the whole application) with the help of an * iterator. * * @return void */ public function handleAvailableChunksWithFinal () { // First check if there are undhandled chunks available assert($this->ifUnhandledChunksWithFinalAvailable()); // Get an entry from the stacker $chunk = $this->getStackInstance()->popNamed(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP); // Split the string with proper separator character $chunkSplits = explode(PackageFragmenter::CHUNK_DATA_HASH_SEPARATOR, $chunk); /* * Make sure three elements are always found: * 0 = Hash * 1 = Serial number * 2 = Raw data */ assert(count($chunkSplits) == 3); // Is the generated hash from data same ("valid") as given hash? if (!$this->isChunkHashValid($chunkSplits)) { // Do some logging self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: Chunk content is not validating against given hash.'); // Re-request this chunk (trust the hash in index # 0) $this->rerequestChunkBySplitsArray($chunkSplits); // Don't process this chunk return; } // END - if // Is the serial number valid (chars 0-9, length equals PackageFragmenter::MAX_SERIAL_LENGTH)? if (!$this->isSerialNumberValid($chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL])) { // Do some logging self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: Chunk serial number ' . $chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL] . ' for hash ' . $chunkSplits[self::CHUNK_SPLITS_INDEX_HASH] . ' is invalid.'); // Re-request this chunk $this->rerequestChunkBySplitsArray($chunkSplits); // Don't process this chunk return; } // END - if /* * It is now known that (as long as the hash algorithm has no * collisions) the content is the same as the sender sends it to this * peer. * * And also the serial number is valid (basicly) at this point. Now the * chunk can be added to the final array. */ $this->addChunkToFinalArray($chunkSplits); // Is the stack now empty? if ($this->getStackInstance()->isStackEmpty(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP)) { // Then mark the final array as complete $this->markFinalArrayAsCompleted(); } // END - if } /** * Checks whether unassembled chunks are available (ready) in final array * * @return $unassembledChunksAvailable Whether unassembled chunks are available */ public function ifUnassembledChunksAvailable () { // For now do only check the array element 'is_complete' $unassembledChunksAvailable = ($this->finalPackageChunks['is_complete'] === TRUE); // Return status return $unassembledChunksAvailable; } /** * Assembles all chunks (except EOP and "hash chunk") back together to the original package data. * * This is done by the following steps: * * 1) Sort the final array with ksort(). This will bring the "hash * chunk" up to the last array index and the EOP chunk to the * pre-last array index * 2) Assemble all chunks except two last (see above step) * 3) While so, do the final check on all hashes * 4) If the package is assembled back together, hash it again for * the very final verification. * * @return void */ public function assembleChunksFromFinalArray () { // Make sure the final array is really completed assert($this->ifUnassembledChunksAvailable()); // Count up stepping $this->finalPackageChunks['assemble_steps']++; // Do the next step switch ($this->finalPackageChunks['assemble_steps']) { case 1: // Sort the chunks array (the serial number shall act as a sorting key) $this->sortChunksArray(); break; case 2: // Prepare the assemble by removing last two indexes $this->preparePackageAssmble(); break; case 3: // Assemble all chunks back together to the original package $this->assembleAllChunksToPackage(); break; case 4: // Verify the raw data by hashing it again $this->verifyRawPackageData(); break; case 5: // Re-initialize handler to reset it to the old state $this->initHandler(); break; default: // Invalid step found self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: Invalid step ' . $this->finalPackageChunks['assemble_steps'] . ' detected.'); break; } // END - switch } /** * Checks whether the raw package data has been assembled back together. * This can be safely assumed when rawPackageData is not empty and the * collection of all chunks is FALSE (because initHandler() will reset it). * * @return $isRawPackageDataAvailable Whether raw package data is available */ public function ifRawPackageDataIsAvailable () { // Check it //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: this->rawPackageData()=' . strlen($this->rawPackageData) . ',ifUnassembledChunksAvailable()=' . intval($this->ifUnassembledChunksAvailable())); $isRawPackageDataAvailable = ((!empty($this->rawPackageData)) && (!$this->ifUnassembledChunksAvailable())); // Return it //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: isRawPackageDataAvailable=' . intval($isRawPackageDataAvailable)); return $isRawPackageDataAvailable; } /** * Handles the finally assembled raw package data by feeding it into another * stacker for further decoding/processing. * * @return void */ public function handledAssembledRawPackageData () { // Assert to make sure that there is raw package data available assert($this->ifRawPackageDataIsAvailable()); // Then feed it into the next stacker //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER[' . __METHOD__ . ':' . __LINE__ . ']: Pushing ' . strlen($this->rawPackageData) . ' bytes to stack ' . self::STACKER_NAME_ASSEMBLED_RAW_DATA . ' ...'); $this->getStackInstance()->pushNamed(self::STACKER_NAME_ASSEMBLED_RAW_DATA, $this->rawPackageData); // ... and reset it $this->rawPackageData = ''; } } // [EOF] ?>