ghazel-em-apn 0.0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d304846e4e9cb3d8b7da79ef8a0e721dfb3a3ba0
4
+ data.tar.gz: bb079ada2ab1b5a9a1234a0ea698ba889c2bedfb
5
+ SHA512:
6
+ metadata.gz: cbdc74902f15c6f187fb0cb2a2ce9aba4954f4a20d42df0950636a45fc52a0b58c918a2d2a98dff32467f84c0aec41cba171f67617cb89fe02b8d5d9e116c238
7
+ data.tar.gz: 8670311ae9a6c723bdb82471c6b1651fcd89f439b9a2dbe1a1f915f0c3715be6295496748d8cdb6642b1e5044fa37faee2d19011fa3e74c49f44faa80dc18d5c
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ certs/*.pem
6
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in em-apn.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT-License)
2
+
3
+ Copyright (c) 2011 GroupMe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # EM-APN - EventMachine'd Apple Push Notifications #
2
+
3
+ We want:
4
+
5
+ * Streamlined for a persistent connection use-case
6
+ * Support for the enhanced protocol, with receipts
7
+
8
+ ## Usage ##
9
+
10
+ In a nutshell:
11
+
12
+ require "em-apn"
13
+
14
+ # Inside a reactor...
15
+ notification = EM::APN::Notification.new(token, :alert => alert)
16
+ client = EM::APN::Client.connect
17
+ client.deliver(notification)
18
+
19
+ Using this interface, the easiest way to configure the connection is by setting
20
+ some environment variables so that EM::APN can find your SSL certificates:
21
+
22
+ ENV["APN_KEY"] = "/path/to/key.pem"
23
+ ENV["APN_CERT"] = "/path/to/cert.pem"
24
+
25
+ Also, by default, the library connects to Apple's sandbox push server. If you
26
+ want to connect to the production server, simply set the `APN_ENV`
27
+ environment variable to `production`:
28
+
29
+ ENV["APN_ENV"] = "production"
30
+
31
+ The gateway and SSL certs can also be set directly when instantiating the object:
32
+
33
+ client = EM::APN::Client.connect(
34
+ :gateway => "some.host",
35
+ :key => "/path/to/key.pem",
36
+ :cert => "/path/to/cert.pem"
37
+ )
38
+
39
+ The client manages an underlying `EM::Connection`, and it will automatically
40
+ reconnect to the gateway when the connection is closed. Callbacks can be set
41
+ on the client to handle error responses from the gateway, connection open & close
42
+ events:
43
+
44
+ client = EM::APN::Client.connect
45
+ client.on_error do |response|
46
+ # See EM::APN::ErrorResponse
47
+ end
48
+
49
+ client.on_close do
50
+ # Do something.
51
+ end
52
+
53
+ client.on_open do
54
+ # Do something
55
+ end
56
+
57
+ In our experience, we've found that Apple immediately closes the connection
58
+ whenever an error is detected, so the error and close callbacks are nearly
59
+ always called one-to-one. These methods exist as a convenience, and the
60
+ callbacks can also be set directly to anything that responds to `#call`:
61
+
62
+ client.error_callback = Proc.new { |response| ... }
63
+ client.close_callback = Proc.new { ... }
64
+ client.open_callback = Proc.new { ... }
65
+
66
+ ### Max Payload Size ###
67
+
68
+ Apple enforces a limit of __256 bytes__ for the __entire payload__.
69
+
70
+ If you attempt to deliver a notification that exceeds that limit, the library
71
+ will raise an `EM::APN::Notification::PayloadTooLarge` exception.
72
+
73
+ To prevent that from happening, you can call `#truncate_alert!` on the
74
+ notification.
75
+
76
+ notification = EM::APN::Notification.new(...)
77
+ notification.truncate_alert!
78
+ client.deliver(notification)
79
+
80
+ ## Inspiration ##
81
+
82
+ Much thanks to:
83
+
84
+ * https://github.com/kdonovan/apn_sender
85
+ * http://blog.technopathllc.com/2010/12/apples-push-notification-with-ruby.html
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ desc "Run specs"
4
+ task :spec do
5
+ spec_files = Dir["spec/**/*_spec.rb"]
6
+ ruby("-S bundle exec rspec #{spec_files.join(" ")}")
7
+ end
8
+
9
+ task :default => :spec
data/certs/.gitignore ADDED
File without changes
data/em-apn.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- mode: ruby; encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "em-apn/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ghazel-em-apn"
7
+ s.version = EventMachine::APN::VERSION
8
+ s.authors = ["Dave Yeu"]
9
+ s.email = ["daveyeu@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{EventMachine-driven Apple Push Notifications}
12
+
13
+ s.rubyforge_project = "em-apn"
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency "eventmachine", ">= 1.0.0.beta.3"
21
+ s.add_dependency "multi_json", ">= 1.8.2"
22
+
23
+ s.add_development_dependency "rspec", "~> 2.6.0"
24
+ end
@@ -0,0 +1,78 @@
1
+ # encoding: UTF-8
2
+
3
+ module EventMachine
4
+ module APN
5
+ class Client
6
+ SANDBOX_GATEWAY = "gateway.sandbox.push.apple.com"
7
+ PRODUCTION_GATEWAY = "gateway.push.apple.com"
8
+ PORT = 2195
9
+ SANDBOX_FEEDBACK_GATEWAY = "feedback.sandbox.push.apple.com"
10
+ PRODUCTION_FEEDBACK_GATEWAY = "feedback.push.apple.com"
11
+ FEEDBACK_PORT = 2196
12
+
13
+
14
+ attr_reader :gateway, :port, :key, :cert, :connection, :error_callback, :close_callback, :open_callback
15
+ attr_reader :feedback_connection, :feedback_gateway, :feedback_port, :feedback_callback
16
+
17
+ # A convenience method for creating and connecting.
18
+ def self.connect(options = {})
19
+ new(options).tap do |client|
20
+ client.connect
21
+ client.connect_feedback
22
+ end
23
+ end
24
+
25
+ def initialize(options = {})
26
+ @key = options[:key] || ENV["APN_KEY"]
27
+ @cert = options[:cert] || ENV["APN_CERT"]
28
+ @port = options[:port] || PORT
29
+
30
+ @gateway = options[:gateway] || ENV["APN_GATEWAY"]
31
+ @gateway ||= (ENV["APN_ENV"] == "production") ? PRODUCTION_GATEWAY : SANDBOX_GATEWAY
32
+
33
+
34
+ @feedback_gateway = options[:feedback_gateway] || ENV["APN_FEEDBACK_GATEWAY"]
35
+ @feedback_gateway ||= (ENV["APN_ENV"] == "production") ? PRODUCTION_FEEDBACK_GATEWAY : SANDBOX_FEEDBACK_GATEWAY
36
+ @feedback_port = options[:feedback_port] || FEEDBACK_PORT
37
+
38
+ @connection = nil
39
+ @feedback_connection = nil
40
+ end
41
+
42
+ def connect
43
+ @connection = EM.connect(gateway, port, Connection, self)
44
+ end
45
+
46
+ def connect_feedback
47
+ @feedback_connection = EM.connect(feedback_gateway, feedback_port, FeedbackConnection, self)
48
+ end
49
+
50
+ def deliver(notification)
51
+ notification.validate!
52
+ connect if connection.nil? || connection.disconnected?
53
+ log(notification)
54
+ connection.send_data(notification.data)
55
+ end
56
+
57
+ def on_error(&block)
58
+ @error_callback = block
59
+ end
60
+
61
+ def on_close(&block)
62
+ @close_callback = block
63
+ end
64
+
65
+ def on_open(&block)
66
+ @open_callback = block
67
+ end
68
+
69
+ def on_feedback(&block)
70
+ @feedback_callback = block
71
+ end
72
+
73
+ def log(notification)
74
+ EM::APN.logger.info("TOKEN=#{notification.token} PAYLOAD=#{notification.payload.inspect}")
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,47 @@
1
+ module EventMachine
2
+ module APN
3
+ class Connection < EM::Connection
4
+ attr_reader :client
5
+
6
+ def initialize(*args)
7
+ super
8
+ @client = args.last
9
+ @disconnected = false
10
+ end
11
+
12
+ def disconnected?
13
+ @disconnected
14
+ end
15
+
16
+ def post_init
17
+ start_tls(
18
+ :private_key_file => client.key,
19
+ :cert_chain_file => client.cert,
20
+ :verify_peer => false
21
+ )
22
+ end
23
+
24
+ def connection_completed
25
+ EM::APN.logger.info("Connection completed")
26
+ client.open_callback.call if client.open_callback
27
+ end
28
+
29
+ def receive_data(data)
30
+ data_array = data.unpack("ccN")
31
+ error_response = ErrorResponse.new(*data_array)
32
+ EM::APN.logger.warn(error_response.to_s)
33
+
34
+ if client.error_callback
35
+ client.error_callback.call(error_response)
36
+ end
37
+ end
38
+
39
+ def unbind
40
+ @disconnected = true
41
+
42
+ EM::APN.logger.info("Connection closed")
43
+ client.close_callback.call if client.close_callback
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ module EventMachine
2
+ module APN
3
+ class ErrorResponse
4
+ DESCRIPTION = {
5
+ 0 => "No errors encountered",
6
+ 1 => "Processing error",
7
+ 2 => "Missing device token",
8
+ 3 => "Missing topic",
9
+ 4 => "Missing payload",
10
+ 5 => "Invalid token size",
11
+ 6 => "Invalid topic size",
12
+ 7 => "Invalid payload size",
13
+ 8 => "Invalid token",
14
+ 255 => "None (unknown)"
15
+ }
16
+
17
+ attr_reader :command, :status_code, :identifier
18
+
19
+ def initialize(command, status_code, identifier)
20
+ @command = command
21
+ @status_code = status_code
22
+ @identifier = identifier
23
+ end
24
+
25
+ def to_s
26
+ "CODE=#{@status_code} ID=#{@identifier} DESC=#{description}"
27
+ end
28
+
29
+ def description
30
+ DESCRIPTION[@status_code] || "Missing description"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ module EventMachine
2
+ module APN
3
+ class FailedDeliveryAttempt
4
+ LENGTH = 38
5
+
6
+ attr_accessor :timestamp, :device_token
7
+
8
+ def initialize(binary_tuple)
9
+ # N => 4 byte timestamp
10
+ # n => 2 byte token_length
11
+ # H64 => 32 byte device_token
12
+ seconds, _, @device_token = binary_tuple.unpack('NnH64')
13
+ raise ArgumentError('invalid format') unless seconds && @device_token
14
+ @timestamp = Time.at(seconds)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,43 @@
1
+ module EventMachine
2
+ module APN
3
+ class FeedbackConnection < EM::Connection
4
+ attr_reader :client
5
+
6
+ def initialize(*args)
7
+ super
8
+ @client = args.last
9
+ @disconnected = false
10
+ end
11
+
12
+ def disconnected?
13
+ @disconnected
14
+ end
15
+
16
+ def post_init
17
+ start_tls(
18
+ :private_key_file => client.key,
19
+ :cert_chain_file => client.cert,
20
+ :verify_peer => false
21
+ )
22
+ end
23
+
24
+ def connection_completed
25
+ EM::APN.logger.info("Feedback connection completed")
26
+ end
27
+
28
+ def receive_data(data)
29
+ attempt = FailedDeliveryAttempt.new(data)
30
+ EM::APN.logger.warn(attempt.to_s)
31
+
32
+ if client.feedback_callback
33
+ client.feedback_callback.call(attempt)
34
+ end
35
+ end
36
+
37
+ def unbind
38
+ @disconnected = true
39
+ EM::APN.logger.info("Feedback connection closed")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ module EventMachine
2
+ module APN
3
+ class LogMessage
4
+ def initialize(response)
5
+ @response = response
6
+ end
7
+
8
+ def log
9
+ EM::APN.logger.info(@response.to_s)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,62 @@
1
+ # encoding: UTF-8
2
+
3
+ module EventMachine
4
+ module APN
5
+ class Notification
6
+ DATA_MAX_BYTES = 256
7
+ ALERT_KEY = "alert"
8
+
9
+ class PayloadTooLarge < StandardError; end
10
+
11
+ attr_reader :token, :identifier
12
+ attr_accessor :expiry
13
+
14
+ def initialize(token, aps = {}, custom = {}, options = {})
15
+ raise "Bad push token: #{token}" if token.nil? || (token.length != 64)
16
+
17
+ @token = token
18
+ @aps = aps.stringify_keys!
19
+ @custom = custom
20
+ @expiry = options[:expiry]
21
+
22
+ self.identifier = options[:identifier] if options[:identifier]
23
+ end
24
+
25
+ def payload
26
+ MultiJson.encode(@custom.merge(:aps => @aps))
27
+ end
28
+
29
+ # Documentation about this format is here:
30
+ # http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html
31
+ def data
32
+ identifier = @identifier || 0
33
+ expiry = @expiry || 0
34
+ size = [payload].pack("a*").size
35
+ data_array = [1, identifier, expiry, 32, token, size, payload]
36
+ data_array.pack("cNNnH*na*")
37
+ end
38
+
39
+ def validate!
40
+ if data.size > DATA_MAX_BYTES
41
+ error = "max is #{DATA_MAX_BYTES} bytes, but got #{data.size}: #{payload.inspect}"
42
+ raise PayloadTooLarge.new(error)
43
+ else
44
+ true
45
+ end
46
+ end
47
+
48
+ def identifier=(new_identifier)
49
+ @identifier = new_identifier.to_i
50
+ end
51
+
52
+ def truncate_alert!
53
+ return unless @aps.has_key?(ALERT_KEY)
54
+
55
+ while data.size > DATA_MAX_BYTES && @aps[ALERT_KEY].size > 0
56
+ @aps[ALERT_KEY].chop!
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ module EventMachine
2
+ module APN
3
+ class Response
4
+ def initialize(notification)
5
+ @notification = notification
6
+ end
7
+
8
+ def to_s
9
+ "TOKEN=#{@notification.token}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+ # Mock Apple push server... because we love to test
3
+ module EventMachine
4
+ module APN
5
+ module Server
6
+ def post_init
7
+ EM::APN.logger.info("Received a new connection")
8
+ @data = ""
9
+
10
+ start_tls(
11
+ :cert_chain_file => ENV["APN_CERT"],
12
+ :private_key_file => ENV["APN_KEY"],
13
+ :verify_peer => false
14
+ )
15
+ end
16
+
17
+ def ssl_handshake_completed
18
+ EM::APN.logger.info("SSL handshake completed")
19
+ end
20
+
21
+ def receive_data(data)
22
+ @data << data
23
+
24
+ # Try to extract the payload header
25
+ headers = @data.unpack("cNNnH64n")
26
+ return if headers.last.nil?
27
+
28
+ # Try to grab the payload
29
+ payload_size = headers.last
30
+ payload = @data[45, payload_size]
31
+ return if payload.length != payload_size
32
+
33
+ @data = @data[45 + payload_size, -1] || ""
34
+
35
+ process(headers, payload)
36
+ end
37
+
38
+ def process(headers, payload)
39
+ message = "APN RECV #{headers[4]} #{payload}"
40
+ EM::APN.logger.info(message)
41
+
42
+ args = MultiJson.decode(payload)
43
+
44
+ # If the alert is 'DISCONNECT', then we fake a bad payload by replying
45
+ # with an error and disconnecting.
46
+ if args["aps"]["alert"] == "DISCONNECT"
47
+ EM::APN.logger.info("Disconnecting")
48
+ send_data([8, 1, 0].pack("ccN"))
49
+ close_connection_after_writing
50
+ end
51
+ end
52
+
53
+ def unbind
54
+ EM::APN.logger.info("Connection closed")
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,37 @@
1
+ # Test helper for EM::APN
2
+ #
3
+ # To use this, start by simply requiring this file after EM::APN has already
4
+ # been loaded
5
+ #
6
+ # require "em-apn"
7
+ # require "em-apn/test_helper"
8
+ #
9
+ # This will nullify actual deliveries and instead, push them onto an accessible
10
+ # list:
11
+ #
12
+ # expect {
13
+ # client.deliver('notification)
14
+ # }.to change { EM::APN.deliveries.size }.by(1)
15
+ #
16
+ # notification = EM::APN.deliveries.first
17
+ # notification.should be_an_instance_of(EM::APN::Notification)
18
+ # notification.payload.should == ...
19
+ #
20
+ module EventMachine
21
+ module APN
22
+ def self.deliveries
23
+ @deliveries ||= []
24
+ end
25
+
26
+ Client.class_eval do
27
+ def connect
28
+ # No-op
29
+ end
30
+
31
+ def deliver(notification)
32
+ log(notification)
33
+ EM::APN.deliveries << notification
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ module APN
3
+ VERSION = "0.0.3.1"
4
+ end
5
+ end
data/lib/em-apn.rb ADDED
@@ -0,0 +1,26 @@
1
+ # encoding: UTF-8
2
+
3
+ require "eventmachine"
4
+ require "multi_json"
5
+ require "logger"
6
+ require "extensions/hash"
7
+ require "em-apn/client"
8
+ require "em-apn/feedback_connection"
9
+ require 'em-apn/failed_delivery_attempt'
10
+ require "em-apn/connection"
11
+ require "em-apn/notification"
12
+ require "em-apn/log_message"
13
+ require "em-apn/response"
14
+ require "em-apn/error_response"
15
+
16
+ module EventMachine
17
+ module APN
18
+ def self.logger
19
+ @logger ||= Logger.new(STDOUT)
20
+ end
21
+
22
+ def self.logger=(new_logger)
23
+ @logger = new_logger
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ class Hash
2
+ unless Hash.instance_methods.include?(:stringify_keys!)
3
+ def stringify_keys!
4
+ keys.each do |key|
5
+ unless key.kind_of?(String)
6
+ self[key.to_s] = delete(key)
7
+ end
8
+ end
9
+
10
+ self
11
+ end
12
+ end
13
+ end
data/script/push ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Send a single push notification
4
+ #
5
+ # $ script/push <TOKEN> "<ALERT>"
6
+ #
7
+
8
+ require "rubygems"
9
+ require "bundler/setup"
10
+ require "em-apn"
11
+
12
+ ENV["APN_KEY"] ||= File.join(File.dirname(__FILE__), "..", "certs", "key.pem")
13
+ ENV["APN_CERT"] ||= File.join(File.dirname(__FILE__), "..", "certs", "cert.pem")
14
+
15
+ token, alert = ARGV
16
+
17
+ if token.nil? || alert.nil?
18
+ puts "Usage: script/push <TOKEN> \"<ALERT>\""
19
+ exit 1
20
+ end
21
+
22
+ EM.run do
23
+ client = EM::APN::Client.connect
24
+ client.deliver(EM::APN::Notification.new(token, :alert => alert))
25
+
26
+ # Hopefully give ourselves enough time to receive a response on failure.
27
+ # Wish there was a better way to do this. Or at least a more timely way.
28
+ EM.add_timer(1) { EM.stop_event_loop }
29
+ end
data/script/server ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Run a mock Apple push server
4
+
5
+ require "rubygems"
6
+ require "bundler/setup"
7
+ require "em-apn"
8
+ require "em-apn/server"
9
+
10
+ ENV["APN_KEY"] = File.join(File.dirname(__FILE__), "..", "certs", "key.pem")
11
+ ENV["APN_CERT"] = File.join(File.dirname(__FILE__), "..", "certs", "cert.pem")
12
+
13
+ EM::APN.logger.info("Starting push server")
14
+
15
+ EM.run do
16
+ EM.start_server("127.0.0.1", 2195, EM::APN::Server)
17
+ end