/*
* Contains: Routines Mail Services support for Apple Push Notification Service.
* Written by: Michael (for addtl writers check SVN comments).
*
* Copyright (c) 2011-2012 Apple Inc. All Rights Reserved.
*
* IMPORTANT NOTE: This file is licensed only for use on Apple-branded
* computers and is subject to the terms and conditions of the Apple Software
* License Agreement accompanying the package this file is a part of.
* You may not port this file to another platform without Apple's written consent.
*
*
* Note: Based heavily on APNBridge
*
* Note: When editing this file set Xcode to "Editor uses tabs/width=4".
* Any other editor or tab setting may not show this file nicely!
*/
#import <syslog.h>
#import "push_notify.h"
#import "APNSNotify.h"
NSString* sandboxOID = @"1.2.840.113635.100.6.3.1";
NSString* productionOID = @"1.2.840.113635.100.6.3.2";
NSString* userIDOID = @"0.9.2342.19200300.100.1.1";
uint32_t g_APN_resp_ID = 101;
@implementation APNSNotify
NSString* prodHost = @"gateway.push.apple.com";
NSString* sandboxHost = @"gateway.sandbox.push.apple.com";
// -----------------------------------------------------------------
// init_with_cert_pref
-(id) init_with_cert_pref: (NSString *)in_cert_pref
{
self = [super initWithName:@"APN"];
if (self) {
[self generate_SSL_settings: in_cert_pref];
if (_ssl_settings == nil) {
[self release];
self = nil;
}
}
return( self );
}
// -----------------------------------------------------------------
// init
-(id) init
{
return( [self init_with_cert_pref: @"apns:com.apple.mail"] );
}
// -----------------------------------------------------------------
// connect
-(void) connect
{
NSString *name;
if (_is_production)
name = prodHost;
else
name = sandboxHost;
syslog(VLOG_NOTICE, "Opening connection to apn server [self connectToHost:name port:2195];
}
// -----------------------------------------------------------------
// newConnectionCreated
-(void) newConnectionCreated
{
syslog(VLOG_INFO, "Connected to apn server }
// -----------------------------------------------------------------
// connectionClosed
-(void) connectionClosed: (NSError *)streamError
{
if ( !streamError )
syslog( streamError ? VLOG_WARNING : VLOG_NOTICE,
"Disconnected from apn server [host UTF8String], [[self topic_name] UTF8String] );
else {
syslog(VLOG_NOTICE, "Disconnected from apn server [host UTF8String], [[self topic_name] UTF8String], [[streamError localizedFailureReason]UTF8String] );
[self startReconnect];
}
// if (streamError)
// [self startReconnect];
}
// -----------------------------------------------------------------
// ssl_config
-(NSDictionary*) ssl_config
{
return( _ssl_settings );
}
// -----------------------------------------------------------------
// generate_SSL_settings
-(void) generate_SSL_settings: (NSString *)in_cert_pref;
{
SecKeychainSetPreferenceDomain(kSecPreferencesDomainSystem);
CFStringRef cp = CFStringCreateCopy(kCFAllocatorDefault, (CFStringRef)in_cert_pref);
SecIdentityRef identity = SecIdentityCopyPreferred(cp, NULL, NULL);
CFRelease(cp);
if ( identity == NULL ) {
syslog(VLOG_ERR, "Couldn't get identity for preference _ssl_settings = nil;
return;
}
CFArrayRef ca = CFArrayCreate(NULL, (const void **)&identity, 1, &kCFTypeArrayCallBacks);
CFRelease(identity);
NSMutableDictionary* sslSettings = [NSMutableDictionary dictionary];
[sslSettings setObject:(id)ca forKey:(NSString*)kCFStreamSSLCertificates];
CFRelease(ca);
[sslSettings setObject:(NSString*)kCFStreamSocketSecurityLevelTLSv1 forKey:(NSString*)kCFStreamSSLLevel];
SecCertificateRef cert;
SecIdentityCopyCertificate(identity, &cert);
NSDictionary* dict = (NSDictionary*)SecCertificateCopyValues(cert, NULL, nil);
CFRelease(cert);
if (dict == nil)
syslog(VLOG_WARNING, "SSL certificate null certificate dictionary!");
#if 0 // debug
NSString *description = [dict description];
NSArray *lines = [description componentsSeparatedByString: @"\n"];
for (NSString *line in lines)
syslog(VLOG_DEBUG, "#endif
if ([dict objectForKey:productionOID] != nil)
_is_production = YES;
else if ([dict objectForKey:sandboxOID] == nil)
syslog(VLOG_WARNING, "Doesn't appear to be valid push cert");
NSArray* nameArray = [[dict objectForKey:(NSString*)kSecOIDX509V1SubjectName] objectForKey:(NSString*)kSecPropertyKeyValue];
for (NSDictionary* nameDict in nameArray) {
if ([[nameDict objectForKey:(NSString*)kSecPropertyKeyLabel] isEqualToString:userIDOID])
_topic_name = [[nameDict objectForKey:(NSString*)kSecPropertyKeyValue] retain];
}
CFRelease((CFDictionaryRef)dict);
syslog(VLOG_INFO, "is production =
if (_is_production)
[sslSettings setObject:prodHost forKey:(NSString*)kCFStreamSSLPeerName];
else
[sslSettings setObject:sandboxHost forKey:(NSString*)kCFStreamSSLPeerName];
_ssl_settings = [sslSettings retain];
} // generate_SSL_settings
// -----------------------------------------------------------------
// is_production
-(BOOL) is_production
{
return( _is_production );
}
// -----------------------------------------------------------------
// topic_name
-(NSString*) topic_name
{
return( _topic_name );
}
// -----------------------------------------------------------------
// hex_log_msg
-(void) hex_log_msg: (const uint8_t *) in_message size: (int)size tag: (NSString *)in_tag
{
int i,j;
NSMutableString * msg = [NSMutableString string];
for (i = 0; i < size; i += 16) {
NSMutableString *hex = [NSMutableString string];
NSMutableString *chars = [NSMutableString string];
for (j = 0; j < 16; j++) {
NSString *hexVal;
NSString *charVal;
if (i + j >= size) {
hexVal = @" ";
charVal = @" ";
} else {
if (isprint(in_message[i+j]))
charVal = [NSString stringWithFormat:@" else
charVal = @".";
hexVal = [NSString stringWithFormat:@" }
[hex appendString:hexVal];
[chars appendString:charVal];
}
[msg appendString:@"push_notify: "];
[msg appendString:hex];
[msg appendString:@" "];
[msg appendString:chars];
[msg appendString:@"\n"];
}
syslog(VLOG_DEBUG, "} // hex_log_msg
// -----------------------------------------------------------------
// haveData
-(NSData *) haveData: (NSData *)in_data
{
if (!in_data || ![in_data length])
return( nil );
uint32_t resp_ID = 0;
char *err_txt = NULL;
size_t data_len = [in_data length];
const uint8_t *data_buff = [in_data bytes];
syslog(VLOG_DEBUG, "notify: have data: length:
if (data_buff[0] == 8 && data_len >= 6) {
int status = data_buff[1];
memcpy(&resp_ID, &data_buff[2], sizeof(uint32_t));
switch ( status ) {
case 0: err_txt = " No error"; break;
case 1: err_txt = " Processing error"; break;
case 2: err_txt = " Missing device token"; break;
case 3: err_txt = " Missing topic"; break;
case 4: err_txt = " Missing payload"; break;
case 5: err_txt = " Invalid token size"; break;
case 6: err_txt = " Invalid topic size"; break;
case 7: err_txt = " Invalid payload size"; break;
case 8: err_txt = " Invalid token"; break;
default: err_txt = " Unknown error"; break;
}
syslog(VLOG_ERR, "Error: Received notification response: } else if ( data_len ){
syslog(VLOG_ERR, "Received unknown notification response with command: [self hex_log_msg: data_buff size: data_len tag: @"receive"];
}
return( nil );
} // haveData
// -----------------------------------------------------------------
// send_notification
-(void) send_notification: (NSString *)in_payload to_device: (NSString *)in_device
{
// verify connection
if ( ![self isConnectedOrConnecting] )
[self connect];
// convert device to binary blob
uint8_t binary_device_token[32];
const char* device_str = [in_device UTF8String];
if (device_str == NULL || strspn(device_str, "0123456789ABCDEFabcdef") != 64 || device_str[64] != '\0') {
syslog(VLOG_WARNING, "Got bad device ID: return;
}
int i;
for (i = 0; i < 64; i += 2) {
int hi = device_str[i] >= 'a' ? device_str[i]-'a'+10 : device_str[i] >= 'A' ? device_str[i]-'A'+10 : device_str[i]-'0';
int lo = device_str[i+1] >= 'a' ? device_str[i+1]-'a'+10 : device_str[i+1] >= 'A' ? device_str[i+1]-'A'+10 : device_str[i+1]-'0';
binary_device_token[i/2] = (hi*16)+lo;
}
const char *payload_str = [in_payload UTF8String];
size_t payload_len = strlen(payload_str);
NSUInteger msg_size = 11 + DEVICE_BINARY_SIZE + 2 + payload_len;
uint8_t *msg_buff = calloc(msg_size, 1);
/* message format is, |COMMAND|ID|EXPIRY|TOKENLEN|TOKEN|PAYLOADLEN|PAYLOAD| */
int offset = 0; // buffer offset
/* command */
msg_buff[offset++] = 1;
/* provider preference ordered ID */
uint32_t order_err_resp = g_APN_resp_ID++;
memcpy(msg_buff+offset, &order_err_resp, sizeof(uint32_t));
offset += sizeof(uint32_t);
/* expiry date network order */
uint32_t msg_expire = htonl(time(NULL) + 86400); // expire message if not delivered in 1 day
memcpy(msg_buff+offset, &msg_expire, sizeof(uint32_t));
offset += sizeof(uint32_t);
/* token length network order */
uint16_t token_len = htons( DEVICE_BINARY_SIZE );
memcpy(msg_buff+offset, &token_len, sizeof(uint16_t));
offset += sizeof(uint16_t);
/* device token */
memcpy(msg_buff+offset, binary_device_token, DEVICE_BINARY_SIZE);
offset += DEVICE_BINARY_SIZE;
/* payload length network order */
uint16_t net_order_payload_len = htons(payload_len);
memcpy(msg_buff+offset, &net_order_payload_len, sizeof(uint16_t));
offset += sizeof(uint16_t);
/* payload */
memcpy(msg_buff+offset, payload_str, payload_len);
offset += payload_len;
[self hex_log_msg: msg_buff size: msg_size tag: @"send"];
NSData* data = [NSData dataWithBytesNoCopy: msg_buff length: msg_size];
[self writeData: data];
} // send_notification
@end