Source Code

00001: <?php
00002: /**
00003:  * Copyright (c) 2009-2010, Laurent Laville <pear@laurent-laville.org>
00004:  *                          Bertrand Mansion <bmansion@mamasam.com>
00005:  *
00006:  * All rights reserved.
00007:  *
00008:  * Redistribution and use in source and binary forms, with or without
00009:  * modification, are permitted provided that the following conditions
00010:  * are met:
00011:  *
00012:  *     * Redistributions of source code must retain the above copyright
00013:  *       notice, this list of conditions and the following disclaimer.
00014:  *     * Redistributions in binary form must reproduce the above copyright
00015:  *       notice, this list of conditions and the following disclaimer in the
00016:  *       documentation and/or other materials provided with the distribution.
00017:  *     * Neither the name of the authors nor the names of its contributors
00018:  *       may be used to endorse or promote products derived from this software
00019:  *       without specific prior written permission.
00020:  *
00021:  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
00022:  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
00023:  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
00024:  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
00025:  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
00026:  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
00027:  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
00028:  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
00029:  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
00030:  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00031:  * POSSIBILITY OF SUCH DAMAGE.
00032:  *
00033:  * PHP version 5
00034:  *
00035:  * @category Networking
00036:  * @package  Net_Growl
00037:  * @author   Laurent Laville <pear@laurent-laville.org>
00038:  * @author   Bertrand Mansion <bmansion@mamasam.com>
00039:  * @license  http://www.opensource.org/licenses/bsd-license.php  BSD
00040:  * @version  CVS: $Id:$
00041:  * @link     http://growl.laurent-laville.org/
00042:  * @since    File available since Release 0.9.0
00043:  */
00044:
00045: /**
00046:  * Growl implements GNTP 1.0 protocol
00047:  *
00048:  * @category Networking
00049:  * @package  Net_Growl
00050:  * @author   Laurent Laville <pear@laurent-laville.org>
00051:  * @author   Bertrand Mansion <bmansion@mamasam.com>
00052:  * @license  http://www.opensource.org/licenses/bsd-license.php  BSD
00053:  * @version  Release: @package_version@
00054:  * @link     http://growl.laurent-laville.org/
00055:  * @link     http://www.growlforwindows.com/gfw/ Growl for Windows Homepage
00056:  * @since    Class available since Release 0.9.0
00057:  */
00058: class Net_Growl_Gntp extends Net_Growl
00059: {
00060:     /**
00061:      * Password hash alorithms supported by Gfw 2.0
00062:      * @var array
00063:      */
00064:     private $_passwordHashAlgorithm = array('md5', 'sha1', 'sha256', 'sha512');
00065:
00066:     /**
00067:      * Class constructor
00068:      *
00069:      * @param mixed  &$application  Can be either a Net_Growl_Application object
00070:      *                              or the application name string
00071:      * @param array  $notifications List of notification types
00072:      * @param string $password      (optional) Password for Growl
00073:      * @param array  $options       (optional) List of options : 'host', 'port',
00074:      *                              'protocol', 'timeout' for Growl socket server.
00075:      *                              'passwordHashAlgorithm', 'encryptionAlgorithm'
00076:      *                              to secure communications.
00077:      *                              'debug' to know what data are sent and received.
00078:      */
00079:     public function __construct(&$application, $notifications = array(),
00080:         $password = '', $options = array()
00081:     ) {
00082:         parent::__construct($application, $notifications, $password, $options);
00083:     }
00084:
00085:     /**
00086:      * Sends the REGISTER message type
00087:      *
00088:      * @return true
00089:      * @throws Net_Growl_Exception if remote server communication failure
00090:      */
00091:     public function sendRegister()
00092:     {
00093:         $binaries      = array();
00094:         $growl_logo    = $this->getDefaultGrowlIcon();
00095:         $growl_logo_id = md5($growl_logo);
00096:
00097:         // Application-Name: <string>
00098:         // Required - The name of the application that is registering
00099:         $data = "Application-Name: "
00100:               .  utf8_encode($this->getApplication()->getGrowlName())
00101:               .  "\r\n";
00102:
00103:         // Application-Icon: <url> | <uniqueid>
00104:         // Optional - The icon of the application
00105:         $icon  = $this->getApplication()->getGrowlIcon();
00106:         if (!empty($icon)) {
00107:             $fp = @fopen($icon, 'rb');
00108:             if ($fp === false) {
00109:                 $this->debug("Invalid Application Icon URL '$icon'", 'warning');
00110:                 // invalid Application Icon URL; force to use default growl logo
00111:                 $icon = '';
00112:             } else {
00113:                 fclose($fp);
00114:             }
00115:         }
00116:         if (empty($icon)) {
00117:             $icon = "x-growl-resource://" . $growl_logo_id;
00118:             $binaries[] = $growl_logo_id;
00119:         }
00120:         $data .= "Application-Icon: " . $icon . "\r\n";
00121:
00122:         // Notifications-Count: <int>
00123:         // Required - The number of notifications being registered
00124:         $notifications = $this->getApplication()->getGrowlNotifications();
00125:         $data .= "Notifications-Count: " . count($notifications) . "\r\n";
00126:
00127:         foreach ($notifications as $name => $options) {
00128:             $data .= "\r\n";
00129:
00130:             // Notification-Name: <string>
00131:             // Required - The name (type) of the notification being registered
00132:             $data .= "Notification-Name: " . utf8_encode($name) . "\r\n";
00133:
00134:             // Notification-Display-Name: <string>
00135:             // Optional - The name of the notification that is displayed to the user
00136:             // (defaults to the same value as Notification-Name)
00137:             if (is_array($options) && isset($options['display'])) {
00138:                 $data .= "Notification-Display-Name: "
00139:                       .  $options['display']
00140:                       .  "\r\n";
00141:             }
00142:
00143:             // Notification-Enabled: <boolean>
00144:             // Optional - Indicates if the notification should be enabled by default
00145:             // (defaults to False)
00146:             if (is_array($options) && isset($options['enabled'])) {
00147:                 $data .= "Notification-Enabled: "
00148:                       .  $this->_toBool($options['enabled'])
00149:                       .  "\r\n";
00150:             }
00151:
00152:             // Notification-Icon: <url> | <uniqueid>
00153:             // Optional - The default icon to use for notifications of this type
00154:             if (is_array($options) && isset($options['icon'])) {
00155:                 $icon = $options['icon'];
00156:                 $fp = @fopen($icon, 'rb');
00157:                 if ($fp === false) {
00158:                     $this->debug("Invalid Notification Icon URL '$icon'", 'warning');
00159:                     // invalid Notification Icon URL; force to use default growl logo
00160:                     $icon = '';
00161:                 } else {
00162:                     fclose($fp);
00163:                 }
00164:                 if (empty($icon)) {
00165:                     $icon = "x-growl-resource://" . $growl_logo_id;
00166:                     $binaries[] = $growl_logo_id;
00167:                 }
00168:                 $data .= "Notification-Icon: " . $icon . "\r\n";
00169:             }
00170:         }
00171:
00172:         $meth = 'REGISTER';
00173:         $crypt_algorithm = strtolower($this->options['encryptionAlgorithm']);
00174:         if ($crypt_algorithm != 'none') {
00175:             // add extra CRLF to header before encryption to fix Gfw 2.0.21 problem
00176:             $data .= "\r\n";
00177:         }
00178:         $data = $this->genMessageStructure($meth, $data);
00179:         if ($crypt_algorithm != 'none') {
00180:             // add extra CRLF to header before encryption to fix Gfw 2.0.21 problem
00181:             $data .= "\r\n";
00182:         }
00183:
00184:         // binary section
00185:         foreach ($binaries as $bin) {
00186:             $res = $this->genMessageStructure($meth, $growl_logo, true);
00187:
00188:             $data .= "\r\n";
00189:             $data .= "Identifier: " . $bin . "\r\n";
00190:             $data .= "Length: " . strlen($res) . "\r\n";
00191:             $data .= "\r\n";
00192:             $data .= $res;
00193:         }
00194:
00195:         // message termination
00196:         // A GNTP request must end with <CRLF><CRLF> (two blank lines)
00197:         $data .= "\r\n";
00198:         $data .= "\r\n";
00199:
00200:         return $this->sendRequest($meth, $data);
00201:     }
00202:
00203:     /**
00204:      * Sends the NOTIFY message type
00205:      *
00206:      * @param string $name        Notification name
00207:      * @param string $title       Notification title
00208:      * @param string $description Notification description
00209:      * @param string $options     Notification options
00210:      *
00211:      * @return true
00212:      * @throws Net_Growl_Exception if remote server communication failure
00213:      */
00214:     public function sendNotify($name, $title, $description, $options)
00215:     {
00216:         $appName     = utf8_encode($this->getApplication()->getGrowlName());
00217:         $name        = utf8_encode($name);
00218:         $title       = utf8_encode($title);
00219:         $description = utf8_encode($description);
00220:         $priority    = isset($options['priority'])
00221:             ? $options['priority'] : self::PRIORITY_NORMAL;
00222:         $icon        = isset($options['icon']) ? $options['icon'] : '';
00223:
00224:         if (!empty($icon)) {
00225:             // check if valid icon URL
00226:             $fp = @fopen($icon, 'rb');
00227:             if ($fp === false) {
00228:                 $this->debug("Invalid Notification Icon URL '$icon'", 'warning');
00229:                 $icon = '';
00230:             } else {
00231:                 fclose($fp);
00232:             }
00233:         }
00234:
00235:         // Application-Name: <string>
00236:         // Required - The name of the application that sending the notification
00237:         // (must match a previously registered application)
00238:         $data = "Application-Name: " . $appName . "\r\n";
00239:
00240:         // Notification-Name: <string>
00241:         // Required - The name (type) of the notification (must match a previously
00242:         // registered notification name registered by the application specified
00243:         // in Application-Name)
00244:         $data .= "Notification-Name: " . $name . "\r\n";
00245:
00246:         // Notification-Title: <string>
00247:         // Required - The notification's title
00248:         $data .= "Notification-Title: " . $title . "\r\n";
00249:
00250:         // Notification-Text: <string>
00251:         // Optional - The notification's text. (defaults to "")
00252:         $data .= "Notification-Text: " . $description . "\r\n";
00253:
00254:         // Notification-Priority: <int>
00255:         // Optional - A higher number indicates a higher priority.
00256:         // This is a display hint for the receiver which may be ignored.
00257:         $data .= "Notification-Priority: " . $priority . "\r\n";
00258:
00259:         if (!empty($icon)) {
00260:             // Notification-Icon: <url>
00261:             // Optional - The icon to display with the notification.
00262:             $data .= "Notification-Icon: " . $icon . "\r\n";
00263:         }
00264:
00265:         // Notification-Sticky: <boolean>
00266:         // Optional - Indicates if the notification should remain displayed
00267:         // until dismissed by the user. (default to False)
00268:         if (is_array($options) && isset($options['sticky'])) {
00269:             $sticky = $options['sticky'];
00270:             $data .= "Notification-Sticky: " .  $this->_toBool($sticky) .  "\r\n";
00271:         }
00272:
00273:         // Notification-ID: <string>
00274:         // Optional - A unique ID for the notification. If present, serves as a hint
00275:         // to the notification system that this notification should replace any
00276:         // existing on-screen notification with the same ID. This can be used
00277:         // to update an existing notification.
00278:         // The notification system may ignore this hint.
00279:         if (is_array($options) && isset($options['ID'])) {
00280:             $data .= "Notification-ID: " .  $options['ID'] .  "\r\n";
00281:         }
00282:
00283:         // Notification-Callback-Context: <string>
00284:         // Optional - Any data (will be passed back in the callback unmodified)
00285:
00286:         // Notification-Callback-Context-Type: <string>
00287:         // Optional, but Required if 'Notification-Callback-Context' is passed.
00288:         // The type of data being passed in Notification-Callback-Context
00289:         // (will be passed back in the callback unmodified). This does not need
00290:         // to be of any pre-defined type, it is only a convenience
00291:         // to the sending application.
00292:         if (is_array($options)
00293:             && (isset($options['CallbackContext'])
00294:             || isset($options['CallbackTarget']))
00295:         ) {
00296:             $data .= "Notification-Callback-Context: "
00297:                   .  $options['CallbackContext']
00298:                   .  "\r\n";
00299:             $data .= "Notification-Callback-Context-Type: "
00300:                   .  $options['CallbackContextType']
00301:                   .  "\r\n";
00302:             $callback = true;
00303:         } else {
00304:             $callback = false;
00305:         }
00306:
00307:         // Notification-Callback-Target: <string>
00308:         // Optional - An alternate target for callbacks from this notification.
00309:         // If passed, the standard behavior of performing the callback over the
00310:         // original socket will be ignored and the callback data will be passed
00311:         // to this target instead.
00312:         if (is_array($options)
00313:             && isset($options['CallbackTarget'])
00314:         ) {
00315:             $query = '';
00316:             if (is_array($options) && isset($options['ID'])) {
00317:                 $query .= '&NotificationID='
00318:                        .  urlencode($options['ID']);
00319:             }
00320:             if (is_array($options) && isset($options['ID'])) {
00321:                 $query .= '&NotificationContext='
00322:                        .  urlencode($options['CallbackContext']);
00323:             }
00324:
00325:             $callbackTarget = $options['CallbackTarget'] ;
00326:
00327:             if (strpos($options['CallbackTarget'], '?') === false) {
00328:                 $callbackTarget .= '?' . substr($query, 1);
00329:             } else {
00330:                 $callbackTarget .= $query;
00331:             }
00332:
00333:             // BOTH methods are provided here for GfW compatibility.
00334:             $data .= "Notification-Callback-Context-Target: "
00335:                   .  $callbackTarget
00336:                   .  "\r\n";
00337:             // header kept for compatibility - @todo remove on final version
00338:             $data .= "Notification-Callback-Context-Target-Method: GET \r\n";
00339:
00340:             // Only those ones should be keep on final version
00341:             $data .= "Notification-Callback-Target: "
00342:                   .  $callbackTarget
00343:                   .  "\r\n";
00344:             // header kept for compatibility - @todo remove on final version
00345:             $data .= "Notification-Callback-Target-Method: GET \r\n";
00346:         }
00347:
00348:         $meth = 'NOTIFY';
00349:         $data = $this->genMessageStructure($meth, $data);
00350:
00351:         // message termination
00352:         // A GNTP request must end with <CRLF><CRLF> (two blank lines)
00353:         $data .= "\r\n";
00354:         $data .= "\r\n";
00355:
00356:         $res = $this->sendRequest($meth, $data, $callback);
00357:         if ($res
00358:             && is_array($options) && isset($options['CallbackFunction'])
00359:             && is_callable($options['CallbackFunction'])
00360:         ) {
00361:             // handle Socket Callbacks
00362:             call_user_func_array(
00363:                 $options['CallbackFunction'],
00364:                 $this->growlNotificationCallback
00365:             );
00366:         }
00367:         return $res;
00368:     }
00369:
00370:     /**
00371:      * Generates full message structure (header + body).
00372:      *
00373:      * @param string $method   Identifies the type of message
00374:      * @param string $data     Request message type data
00375:      * @param bool   $binaries (optional) Do not encrypt binary data header
00376:      *
00377:      * @return string
00378:      */
00379:     protected function genMessageStructure($method, $data, $binaries = false)
00380:     {
00381:         static $keys;
00382:
00383:         $password = $this->getApplication()->getGrowlPassword();
00384:
00385:         if (empty($password)) {
00386:             $req = "GNTP/1.0 $method NONE\r\n";
00387:             $cipherText = $data;
00388:         } else {
00389:             if (!isset($keys)) {
00390:                 $password = utf8_encode($password);
00391:                 $keys     = $this->_genKey($password);
00392:             }
00393:             list($hash, $key)         = $keys;
00394:             list($crypt, $cipherText) = $this->_genEncryption($key, $data);
00395:             if ($binaries) {
00396:                 return $cipherText;
00397:             }
00398:             $req = "GNTP/1.0 $method $crypt $hash\r\n";
00399:         }
00400:         $req .= $cipherText;
00401:
00402:         return $req;
00403:     }
00404:
00405:     /**
00406:      * Generates Security Header message part.
00407:      *
00408:      * The authorization of messages is accomplished by passing key information
00409:      * that proves that the sending application knows a shared secret with the
00410:      * notification system, namely a password. Users that want to authorize
00411:      * applications must share with them a password that will be used for both
00412:      * authorization and encryption.
00413:      *
00414:      * Note: By default, authorization is not required for requests orginating
00415:      *       on the local machine.
00416:      *
00417:      * @param string $password Both client and server should know the password
00418:      *
00419:      * @return array
00420:      * @throws Net_Growl_Exception on wrong password hash algorithm
00421:      */
00422:     private function _genKey($password)
00423:     {
00424:         $hash_algorithm = strtolower($this->options['passwordHashAlgorithm']);
00425:         if ($hash_algorithm == 'none') {
00426:             return array('NONE', '');
00427:         }
00428:         if (!in_array($hash_algorithm, hash_algos())) {
00429:             // Hash algo unknown
00430:             $message = 'Password hash algorithm not supported by php Mcrypt.';
00431:             throw new Net_Growl_Exception($message);
00432:         }
00433:         if (!in_array($hash_algorithm, $this->_passwordHashAlgorithm)) {
00434:             // Hash algo incompatible with Gfw 2.0
00435:             $message = 'Password hash algorithm is not compatible with Gfw 2.0';
00436:             throw new Net_Growl_Exception($message);
00437:         }
00438:         $saltVal   = mt_rand();
00439:         $saltHex   = dechex($saltVal);
00440:         $saltBytes = pack("H*", $saltHex);
00441:
00442:         $passHex   = bin2hex($password);
00443:         $passBytes = pack("H*", $passHex);
00444:         $keyBasis  = $passBytes . $saltBytes;
00445:
00446:         $key     = hash($hash_algorithm, $keyBasis, true);
00447:         $keyHash = hash($hash_algorithm, $key);
00448:
00449:         return array(strtoupper("$hash_algorithm:$keyHash.$saltHex"), $key);
00450:     }
00451:
00452:     /**
00453:      * Generates Encryption Header message part.
00454:      *
00455:      * @param string $key       Key generated from the password and salt
00456:      * @param string $plainText Request message type data
00457:      *
00458:      * @return array
00459:      * @throws Net_Growl_Exception on wrong hash/crypt algorithms usage
00460:      */
00461:     private function _genEncryption($key, $plainText)
00462:     {
00463:         static $ivVal;
00464:
00465:         $hash_algorithm  = strtolower($this->options['passwordHashAlgorithm']);
00466:         $crypt_algorithm = strtolower($this->options['encryptionAlgorithm']);
00467:         $crypt_mode      = MCRYPT_MODE_CBC;
00468:
00469:         $k = array_search($hash_algorithm, $this->_passwordHashAlgorithm);
00470:
00471:         switch ($crypt_algorithm) {
00472:         case 'aes':
00473:             if ($k < 2) {
00474:                 $message = "Password hash ($hash_algorithm)"
00475:                         .  " and encryption ($crypt_algorithm) algorithms"
00476:                         .  " are not compatible."
00477:                         .  " Please uses SHA256 or SHA512 instead.";
00478:                 throw new Net_Growl_Exception($message);
00479:             }
00480:             $cipher = MCRYPT_RIJNDAEL_128;
00481:             // Be compatible with Gfw 2, PHP Mcrypt ext. returns 32 in this case
00482:             $key_size = 24;
00483:             break;
00484:         case 'des':
00485:             $cipher = MCRYPT_DES;
00486:             break;
00487:         case '3des':
00488:             if ($k < 2) {
00489:                 $message = "Password hash ($hash_algorithm)"
00490:                         .  " and encryption ($crypt_algorithm) algorithms"
00491:                         .  " are not compatible."
00492:                         .  " Please uses SHA256 or SHA512 instead.";
00493:                 throw new Net_Growl_Exception($message);
00494:             }
00495:             $cipher = MCRYPT_3DES;
00496:             break;
00497:         case 'none':  // No encryption required
00498:             return array('NONE', $plainText);
00499:         default:      // Encryption algorithm unknown
00500:             $message = "Invalid encryption algorithm ($crypt_algorithm)";
00501:             throw new Net_Growl_Exception($message);
00502:         }
00503:
00504:         // All encryption algorithms should use
00505:         // a block mode of CBC (Cipher Block Chaining)
00506:
00507:         $td = mcrypt_module_open($cipher, '', $crypt_mode, '');
00508:
00509:         $iv_size    = mcrypt_enc_get_iv_size($td);
00510:         $block_size = mcrypt_enc_get_block_size($td);
00511:         if (!isset($key_size)) {
00512:             $key_size = mcrypt_enc_get_key_size($td);
00513:         }
00514:
00515:         // Here's our 128-bit IV which is used for both 256-bit and 128-bit keys.
00516:         if (!isset($ivVal)) {
00517:             $ivVal = mcrypt_create_iv($iv_size, MCRYPT_RAND);
00518:         }
00519:         $ivHex = bin2hex($ivVal);
00520:
00521:         // Different encryption algorithms require different key lengths
00522:         // and IV sizes, so use the first X bytes of the key as required.
00523:         $key = substr($key, 0, $key_size);
00524:
00525:         $init = mcrypt_generic_init($td, $key, $ivVal);
00526:         if ($init != -1) {
00527:             if ($crypt_mode == MCRYPT_MODE_CBC) {
00528:                 /**
00529:                  * Pads a string using the RSA PKCS7 padding standards
00530:                  * so that its length is a multiple of the blocksize.
00531:                  * $block_size - (strlen($text) % $block_size) bytes are added,
00532:                  * each of which is equal to
00533:                  * chr($block_size - (strlen($text) % $block_size)
00534:                  */
00535:                 $length = strlen($plainText);
00536:                 $pad = $block_size - ($length % $block_size);
00537:                 $plainText = str_pad($plainText, $length + $pad, chr($pad));
00538:             }
00539:             $cipherText = mcrypt_generic($td, $plainText);
00540:             mcrypt_generic_deinit($td);
00541:             mcrypt_module_close($td);
00542:         } else {
00543:             $cipherText = $plainText;
00544:         }
00545:
00546:         return array(strtoupper("$crypt_algorithm:$ivHex"), $cipherText);
00547:     }
00548:
00549:     /**
00550:      * Translates boolean value to comprehensible text for GNTP messages
00551:      *
00552:      * @param mixed $value Compatible Boolean String or value to translate
00553:      *
00554:      * @return string
00555:      */
00556:     private function _toBool($value)
00557:     {
00558:         if (preg_match('/^([Tt]rue|[Yy]es)$/', $value)) {
00559:             return 'True';
00560:         }
00561:         if (preg_match('/^([Ff]alse|[Nn]o)$/', $value)) {
00562:             return 'False';
00563:         }
00564:         if ((bool)$value === true) {
00565:             return 'True';
00566:         }
00567:         return 'False';
00568:     }
00569: }
00570: ?>