/*
 * Copyright (c) 2012 Apple Computer, Inc. All Rights Reserved.
 * 
 * @APPLE_LICENSE_HEADER_START@
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 */

//
//  main.m
//  ckd-xpc
//
//  Created by John Hurley on 7/19/12.
//  Copyright (c) 2012 John Hurley. All rights reserved.
//

/*
    This XPC service is essentially just a proxy to iCloud KVS, which exists since
    the main security code cannot link against Foundation.
    
    See sendTSARequestWithXPC in tsaSupport.c for how to call the service
    
    send message to app with xpc_connection_send_message
    
    For now, build this with:
    
        ~rc/bin/buildit .  --rootsDirectory=/var/tmp -noverify -offline -target CloudKeychainProxy

    and install or upgrade with:
        
        darwinup install /var/tmp/sec.roots/sec~dst
        darwinup upgrade /var/tmp/sec.roots/sec~dst
 
    You must use darwinup during development to update system caches
*/

//------------------------------------------------------------------------------------------------

#include <AssertMacros.h>

#import <Foundation/Foundation.h>
#import <Security/Security.h>
#import <utilities/SecCFRelease.h>
#import <xpc/xpc.h>
#import <xpc/private.h>
#import <CoreFoundation/CFXPCBridge.h>
#import <sysexits.h>
#import <syslog.h>
#import <CommonCrypto/CommonDigest.h>
#include <utilities/SecXPCError.h>

#import "SOSCloudKeychainConstants.h"
#import "CKDKVSProxy.h"

void finalize_connection(void *not_used);
void handle_connection_event(const xpc_connection_t peer);
static void cloudkeychainproxy_peer_dictionary_handler(const xpc_connection_t peer, xpc_object_t event);

static bool operation_put_dictionary(xpc_object_t event);
static bool operation_get_v2(xpc_object_t event);

int ckdproxymain(int argc, const char *argv[]);

static void describeXPCObject(char *prefix, xpc_object_t object)
{
//#ifndef NDEBUG
    // This is useful for debugging.
    if (object)
    {
        char *desc = xpc_copy_description(object);
        secdebug(XPROXYSCOPE, "%s%s\n", prefix, desc);
        free(desc);
    }
    else
        secdebug(XPROXYSCOPE, "%s<NULL>\n", prefix);

//#endif
}

static void cloudkeychainproxy_peer_dictionary_handler(const xpc_connection_t peer, xpc_object_t event)
{
    bool result = false;
    int err = 0;
    
    require_action_string(xpc_get_type(event) == XPC_TYPE_DICTIONARY, xit, err = -51, "expected XPC_TYPE_DICTIONARY");

    const char *operation = xpc_dictionary_get_string(event, kMessageKeyOperation);
    require_action(operation, xit, result = false);
    
    // Check protocol version
    uint64_t version = xpc_dictionary_get_uint64(event, kMessageKeyVersion);
    secdebug(XPROXYSCOPE, "Reply version: %lld\n", version);
    require_action(version == kCKDXPCVersion, xit, result = false);

    // Operations
    secdebug(XPROXYSCOPE, "Handling %s operation", operation);

    if (operation && !strcmp(operation, kOperationPUTDictionary))
         operation_put_dictionary(event);
    else
    if (operation && !strcmp(operation, kOperationGETv2))
         operation_get_v2(event);
    else
    if (operation && !strcmp(operation, kOperationClearStore))
    {
        [[UbiqitousKVSProxy sharedKVSProxy] clearStore];
        xpc_object_t replyMessage = xpc_dictionary_create_reply(event);
        if (replyMessage)   // Caller wanted an ACK, so give one
        {
            xpc_dictionary_set_string(replyMessage, kMessageKeyValue, "ACK");
            xpc_connection_send_message(peer, replyMessage);
        }
    }
    else if (operation && !strcmp(operation, kOperationRemoveObjectForKey))
    {
        const char *keyToRemove = xpc_dictionary_get_string(event, kMessageKeyKey);
        [[UbiqitousKVSProxy sharedKVSProxy] removeObjectForKey:[NSString stringWithCString:keyToRemove encoding:NSUTF8StringEncoding]];
        xpc_object_t replyMessage = xpc_dictionary_create_reply(event);
        if (replyMessage)   // Caller wanted an ACK, so give one
        {
            xpc_dictionary_set_string(replyMessage, kMessageKeyValue, "ACK");
            xpc_connection_send_message(peer, replyMessage);
        }
    }
    else if (operation && !strcmp(operation, kOperationSynchronize))
    {
        [[UbiqitousKVSProxy sharedKVSProxy] requestSynchronization:YES];
        xpc_object_t replyMessage = xpc_dictionary_create_reply(event);
        if (replyMessage)   // Caller wanted an ACK, so give one
        {
            xpc_dictionary_set_string(replyMessage, kMessageKeyValue, "ACK");
            xpc_connection_send_message(peer, replyMessage);
        }
    }
    else if (operation && !strcmp(operation, kOperationSynchronizeAndWait))
    {
        xpc_object_t xkeysToGetDict = xpc_dictionary_get_value(event, kMessageKeyValue);
        xpc_object_t replyMessage = xpc_dictionary_create_reply(event);
        xpc_object_t xkeys = xpc_dictionary_get_value(xkeysToGetDict, kMessageKeyKeysToGet);
        NSArray *keysToGet = (__bridge NSArray *)(_CFXPCCreateCFObjectFromXPCObject(xkeys));
        [[UbiqitousKVSProxy sharedKVSProxy] waitForSynchronization:keysToGet
            handler:^(NSDictionary *values, NSError *err) {
                if (replyMessage)   // Caller wanted an ACK, so give one
                {
                    secdebug("keytrace", "Result from [[UbiqitousKVSProxy sharedKVSProxy] waitForSynchronization:]: %@", err);
                    xpc_object_t xobject = values ? _CFXPCCreateXPCObjectFromCFObject((__bridge CFTypeRef)(values)) : xpc_null_create();
                    xpc_dictionary_set_value(replyMessage, kMessageKeyValue, xobject);
                    if (err)
                    {
                        xpc_object_t xerrobj = SecCreateXPCObjectWithCFError((CFErrorRef)CFBridgingRetain(err));
                        xpc_dictionary_set_value(replyMessage, kMessageKeyError, xerrobj);
                        CFReleaseSafe((__bridge CFTypeRef)(err));
                    }
                    xpc_connection_send_message(peer, replyMessage);
                }
        }];
        CFReleaseSafe((__bridge CFTypeRef)(keysToGet));
    }
    else if (operation && !strcmp(operation, kOperationRegisterKeysAndGet))
    {
        xpc_object_t xkeysToRegisterDict = xpc_dictionary_get_value(event, kMessageKeyValue);
//        describeXPCObject("xkeysToRegister: ", xkeysToRegisterDict);
        // KTR = keysToRegister
        bool getNewKeysOnly = xpc_dictionary_get_bool(xkeysToRegisterDict, kMessageKeyGetNewKeysOnly);
        xpc_object_t xKTRallkeys = xpc_dictionary_get_value(xkeysToRegisterDict, kMessageKeyKeysToGet);
        xpc_object_t xKTRrequiresFirstUnlock = xpc_dictionary_get_value(xkeysToRegisterDict, kMessageKeyKeysRequireFirstUnlock);
        xpc_object_t xKTRrequiresUnlock = xpc_dictionary_get_value(xkeysToRegisterDict, kMessageKeyKeysRequiresUnlocked);

        NSArray *KTRallkeys = (__bridge NSArray *)(_CFXPCCreateCFObjectFromXPCObject(xKTRallkeys));
        NSArray *KTRrequiresFirstUnlock = (__bridge NSArray *)(_CFXPCCreateCFObjectFromXPCObject(xKTRrequiresFirstUnlock));
        NSArray *KTRrequiresUnlock = (__bridge NSArray *)(_CFXPCCreateCFObjectFromXPCObject(xKTRrequiresUnlock));

        id object = [[UbiqitousKVSProxy sharedKVSProxy] registerKeysAndGet:getNewKeysOnly always:KTRallkeys reqFirstUnlock:KTRrequiresFirstUnlock reqUnlocked:KTRrequiresUnlock];
        
        CFReleaseSafe((__bridge CFTypeRef)(KTRallkeys));
        CFReleaseSafe((__bridge CFTypeRef)(KTRrequiresFirstUnlock));
        CFReleaseSafe((__bridge CFTypeRef)(KTRrequiresUnlock));

        secdebug("keytrace", "Result from [[UbiqitousKVSProxy sharedKVSProxy] registerKeysAndGet:]: %@", object);
        
        xpc_object_t xobject = object ? _CFXPCCreateXPCObjectFromCFObject((__bridge CFTypeRef)(object)) : xpc_null_create();
               
        xpc_object_t replyMessage = xpc_dictionary_create_reply(event);
        xpc_dictionary_set_value(replyMessage, kMessageKeyValue, xobject);
        xpc_connection_send_message(peer, replyMessage);
        secdebug(XPROXYSCOPE, "RegisterKeysAndGet message sent");
    }
    else if (operation && !strcmp(operation, kOperationUILocalNotification))
    {
        xpc_object_t xLocalNotificationDict = xpc_dictionary_get_value(event, kMessageKeyValue);
//        describeXPCObject("xLocalNotificationDict: ", xLocalNotificationDict);
        NSDictionary *localNotificationDict = (__bridge NSDictionary *)(_CFXPCCreateCFObjectFromXPCObject(xLocalNotificationDict));
        int64_t outFlags = 0;
        id object = [[UbiqitousKVSProxy sharedKVSProxy] localNotification:localNotificationDict outFlags:&outFlags];
        secdebug(XPROXYSCOPE, "Result from [[UbiqitousKVSProxy sharedKVSProxy] localNotification:]: %@", object);
        xpc_object_t xobject = object ? _CFXPCCreateXPCObjectFromCFObject((__bridge CFTypeRef)(object)) : xpc_null_create();
        xpc_object_t replyMessage = xpc_dictionary_create_reply(event);
        xpc_dictionary_set_int64(xobject, kMessageKeyNotificationFlags, outFlags);
        xpc_dictionary_set_value(replyMessage, kMessageKeyValue, xobject);
        xpc_connection_send_message(peer, replyMessage);
        secdebug(XPROXYSCOPE, "localNotification reply sent");
        CFReleaseSafe((__bridge CFTypeRef)(localNotificationDict));
    }
    else if (operation && !strcmp(operation, kOperationSetParams))
    {
        xpc_object_t xParamsDict = xpc_dictionary_get_value(event, kMessageKeyValue);
        NSDictionary *paramsDict = (__bridge NSDictionary *)(_CFXPCCreateCFObjectFromXPCObject(xParamsDict));
        [[UbiqitousKVSProxy sharedKVSProxy] setParams:paramsDict];
        xpc_object_t replyMessage = xpc_dictionary_create_reply(event);
        if (replyMessage)   // Caller wanted an ACK, so give one
        {
            xpc_dictionary_set_string(replyMessage, kMessageKeyValue, "ACK");
            xpc_connection_send_message(peer, replyMessage);
        }
        secdebug(XPROXYSCOPE, "setParams reply sent");
        CFReleaseSafe((__bridge CFTypeRef)(paramsDict));
    }
    else if (operation && !strcmp(operation, kOperationRequestSyncWithAllPeers))
    {
        [[UbiqitousKVSProxy sharedKVSProxy] requestSyncWithAllPeers];
        xpc_object_t replyMessage = xpc_dictionary_create_reply(event);
        if (replyMessage)   // Caller wanted an ACK, so give one
        {
            xpc_dictionary_set_string(replyMessage, kMessageKeyValue, "ACK");
            xpc_connection_send_message(peer, replyMessage);
        }
        secdebug(XPROXYSCOPE, "RequestSyncWithAllPeers reply sent");
    }
    else
    {
        char *description = xpc_copy_description(event);
        secdebug(XPROXYSCOPE, "Unknown op=%s request from pid %d: %s", operation, xpc_connection_get_pid(peer), description);
        free(description);
    }
    result = true;
xit:
    if (!result)
        describeXPCObject("handle_operation fail: ", event);
}

void finalize_connection(void *not_used)
{
    secdebug(XPROXYSCOPE, "finalize_connection");
    [[UbiqitousKVSProxy sharedKVSProxy] requestSynchronization:YES];
	xpc_transaction_end();
}

static bool operation_put_dictionary(xpc_object_t event)
{
    // PUT a set of objects into the KVS store. Return false if error

    describeXPCObject("operation_put_dictionary event: ", event);
    xpc_object_t xvalue = xpc_dictionary_get_value(event, kMessageKeyValue);
    if (!xvalue)
        return false;

    CFTypeRef cfvalue = _CFXPCCreateCFObjectFromXPCObject(xvalue);
    if (cfvalue && (CFGetTypeID(cfvalue)==CFDictionaryGetTypeID()))
    {
        [[UbiqitousKVSProxy sharedKVSProxy] setObjectsFromDictionary:(__bridge NSDictionary *)cfvalue];
        CFReleaseSafe(cfvalue);
        return true;
    }
    else{
        describeXPCObject("operation_put_dictionary unable to convert to CF: ", xvalue);
        CFReleaseSafe(cfvalue);
    }
    return false;
}

static bool operation_get_v2(xpc_object_t event)
{
    // GET a set of objects from the KVS store. Return false if error    
    describeXPCObject("operation_get_v2 event: ", event);
    xpc_connection_t peer = xpc_dictionary_get_remote_connection(event);
    describeXPCObject("operation_get_v2: peer: ", peer);

    xpc_object_t replyMessage = xpc_dictionary_create_reply(event);
    if (!replyMessage)
    {
        secdebug(XPROXYSCOPE, "can't create replyMessage");
        assert(true);   //must have a reply handler
        return false;
    }
    xpc_object_t returnedValues = xpc_dictionary_create(NULL, NULL, 0);
    if (!returnedValues)
    {
        secdebug(XPROXYSCOPE, "can't create returnedValues");
        assert(true);   // must have a spot for the returned values
        return false;
    }

    xpc_object_t xvalue = xpc_dictionary_get_value(event, kMessageKeyValue);
    if (!xvalue)
    {
        secdebug(XPROXYSCOPE, "missing \"value\" key");
        return false;
    }
    
    xpc_object_t xkeystoget = xpc_dictionary_get_value(xvalue, kMessageKeyKeysToGet);
    if (xkeystoget)
    {
        secdebug(XPROXYSCOPE, "got xkeystoget");
        CFTypeRef keystoget = _CFXPCCreateCFObjectFromXPCObject(xkeystoget);
        if (!keystoget || (CFGetTypeID(keystoget)!=CFArrayGetTypeID()))     // not "getAll", this is an error of some kind
        {
            secdebug(XPROXYSCOPE, "can't convert keystoget or is not an array");
            CFReleaseSafe(keystoget);
            return false;
        }
        
        [(__bridge NSArray *)keystoget enumerateObjectsUsingBlock: ^ (id obj, NSUInteger idx, BOOL *stop)
        {
            NSString *key = (NSString *)obj;
            id object = [[UbiqitousKVSProxy sharedKVSProxy] get:key];
            secdebug(XPROXYSCOPE, "[UbiqitousKVSProxy sharedKVSProxy] get: key: %@, object: %@", key, object);
            xpc_object_t xobject = object ? _CFXPCCreateXPCObjectFromCFObject((__bridge CFTypeRef)object) : xpc_null_create();
            xpc_dictionary_set_value(returnedValues, [key UTF8String], xobject);
            describeXPCObject("operation_get_v2: value from kvs: ", xobject);
        }];
    }
    else    // get all values from kvs
    {
        secdebug(XPROXYSCOPE, "get all values from kvs");
        NSDictionary *all = [[UbiqitousKVSProxy sharedKVSProxy] getAll];
        [all enumerateKeysAndObjectsUsingBlock: ^ (id key, id obj, BOOL *stop)
        {
            xpc_object_t xobject = obj ? _CFXPCCreateXPCObjectFromCFObject((__bridge CFTypeRef)obj) : xpc_null_create();
            xpc_dictionary_set_value(returnedValues, [(NSString *)key UTF8String], xobject);
        }];
    }

    xpc_dictionary_set_uint64(replyMessage, kMessageKeyVersion, kCKDXPCVersion);
    xpc_dictionary_set_value(replyMessage, kMessageKeyValue, returnedValues);
    xpc_connection_send_message(peer, replyMessage);

    return true;
}

static void initializeProxyObjectWithConnection(const xpc_connection_t connection)
{
    [[UbiqitousKVSProxy sharedKVSProxy] setItemsChangedBlock:^(CFDictionaryRef values)
        {
            secdebug(XPROXYSCOPE, "UbiqitousKVSProxy called back");
            xpc_object_t xobj = _CFXPCCreateXPCObjectFromCFObject(values);
            xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
            xpc_dictionary_set_uint64(message, kMessageKeyVersion, kCKDXPCVersion);
            xpc_dictionary_set_string(message, kMessageKeyOperation, kMessageOperationItemChanged);
            xpc_dictionary_set_value(message, kMessageKeyValue, xobj?xobj:xpc_null_create());
            xpc_connection_send_message(connection, message);  // Send message; don't wait for a reply
        }];
}

static void cloudkeychainproxy_peer_event_handler(xpc_connection_t peer, xpc_object_t event) 
{
    describeXPCObject("peer: ", peer);
	xpc_type_t type = xpc_get_type(event);
	if (type == XPC_TYPE_ERROR) {
		if (event == XPC_ERROR_CONNECTION_INVALID) {
			// The client process on the other end of the connection has either
			// crashed or cancelled the connection. After receiving this error,
			// the connection is in an invalid state, and you do not need to
			// call xpc_connection_cancel(). Just tear down any associated state
			// here.
		} else if (event == XPC_ERROR_TERMINATION_IMMINENT) {
			// Handle per-connection termination cleanup.
		}
	} else {
		assert(type == XPC_TYPE_DICTIONARY);
		// Handle the message.
    //    describeXPCObject("dictionary:", event);
        cloudkeychainproxy_peer_dictionary_handler(peer, event);
	}
}

static void cloudkeychainproxy_event_handler(xpc_connection_t peer)
{
	// By defaults, new connections will target the default dispatch
	// concurrent queue.

    if (xpc_get_type(peer) != XPC_TYPE_CONNECTION)
    {
        secdebug(XPROXYSCOPE, "expected XPC_TYPE_CONNECTION");
        return;
    }
    initializeProxyObjectWithConnection(peer);
	xpc_connection_set_event_handler(peer, ^(xpc_object_t event)
    {
        cloudkeychainproxy_peer_event_handler(peer, event);
	});
	
	// This will tell the connection to begin listening for events. If you
	// have some other initialization that must be done asynchronously, then
	// you can defer this call until after that initialization is done.
	xpc_connection_resume(peer);
}

static void diagnostics(int argc, const char *argv[])
{
    @autoreleasepool
    {
        NSDictionary *all = [[UbiqitousKVSProxy sharedKVSProxy] getAll];
        NSLog(@"All: %@",all);
    }
}

int ckdproxymain(int argc, const char *argv[])
{
    secdebug(XPROXYSCOPE, "Starting CloudKeychainProxy");
    char *wait4debugger = getenv("WAIT4DEBUGGER");
    if (wait4debugger && !strcasecmp("YES", wait4debugger))
    {
        syslog(LOG_ERR, "Waiting for debugger");
        kill(getpid(), SIGSTOP);
    }

    if (argc > 1) {
        diagnostics(argc, argv);
        return 0;
    }
    
#if !TARGET_IPHONE_SIMULATOR    
    xpc_track_activity();   // sets us up for idle timeout handling
#endif

    // DISPATCH_TARGET_QUEUE_DEFAULT
	xpc_connection_t listener = xpc_connection_create_mach_service(xpcServiceName, NULL, XPC_CONNECTION_MACH_SERVICE_LISTENER);
	xpc_connection_set_event_handler(listener, ^(xpc_object_t object){ cloudkeychainproxy_event_handler(object); });

    [UbiqitousKVSProxy sharedKVSProxy];

    // It looks to me like there is insufficient locking to allow a request to come in on the XPC connection while doing the initial all items.
    // Therefore I'm leaving the XPC connection suspended until that has time to process.
	xpc_connection_resume(listener);

    @autoreleasepool
    {
        secdebug(XPROXYSCOPE, "Starting mainRunLoop");
        NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
        [runLoop run];
    }

    secdebug(XPROXYSCOPE, "Exiting CloudKeychainProxy");

    return EXIT_FAILURE;
}
