class Growl::GNTP

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:

Notably, subscription support is not implemented.

This implementation is based on information from www.growlforwindows.com/gfw/help/gntp.aspx

Constants

PORT

Growl GNTP port

Attributes

encrypt[RW]

Enables encryption for request bodies.

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

icon[RW]

Sets the application icon

The icon may be any image NSImage supports

notifications[R]

Hash of notifications registered with the server

password[RW]

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

Public Class Methods

new(host, application, notification_names = nil) click to toggle source

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.

# 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

Public Instance Methods

add_notification(name, display_name = nil, icon = nil, enabled = true) click to toggle source

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.

# 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) click to toggle source

Creates a symmetric encryption cipher for key based on the encrypt method.

# 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
connect() click to toggle source

Creates a TCP connection to the chosen host

# File lib/ruby-growl/gntp.rb, line 232
def connect
  TCPSocket.new @host, PORT
end
key_hash(algorithm) click to toggle source

Returns an encryption key, authentication hash and random salt for the given hash algorithm.

# 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) click to toggle source

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.

# 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 = {}) click to toggle source

Creates a type packet (such as REGISTER or NOTIFY) with the given headers and resources. Handles authentication and encryption of the packet.

# 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) click to toggle source

Creates a notify packet. See notify for parameter details.

# 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_register() click to toggle source

Creates a registration packet

# 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) click to toggle source

Parses the value for header into the correct ruby type

# 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) click to toggle source

Receives and handles the response packet from the server and either raises an error or returns a headers Hash.

# 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
register() click to toggle source

Sends a registration packet based on the given notifications

# File lib/ruby-growl/gntp.rb, line 520
def register
  send packet_register
end
salt() click to toggle source

Creates a random salt for use in authentication and encryption

# File lib/ruby-growl/gntp.rb, line 527
def salt
  OpenSSL::Random.random_bytes 16
end
send(packet) { |callback| ... } click to toggle source

Sends packet to the server and yields a callback, if given

# 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