/*
* Contains: Routines Mail Services support for Apple Push Notification Service.
* Written by: Michael (for addtl writers check SVN comments).
*
* Copyright (c) 2010-2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
* 3. Neither the name of Apple Inc. ("Apple") nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* 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: 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 "push_notify.h"
#import "APNSNotify.h"
#import "APNSFeedback.h"
#import <asl.h>
#import <sys/stat.h>
#import <sys/param.h>
#import <sys/socket.h>
#import <sys/un.h>
#import <Foundation/Foundation.h>
#import <ServerFoundation/XSTask.h>
#import <OpenDirectory/NSOpenDirectory.h>
#import <DirectoryService/DirServicesConst.h>
int sig_usr1 = 1;
int sig_usr2 = 0;
int got_sighup = 0;
int got_sigterm = 0;
time_t g_check_feedback_time = 0;
aslclient g_asl_err = NULL;
aslclient g_asl_info = NULL;
aslclient g_asl_debug = NULL;
ODNode *g_od_search_node = nil;
NSAutoreleasePool *gPool = nil;
// -----------------------------------------------------------------
// exit_with_error ()
void exit_with_error ( const char *msg, int code )
{
log_err(" [gPool release];
exit( code );
} // exit_with_error
// -----------------------------------------------------------------------------
// open_logs ()
void open_logs ( void )
{
// get app name
NSString *app_name = [[[NSProcessInfo processInfo] processName] lastPathComponent];
// open error log
g_asl_err = asl_open([app_name UTF8String], "com.apple.push_notify.err", 0);
if ( !g_asl_err )
fprintf(stderr, "Could not initialize ASL logging for mail error log.\n" );
else
asl_set_filter(g_asl_err, ASL_FILTER_MASK_UPTO(ASL_LEVEL_ERR));
// open info log
g_asl_info = asl_open([app_name UTF8String], "com.apple.push_notify.info", 0);
if ( !g_asl_info )
fprintf(stderr, "Could not initialize ASL logging for mail info log.\n" );
else
asl_set_filter(g_asl_info, ASL_FILTER_MASK_UPTO(ASL_LEVEL_INFO));
// open debug log
g_asl_debug = asl_open([app_name UTF8String], "com.apple.push_notify.debug", 0);
if ( !g_asl_debug )
fprintf(stderr, "Could not initialize ASL logging for mail debug log.\n" );
else
asl_set_filter(g_asl_debug, ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG));
} //open_logs
// -----------------------------------------------------------------------------
// log_err ()
void log_err ( const char *in_format, ... )
{
if ( !g_asl_err )
return;
va_list args;
va_start(args, in_format);
asl_vlog( g_asl_err, NULL, ASL_LEVEL_ERR, in_format, args );
va_end( args );
} // log_err
// -----------------------------------------------------------------------------
// log_warning ()
void log_warning ( const char *in_format, ... )
{
if ( !g_asl_err )
return;
va_list args;
va_start(args, in_format);
asl_vlog( g_asl_err, NULL, ASL_LEVEL_WARNING, in_format, args );
va_end( args );
} // log_warning
// -----------------------------------------------------------------------------
// log_info ()
void log_info ( const char *in_format, ... )
{
if ( !g_asl_info )
return;
va_list args;
va_start(args, in_format);
asl_vlog( g_asl_info, NULL, ASL_LEVEL_INFO, in_format, args );
va_end( args );
} // log_info
// -----------------------------------------------------------------------------
// log_debug ()
void log_debug ( const char *in_format, ... )
{
if ( !g_asl_debug )
return;
if ( sig_usr1 ) {
va_list args;
va_start(args, in_format);
asl_vlog( g_asl_debug, NULL, ASL_LEVEL_DEBUG, in_format, args );
va_end( args );
}
} // log_debug
// -----------------------------------------------------------------
// set_rlimits ()
int set_rlimits ( void )
{
struct rlimit limit[1] = {{ 0, 0 }};
if (getrlimit(RLIMIT_CORE, limit) == -1)
return -1;
limit->rlim_cur = 0;
return(setrlimit(RLIMIT_CORE, limit));
} // set_rlimits
// -----------------------------------------------------------------
// sighup_handler ()
void sighup_handler( int sig __attribute__((unused)) )
{
got_sighup = 1;
} // sighup_handler
// -----------------------------------------------------------------
// sigterm_handler ()
void sigterm_handler ( int sig __attribute__((unused)) )
{
got_sigterm = 1;
} // sigterm_handler
// -----------------------------------------------------------------
// sigalrm_handler ()
void sigalrm_handler(int sig __attribute__((unused)))
{
return;
} // sigalrm_handler
// -----------------------------------------------------------------
// sigusr1_handler ()
void sigusr1_handler(int sig __attribute__((unused)))
{
if ( sig_usr1 ) {
sig_usr1 = 0;
log_info("verbose logging disabled" );
} else {
sig_usr1 = 1;
log_info("verbose logging enabled" );
}
} //sigusr1_handler
// -----------------------------------------------------------------
// sigusr2_handler ()
void sigusr2_handler(int sig __attribute__((unused)))
{
if ( sig_usr2 )
sig_usr2 = 0;
else
sig_usr2 = 1;
} //sigusr2_handler
// -----------------------------------------------------------------
// sighandler_setup ()
void sighandler_setup ( void )
{
struct sigaction action;
sigemptyset( &action.sa_mask );
action.sa_flags |= SA_RESTART;
action.sa_handler = sighup_handler;
if ( sigaction( SIGHUP, &action, NULL ) < 0 )
exit_with_error("unable to install signal handler for SIGHUP:
action.sa_handler = sigalrm_handler;
if ( sigaction( SIGALRM, &action, NULL ) < 0 )
exit_with_error("unable to install signal handler for SIGALRM:
action.sa_handler = sigusr1_handler;
if ( sigaction( SIGUSR1, &action, NULL ) < 0 )
exit_with_error("unable to install signal handler for SIGUSR1:
action.sa_handler = sigusr2_handler;
if ( sigaction( SIGUSR2, &action, NULL ) < 0 )
exit_with_error("unable to install signal handler for SIGUSR2:
action.sa_handler = sigterm_handler;
if ( sigaction( SIGTERM, &action, NULL ) < 0 )
exit_with_error("unable to install signal handler for SIGTERM:
if ( sigaction( SIGINT, &action, NULL) < 0 )
exit_with_error("unable to install signal handler for SIGINT:
} // sighandler_setup
// -----------------------------------------------------------------
// verify_map_file ()
void verify_map_file ( void )
{
NSDictionary *attr = [[NSDictionary dictionaryWithObjectsAndKeys:
@"root", NSFileOwnerAccountName,
@"mail", NSFileGroupOwnerAccountName,
[NSNumber numberWithUnsignedLong: 0640], NSFilePosixPermissions,
nil]autorelease];
if ( ![[NSFileManager defaultManager] fileExistsAtPath: DEVICE_MAPS_PATH] ) {
NSDictionary *a_dict = [[NSDictionary dictionaryWithObjectsAndKeys: nil] autorelease];
[a_dict writeToFile: DEVICE_MAPS_PATH atomically: YES];
}
[[NSFileManager defaultManager] setAttributes: attr ofItemAtPath: DEVICE_MAPS_PATH error: nil];
} // verify_map_file
// -----------------------------------------------------------------
// setup_socket_path ()
void setup_socket_path ( const char *in_path )
{
NSString *ns_str_path = [[NSString stringWithUTF8String: in_path]autorelease];
NSDictionary *ns_dir_attr = [[NSDictionary dictionaryWithObjectsAndKeys:
@"_dovecot", NSFileOwnerAccountName,
@"mail", NSFileGroupOwnerAccountName,
[NSNumber numberWithUnsignedLong: 0750], NSFilePosixPermissions,
nil]autorelease];
if ( ![[NSFileManager defaultManager] fileExistsAtPath: ns_str_path isDirectory: nil] )
[[NSFileManager defaultManager] createDirectoryAtPath: ns_str_path withIntermediateDirectories: YES attributes: ns_dir_attr error: nil];
else
[[NSFileManager defaultManager] setAttributes: ns_dir_attr ofItemAtPath: ns_str_path error: nil];
} // setup_socket_path
// -----------------------------------------------------------------
// get_socket ()
int get_socket ( int in_buff_size )
{
const char *socket_path = "/var/dovecot/push_notify";
struct sockaddr_un sock_addr;
log_debug("opening socket:
// setup socket directories
setup_socket_path("/var/dovecot");
int out_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
if (out_socket < 0) {
log_err("open socket: \" return( -1 );
}
if ( in_buff_size ) {
int rc = 0;
int optval = 0;
socklen_t optlen;
if ( sig_usr1 ) {
rc = getsockopt(out_socket, SOL_SOCKET, SO_RCVBUF, (char *)&optval, &optlen);
if ( !rc )
log_debug("socket get size: }
optlen = sizeof(in_buff_size);
rc = setsockopt(out_socket, SOL_SOCKET, SO_RCVBUF, &in_buff_size, optlen);
if ( !rc )
log_debug("socket receive buffer size: else
log_err("setsockopt(SO_RCVBUF) failed: }
// bind it to a local file
sock_addr.sun_family = AF_UNIX;
strlcpy(sock_addr.sun_path, socket_path, sizeof(sock_addr.sun_path));
unlink(sock_addr.sun_path);
int len = sizeof(sock_addr.sun_family) + strlen(sock_addr.sun_path) + 1;
int result = bind(out_socket, (struct sockaddr *)&sock_addr, len);
if (result < 0) {
log_err("bind() to socket: \" return( -1 );
}
// setup socket permissions
chmod( sock_addr.sun_path, 0666 );
log_debug("socket opened:
return( out_socket );
} // get_socket
// -----------------------------------------------------------------
// od_init ()
int od_init ( void )
{
int rc = 0;
log_debug("initializing open directory session" );
NSError *nsError = nil;
g_od_search_node = [ODNode nodeWithSession: [ODSession defaultSession] type: kODNodeTypeAuthentication error: &nsError];
if ( g_od_search_node == nil ) {
rc = 1;
if (nsError != nil)
log_err("Error: unable to open search node: else
log_err("Error: unable to open search node");
}
log_debug("open directory session initialized successfully" );
return( rc );
} // od_init
// -----------------------------------------------------------------
// get_user_guid ()
int get_user_guid ( const char *in_user, char *out_guid )
{
int result = 0;
NSAutoreleasePool *my_pool = [[NSAutoreleasePool alloc] init];
log_debug("getting GUID for user:
ODQuery *od_query = [ODQuery queryWithNode: g_od_search_node
forRecordTypes: [NSArray arrayWithObject: @kDSStdRecordTypeUsers]
attribute: @kDSNAttrRecordName
matchType: kODMatchEqualTo
queryValues: [NSString stringWithUTF8String: in_user]
returnAttributes: [NSArray arrayWithObject: @kDS1AttrGeneratedUID]
maximumResults: 1
error: nil];
if ( od_query != nil ) {
NSArray *nsArray_records = [od_query resultsAllowingPartial: NO error: nil];
if ( (nsArray_records != nil) && [nsArray_records count] ) {
ODRecord *od_record = [nsArray_records objectAtIndex: 0];
if ( od_record != nil ) {
// get the real name
NSArray *nsArray_values = [od_record valuesForAttribute: @kDS1AttrGeneratedUID error: nil];
if ( nsArray_values != nil ) {
NSString *nsStr_name = [nsArray_values objectAtIndex: 0];
if ( (nsStr_name != nil) && [nsStr_name length] ) {
strlcpy( out_guid, [nsStr_name UTF8String], GUID_BUFF_SIZE );
result = 1;
}
}
}
}
}
if ( result )
log_debug("GUID: else
log_debug("No GUID: for user:
[my_pool release];
return( result );
} // get_user_guid
// -----------------------------------------------------------------
// received_data_callback ()
void received_data_callback (CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *in_data, void *in_info)
{
int a_socket = -1;
msg_data_t message_data;
NSMutableDictionary *user_acct_map = nil;
NSMutableDictionary *device_map_data= nil;
char guid[GUID_BUFF_SIZE];
// read from unix socket
a_socket = CFSocketGetNative(s);
NSData *msg_data = (NSData *)in_data;
ssize_t msg_size = [msg_data length];
if ( msg_size != sizeof(msg_data_t) ) {
log_err("Error: received invalid messagege size. return;
}
APNSNotify *apn_conn = (APNSNotify *)in_info;
if ( msg_size == 0 ) {
log_err("Error: missing notification context info");
return;
}
// extract message data
[msg_data getBytes: &message_data length: sizeof(msg_data_t)];
switch ( message_data.msg[0] ) {
case '1':
// create node, user ID only
// Depricated in 10.7
log_warning("create node: depricated in OS X Server 10.7");
break;
case '2':
// register from IMAP ID command
log_debug("register device: user:
if (!strlen(message_data.d1) || !strlen(message_data.d2) || !strlen(message_data.d3)) {
log_err("Error: missing registration data");
return;
}
device_map_data = [NSMutableDictionary dictionaryWithContentsOfFile: DEVICE_MAPS_PATH];
if ( !device_map_data ) {
device_map_data = [[[NSMutableDictionary alloc] init] autorelease];
}
user_acct_map = [[NSDictionary dictionaryWithObjectsAndKeys:
[NSString stringWithUTF8String: message_data.d2], @"aps-account-id",
[NSString stringWithUTF8String: message_data.d3], @"aps-device-token",
nil]autorelease];
if ( get_user_guid(message_data.d1, guid) ) {
NSString *guid_str = [NSString stringWithUTF8String: guid];
NSMutableArray *devices = [device_map_data objectForKey: guid_str];
if ( !devices ) {
devices = [[[NSMutableArray alloc] init] autorelease];
} else if ( [devices containsObject: user_acct_map]) {
return;
} else {
NSEnumerator *nsEnum = [devices objectEnumerator];
NSMutableDictionary *acct_map = nil;
while ( (acct_map = [nsEnum nextObject]) ) {
NSString *token = [acct_map objectForKey: @"aps-device-token"];
NSString *acct_id = [acct_map objectForKey: @"aps-account-id"];
if ( token && acct_id ) {
NSString *time_stamp = [acct_map objectForKey: @"time-stamp"];
if ( time_stamp ) {
[acct_map removeObjectForKey: @"time-stamp"];
log_debug("device: [device_map_data writeToFile: DEVICE_MAPS_PATH atomically: YES];
return;
}
}
}
}
// add device
[devices addObject: user_acct_map];
[device_map_data setObject: devices forKey: [NSString stringWithUTF8String: guid]];
[device_map_data writeToFile: DEVICE_MAPS_PATH atomically: YES];
}
break;
case '3':
// publish/notify to node
log_debug("send notification: user:
if ( get_user_guid(message_data.d1, guid) ) {
device_map_data = [NSMutableDictionary dictionaryWithContentsOfFile: DEVICE_MAPS_PATH];
if ( device_map_data ) {
NSArray *devices = [device_map_data objectForKey: [NSString stringWithUTF8String: guid]];
if ( devices ) {
NSEnumerator *nsEnum = [devices objectEnumerator];
while ( (user_acct_map = [nsEnum nextObject]) ) {
NSString *token = [user_acct_map objectForKey: @"aps-device-token"];
NSString *acct_id = [user_acct_map objectForKey: @"aps-account-id"];
if ( token && acct_id ) {
NSString *time_stamp = [user_acct_map objectForKey: @"time-stamp"];
if ( !time_stamp ) {
log_debug("sending notification to: message_data.d1, [acct_id UTF8String], [token UTF8String]);
NSString *payload = [NSString stringWithFormat: @"{ \"aps\" : { \"account-id\" : \" log_debug("notification payload: [apn_conn send_notification: payload to_device: token];
} else
log_debug("Warning: device not registered: } else
log_debug("Warning: missing device and/or account id for: }
} else
log_debug("Warning: no device map found for: } else
log_debug("Warning: no device maps file: } else
log_debug("Warning: no GUID found for:
break;
default:
log_err("Error: unknown message type: break;
}
} // received_data_callback
// -----------------------------------------------------------------
// main ()
int main ( int argc, char **argv )
{
int ch;
int buff_size = BUFF_SIZE;
char *value = NULL;
while ( (ch = getopt(argc, argv, "b:")) != EOF ) {
switch( ch ) {
case 'b':
// buffer size
value = optarg;
if ( value ) {
NSString *value_str = [NSString stringWithCString: value encoding: NSUTF8StringEncoding];
buff_size = [value_str intValue];
if ( buff_size < BUFF_SIZE )
buff_size = BUFF_SIZE;
else if ( buff_size > MAX_BUF_SIZE )
buff_size = MAX_BUF_SIZE;
}
break;
default:
break;
}
}
open_logs();
// setup the pid file
char *pidfile = "/var/run/push_notify.pid";
int pid_fd = open(pidfile, O_CREAT | O_RDWR | O_EXLOCK | O_NONBLOCK, 0644);
if (pid_fd == -1) {
fprintf(stderr, "ERROR: can't open pidfile: exit(EXIT_FAILURE);
} else {
char buf[100];
snprintf(buf, sizeof(buf), " if (lseek(pid_fd, 0, SEEK_SET) == -1 ||
ftruncate(pid_fd, 0) == -1 ||
write(pid_fd, buf, strlen(buf)) == -1) {
fprintf(stderr, "ERROR: unable to write to pid file\n");
exit(EXIT_FAILURE);
}
fsync(pid_fd);
}
gPool = [[NSAutoreleasePool alloc] init];
// begin setup
log_info("initializing mail notification services");
// setrlimit
set_rlimits();
// set signal handlers
sighandler_setup();
// verify maps file
verify_map_file();
// create socket we are going to use for listening
int a_socket = -1;
while ( (a_socket = get_socket(buff_size)) < 0 ) {
sleep(60);
if (got_sigterm)
exit(0);
}
// initialize open directory
// - no need to contiure if OD is failing
while ( od_init() == 1 ) {
sleep(60);
if (got_sigterm)
exit(0);
}
// setup notification & feedback connections
APNSNotify *apn_conn = [[[APNSNotify alloc] init_with_cert_pref: @"apns:com.apple.mail"] retain];
APNSFeedback *apn_feedback = [[[APNSFeedback alloc] init_with_connection: apn_conn] retain];
NSString *topic = [apn_conn topic_name];
log_info("setting server topic:
// set topic in IMAP server to return to client
XSRunTask(SERVER_ADMIN, [NSArray arrayWithObjects: @"settings", @"mail:imap:aps_topic", @"=", topic, nil], nil, nil, nil, nil);
// create socket run loop
CFSocketContext socket_ctx = { 0, apn_conn, NULL, NULL, nil };
CFSocketRef listenSocket = CFSocketCreateWithNative(NULL, a_socket, kCFSocketDataCallBack, received_data_callback, &socket_ctx);
CFRunLoopSourceRef rl_source = CFSocketCreateRunLoopSource(NULL, listenSocket, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rl_source, kCFRunLoopDefaultMode);
CFRelease(rl_source);
// begin heavy lifting
log_info("starting mail notification services");
// main loop
for (;;) {
if ( got_sigterm != 0 ) {
// Say goodnight, Gracie
log_info("terminating mail notification services (SIGTERM)");
close(a_socket);
exit(EXIT_SUCCESS);
}
if ( got_sighup ) {
// right now this is a do-nothing check
log_info("SIGHUP received");
got_sighup = 0;
g_check_feedback_time = 0;
}
if ( time(NULL) > g_check_feedback_time ) {
[apn_feedback connect];
g_check_feedback_time = time(NULL) + (3600 * 12);
}
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, 0);
}
return( EXIT_SUCCESS );
}