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: ?>