Class: Growl::GNTP
- Inherits:
-
Object
- Object
- Growl::GNTP
- Defined in:
- lib/ruby-growl/gntp.rb
Overview
Growl Notification Transport Protocol 1.0
In growl 1.3, GNTP replaced the UDP growl protocol from earlier versions. GNTP has some new features beyond those supported in earlier versions including:
-
Callback support
-
Notification icons
-
Encrypted notifications (not supported by growl at this time)
Notably, subscription support is not implemented.
This implementation is based on information from www.growlforwindows.com/gfw/help/gntp.aspx
Defined Under Namespace
Classes: AlreadyProcessed, Error, InternalServerError, InvalidRequest, NetworkFailure, NotAuthorized, NotificationDisabled, RequiredHeaderMissing, ResponseError, TimedOut, UnknownApplication, UnknownNotification, UnknownProtocol, UnknownProtocolVersion
Constant Summary collapse
- PORT =
Growl GNTP port
23053
- ERROR_MAP =
:nodoc:
{ # :nodoc: 200 => Growl::GNTP::TimedOut, 201 => Growl::GNTP::NetworkFailure, 300 => Growl::GNTP::InvalidRequest, 301 => Growl::GNTP::UnknownProtocol, 302 => Growl::GNTP::UnknownProtocolVersion, 303 => Growl::GNTP::RequiredHeaderMissing, 400 => Growl::GNTP::NotAuthorized, 401 => Growl::GNTP::UnknownApplication, 402 => Growl::GNTP::UnknownNotification, 403 => Growl::GNTP::AlreadyProcessed, 404 => Growl::GNTP::NotificationDisabled, 500 => Growl::GNTP::InternalServerError, }
- ENCRYPTION_ALGORITHMS =
:nodoc:
{ # :nodoc: 'DES' => 'DES-CBC', '3DES' => 'DES-EDE3-CBC', 'AES' => 'AES-192-CBC', }
Instance Attribute Summary collapse
-
#encrypt ⇒ Object
Enables encryption for request bodies.
-
#icon ⇒ Object
Sets the application icon.
-
#notifications ⇒ Object
readonly
Hash of notifications registered with the server.
-
#password ⇒ Object
Password for authenticating and encrypting requests.
-
#uuid ⇒ Object
Objects used to generate UUIDs.
Instance Method Summary collapse
-
#add_notification(name, display_name = nil, icon = nil, enabled = true) ⇒ Object
Adds a notification with
name
(internal) anddisplay_name
(shown to user). -
#cipher(key, iv = nil) ⇒ Object
Creates a symmetric encryption cipher for
key
based on the #encrypt method. -
#connect ⇒ Object
Creates a TCP connection to the chosen #host.
-
#initialize(host, application, notification_names = nil) ⇒ GNTP
constructor
Creates a new Growl::GNTP instance that will communicate with
host
and has the givenapplication
name, and will send the givennotification_names
. -
#key_hash(algorithm) ⇒ Object
Returns an encryption key, authentication hash and random salt for the given hash
algorithm
. -
#notify(notification, title, text = nil, priority = 0, sticky = false, coalesce_id = nil, callback_url = nil, &block) ⇒ Object
Sends a
notification
with the giventitle
andtext
. -
#packet(type, headers, resources = {}) ⇒ Object
Creates a
type
packet (such as REGISTER or NOTIFY) with the givenheaders
andresources
. -
#packet_notify(notification, title, text, priority, sticky, coalesce_id, callback) ⇒ Object
Creates a notify packet.
-
#packet_register ⇒ Object
Creates a registration packet.
-
#parse_header(header, value) ⇒ Object
Parses the
value
forheader
into the correct ruby type. -
#receive(packet) ⇒ Object
Receives and handles the response
packet
from the server and either raises an error or returns a headers Hash. -
#register ⇒ Object
Sends a registration packet based on the given notifications.
-
#salt ⇒ Object
Creates a random salt for use in authentication and encryption.
-
#send(packet) ⇒ Object
Sends
packet
to the server and yields a callback, if given.
Constructor Details
#initialize(host, application, notification_names = nil) ⇒ GNTP
Creates a new Growl::GNTP instance that will communicate with host
and has the given application
name, and will send the given notification_names
.
If you wish to set icons or display names for notifications, use add_notification instead of sending notification_names
.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/ruby-growl/gntp.rb', line 180 def initialize host, application, notification_names = nil @host = host @application = application @notifications = {} @uuid = UUID.new notification_names.each do |name| add_notification name end if notification_names @encrypt = 'NONE' @password = nil @icon = nil end |
Instance Attribute Details
#encrypt ⇒ Object
Enables encryption for request bodies.
Note that this does not appear to be supported in a released version of growl.
147 148 149 |
# File 'lib/ruby-growl/gntp.rb', line 147 def encrypt @encrypt end |
#icon ⇒ Object
Sets the application icon
The icon may be any image NSImage supports
154 155 156 |
# File 'lib/ruby-growl/gntp.rb', line 154 def icon @icon end |
#notifications ⇒ Object (readonly)
Hash of notifications registered with the server
164 165 166 |
# File 'lib/ruby-growl/gntp.rb', line 164 def notifications @notifications end |
#password ⇒ Object
Password for authenticating and encrypting requests. If this is set, authentication automatically takes place.
170 171 172 |
# File 'lib/ruby-growl/gntp.rb', line 170 def password @password end |
#uuid ⇒ Object
Objects used to generate UUIDs
159 160 161 |
# File 'lib/ruby-growl/gntp.rb', line 159 def uuid @uuid end |
Instance Method Details
#add_notification(name, display_name = nil, icon = nil, enabled = true) ⇒ Object
Adds a notification with name
(internal) and display_name
(shown to user). The icon
map be an image (anything NSImage supports) or a URI (which is unsupported in growl 1.3). If the notification is enabled
it will be displayed by default.
201 202 203 |
# File 'lib/ruby-growl/gntp.rb', line 201 def add_notification name, display_name = nil, icon = nil, enabled = true @notifications[name] = display_name, icon, enabled end |
#cipher(key, iv = nil) ⇒ Object
Creates a symmetric encryption cipher for key
based on the #encrypt method.
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/ruby-growl/gntp.rb', line 209 def cipher key, iv = nil algorithm = ENCRYPTION_ALGORITHMS[@encrypt] raise Error, "unknown GNTP encryption mode #{@encrypt}" unless algorithm cipher = OpenSSL::Cipher.new algorithm cipher.encrypt cipher.key = key if iv then cipher.iv = iv else iv = cipher.random_iv end return cipher, iv end |
#connect ⇒ Object
Creates a TCP connection to the chosen #host
231 232 233 |
# File 'lib/ruby-growl/gntp.rb', line 231 def connect TCPSocket.new @host, PORT end |
#key_hash(algorithm) ⇒ Object
Returns an encryption key, authentication hash and random salt for the given hash algorithm
.
239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/ruby-growl/gntp.rb', line 239 def key_hash algorithm key = @password.dup.force_encoding Encoding::BINARY salt = self.salt basis = "#{key}#{salt}" key = algorithm.digest basis hash = algorithm.hexdigest key return key, hash, salt end |
#notify(notification, title, text = nil, priority = 0, sticky = false, coalesce_id = nil, callback_url = nil, &block) ⇒ Object
Sends a notification
with the given title
and text
. The priority
may be between -2 (lowest) and 2 (highest). sticky
will indicate the notification must be manually dismissed. callback_url
is supposed to open the given URL on the server’s web browser when clicked, but I haven’t seen this work.
If a block is given, it is called when the notification is clicked, times out, or is manually dismissed.
261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/ruby-growl/gntp.rb', line 261 def notify(notification, title, text = nil, priority = 0, sticky = false, coalesce_id = nil, callback_url = nil, &block) raise ArgumentError, 'provide either a url or a block for callbacks, ' \ 'not both' if block and callback_url callback = callback_url || block_given? packet = packet_notify(notification, title, text, priority, sticky, coalesce_id, callback) send packet, &block end |
#packet(type, headers, resources = {}) ⇒ Object
Creates a type
packet (such as REGISTER or NOTIFY) with the given headers
and resources
. Handles authentication and encryption of the packet.
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/ruby-growl/gntp.rb', line 280 def packet type, headers, resources = {} packet = [] body = [] body << "Application-Name: #{@application}" body << "Origin-Software-Name: ruby-growl" body << "Origin-Software-Version: #{Growl::VERSION}" body << "Origin-Platform-Name: ruby" body << "Origin-Platform-Version: #{RUBY_VERSION}" body << "Connection: close" body.concat headers body << nil body = body.join "\r\n" if @password then digest = Digest::SHA512 key, hash, salt = key_hash digest key_info = "SHA512:#{hash}.#{Digest.hexencode salt}" end if @encrypt == 'NONE' then packet << ["GNTP/1.0", type, "NONE", key_info].compact.join(' ') packet << body else encipher, iv = cipher key encrypt_info = "#{@encrypt}:#{Digest.hexencode iv}" packet << "GNTP/1.0 #{type} #{encrypt_info} #{key_info}" encrypted = encipher.update body encrypted << encipher.final packet << encrypted end resources.each do |id, data| if iv then encipher, = cipher key, iv encrypted = encipher.update data encrypted << encipher.final data = encrypted end packet << "Identifier: #{id}" packet << "Length: #{data.length}" packet << nil packet << data packet << nil end packet << nil packet << nil packet.join "\r\n" end |
#packet_notify(notification, title, text, priority, sticky, coalesce_id, callback) ⇒ Object
Creates a notify packet. See #notify for parameter details.
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 |
# File 'lib/ruby-growl/gntp.rb', line 342 def packet_notify(notification, title, text, priority, sticky, coalesce_id, callback) raise ArgumentError, "invalid priority level #{priority}" unless priority >= -2 and priority <= 2 resources = {} _, icon, = @notifications[notification] if URI === icon then icon_uri = icon elsif icon then id = @uuid.generate resources[id] = icon end headers = [] headers << "Notification-ID: #{@uuid.generate}" headers << "Notification-Coalescing-ID: #{coalesce_id}" if coalesce_id headers << "Notification-Name: #{notification}" headers << "Notification-Title: #{title}" headers << "Notification-Text: #{text}" if text headers << "Notification-Priority: #{priority}" if priority.nonzero? headers << "Notification-Sticky: True" if sticky headers << "Notification-Icon: #{icon}" if icon_uri headers << "Notification-Icon: x-growl-resource://#{id}" if id if callback then headers << "Notification-Callback-Context: context" headers << "Notification-Callback-Context-Type: type" headers << "Notification-Callback-Target: #{callback}" unless callback == true end packet :NOTIFY, headers, resources end |
#packet_register ⇒ Object
Creates a registration packet
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 |
# File 'lib/ruby-growl/gntp.rb', line 382 def packet_register resources = {} headers = [] case @icon when URI then headers << "Application-Icon: #{@icon}" when NilClass then # ignore else app_icon_id = @uuid.generate headers << "Application-Icon: x-growl-resource://#{app_icon_id}" resources[app_icon_id] = @icon end headers << "Notifications-Count: #{@notifications.length}" headers << nil @notifications.each do |name, (display_name, icon, enabled)| headers << "Notification-Name: #{name}" headers << "Notification-Display-Name: #{display_name}" if display_name headers << "Notification-Enabled: true" if enabled # This does not appear to be used by growl so ruby-growl sends the # icon with every notification. if URI === icon then headers << "Notification-Icon: #{icon}" elsif icon then id = @uuid.generate headers << "Notification-Icon: x-growl-resource://#{id}" resources[id] = icon end headers << nil end headers.pop # remove trailing nil packet :REGISTER, headers, resources end |
#parse_header(header, value) ⇒ Object
Parses the value
for header
into the correct ruby type
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 |
# File 'lib/ruby-growl/gntp.rb', line 431 def parse_header header, value return [header, nil] if value == '(null)' case header when 'Notification-Enabled', 'Notification-Sticky' then if value =~ /^(true|yes)$/i then [header, true] elsif value =~ /^(false|no)$/i then [header, false] else [header, value] end when 'Notification-Callback-Timestamp' then [header, Time.parse(value)] when 'Error-Code', 'Notifications-Count', 'Notifications-Priority', 'Subscriber-Port', 'Subscription-TTL' then [header, value.to_i] when 'Application-Name', 'Error-Description', 'Notification-Callback-Context', 'Notification-Callback-Context-Type', 'Notification-Callback-Target', 'Notification-Coalescing-ID', 'Notification-Display-Name', 'Notification-ID', 'Notification-Name', 'Notification-Text', 'Notification-Title', 'Origin-Machine-Name', 'Origin-Platform-Name', 'Origin-Platform-Version', 'Origin-Software-Version', 'Origin-Sofware-Name', 'Subscriber-ID', 'Subscriber-Name' then value.force_encoding Encoding::UTF_8 [header, value] when 'Application-Icon', 'Notification-Icon' then value = URI value [header, value] else [header, value] end end |
#receive(packet) ⇒ Object
Receives and handles the response packet
from the server and either raises an error or returns a headers Hash.
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 |
# File 'lib/ruby-growl/gntp.rb', line 486 def receive packet $stderr.puts "> #{packet.gsub(/\r\n/, "\n> ")}" if $DEBUG packet = packet.strip.split "\r\n" info = packet.shift info =~ %r%^GNTP/([\d.]+) (\S+) (\S+)$% version = $1 = $2 encryption = $3 raise Error, "invalid info line #{info.inspect}" unless version headers = packet.flat_map do |header| key, value = header.split ': ', 2 parse_header key, value end headers = Hash[*headers] return headers if %w[-OK -CALLBACK].include? error_code = headers['Error-Code'] error_class = ERROR_MAP[error_code] = headers['Error-Description'] raise error_class.new(, headers) end |
#register ⇒ Object
Sends a registration packet based on the given notifications
520 521 522 |
# File 'lib/ruby-growl/gntp.rb', line 520 def register send packet_register end |
#salt ⇒ Object
Creates a random salt for use in authentication and encryption
527 528 529 |
# File 'lib/ruby-growl/gntp.rb', line 527 def salt OpenSSL::Random.random_bytes 16 end |
#send(packet) ⇒ Object
Sends packet
to the server and yields a callback, if given
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 |
# File 'lib/ruby-growl/gntp.rb', line 534 def send packet socket = connect $stderr.puts "< #{packet.gsub(/\r\n/, "\n< ")}" if $DEBUG socket.write packet result = receive socket.gets "\r\n\r\n\r\n" if block_given? then callback = receive socket.gets "\r\n\r\n\r\n" yield callback end result end |