├── README ├── LICENSE ├── example.tcl └── apns.tcl /README: -------------------------------------------------------------------------------- 1 | This Tcl code interfaces with the Apple Push Notification Service (APNS) 2 | to allow you to send iPhone/iOS alerts. 3 | 4 | We have been using this code in a production environment since May 2010. 5 | It is currently delivering nearly 20k successful push alerts to our users 6 | per day (with a peak of nearly 400 alerts per minute) and we expect 7 | that it should continue to be able to scale to much higher levels. 8 | 9 | This code has been developed on FreeBSD 8.0 with Tcl 8.5.9, but the 10 | code does not have any unusual OS dependencies and should work on 11 | other Tcl-supported environments. 12 | 13 | Your iPhone application will need to be responsible for communicating 14 | the deviceToken to your backend in advance. The basic syntax of the 15 | transmission will look like this: 16 | 17 | set json "{\"aps\": \"hello message $i\"}" 18 | set deviceToken "123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" 19 | send_apns $deviceToken $json 20 | 21 | The above send_apns is a blocking call, but event driven handlers are 22 | used in the background to perform all socket connecting, reading, 23 | writing, and timeouts. The network connection is kept persistent 24 | between calls to send_apns in order to maximize throughput. 25 | 26 | Periodically, a separate proc "connect_and_receive_apns_feedback" can 27 | be called to retrieve a list of deviceTokens that have bounced by the 28 | APNS, and you can process the list to remove those dead devices from 29 | your database. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011, FlightAware, LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of FlightAware, LLC nor the names of its 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /example.tcl: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/tclsh8.5 2 | 3 | 4 | source apns.tcl 5 | 6 | 7 | # If you need to change the timeout parameters, you can uncomment the following... 8 | #set ::apns::connect_timeout_ms 5000 9 | #set ::apns::write_timeout_ms 2000 10 | #set ::apns::feedback_timeout_ms 30000 11 | 12 | if {1} { 13 | # for development, use Apple's sandbox servers. 14 | # Messages will still get delivered, but Apple is less paranoid about misbehaving or abusive 15 | set ::apns::gateway_host gateway.sandbox.push.apple.com 16 | set ::apns::gateway_port 2195 17 | 18 | set ::apns::feedback_host feedback.sandbox.push.apple.com 19 | set ::apns::feedback_port 2196 20 | 21 | set ::apns::certificate apns-sandbox.crt 22 | set ::apns::private_key apns-sandbox.key 23 | 24 | } else { 25 | # for production, use Apple's real servers. 26 | set ::apns::gateway_host gateway.push.apple.com 27 | set ::apns::gateway_port 2195 28 | 29 | set ::apns::feedback_host feedback.push.apple.com 30 | set ::apns::feedback_port 2196 31 | 32 | set ::apns::certificate apns-production.crt 33 | set ::apns::private_key apns-production.key 34 | } 35 | 36 | # A simple logging proc. 37 | # In a real service, you might want to make this write to syslog or something. 38 | proc logmsg {msg} { 39 | puts $msg 40 | } 41 | 42 | # A simple logging proc. 43 | # In a real service, you might want to make this write to syslog or something. 44 | proc logerr {msg} { 45 | puts stderr $msg 46 | } 47 | 48 | 49 | proc main {} { 50 | 51 | while 1 { 52 | # send a message to a few arbitrary deviceTokens. 53 | # In a real service, this would probably involve a SELECT from your database. 54 | for {set i 0} {$i < 10} {incr i} { 55 | set json "{\"aps\": \"hello message $i\"}" 56 | set deviceToken "123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF$i" 57 | ::apns::send_apns $deviceToken $json 58 | logmsg "sent" 59 | after 1000 60 | logmsg "looping" 61 | } 62 | 63 | # wait for a little bit for the Feedback server. 64 | logmsg "Sleeping..." 65 | after 5000 66 | 67 | # check with the Feedback server to see if there were any bounced devices. 68 | # In a real service, you would mark the devices as bad in your database and not send future messages. 69 | ::apns::connect_and_receive_apns_feedback 70 | if {[llength $::apns::bad_device_tokens] > 0} { 71 | logmsg "have [llength $::apns::bad_device_tokens] waiting device_tokens" 72 | } else { 73 | logmsg "no bad_device_tokens waiting" 74 | } 75 | } 76 | } 77 | 78 | 79 | main 80 | -------------------------------------------------------------------------------- /apns.tcl: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2010-2011, FlightAware, LLC 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are 7 | # met: 8 | # 9 | # * Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following 14 | # disclaimer in the documentation and/or other materials provided 15 | # with the distribution. 16 | # 17 | # * Neither the name of FlightAware, LLC nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | package require tls 34 | 35 | 36 | namespace eval ::apns { 37 | 38 | # timeouts for network operations 39 | variable connect_timeout_ms 5000 40 | variable write_timeout_ms 2000 41 | variable feedback_timeout_ms 30000 42 | 43 | # connection details for the gateway server 44 | variable gateway_host "gateway.push.apple.com" 45 | variable gateway_port 2195 46 | 47 | # connection details for the feedback server 48 | variable feedback_host "feedback.push.apple.com" 49 | variable feedback_port 2196 50 | 51 | # filenames of your personal certificate and private-key 52 | variable certificate "certificate.pem" 53 | variable private_key "private_key.pem" 54 | 55 | # ------ 56 | 57 | variable gateway_chan 58 | variable gateway_signal 59 | variable feedback_chan 60 | variable feedback_readbuf 61 | variable feedback_signal 62 | variable bad_device_tokens 63 | 64 | # -------------------------------------------------- 65 | # -------------------------------------------------- 66 | 67 | # Close the gateway socket. 68 | proc close_apns_gateway {} { 69 | variable gateway_chan 70 | logmsg "Closing gateway connection..." 71 | if {[info exists gateway_chan]} { 72 | catch { close $gateway_chan } 73 | unset -nocomplain gateway_chan 74 | } 75 | } 76 | 77 | # Helper callback used to handle reading data when the socket becomes readable. 78 | proc readable_callback_apns_drain {chan signame} { 79 | if {[eof $chan]} { 80 | fileevent $chan readable {} 81 | fileevent $chan writable {} 82 | logmsg "readable_callback_apns_drain: Closed secure network drain connection due to EOF"; 83 | set $signame -1 84 | return 85 | } 86 | set drained [read $chan] 87 | if {[string length $drained] != 0} { 88 | fileevent $chan readable {} 89 | fileevent $chan writable {} 90 | logerr "readable_callback_apns_drain: Read unexpected: $drained" 91 | set $signame -1 92 | return 93 | } 94 | set $signame 1 95 | } 96 | 97 | # Helper callback used to handle sending data when the socket becomes writable. 98 | proc writable_callback_apns_handshake {chan signame} { 99 | if {[catch {tls::handshake $chan} result]} { 100 | fileevent $chan readable {} 101 | fileevent $chan writable {} 102 | logerr "writable_callback_apns_handshake: Error during handshake: $result" 103 | set $signame -1 104 | return 105 | } elseif {$result} { 106 | fileevent $chan writable {} 107 | #puts "writable_callback_apns_handshake: Handshake complete" 108 | array set certinfo [tls::status $chan] 109 | logmsg "writable_callback_apns_handshake: connected to server with $certinfo(subject)" 110 | #parray certinfo 111 | set $signame 1 112 | return 113 | } else { 114 | logmsg "writable_callback_apns_handshake: Handshake still in progress" 115 | } 116 | } 117 | 118 | 119 | # Helper callback used to handle sending data when the socket becomes writable. 120 | proc writable_callback_apns_transmit {chan signame payload} { 121 | fileevent $chan writable {} 122 | if {[catch {puts -nonewline $chan $payload; flush $chan} result]} { 123 | logerr "writable_callback_apns_transmit: Error during transmit: $result" 124 | set $signame -1 125 | } else { 126 | puts "debug writable_callback_apns_transmit: Transmit complete" 127 | set $signame 1 128 | } 129 | } 130 | 131 | 132 | # Helper callback used when a read or write operations takes too long. 133 | proc timeout_callback_apns {chan signame} { 134 | logerr "timeout_callback_apns: Timeout occurred" 135 | set $signame -1 136 | catch {fileevent $chan writable {}} 137 | catch {fileevent $chan readable {}} 138 | } 139 | 140 | 141 | # Open a connection to the gateway server 142 | # Returns 0 on error. 143 | proc connect_apns_gateway {} { 144 | variable gateway_chan 145 | variable gateway_signal 146 | variable gateway_host 147 | variable gateway_port 148 | variable certificate 149 | variable private_key 150 | variable connect_timeout_ms 151 | 152 | logmsg "Opening secure gateway connection to $gateway_host:$gateway_port ..." 153 | set gateway_chan [tls::socket -async -certfile $certificate -keyfile $private_key $gateway_host $gateway_port] 154 | 155 | # start in binary/blocking mode until the SSL negotiation is finished. 156 | fconfigure $gateway_chan -encoding binary -buffering none -blocking 1 157 | 158 | # wait for the negotiation to complete or timeout. 159 | set ::apns::gateway_signal 0 160 | fileevent $gateway_chan writable [list writable_callback_apns_handshake $gateway_chan ::apns::gateway_signal] 161 | fileevent $gateway_chan readable [list readable_callback_apns $gateway_chan ::apns::gateway_signal] 162 | set afterID [after $connect_timeout_ms timeout_callback_apns $gateway_chan ::apns::gateway_signal] 163 | vwait ::apns::gateway_signal 164 | 165 | after cancel $afterID 166 | fileevent $gateway_chan readable {} 167 | fileevent $gateway_chan writable {} 168 | 169 | #puts "connect_apns_gateway: done connecting, status $::apns::gateway_signal" 170 | 171 | if {$::apns::gateway_signal < 0} { 172 | close_apns_gateway 173 | return 0 174 | } 175 | 176 | # switch to non-blocking mode. 177 | fconfigure $gateway_chan -blocking 0 178 | return 1 179 | } 180 | 181 | 182 | # Connect to the gateway server, but only if not already connected 183 | # Returns 0 on error, 1 on success (already connected or connect succeeded). 184 | proc connect_apns_gateway_ifneeded {} { 185 | variable gateway_chan 186 | 187 | # if no socket, then try to open one. 188 | if {![info exists gateway_chan]} { 189 | return [connect_apns_gateway] 190 | } 191 | 192 | # already have connection, so assume it is good. 193 | return 1 194 | } 195 | 196 | # Send an alert to the gateway service. 197 | # Throws error on transmit failure. 198 | proc send_apns {deviceToken payload} { 199 | variable gateway_chan 200 | variable gateway_signal 201 | variable write_timeout_ms 202 | 203 | # make sure it isn't too long. 204 | if {[string length $deviceToken] > 64 || [string length $deviceToken] % 2 != 0 || [string length $deviceToken] == 0} { 205 | error "send_apns: invalid deviceToken" 206 | } 207 | if {[string length $payload] > 256} { 208 | error "send_apns: too long" 209 | } 210 | 211 | # format the outgoing network packet. 212 | # message format is, |COMMAND|TOKENLEN|TOKEN|PAYLOADLEN|PAYLOAD| 213 | 214 | # method 1 215 | #set formatstr [format "cSH%dSa%d" [string length $deviceToken] [string bytelength $payload]] 216 | #set msg [binary format $formatstr 0 [expr [string length $deviceToken]/2] $deviceToken [string bytelength $payload] [encoding convertto utf-8 $payload]] 217 | 218 | # method 2 219 | #set msg [binary format "cSH*Sa*" 0 [expr [string length $deviceToken]/2] $deviceToken [string bytelength $payload] [encoding convertto utf-8 $payload]] 220 | 221 | # method 3 222 | set msg [binary format "cSH*Sa*" 0 [expr [string length $deviceToken]/2] $deviceToken [string length $payload] $payload] 223 | 224 | binary scan $msg "H*" hexmsg 225 | puts "debug outgoing message: $hexmsg" 226 | 227 | # send the packet. 228 | for {set retry 0} {$retry<10} {incr retry} { 229 | if {[catch { 230 | # connect if needed. 231 | if {![connect_apns_gateway_ifneeded]} { 232 | error "send_apns: failed to connect" 233 | } 234 | 235 | # wait for the to complete or timeout. 236 | set ::apns::gateway_signal 0 237 | fileevent $gateway_chan writable [list writable_callback_apns_transmit $gateway_chan ::apns::gateway_signal $msg] 238 | fileevent $gateway_chan readable [list readable_callback_apns_drain $gateway_chan ::apns::gateway_signal] 239 | set afterID [after $write_timeout_ms timeout_callback_apns $gateway_chan ::apns::gateway_signal)] 240 | vwait ::apns::gateway_signal 241 | 242 | # cleanup after waiting. 243 | after cancel $afterID 244 | fileevent $gateway_chan readable {} 245 | fileevent $gateway_chan writable {} 246 | 247 | if {$::apns::gateway_signal != 1} { 248 | error "send_apns: send failure, got $::apns::gateway_signal" 249 | } 250 | } result] == 1} { 251 | logerr "send_apns: Error occurred: $result" 252 | close_apns_gateway 253 | } else { 254 | logmsg "send_apns: Successfully sent" 255 | return 1 256 | } 257 | 258 | } 259 | error "send_apns: failed after 10 retries" 260 | } 261 | 262 | 263 | # -------------------------------------------------- 264 | # -------------------------------------------------- 265 | 266 | # Close the feedback socket. 267 | proc close_apns_feedback {} { 268 | variable feedback_chan 269 | logmsg "Closing feedback connection..." 270 | if {[info exists feedback_chan]} { 271 | catch { close $feedback_chan } 272 | unset -nocomplain feedback_chan 273 | } 274 | } 275 | 276 | 277 | # Helper callback used to read data when the feedback socket becomes readable. 278 | proc readable_callback_apns_feedback {chan signame} { 279 | variable feedback_readbuf 280 | 281 | if {[eof $chan]} { 282 | logmsg "readable_callback_apns_feedback: Closed secure network feedback connection due to EOF"; 283 | fileevent $chan readable {} 284 | fileevent $chan writable {} 285 | set $signame 1 286 | return 287 | } 288 | if {[string length $feedback_readbuf] < 6} { 289 | # read enough to interpret the header 290 | set readamt [expr 6-[string length $feedback_readbuf]] 291 | append feedback_readbuf [read $chan $readamt] 292 | } 293 | if {[string length $feedback_readbuf] >= 6} { 294 | # parse the header and compute the total message size. 295 | if {[binary scan $feedback_readbuf "IuSu" timestamp tokenlen] != 2} { 296 | logerr "readable_callback_apns_feedback: Parse failure reading secure network connection"; 297 | fileevent $chan readable {} 298 | fileevent $chan writable {} 299 | set $signame -1 300 | return 301 | } 302 | set msgsize [expr 6+$tokenlen] 303 | #puts "debug: Whole message size will be $msgsize (have [string length $feedback_readbuf] now)" 304 | 305 | # read enough to get the entire message 306 | if {[string length $feedback_readbuf] < $msgsize} { 307 | set readamt [expr $msgsize-[string length $feedback_readbuf]] 308 | append feedback_readbuf [read $chan $readamt] 309 | } 310 | 311 | # if we have an entire message, then parse it. 312 | if {[string length $feedback_readbuf] >= $msgsize} { 313 | set formatstr [format "IuSuH%da*" [expr 2*$tokenlen]] 314 | if {[binary scan $feedback_readbuf $formatstr timestamp tokenlen device_token feedback_readbuf] != 4} { 315 | logerr "readable_callback_apns_feedback: Parse failure reading secure network connection" 316 | fileevent $chan readable {} 317 | fileevent $chan writable {} 318 | set $signame -1 319 | return 320 | } 321 | 322 | # excellent, we have parsed an entire message. 323 | logmsg "Got bounce feedback [clock format $timestamp -format {%Y-%m-%d %H:%M:%S} -timezone :UTC] for $device_token" 324 | if {[lsearch -exact $bad_device_tokens $device_token] == -1} { 325 | lappend bad_device_tokens $device_token 326 | } 327 | } 328 | } 329 | } 330 | 331 | 332 | # Connect to the feedback server, reading any responses, and then disconnect. 333 | # Appends the responses to ::apns::bad_device_tokens 334 | # Returns 0 on error. 335 | proc connect_and_receive_apns_feedback {} { 336 | variable bad_device_tokens 337 | variable feedback_chan 338 | variable feedback_readbuf 339 | variable feedback_host 340 | variable feedback_port 341 | variable certificate 342 | variable private_key 343 | variable connect_timeout_ms 344 | variable feedback_timeout_ms 345 | 346 | if {![info exists bad_device_tokens]} { 347 | set bad_device_tokens {} 348 | } 349 | 350 | # open new connection 351 | logmsg "Opening secure feedback connection to $feedback_host:$feedback_port ..." 352 | set feedback_chan [tls::socket -async -certfile $certificate -keyfile $private_key $feedback_host $feedback_port] 353 | 354 | # start in binary/blocking mode until the SSL negotiation is finished. 355 | fconfigure $feedback_chan -encoding binary -buffering none -blocking 1 356 | 357 | # wait for the negotiation to complete or timeout. 358 | set ::apns::feedback_signal 0 359 | set feedback_readbuf {} 360 | fileevent $feedback_chan writable [list writable_callback_apns_handshake $feedback_chan ::apns::feedback_signal] 361 | fileevent $feedback_chan readable [list readable_callback_apns_feedback $feedback_chan ::apns::feedback_signal] 362 | set afterID [after $connect_timeout_ms [list timeout_callback_apns $feedback_chan ::apns::feedback_signal]] 363 | logmsg "Waiting for feedback_signal" 364 | vwait ::apns::feedback_signal 365 | 366 | after cancel $afterID 367 | fileevent $feedback_chan readable {} 368 | fileevent $feedback_chan writable {} 369 | 370 | logmsg "Finished waiting" 371 | 372 | if {$::apns::feedback_signal < 0} { 373 | close_apns_feedback 374 | return 0 375 | } 376 | 377 | logmsg "Reading from feedback" 378 | 379 | # switch to non-blocking mode and wait until timeout or EOF. 380 | set ::apns::feedback_signal 0 381 | fconfigure $feedback_chan -blocking 0 382 | fileevent $feedback_chan readable [list readable_callback_apns_feedback $feedback_chan ::apns::feedback_signal] 383 | set afterID [after $feedback_timeout_ms [list timeout_callback_apns $feedback_chan ::apns::feedback_signal]] 384 | vwait ::apns::feedback_signal 385 | 386 | # done with connection so close it 387 | close_apns_feedback 388 | 389 | if {$::apns::feedback_signal < 0} { 390 | return 0 391 | } 392 | return 1 393 | } 394 | 395 | 396 | # -------------------------------------------------- 397 | # -------------------------------------------------- 398 | 399 | } 400 | --------------------------------------------------------------------------------