X-Git-Url: https://git.mxchange.org/?p=hub.git;a=blobdiff_plain;f=application%2Fhub%2Fmain%2Fhandler%2Fchunks%2Fclass_ChunkHandler.php;h=739e07e247bfd2b7a0ae9bca34f214d1d706fdc9;hp=14a726563dd477563c8596d6816cc2c27c7b63f4;hb=b7c47cd5ca83bb9f8d5a1b46d553b5f1f2bbd80f;hpb=c4cc97b7c641a57ec0630d995ca8e0d70fdeda2d diff --git a/application/hub/main/handler/chunks/class_ChunkHandler.php b/application/hub/main/handler/chunks/class_ChunkHandler.php index 14a726563..739e07e24 100644 --- a/application/hub/main/handler/chunks/class_ChunkHandler.php +++ b/application/hub/main/handler/chunks/class_ChunkHandler.php @@ -4,7 +4,7 @@ * * @author Roland Haeder * @version 0.0.0 - * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2011 Hub Developer Team + * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2012 Hub Developer Team * @license GNU GPL 3.0 or any newer version * @link http://www.ship-simu.org * @@ -26,6 +26,41 @@ 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 = ''; /** * Protected constructor @@ -38,6 +73,9 @@ class ChunkHandler extends BaseHandler implements HandleableChunks, Registerable // Set handler name $this->setHandlerName('chunk'); + + // Initialize handler + $this->initHandler(); } /** @@ -54,6 +92,8 @@ class ChunkHandler extends BaseHandler implements HandleableChunks, Registerable // Init all stacker $stackerInstance->initStacker(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP); + $stackerInstance->initStacker(self::STACKER_NAME_CHUNKS_WITHOUT_FINAL); + $stackerInstance->initStacker(self::STACKER_NAME_ASSEMBLED_RAW_DATA); // Set the stacker in this handler $handlerInstance->setStackerInstance($stackerInstance); @@ -64,10 +104,252 @@ class ChunkHandler extends BaseHandler implements HandleableChunks, Registerable // ... and set it in this handler $handlerInstance->setCryptoInstance($cryptoInstance); + // 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 + $handlerInstance->setFragmenterInstance($fragmenterInstance); + // Return the prepared instance return $handlerInstance; } + /** + * Initializes the handler + * + * @return void + */ + private function initHandler () { + // 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) { + // Now hash the raw data again + $chunkHash = $this->getCryptoInstance()->hashString($chunkSplits[self::CHUNK_SPLITS_INDEX_RAW_DATA], $chunkSplits[self::CHUNK_SPLITS_INDEX_HASH], false); + + // Debug output + //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER: chunkHash=' . $chunkHash . ',chunkSplits[chunk_hash]=' . $chunkSplits[self::CHUNK_SPLITS_INDEX_HASH] . ',chunkSplits[serial]=' . $chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL] . ',chunkSplits[raw_data]=' . $chunkSplits[self::CHUNK_SPLITS_INDEX_RAW_DATA]); + + // Check it + $isValid = ($chunkSplits[self::CHUNK_SPLITS_INDEX_HASH] === $chunkHash); + + // ... 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: 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 () { + // Reset the serial number generator + $this->getFragmenterInstance()->resetSerialNumber(); + + // "Walk" through all (content) chunks + foreach ($this->finalPackageChunks['content'] as $serialNumber => $content) { + // Get next serial number + $nextSerial = $this->getFragmenterInstance()->getNextHexSerialNumber(); + + // Debug output + //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER: 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 + foreach ($this->finalPackageChunks['content'] as $serialNumber => $content) { + // Debug message + //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER: serialNumber=' . $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 + $this->rawPackageData .= $content; + } // END - foreach + + // Debug output + //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER: eopChunk[1]=' . $this->eopChunk[1] . ',' . chr(10) . 'index=' . (count($this->chunkHashes) - 2) . ',' . chr(10) . '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]); + } + + /** + * Verifies the finally assembled raw package data by comparing it against + * the final hash. + * + * @return void + */ + private function verifyRawPackageData () { + // Hash the raw package data for final verification + $finalHash = $this->getCryptoInstance()->hashString($this->rawPackageData, $this->eopChunk[0], false); + + // Is it the same? + assert($finalHash == $this->eopChunk[0]); + } + /** * Adds all chunks if the last one verifies as a 'final chunk'. * @@ -82,6 +364,12 @@ class ChunkHandler extends BaseHandler implements HandleableChunks, Registerable throw new FinalChunkVerificationException(array($this, $chunks), BaseListener::EXCEPTION_FINAL_CHUNK_VERIFICATION); } // END - if + // Do we have some pending chunks (no final)? + while (!$this->getStackerInstance()->isStackEmpty(self::STACKER_NAME_CHUNKS_WITHOUT_FINAL)) { + // Then get it first and add it before the EOP chunks + array_unshift($chunks, $this->getStackerInstance()->popNamed(self::STACKER_NAME_CHUNKS_WITHOUT_FINAL)); + } // END - while + // Add all chunks to the FIFO stacker foreach ($chunks as $chunk) { // Add the chunk @@ -89,12 +377,26 @@ class ChunkHandler extends BaseHandler implements HandleableChunks, Registerable } // 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->getStackerInstance()->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 ifUnhandledChunksAvailable () { + public function ifUnhandledChunksWithFinalAvailable () { // Simply check if the stacker is not empty $unhandledChunks = $this->getStackerInstance()->isStackEmpty(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP) === false; @@ -109,9 +411,9 @@ class ChunkHandler extends BaseHandler implements HandleableChunks, Registerable * * @return void */ - public function handleAvailableChunks () { + public function handleAvailableChunksWithFinal () { // First check if there are undhandled chunks available - assert($this->ifUnhandledChunksAvailable()); + assert($this->ifUnhandledChunksWithFinalAvailable()); // Get an entry from the stacker $chunk = $this->getStackerInstance()->popNamed(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP); @@ -127,21 +429,140 @@ class ChunkHandler extends BaseHandler implements HandleableChunks, Registerable */ assert(count($chunkSplits) == 3); - // Now hash the raw data again - $chunkHash = $this->getCryptoInstance()->hashString($chunkSplits[2], $chunkSplits[0], false); + // Is the generated hash from data same ("valid") as given hash? + if (!$this->isChunkHashValid($chunkSplits)) { + // Do some logging + self::createDebugInstance(__CLASS__)->debugOutput('CHUNK-HANDLER: Chunk content is not validating against given hash.'); - // Debug output - /* NOISY-DEBUG: */ $this->debugOutput('CHUNK-HANDLER: chunkHash=' . $chunkHash . ',chunkSplits[0]=' . $chunkSplits[0] . ',chunkSplits[1]=' . $chunkSplits[1]); - - // Is it the same? - if ($chunkSplits[0] != $chunkHash) { // Re-request this chunk (trust the hash in index # 0) $this->rerequestChunkBySplitsArray($chunkSplits); // Don't process this chunk return; } // END - if - die('chunk=' . $chunk . chr(10)); + + // 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: 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->getStackerInstance()->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: 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 + $isRawPackageDataAvailable = ((!empty($this->rawPackageData)) && (!$this->ifUnassembledChunksAvailable())); + + // Return it + 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 + $this->getStackerInstance()->pushNamed(self::STACKER_NAME_ASSEMBLED_RAW_DATA, $this->rawPackageData); + + // ... and reset it + $this->rawPackageData = ''; } }