#!/usr/bin/env ruby # -*- coding: utf-8 -*- require 'rubygems' require 'sinatra' require 'sequel' require 'socket' require 'openssl' require 'cgi' require 'rufus/scheduler' require 'eventmachine' require 'sinatra/base' require 'yaml' ############################################################ ## Initilization Setup ############################################################ LIBDIR = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) ROOTDIR = File.expand_path(File.join(File.dirname(__FILE__), '..')) unless $LOAD_PATH.include?(LIBDIR) $LOAD_PATH << LIBDIR end unless File.exist?("#{Dir.pwd}/ass.yml") then puts 'create config file: ass.yml' system "cp #{ROOTDIR}/ass.yml #{Dir.pwd}/ass.yml" end unless File.exist?("#{Dir.pwd}/cron") then puts "create a demo 'cron' script" system "cp #{ROOTDIR}/cron #{Dir.pwd}/cron" end ############################################################ ## Configuration Setup ############################################################ env = ENV['SINATRA_ENV'] || "development" config = YAML.load_file("#{Dir.pwd}/ass.yml") $timer = "#{config['timer']}".to_i $cron = config['cron'] || 'cron' $port = config['port'] || 4567 $mode = config['mode'] || env $VERSION = File.open("#{ROOTDIR}/VERSION", "rb").read $apps = config['apps'] || [] ############################################################ ## Certificate Key Setup ############################################################ $certkey = {} def check_cert $apps.each { |app| unless File.exist?("#{Dir.pwd}/#{app}_#{$mode}.pem") then puts "Please provide #{app}_#{$mode}.pem under '#{Dir.pwd}/' directory" return false; else puts "'#{app}'s #{$mode} PEM: (#{app}_#{$mode}.pem)" certfile = File.read("#{Dir.pwd}/#{app}_#{$mode}.pem") openSSLContext = OpenSSL::SSL::SSLContext.new openSSLContext.cert = OpenSSL::X509::Certificate.new(certfile) openSSLContext.key = OpenSSL::PKey::RSA.new(certfile) $certkey["#{app}"] = openSSLContext end } return true end unless check_cert then puts "1: please provide certificate key pem file under current directory, name should be: appid_dev.pem for development and appid_prod.pem for production" puts "2: edit your ass.yml under current directory" puts "3: run ass" puts "4: iOS Client: in AppDelegate file, didRegisterForRemoteNotificationsWithDeviceToken method should access url below:" $apps.each { |app| puts "'#{app}'s registration url: http://serverIP:#{$port}/v1/apps/#{app}/DeviceToken" } puts "5: Server: cron should access 'curl http://localhost:#{$port}/v1/app/push/{messages}/{pid}' to send push message" exit else puts "*"*80 puts "Apple Service Server(#{$VERSION}) is Running ..." puts "Push Notification Service: Enabled" puts "Mode: #{$mode}" puts "Port: #{$port}" puts "Cron Job: '#{Dir.pwd}/#{$cron}' script is running every #{$timer} #{($timer == 1) ? 'minute' : 'minutes'} " unless "#{$timer}".to_i == 0 puts "*"*80 end ############################################################ ## Sequel Database Setup ############################################################ unless File.exist?("#{Dir.pwd}/ass.db") then $DB = Sequel.connect("sqlite://#{Dir.pwd}/ass.db") $DB.create_table :tokens do primary_key :id String :app, :unique => false, :null => false String :token, :unique => true, :null => false, :size => 100 index [:app, :token] end $DB.create_table :pushes do primary_key :id String :pid, :unique => true, :null => false, :size => 100 index :pid end else $DB = Sequel.connect("sqlite://#{Dir.pwd}/ass.db") end Token = $DB[:tokens] Push = $DB[:pushes] ############################################################ ## Timer Job Setup ############################################################ scheduler = Rufus::Scheduler.start_new unless $timer == 0 then scheduler.every "#{$timer}m" do puts "running job: '#{Dir.pwd}/#{$cron}' every #{$timer} #{($timer == 1) ? 'minute' : 'minutes'}" system "./#{$cron}" end else puts "1: How to register notification? (Client Side)" puts puts "In AppDelegate file, inside didRegisterForRemoteNotificationsWithDeviceToken method access url below to register device token:" $apps.each { |app| puts "'#{app}'s registration url: http://serverIP:#{$port}/v1/apps/#{app}/DeviceToken" } puts puts "2: How to send push notification? (Server Side)" puts $apps.each { |app| puts "curl http://localhost:#{$port}/v1/apps/#{app}/push/{message}/{pid}" } puts puts "Note:" puts "param1 (message): push notification message you want to send, remember the message should be html escaped" puts "param2 (pid ): unique string to mark the message, for example current timestamp or md5/sha1 digest" puts puts "*"*80 end ############################################################ ## Apple Service Server based on Sinatra ############################################################ class App < Sinatra::Base set :port, "#{$port}".to_i if "#{$mode}".strip == 'development' then set :show_exceptions, true set :dump_errors, true else set :show_exceptions, false set :dump_errors, false end get '/' do o = "Apple Service Server #{$VERSION}

" + "author: Eiffel(Q)
email: eiffelqiu@gmail.com

" o += "1: How to register notification? (Client Side)

" o += "In AppDelegate file, inside didRegisterForRemoteNotificationsWithDeviceToken method access url below to register device token:

" $apps.each { |app| o += "'#{app}': http://serverIP:#{$port}/v1/apps/#{app}/DeviceToken
" } o += "
2: How to send push notification? (Server Side)

" $apps.each { |app| o += "curl http://localhost:#{$port}/v1/apps/#{app}/push/{message}/{pid}
" } o += "
Note:
" o += "param1 (message): push notification message you want to send, remember the message should be html escaped
" o += "param2 (pid ): unique string to mark the message, for example current timestamp or md5/sha1 digest
" o end $apps.each { |app| get "/v1/apps/#{app}/:token" do puts "[#{params[:token]}] was added to '#{app}'" o = Token.first(:token => params[:token]) unless o Token.insert( :app => app, :token => params[:token] ) end end get "/v1/apps/#{app}/push/:message/:pid" do message = CGI::unescape(params[:message]) pid = params[:pid] puts "'#{message}' was sent to (#{app}) with pid: [#{pid}]" @push = Token.where(:app => app) @exist = Push.first(:pid => pid) unless @exist openSSLContext = $certkey["#{app}"] # Connect to port 2195 on the server. sock = nil if $mode == 'production' then sock = TCPSocket.new('gateway.push.apple.com', 2195) else sock = TCPSocket.new('gateway.sandbox.push.apple.com', 2195) end # do our SSL handshaking sslSocket = OpenSSL::SSL::SSLSocket.new(sock, openSSLContext) sslSocket.connect #Push.create( :pid => pid ) Push.insert(:pid => pid) # write our packet to the stream @push.each do |o| tokenText = o[:token] # pack the token to convert the ascii representation back to binary tokenData = [tokenText].pack('H*') # construct the payload payload = "{\"aps\":{\"alert\":\"#{message}\", \"badge\":1}}" # construct the packet packet = [0, 0, 32, tokenData, 0, payload.length, payload].pack("ccca*cca*") # read our certificate and set up our SSL context sslSocket.write(packet) end # cleanup sslSocket.close sock.close end end } end