]> git.mxchange.org Git - hub.git/blob - application/hub/main/handler/chunks/class_ChunkHandler.php
'hub' project continued:
[hub.git] / application / hub / main / handler / chunks / class_ChunkHandler.php
1 <?php
2 /**
3  * A Chunk handler
4  *
5  * @author              Roland Haeder <webmaster@ship-simu.org>
6  * @version             0.0.0
7  * @copyright   Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2011 Hub Developer Team
8  * @license             GNU GPL 3.0 or any newer version
9  * @link                http://www.ship-simu.org
10  *
11  * This program is free software: you can redistribute it and/or modify
12  * it under the terms of the GNU General Public License as published by
13  * the Free Software Foundation, either version 3 of the License, or
14  * (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  * GNU General Public License for more details.
20  *
21  * You should have received a copy of the GNU General Public License
22  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23  */
24 class ChunkHandler extends BaseHandler implements HandleableChunks, Registerable {
25         /**
26          * Stacker for chunks with final EOP
27          */
28         const STACKER_NAME_CHUNKS_WITH_FINAL_EOP = 'final_chunks';
29         const STACKER_NAME_ASSEMBLED_RAW_DATA    = 'chunk_raw_data';
30
31         /**
32          * Chunk splits:
33          * 0 = Hash
34          * 1 = Serial number
35          * 2 = Raw data
36          */
37         const CHUNK_SPLITS_INDEX_HASH     = 0;
38         const CHUNK_SPLITS_INDEX_SERIAL   = 1;
39         const CHUNK_SPLITS_INDEX_RAW_DATA = 2;
40
41         /**
42          * The final array for assembling the original package back together
43          */
44         private $finalPackageChunks = array();
45
46         /**
47          * Array of chunk hashes
48          */
49         private $chunkHashes = array();
50
51         /**
52          * Raw EOP chunk data in an array:
53          *
54          * 0 = Final hash,
55          * 1 = Hash of last chunk
56          */
57         private $eopChunk = array();
58
59         /**
60          * Raw package data
61          */
62         private $rawPackageData = '';
63
64         /**
65          * Protected constructor
66          *
67          * @return      void
68          */
69         protected function __construct () {
70                 // Call parent constructor
71                 parent::__construct(__CLASS__);
72
73                 // Set handler name
74                 $this->setHandlerName('chunk');
75
76                 // Initialize handler
77                 $this->initHandler();
78         }
79
80         /**
81          * Creates an instance of this class
82          *
83          * @return      $handlerInstance        An instance of a chunk Handler class
84          */
85         public final static function createChunkHandler () {
86                 // Get new instance
87                 $handlerInstance = new ChunkHandler();
88
89                 // Get a FIFO stacker
90                 $stackerInstance = ObjectFactory::createObjectByConfiguredName('chunk_handler_stacker_class');
91
92                 // Init all stacker
93                 $stackerInstance->initStacker(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP);
94                 $stackerInstance->initStacker(self::STACKER_NAME_ASSEMBLED_RAW_DATA);
95
96                 // Set the stacker in this handler
97                 $handlerInstance->setStackerInstance($stackerInstance);
98
99                 // Get a crypto instance ...
100                 $cryptoInstance = ObjectFactory::createObjectByConfiguredName('crypto_class');
101
102                 // ... and set it in this handler
103                 $handlerInstance->setCryptoInstance($cryptoInstance);
104
105                 // Get a fragmenter instance for later verification of serial numbers (e.g. if all are received)
106                 $fragmenterInstance = FragmenterFactory::createFragmenterInstance('package');
107
108                 // Set it in this handler
109                 $handlerInstance->setFragmenterInstance($fragmenterInstance);
110
111                 // Return the prepared instance
112                 return $handlerInstance;
113         }
114
115         /**
116          * Initializes the handler
117          *
118          * @return      void
119          */
120         private function initHandler () {
121                 // Init finalPackageChunks
122                 $this->finalPackageChunks = array(
123                         // Array for package content
124                         'content'        => array(),
125                         // ... and for the hashes
126                         'hashes'         => array(),
127                         // ... marker for that the final array is complete for assembling all chunks
128                         'is_complete'    => false,
129                         // ... steps done to assemble all chunks
130                         'assemble_steps' => 0,
131                 );
132
133                 // ... chunkHashes:
134                 $this->chunkHashes = array();
135
136                 // ... eopChunk:
137                 $this->eopChunk = array(
138                         0 => 'INVALID',
139                         1 => 'INVALID',
140                 );
141         }
142
143         /**
144          * Checks whether the hash generated from package content is the same ("valid") as given
145          *
146          * @param       $chunkSplits    An array from a splitted chunk
147          * @return      $isValid                Whether the hash is "valid"
148          */
149         private function isChunkHashValid (array $chunkSplits) {
150                 // Now hash the raw data again
151                 $chunkHash = $this->getCryptoInstance()->hashString($chunkSplits[self::CHUNK_SPLITS_INDEX_RAW_DATA], $chunkSplits[self::CHUNK_SPLITS_INDEX_HASH], false);
152
153                 // Debug output
154                 //* NOISY-DEBUG: */ $this->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]);
155
156                 // Check it
157                 $isValid = ($chunkSplits[self::CHUNK_SPLITS_INDEX_HASH] === $chunkHash);
158
159                 // ... and return it
160                 return $isValid;
161         }
162
163         /**
164          * Checks whether the given serial number is valid
165          *
166          * @param       $serialNumber   A serial number from a chunk
167          * @return      $isValid                Whether the serial number is valid
168          */
169         private function isSerialNumberValid ($serialNumber) {
170                 // Check it
171                 $isValid = ((strlen($serialNumber) == PackageFragmenter::MAX_SERIAL_LENGTH) && ($this->bigintval($serialNumber, false) === $serialNumber));
172
173                 // Return result
174                 return $isValid;
175         }
176
177         /**
178          * Adds the chunk to the final array which will be used for the final step
179          * which will be to assemble all chunks back to the original package content
180          * and for the final hash check.
181          *
182          * This method may throw an exception if a chunk with the same serial number
183          * has already been added to avoid mixing chunks from different packages.
184          *
185          * @param       $chunkSplits    An array from a splitted chunk
186          * @return      void
187          */
188         private function addChunkToFinalArray (array $chunkSplits) {
189                 // Is the serial number (index 1) already been added?
190                 if (isset($this->finalPackageChunks[$chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL]])) {
191                         // Then throw an exception
192                         throw new ChunkAlreadyAssembledException(array($this, $chunkSplits), self::EXCEPTION_CHUNK_ALREADY_ASSEMBLED);
193                 } // END - if
194
195                 // Add the chunk data (index 2) to the final array and use the serial number as index
196                 $this->finalPackageChunks['content'][$chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL]] = $chunkSplits[self::CHUNK_SPLITS_INDEX_RAW_DATA];
197
198                 // ... and the hash as well
199                 $this->finalPackageChunks['hashes'][$chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL]] = $chunkSplits[self::CHUNK_SPLITS_INDEX_HASH];
200         }
201
202         /**
203          * Marks the final array as completed, do only this if you really have all
204          * chunks together including EOP and "hash chunk".
205          *
206          * @return      void
207          */
208         private function markFinalArrayAsCompleted () {
209                 /*
210                  * As for now, just set the array element. If any further steps are
211                  * being added, this should always be the last step.
212                  */
213                 $this->finalPackageChunks['is_complete'] = true;
214         }
215
216         /**
217          * Sorts the chunks array by using the serial number as a sorting key. In
218          * most situations a call of ksort() is enough to accomblish this. So this
219          * method may only call ksort() on the chunks array.
220          *
221          * This method sorts 'content' and 'hashes' so both must have used the
222          * serial numbers as array indexes.
223          *
224          * @return      void
225          */
226         private function sortChunksArray () {
227                 // Sort 'content' first
228                 ksort($this->finalPackageChunks['content']);
229
230                 // ... then 'hashes'
231                 ksort($this->finalPackageChunks['hashes']);
232         }
233
234         /**
235          * Prepares the package assemble by removing last chunks (last shall be
236          * hash chunk, pre-last shall be EOP chunk) and verify that all serial
237          * numbers are valid (same as PackageFragmenter class would generate).
238          *
239          * @return      void
240          */
241         private function preparePackageAssmble () {
242                 // Make sure both arrays have same count (this however should always be true)
243                 assert(count($this->finalPackageChunks['hashes']) == count($this->finalPackageChunks['content']));
244
245                 /*
246                  * Remove last element (hash chunk) from 'hashes'. This hash will never
247                  * be needed, so ignore it.
248                  */
249                 array_pop($this->finalPackageChunks['hashes']);
250
251                 // ... and from 'content' as well but save it for later use
252                 $this->chunkHashes = explode(PackageFragmenter::CHUNK_HASH_SEPARATOR, substr(array_pop($this->finalPackageChunks['content']), strlen(PackageFragmenter::HASH_CHUNK_IDENTIFIER)));
253
254                 // Remove EOP chunk and keep a copy of it
255                 array_pop($this->finalPackageChunks['hashes']);
256                 $this->eopChunk = explode(PackageFragmenter::CHUNK_HASH_SEPARATOR, substr(array_pop($this->finalPackageChunks['content']), strlen(PackageFragmenter::END_OF_PACKAGE_IDENTIFIER)));
257
258                 // Verify all serial numbers
259                 $this->verifyChunkSerialNumbers();
260         }
261
262         /**
263          * Verifies all chunk serial numbers by using a freshly initialized
264          * fragmenter instance. Do ALWAYS sort the array and array_pop() the hash
265          * chunk before calling this method to avoid re-requests of many chunks.
266          *
267          * @return      void
268          */
269         private function verifyChunkSerialNumbers () {
270                 // Reset the serial number generator
271                 $this->getFragmenterInstance()->resetSerialNumber();
272
273                 // "Walk" through all (content) chunks
274                 foreach ($this->finalPackageChunks['content'] as $serialNumber=>$content) {
275                         // Get next serial number
276                         $nextSerial = $this->getFragmenterInstance()->getNextHexSerialNumber();
277
278                         // Debug output
279                         //* NOISY-DEBUG */ $this->debugOutput('CHUNK-HANDLER: serialNumber=' . $serialNumber . ',nextSerial=' . $nextSerial);
280
281                         // Is it not the same? Then re-request it
282                         if ($serialNumber != $nextSerial) {
283                                 // This is invalid, so remove it
284                                 unset($this->finalPackageChunks['content'][$serialNumber]);
285                                 unset($this->finalPackageChunks['hashes'][$serialNumber]);
286
287                                 // And re-request it with valid serial number (and hash chunk)
288                                 $this->rerequestChunkBySerialNumber($nextSerial);
289                         } // END - if
290                 } // END - foreach
291         }
292
293         /**
294          * Assembles and verifies ("final check") chunks back together to the
295          * original package (raw data for the start). This method should only be
296          * called AFTER the EOP and final-chunk chunk have been removed.
297          *
298          * @return      void
299          */
300         private function assembleAllChunksToPackage () {
301                 // If chunkHashes is not filled, don't continue
302                 assert(count($this->chunkHashes) > 0);
303
304                 // Init raw package data string
305                 $this->rawPackageData = '';
306
307                 // That went well, so start assembling all chunks
308                 foreach ($this->finalPackageChunks['content'] as $serialNumber=>$content) {
309                         // Is this chunk valid? This should be the case
310                         assert($this->isChunkHashValid(array(
311                                 self::CHUNK_SPLITS_INDEX_HASH     => $this->finalPackageChunks['hashes'][$serialNumber],
312                                 self::CHUNK_SPLITS_INDEX_RAW_DATA => $content
313                         )));
314
315                         // ... and is also in the hash chunk?
316                         assert(in_array($this->finalPackageChunks['hashes'][$serialNumber], $this->chunkHashes));
317
318                         // Verification okay, add it to the raw data
319                         $this->rawPackageData .= $content;
320                 } // END - foreach
321
322                 // Debug output
323                 //* NOISY-DEBUG: */ $this->debugOutput('CHUNK-HANDLER: eopChunk[1]=' . $this->eopChunk[1] . ',' . chr(10) . 'index=' . (count($this->chunkHashes) - 2) . ',' . chr(10) . 'chunkHashes='.print_r($this->chunkHashes,true));
324
325                 // The last chunk hash must match with the one from eopChunk[1]
326                 assert($this->eopChunk[1] == $this->chunkHashes[count($this->chunkHashes) - 2]);
327         }
328
329         /**
330          * Verifies the finally assembled raw package data by comparing it against
331          * the final hash.
332          *
333          * @return      void
334          */
335         private function verifyRawPackageData () {
336                 // Hash the raw package data for final verification
337                 $finalHash = $this->getCryptoInstance()->hashString($this->rawPackageData, $this->eopChunk[0], false);
338
339                 // Is it the same?
340                 assert($finalHash == $this->eopChunk[0]);
341         }
342
343         /**
344          * Adds all chunks if the last one verifies as a 'final chunk'.
345          *
346          * @param       $chunks         An array with chunks, the last one should be a 'final'
347          * @return      void
348          * @throws      FinalChunkVerificationException         If the final chunk does not start with 'EOP:'
349          */
350         public function addAllChunksWithFinal (array $chunks) {
351                 // Validate final chunk
352                 if (!$this->isValidFinalChunk($chunks)) {
353                         // Last chunk is not valid
354                         throw new FinalChunkVerificationException(array($this, $chunks), BaseListener::EXCEPTION_FINAL_CHUNK_VERIFICATION);
355                 } // END - if
356
357                 // Add all chunks to the FIFO stacker
358                 foreach ($chunks as $chunk) {
359                         // Add the chunk
360                         $this->getStackerInstance()->pushNamed(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP, $chunk);
361                 } // END - foreach
362         }
363
364         /**
365          * Checks whether unhandled chunks are available
366          *
367          * @return      $unhandledChunks        Whether unhandled chunks are left
368          */
369         public function ifUnhandledChunksWithFinalAvailable () {
370                 // Simply check if the stacker is not empty
371                 $unhandledChunks = $this->getStackerInstance()->isStackEmpty(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP) === false;
372
373                 // Return result
374                 return $unhandledChunks;
375         }
376
377         /**
378          * Handles available chunks by processing one-by-one (not all together,
379          * this would slow-down the whole application) with the help of an
380          * iterator.
381          *
382          * @return      void
383          */
384         public function handleAvailableChunksWithFinal () {
385                 // First check if there are undhandled chunks available
386                 assert($this->ifUnhandledChunksWithFinalAvailable());
387
388                 // Get an entry from the stacker
389                 $chunk = $this->getStackerInstance()->popNamed(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP);
390
391                 // Split the string with proper separator character
392                 $chunkSplits = explode(PackageFragmenter::CHUNK_DATA_HASH_SEPARATOR, $chunk);
393
394                 /*
395                  * Make sure three elements are always found:
396                  * 0 = Hash
397                  * 1 = Serial number
398                  * 2 = Raw data
399                  */
400                 assert(count($chunkSplits) == 3);
401
402                 // Is the generated hash from data same ("valid") as given hash?
403                 if (!$this->isChunkHashValid($chunkSplits)) {
404                         // Do some logging
405                         $this->debugOutput('CHUNK-HANDLER: Chunk content is not validating against given hash.');
406
407                         // Re-request this chunk (trust the hash in index # 0)
408                         $this->rerequestChunkBySplitsArray($chunkSplits);
409
410                         // Don't process this chunk
411                         return;
412                 } // END - if
413
414                 // Is the serial number valid (chars 0-9, length equals PackageFragmenter::MAX_SERIAL_LENGTH)?
415                 if (!$this->isSerialNumberValid($chunkSplits[self::CHUNK_SPLITS_INDEX_SERIAL])) {
416                         // Do some logging
417                         $this->debugOutput('CHUNK-HANDLER: Chunk serial numberĀ for hash ' . $chunkSplits[self::CHUNK_SPLITS_INDEX_HASH] . ' is invalid.');
418
419                         // Re-request this chunk
420                         $this->rerequestChunkBySplitsArray($chunkSplits);
421
422                         // Don't process this chunk
423                         return;
424                 } // END - if
425
426                 /*
427                  * It is now known that (as long as the hash algorithm has no
428                  * collisions) the content is the same as the sender sends it to this
429                  * peer.
430                  *
431                  * And also the serial number is valid (basicly) at this point. Now the
432                  * chunk can be added to the final array.
433                  */
434                 $this->addChunkToFinalArray($chunkSplits);
435
436                 // Is the stack now empty?
437                 if ($this->getStackerInstance()->isStackEmpty(self::STACKER_NAME_CHUNKS_WITH_FINAL_EOP)) {
438                         // Then mark the final array as complete
439                         $this->markFinalArrayAsCompleted();
440                 } // END - if
441         }
442
443         /**
444          * Checks whether unassembled chunks are available (ready) in final array
445          *
446          * @return      $unassembledChunksAvailable             Whether unassembled chunks are available
447          */
448         public function ifUnassembledChunksAvailable () {
449                 // For now do only check the array element 'is_complete'
450                 $unassembledChunksAvailable = ($this->finalPackageChunks['is_complete'] === true);
451
452                 // Return status
453                 return $unassembledChunksAvailable;
454         }
455
456         /**
457          * Assembles all chunks (except EOP and "hash chunk") back together to the original package data.
458          *
459          * This is done by the following steps:
460          *
461          * 1) Sort the final array with ksort(). This will bring the "hash
462          *    chunk" up to the last array index and the EOP chunk to the
463          *    pre-last array index
464          * 2) Assemble all chunks except two last (see above step)
465          * 3) While so, do the final check on all hashes
466          * 4) If the package is assembled back together, hash it again for
467          *    the very final verification.
468          *
469          * @return      void
470          */
471         public function assembleChunksFromFinalArray () {
472                 // Make sure the final array is really completed
473                 assert($this->ifUnassembledChunksAvailable());
474
475                 // Count up stepping
476                 $this->finalPackageChunks['assemble_steps']++;
477
478                 // Do the next step
479                 switch ($this->finalPackageChunks['assemble_steps']) {
480                         case 1: // Sort the chunks array (the serial number shall act as a sorting key)
481                                 $this->sortChunksArray();
482                                 break;
483
484                         case 2: // Prepare the assemble by removing last two indexes
485                                 $this->preparePackageAssmble();
486                                 break;
487
488                         case 3: // Assemble all chunks back together to the original package
489                                 $this->assembleAllChunksToPackage();
490                                 break;
491
492                         case 4: // Verify the raw data by hashing it again
493                                 $this->verifyRawPackageData();
494                                 break;
495
496                         case 5: // Re-initialize handler to reset it to the old state
497                                 $this->initHandler();
498                                 break;
499
500                         default: // Invalid step found
501                                 $this->debugOutput('CHUNK-HANDLER: Invalid step ' . $this->finalPackageChunks['assemble_steps'] . ' detected.');
502                                 break;
503                 } // END - switch
504         }
505
506         /**
507          * Checks whether the raw package data has been assembled back together.
508          * This can be safely assumed when rawPackageData is not empty and the
509          * collection of all chunks is false (because initHandler() will reset it).
510          *
511          * @return      $isRawPackageDataAvailable      Whether raw package data is available
512          */
513         public function ifRawPackageDataIsAvailable () {
514                 // Check it
515                 $isRawPackageDataAvailable = ((!empty($this->rawPackageData)) && (!$this->ifUnassembledChunksAvailable()));
516
517                 // Return it
518                 return $isRawPackageDataAvailable;
519         }
520
521         /**
522          * Handles the finally assembled raw package data by feeding it into another
523          * stacker for further decoding/processing.
524          *
525          * @return      void
526          */
527         public function handledAssembledRawPackageData () {
528                 // Assert to make sure that there is raw package data available
529                 assert($this->ifRawPackageDataIsAvailable());
530
531                 // Then feed it into the next stacker
532                 $this->getStackerInstance()->pushNamed(self::STACKER_NAME_ASSEMBLED_RAW_DATA, $this->rawPackageData);
533
534                 // ... and reset it
535                 $this->rawPackageData = '';
536         }
537 }
538
539 // [EOF]
540 ?>