ghazel-em-apn 0.0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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