Class: Growl::GNTP

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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.



181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/ruby-growl/gntp.rb', line 181

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

#encryptObject

Enables encryption for request bodies.

Note that this does not appear to be supported in a released version of growl.



148
149
150
# File 'lib/ruby-growl/gntp.rb', line 148

def encrypt
  @encrypt
end

#iconObject

Sets the application icon

The icon may be any image NSImage supports



155
156
157
# File 'lib/ruby-growl/gntp.rb', line 155

def icon
  @icon
end

#notificationsObject (readonly)

Hash of notifications registered with the server



165
166
167
# File 'lib/ruby-growl/gntp.rb', line 165

def notifications
  @notifications
end

#passwordObject

Password for authenticating and encrypting requests. If this is set, authentication automatically takes place.



171
172
173
# File 'lib/ruby-growl/gntp.rb', line 171

def password
  @password
end

#uuidObject

Objects used to generate UUIDs



160
161
162
# File 'lib/ruby-growl/gntp.rb', line 160

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.



202
203
204
# File 'lib/ruby-growl/gntp.rb', line 202

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.

Raises:



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/ruby-growl/gntp.rb', line 210

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

#connectObject

Creates a TCP connection to the chosen #host



232
233
234
# File 'lib/ruby-growl/gntp.rb', line 232

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.



240
241
242
243
244
245
246
247
248
249
250
# File 'lib/ruby-growl/gntp.rb', line 240

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.

Raises:

  • (ArgumentError)


262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/ruby-growl/gntp.rb', line 262

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.



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
338
# File 'lib/ruby-growl/gntp.rb', line 281

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.force_encoding("ASCII-8BIT")
  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.

Raises:

  • (ArgumentError)


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
378
# File 'lib/ruby-growl/gntp.rb', line 343

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_registerObject

Creates a registration packet



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
427
# File 'lib/ruby-growl/gntp.rb', line 383

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



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
481
# File 'lib/ruby-growl/gntp.rb', line 432

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.

Raises:



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 487

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
  message = $2

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

  error_code = headers['Error-Code']
  error_class = ERROR_MAP[error_code]
  error_message = headers['Error-Description']

  raise error_class.new(error_message, headers)
end

#registerObject

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

#saltObject

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