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



Growl GNTP port



Enables encryption for request bodies.

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


Sets the application icon

The icon may be any image NSImage supports


Hash of notifications registered with the server


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          =

  notification_names.each do |name|
    add_notification name
  end if notification_names

  @encrypt  = 'NONE'
  @password = nil
  @icon     = nil

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
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 = algorithm

  cipher.key = key

  if iv then
    cipher.iv = iv
    iv = cipher.random_iv

  return cipher, iv
connect() click to toggle source

Creates a TCP connection to the chosen host

# File lib/ruby-growl/gntp.rb, line 232
def connect @host, PORT
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
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
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}"

  if @encrypt == 'NONE' then
    packet << ["GNTP/1.0", type, "NONE", key_info].compact.join(' ')
    packet << body.force_encoding("ASCII-8BIT")
    encipher, iv = cipher key

    encrypt_info = "#{@encrypt}:#{Digest.hexencode iv}"

    packet << "GNTP/1.0 #{type} #{encrypt_info} #{key_info}"

    encrypted = encipher.update body
    encrypted <<

    packet << encrypted

  resources.each do |id, data|
    if iv then
      encipher, = cipher key, iv

      encrypted = encipher.update data
      encrypted <<

      data = encrypted

    packet << "Identifier: #{id}"
    packet << "Length: #{data.length}"
    packet << nil
    packet << data
    packet << nil

  packet << nil
  packet << nil

  packet.join "\r\n"
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,
  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

  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

  packet :NOTIFY, headers, resources
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
    app_icon_id = @uuid.generate

    headers << "Application-Icon: x-growl-resource://#{app_icon_id}"

    resources[app_icon_id] = @icon

  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

    headers << nil

  headers.pop # remove trailing nil

  packet :REGISTER, headers, resources
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]
      [header, value]
  when 'Notification-Callback-Timestamp' then
    [header, Time.parse(value)]
  when 'Error-Code',
       'Subscription-TTL' then
    [header, value.to_i]
  when 'Application-Name',
       'Subscriber-Name' then
    value.force_encoding Encoding::UTF_8

    [header, value]
  when 'Application-Icon',
       'Notification-Icon' then
    value = URI value
    [header, value]
    [header, value]
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

  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, headers)
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
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
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