Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c91a8e08c4 | |||
| 6a8a48dfd9 | |||
| 5b46a5c6de | |||
| a854649e09 | |||
| 36de866909 | |||
| 9371147fd1 | |||
| 60288f8454 | |||
| 5b4a2e5e94 | |||
| 2d1b534f4d | |||
| 26d42ca585 | |||
| 900b107dc0 | |||
| 65738cad39 | |||
| ff5768048c | |||
| 9b6770afe1 | |||
| b4c0e1e72b | |||
| 5bab81d099 | |||
| 3622125a62 | |||
| 4850bfbb38 | |||
| 9dd461a972 | |||
| 0469f7ae05 | |||
| fc18d96ff4 | |||
| bd5d2e80f5 | |||
| bdbeb1be5f | |||
| d0868e486c | |||
| 766aee9700 | |||
| 8c4e208eda | |||
| 9325a4ac64 | |||
| bee2611e60 | |||
| e638a42547 | |||
| a8c6176b04 | |||
| b5d1acba22 | |||
| 7474d4fba4 | |||
| 9570efd717 | |||
| 671c168e56 | |||
| dd354b9629 | |||
| a8cd4e598d | |||
| 2458c41eb6 | |||
| a4215b3f06 | |||
| 0078f3d9fe | |||
| 57be4eecd9 | |||
| 504ab4d111 | |||
| 4f83e4ae03 | |||
| fee9e1ba80 | |||
| d4af68951f | |||
| 49ca3dd04a | |||
| 8ce6167bf2 | |||
| 97b9f54d60 | |||
| 4bf0730f90 | |||
| 1d88d5284d | |||
| f1fc60312a | |||
| 911d456b2d | |||
| 6bf99f4906 | |||
| 95e1e2c90d | |||
| 7e623e8ece | |||
| d92a31d302 | |||
| f6b9ff9905 | |||
| db1795aa38 | |||
| 4671c607ff | |||
| db26eef28a | |||
| 03ed94c746 | |||
| a47eef9bed | |||
| 60fc2d62b9 | |||
| bb1e33479e | |||
| 11beb0ad7f | |||
| 3d464c937a | |||
| 7151d861c9 | |||
| a1c4d59993 | |||
| 9740ddabde | |||
| f8878beb51 | |||
| 228a115d8e | |||
| 5c05f0e87d | |||
| c16532a797 | |||
| d6bcfbce6f | |||
| 721db6ded0 | |||
| 76704ba1a7 | |||
| dc6c225b96 | |||
| db675a8d47 | |||
| 924d76b72c | |||
| b07f7ed111 | |||
| bae2002df4 | |||
| f2c61faff2 | |||
| ae428376bf | |||
| d8a2c0b1e3 | |||
| d1cdf93a78 | |||
| 1cdc1d9470 | |||
| 5a3b3356bf | |||
| 990fd20a2c | |||
| 92cf70b7c3 | |||
| 4054812b59 | |||
| f06194265c | |||
| ef4e5fac48 | |||
| 6e2f395387 | |||
| 7a92a39318 | |||
| d282c64eb3 | |||
| 1f08b8994c | |||
| 0ee898f1eb | |||
| ce4bef70f8 | |||
| a4181c56b1 | |||
| 342e1ea5fa | |||
| 61a998b9f6 | |||
| 904c8029b0 | |||
| b4851e734b | |||
| fa3fa3861a | |||
| 5a8d199280 | |||
| 6d0e1245a3 | |||
| 887285950e | |||
| 16c58f2ddc | |||
| 81230706f6 | |||
| db49da73f6 | |||
| 3e17fa0ac0 | |||
| c3100d3506 | |||
| 26b9cf5951 | |||
| 7dcd81db61 | |||
| d9fa47d450 | |||
| 31edda6fef | |||
| 02902e8d71 | |||
| a447696c7e | |||
| e6e2d7479b | |||
| bd155e619a | |||
| 655a4ec5f5 | |||
| 3bc66ec840 | |||
| 285e0bdc27 | |||
| 42bfca18d7 | |||
| b46b72217b | |||
| 2ec71d4b35 | |||
| 7bd150767a | |||
| 8e0fe92f07 | |||
| 63aefb48e7 | |||
| 2955df114d | |||
| 74794b38db | |||
| 3774e64a66 | |||
| beabeacd6c | |||
| 58cfc7f7ad | |||
| 33c9ece5f2 | |||
| 2dfbf18892 | |||
| 15bf499f43 | |||
| ea7b72505f | |||
| d17fc23a6e | |||
| 4baeb1b201 | |||
| 1a35120bd2 | |||
| 6c942d4f61 | |||
| a150193f46 | |||
| 09f569dc35 | |||
| dab8c2d943 | |||
| a6a68043e3 | |||
| cd5fc43244 | |||
| 07169cde8f | |||
| 72ce3d52e5 | |||
| ad0c4c002b | |||
| c6a3cc3e55 | |||
| d753d500d6 | |||
| bd4a19a650 | |||
| 691a5618d3 | |||
| 6f5c09a317 | |||
| 113d2d1cf7 | |||
| c2dae37645 | |||
| afad1e33d2 | |||
| 62948334a6 | |||
| 276475f8bd | |||
| d373d12ac6 | |||
| 59265a0211 | |||
| 0df5d10ed2 | |||
| dc0540d770 | |||
| 887ca43b80 | |||
| e5116e9af9 | |||
| 2a06708105 | |||
| f413e5756d | |||
| a64cb34049 | |||
| 4d6e31bdc9 | |||
| 77bb7a7201 | |||
| 420950b679 | |||
| 2e3b284158 | |||
| 7f6c6aeb19 | |||
| 3ad0bc7f20 | |||
| 1a28d441c7 | |||
| 4bb3fa6ba9 | |||
| 4101abccd1 | |||
| af41057118 | |||
| 39133a521d | |||
| 6d63a3868b | |||
| af7bdade34 | |||
| e86adc4cbf | |||
| 25a8f8e9b3 | |||
| 9dd9615048 | |||
| 6812636871 | |||
| 22cd4d6bc2 | |||
| eddfa2061c | |||
| 7ba8670fbf | |||
| b4784659dd | |||
| eb0068f9ef |
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>--ignore</key>
|
||||
<array>
|
||||
<string>Mac</string>
|
||||
<string>Touch</string>
|
||||
</array>
|
||||
<key>--project-company</key>
|
||||
<string>Noodlewerk</string>
|
||||
<key>--project-name</key>
|
||||
<string>NWPusher</string>
|
||||
<key>--company-id</key>
|
||||
<string>com.noodlewerk</string>
|
||||
<key>--warn-missing-arg</key>
|
||||
<false/>
|
||||
<key>--repeat-first-par</key>
|
||||
<false/>
|
||||
<key>--output</key>
|
||||
<string>.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,155 @@
|
||||
Change Log
|
||||
==========
|
||||
|
||||
### master (unreleased)
|
||||
|
||||
* add certificate type to description
|
||||
|
||||
### 0.7.5 (2017-04-25)
|
||||
|
||||
* Add support for new certificate type (passes)
|
||||
* Fix rich text (#49)
|
||||
* Fix app name (#47)
|
||||
* Update docs pushing to macOS
|
||||
|
||||
### 0.7.4 (2017-01-16)
|
||||
|
||||
* Add support for handshake error (internal error)
|
||||
* Add detection of p12 without password
|
||||
|
||||
### 0.7.3 (2016-09-19)
|
||||
|
||||
* Add support for Watch Kit certificates (DanielFontes)
|
||||
* Add support for handshake error (certificate unknown)
|
||||
|
||||
### 0.7.2 (2016-07-19)
|
||||
|
||||
* Added support for Carthage, thanks to @zats
|
||||
|
||||
### 0.7.1 (2016-04-27)
|
||||
|
||||
* Remove Mac enum from Touch target
|
||||
|
||||
### 0.7.0 (2016-01-07)
|
||||
|
||||
* Add support for simplified certificates (pull request by 666tos)
|
||||
|
||||
### 0.6.4 (2015-12-20)
|
||||
|
||||
* Add support for new certificate types (web, simplified, voip)
|
||||
* Add support for handshake errors (dark wake and closed abort)
|
||||
|
||||
### 0.6.3 (2015-01-22)
|
||||
|
||||
* Add certificate expiration date to listing
|
||||
* Add expiration and revocation error message
|
||||
|
||||
### 0.6.2 (2015-01-15)
|
||||
|
||||
* Add underlying error reason code
|
||||
|
||||
### 0.6.1 (2015-01-14)
|
||||
|
||||
* Add SSL handshake error codes
|
||||
|
||||
### 0.6.0 (2014-10-30)
|
||||
|
||||
* Remove kNWSuccess
|
||||
* Remove deprecated
|
||||
|
||||
### 0.5.4 (2014-10-28)
|
||||
|
||||
* Deprecate fetching
|
||||
* Document all framework stuff
|
||||
|
||||
### 0.5.3 (2014-10-22)
|
||||
|
||||
* Set NWHub defaults to autoConnect:YES
|
||||
* Add a bunch of helpers to NWHub and NWPusher
|
||||
|
||||
### 0.5.2 (2014-10-19)
|
||||
|
||||
* Add convenient connect methods
|
||||
* Introduce Cocoa-style error handling (NSError)
|
||||
* Deprecate C-style error handling (NWError)
|
||||
* Cleanup readme examples
|
||||
* Add troubleshooting in readme
|
||||
|
||||
### 0.5.1 (2014-07-26)
|
||||
|
||||
* Add OpenSSL readme
|
||||
* Fix syntax
|
||||
|
||||
### 0.4.3 (2014-04-09)
|
||||
|
||||
* Add read feedback demo app
|
||||
* Add log view demo app
|
||||
|
||||
### 0.4.2 (2014-03-31)
|
||||
|
||||
* Intro new config format Mac app
|
||||
* Cleanup Mac demo
|
||||
* Add demo device token history
|
||||
|
||||
### 0.4.1 (2014-03-24)
|
||||
|
||||
* Fix leaks
|
||||
* Fix APN error reporting
|
||||
* Add p12 import to demo
|
||||
|
||||
### 0.4.0 (2014-03-23)
|
||||
|
||||
* Add detailed error reporting
|
||||
* Redo readme
|
||||
* Fix demo
|
||||
* Redesign API
|
||||
|
||||
### 0.3.5 (2014-03-21)
|
||||
|
||||
* Support multiple identities
|
||||
* Add inspect tool
|
||||
* Fix mem leak
|
||||
* Fix failed fetch
|
||||
|
||||
### 0.3.4 (2014-03-17)
|
||||
|
||||
* Tweak demo menu
|
||||
* Remove iosock, fix ssl connection
|
||||
* Add support Mac push certs
|
||||
* Merge pull request #7 from AriX/master
|
||||
|
||||
### 0.3.3 (2014-03-03)
|
||||
|
||||
* Add support of expiry and priority
|
||||
* Add support for Apple's third binary format
|
||||
* Fix ssl connect retry
|
||||
* Tweak readme, add links
|
||||
* Merge pull request #5 from ChristianKienle/master
|
||||
* Fix feedback fetching
|
||||
* Add helpers to hub and feedback
|
||||
* Add demo auto-increment payload counter
|
||||
|
||||
### 0.3.2 (2014-01-27)
|
||||
|
||||
* Cleanup project
|
||||
|
||||
### 0.3.1 (2014-01-27)
|
||||
|
||||
* Add Podspec
|
||||
* Auto sandbox detection
|
||||
* Add NWHub on top of NWPusher
|
||||
* Lots of demo fixes
|
||||
|
||||
### 0.3.0 (2014-01-15)
|
||||
|
||||
* Add support for new push format
|
||||
* Add troubleshooting in readme
|
||||
* Add reconnect to demo app
|
||||
|
||||
### 0.2.0 (2013-04-27)
|
||||
|
||||
* Add demo application
|
||||
|
||||
### 0.1.0 (2012-09-10)
|
||||
|
||||
* First release of the framework
|
||||
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// NWHub.h
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2014 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWType.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class NWNotification, NWPusher;
|
||||
|
||||
/** Allows callback on errors while pushing to and reading from server.
|
||||
|
||||
Check out `NWHub` for more details.
|
||||
*/
|
||||
@protocol NWHubDelegate <NSObject>
|
||||
/** The notification failed during or after pushing. */
|
||||
- (void)notification:(NWNotification *)notification didFailWithError:(NSError *)error;
|
||||
@end
|
||||
|
||||
/** Helper on top of `NWPusher` that hides the details of pushing and reading.
|
||||
|
||||
This class provides a more convenient way of pushing notifications to the APNs. It deals with the trouble of assigning a unique identifier to every notification and the handling of error responses from the server. It hides the latency that comes with transmitting the pushes, allowing you to simply push your notifications and getting notified of errors through the delegate. If this feels over-abstracted, then definitely check out the `NWPusher` class, which will give you full control.
|
||||
|
||||
There are two set of methods for pushing notifications: the easy and the pros. The former will just do the pushing and reconnect if the connection breaks. This is your low-worry solution, provided that you call `readFailed` every so often (seconds) to handle error data from the server. The latter will give you a little more control and a little more responsibility.
|
||||
*/
|
||||
@interface NWHub : NSObject
|
||||
|
||||
/** @name Properties */
|
||||
|
||||
/** The pusher instance that does the actual work. */
|
||||
@property (nonatomic, strong) NWPusher *pusher;
|
||||
|
||||
/** Assign a delegate to get notified when something fails during or after pushing. */
|
||||
@property (nonatomic, weak) id<NWHubDelegate> delegate;
|
||||
|
||||
/** The type of notification serialization we'll be using. */
|
||||
@property (nonatomic, assign) NWNotificationType type;
|
||||
|
||||
/** The timespan we'll hold on to a notification after pushing, allowing the server to respond. */
|
||||
@property (nonatomic, assign) NSTimeInterval feedbackSpan;
|
||||
|
||||
/** The index incremented on every notification push, used as notification identifier. */
|
||||
@property (nonatomic, assign) NSUInteger index;
|
||||
|
||||
/** @name Initialization */
|
||||
|
||||
/** Create and return a hub object with a delegate object assigned. */
|
||||
- (instancetype)initWithDelegate:(id<NWHubDelegate>)delegate;
|
||||
|
||||
/** Create and return a hub object with a delegate and pusher object assigned. */
|
||||
- (instancetype)initWithPusher:(NWPusher *)pusher delegate:(id<NWHubDelegate>)delegate;
|
||||
|
||||
/** Create, connect and returns an instance with delegate and identity. */
|
||||
+ (instancetype)connectWithDelegate:(id<NWHubDelegate>)delegate identity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** Create, connect and returns an instance with delegate and identity. */
|
||||
+ (instancetype)connectWithDelegate:(id<NWHubDelegate>)delegate PKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** @name Connecting */
|
||||
|
||||
/** Connect the pusher using the identity to setup the SSL connection. */
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** Connect the pusher using the PKCS #12 data to setup the SSL connection. */
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** Reconnect with the server, to recover from a closed or defect connection. */
|
||||
- (BOOL)reconnectWithError:(NSError **)error;
|
||||
|
||||
/** Close the connection, allows reconnecting. */
|
||||
- (void)disconnect;
|
||||
|
||||
/** @name Pushing (easy) */
|
||||
|
||||
/** Push a JSON string payload to a device with token string.
|
||||
@see pushNotifications:
|
||||
*/
|
||||
- (NSUInteger)pushPayload:(NSString *)payload token:(NSString *)token;
|
||||
|
||||
/** Push a JSON string payload to multiple devices with token strings.
|
||||
@see pushNotifications:
|
||||
*/
|
||||
- (NSUInteger)pushPayload:(NSString *)payload tokens:(NSArray *)tokens;
|
||||
|
||||
/** Push multiple JSON string payloads to a device with token string.
|
||||
@see pushNotifications:
|
||||
*/
|
||||
- (NSUInteger)pushPayloads:(NSArray *)payloads token:(NSString *)token;
|
||||
|
||||
/** Push multiple notifications, each representing a payload and a device token.
|
||||
|
||||
This will assign each notification a unique identifier if none was set yet. If pushing fails it will reconnect. This method can be used rather carelessly; any thing goes. However, this also means that a failed notification might break the connection temporarily, losing a notification here or there. If you are sending bulk and don't care too much about this, then you'll be fine. If not, consider using `pushNotification:autoReconnect:error:`.
|
||||
|
||||
Make sure to call `readFailed` on a regular basis to allow server error responses to be handled and the delegate to be called.
|
||||
|
||||
Returns the number of notifications that failed, preferably zero.
|
||||
|
||||
@see readFailed
|
||||
*/
|
||||
- (NSUInteger)pushNotifications:(NSArray *)notifications;
|
||||
|
||||
/** Read the response from the server to see if any pushes have failed.
|
||||
|
||||
Due to transmission latency it usually takes a couple of milliseconds for the server to respond to errors. This methods reads the server response and handles the errors. Make sure to call this regularly to catch up on malformed notifications.
|
||||
|
||||
@see pushNotifications:
|
||||
*/
|
||||
- (NSUInteger)readFailed;
|
||||
|
||||
/** @name Pushing (pros) */
|
||||
|
||||
/** Push a notification and reconnect if anything failed.
|
||||
|
||||
This will assign the notification a unique (incremental) identifier and feed it to the internal pusher. If this succeeds, the notification is stored for later lookup by `readFailed:autoReconnect:error:`. If it fails, the delegate will be invoked and it will reconnect if set to auto-reconnect.
|
||||
|
||||
@see readFailed:autoReconnect:error:
|
||||
*/
|
||||
- (BOOL)pushNotification:(NWNotification *)notification autoReconnect:(BOOL)reconnect error:(NSError **)error;
|
||||
|
||||
/** Read the response from the server and reconnect if anything failed.
|
||||
|
||||
If the APNs finds something wrong with a notification, it will write back the identifier and error code. As this involves transmission to and from the server, it takes just a little while to get this failure info. This method should therefore be invoked a little (say a second) after pushing to see if anything was wrong. On a slow connection this might take longer than the interval between push messages, in which case the reported notification was *not* the last one sent.
|
||||
|
||||
From the server we only get the notification identifier and the error message. This method translates this back into the original notification by keeping track of all notifications sent in the past 30 seconds. If somehow the original notification cannot be found, it will assign `NSNull`.
|
||||
|
||||
Usually, when a notification fails, the server will drop the connection. To prevent this from causing any more problems, the connection can be reestablished by setting it to reconnect automatically.
|
||||
|
||||
@see trimIdentifiers
|
||||
@see feedbackSpan
|
||||
*/
|
||||
- (BOOL)readFailed:(NWNotification **)notification autoReconnect:(BOOL)reconnect error:(NSError **)error;
|
||||
|
||||
/** Let go of old notification, after you read the failed notifications.
|
||||
|
||||
This class keeps track of all notifications sent so we can look them up later based on their identifier. This allows it to translate identifiers back into the original notification. To limit the amount of memory all older notifications should be trimmed from this lookup, which is done by this method. This is done based on the `feedbackSpan`, which defaults to 30 seconds.
|
||||
|
||||
Be careful not to call this function without first reading all failed notifications, using `readFailed:autoReconnect:error:`.
|
||||
|
||||
@see readFailed:autoReconnect:error:
|
||||
@see feedbackSpan
|
||||
*/
|
||||
- (BOOL)trimIdentifiers;
|
||||
|
||||
// deprecated
|
||||
|
||||
+ (instancetype)connectWithDelegate:(id<NWHubDelegate>)delegate identity:(NWIdentityRef)identity error:(NSError **)error __deprecated;
|
||||
+ (instancetype)connectWithDelegate:(id<NWHubDelegate>)delegate PKCS12Data:(NSData *)data password:(NSString *)password error:(NSError **)error __deprecated;
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity error:(NSError **)error __deprecated;
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError **)error __deprecated;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// NWHub.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2014 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWHub.h"
|
||||
#import "NWPusher.h"
|
||||
#import "NWNotification.h"
|
||||
#import "NWSecTools.h"
|
||||
|
||||
|
||||
@implementation NWHub {
|
||||
NSMutableDictionary *_notificationForIdentifier;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
return [self initWithPusher:[[NWPusher alloc] init] delegate:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithDelegate:(id<NWHubDelegate>)delegate
|
||||
{
|
||||
return [self initWithPusher:[[NWPusher alloc] init] delegate:delegate];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPusher:(NWPusher *)pusher delegate:(id<NWHubDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_index = 1;
|
||||
_feedbackSpan = 30;
|
||||
_pusher = pusher;
|
||||
_delegate = delegate;
|
||||
_notificationForIdentifier = @{}.mutableCopy;
|
||||
_type = kNWNotificationType2;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Connecting
|
||||
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [_pusher connectWithIdentity:identity environment:environment error:error];
|
||||
}
|
||||
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [_pusher connectWithPKCS12Data:data password:password environment:environment error:error];
|
||||
}
|
||||
|
||||
- (BOOL)reconnectWithError:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [_pusher reconnectWithError:error];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
[_pusher disconnect];
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithDelegate:(id<NWHubDelegate>)delegate identity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NWHub *hub = [[NWHub alloc] initWithDelegate:delegate];
|
||||
return identity && [hub connectWithIdentity:identity environment:environment error:error] ? hub : nil;
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithDelegate:(id<NWHubDelegate>)delegate PKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NWHub *hub = [[NWHub alloc] initWithDelegate:delegate];
|
||||
return data && [hub connectWithPKCS12Data:data password:password environment:environment error:error] ? hub : nil;
|
||||
}
|
||||
|
||||
#pragma mark - Pushing without NSError
|
||||
|
||||
- (NSUInteger)pushPayload:(NSString *)payload token:(NSString *)token
|
||||
{
|
||||
NWNotification *notification = [[NWNotification alloc] initWithPayload:payload token:token identifier:0 expiration:nil priority:0];
|
||||
return [self pushNotifications:@[notification]];
|
||||
}
|
||||
|
||||
- (NSUInteger)pushPayload:(NSString *)payload tokens:(NSArray *)tokens
|
||||
{
|
||||
NSMutableArray *notifications = @[].mutableCopy;
|
||||
for (NSString *token in tokens) {
|
||||
NWNotification *notification = [[NWNotification alloc] initWithPayload:payload token:token identifier:0 expiration:nil priority:0];
|
||||
[notifications addObject:notification];
|
||||
}
|
||||
return [self pushNotifications:notifications];
|
||||
}
|
||||
|
||||
- (NSUInteger)pushPayloads:(NSArray *)payloads token:(NSString *)token
|
||||
{
|
||||
NSMutableArray *notifications = @[].mutableCopy;
|
||||
for (NSString *payload in payloads) {
|
||||
NWNotification *notification = [[NWNotification alloc] initWithPayload:payload token:token identifier:0 expiration:nil priority:0];
|
||||
[notifications addObject:notification];
|
||||
}
|
||||
return [self pushNotifications:notifications];
|
||||
}
|
||||
|
||||
- (NSUInteger)pushNotifications:(NSArray *)notifications
|
||||
{
|
||||
NSUInteger fails = 0;
|
||||
for (NWNotification *notification in notifications) {
|
||||
BOOL success = [self pushNotification:notification autoReconnect:YES error:nil];
|
||||
if (!success) {
|
||||
fails++;
|
||||
}
|
||||
}
|
||||
return fails;
|
||||
}
|
||||
|
||||
#pragma mark - Pushing with NSError
|
||||
|
||||
- (BOOL)pushNotification:(NWNotification *)notification autoReconnect:(BOOL)reconnect error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
if (!notification.identifier) notification.identifier = _index++;
|
||||
NSError *e = nil;
|
||||
BOOL pushed = [_pusher pushNotification:notification type:_type error:&e];
|
||||
if (!pushed) {
|
||||
if (error) *error = e;
|
||||
if ([_delegate respondsToSelector:@selector(notification:didFailWithError:)]) {
|
||||
[_delegate notification:notification didFailWithError:e];
|
||||
}
|
||||
if (reconnect && e.code == kNWErrorWriteClosedGraceful) {
|
||||
[self reconnectWithError:error];
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
_notificationForIdentifier[@(notification.identifier)] = @[notification, NSDate.date];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)pushNotifications:(NSArray *)notifications autoReconnect:(BOOL)reconnect error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
for (NWNotification *notification in notifications) {
|
||||
BOOL success = [self pushNotification:notification autoReconnect:reconnect error:error];
|
||||
if (!success) {
|
||||
return success;
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Reading failed
|
||||
|
||||
- (NSUInteger)readFailed
|
||||
{
|
||||
NSArray *failed = nil;
|
||||
[self readFailed:&failed max:1000 autoReconnect:YES error:nil];
|
||||
return failed.count;
|
||||
}
|
||||
|
||||
- (BOOL)readFailed:(NSArray **)notifications max:(NSUInteger)max autoReconnect:(BOOL)reconnect error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSMutableArray *n = @[].mutableCopy;
|
||||
for (NSUInteger i = 0; i < max; i++) {
|
||||
NWNotification *notification = nil;
|
||||
BOOL read = [self readFailed:¬ification autoReconnect:reconnect error:error];
|
||||
if (!read) {
|
||||
return read;
|
||||
}
|
||||
if (!notification) {
|
||||
break;
|
||||
}
|
||||
[n addObject:notification];
|
||||
}
|
||||
if (notifications) *notifications = n;
|
||||
[self trimIdentifiers];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)readFailed:(NWNotification **)notification autoReconnect:(BOOL)reconnect error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSUInteger identifier = 0;
|
||||
NSError *apnError = nil;
|
||||
BOOL read = [_pusher readFailedIdentifier:&identifier apnError:&apnError error:error];
|
||||
if (!read) {
|
||||
return read;
|
||||
}
|
||||
if (apnError) {
|
||||
NWNotification *n = _notificationForIdentifier[@(identifier)][0];
|
||||
if (notification) *notification = n ?: (NWNotification *)NSNull.null;
|
||||
if ([_delegate respondsToSelector:@selector(notification:didFailWithError:)]) {
|
||||
[_delegate notification:n didFailWithError:apnError];
|
||||
}
|
||||
if (reconnect) {
|
||||
[self reconnectWithError:error];
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)trimIdentifiers
|
||||
{
|
||||
NSDate *oldBefore = [NSDate dateWithTimeIntervalSinceNow:-_feedbackSpan];
|
||||
NSArray *old = [[_notificationForIdentifier keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
|
||||
return [oldBefore compare:obj[1]] == NSOrderedDescending;
|
||||
}] allObjects];
|
||||
[_notificationForIdentifier removeObjectsForKeys:old];
|
||||
return !!old.count;
|
||||
}
|
||||
|
||||
#pragma mark - Deprecated
|
||||
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithIdentity:identity environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithPKCS12Data:data password:password environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithDelegate:(id<NWHubDelegate>)delegate identity:(NWIdentityRef)identity error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithDelegate:delegate identity:identity environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithDelegate:(id<NWHubDelegate>)delegate PKCS12Data:(NSData *)data password:(NSString *)password error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithDelegate:delegate identity:data environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// NWNotification.h
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2014 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWType.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/** A single push message, containing the receiver device token, the payload, and delivery attributes.
|
||||
|
||||
This class represents a single push message, or *remote notification* as Apple calls it. It consists of device token, payload, and some optional attributes. The device token is a unique reference to a single installed app on a single Apple device. The payload is a JSON-formatted string that is delivered into the app. Among app-specific data, this payload contains information on how the device should handle and display this notification.
|
||||
|
||||
Then there is a number of additional attributes Apple has been adding over the years. The *identifier* is used in error data that we get back from the server. This allows us to associate the error with the notification. The *expiration* date tells the delivery system when it should stop trying to deliver the notification to the device. Priority indicates whether to conserve power on delivery.
|
||||
|
||||
There are different data formats into which a notification can be serialized. Older formats do not support all attributes. While this class supports all formats, it uses the latest format by default.
|
||||
|
||||
Read more about this in Apple's documentation under *Provider Communication with Apple Push Notification Service* and *The Notification Payload*.
|
||||
*/
|
||||
@interface NWNotification : NSObject
|
||||
|
||||
/** @name Properties */
|
||||
|
||||
/** String representation of serialized JSON. */
|
||||
@property (nonatomic, strong) NSString *payload;
|
||||
|
||||
/** UTF-8 data representation of serialized JSON. */
|
||||
@property (nonatomic, strong) NSData *payloadData;
|
||||
|
||||
/** Hex string representation of the device token. */
|
||||
@property (nonatomic, strong) NSString *token;
|
||||
|
||||
/** Data representation of the device token. */
|
||||
@property (nonatomic, strong) NSData *tokenData;
|
||||
|
||||
/** Identifier used for correlating server response on error. */
|
||||
@property (nonatomic, assign) NSUInteger identifier;
|
||||
|
||||
/** The expiration date after which the server will not attempt to deliver. */
|
||||
@property (nonatomic, strong) NSDate *expiration;
|
||||
|
||||
/** Epoch seconds representation of expiration date. */
|
||||
@property (nonatomic, assign) NSUInteger expirationStamp;
|
||||
|
||||
/** Notification priority used by server for delivery optimization. */
|
||||
@property (nonatomic, assign) NSUInteger priority;
|
||||
|
||||
/** Indicates whether the expiration date should be serialized. */
|
||||
@property (nonatomic, assign) BOOL addExpiration;
|
||||
|
||||
/** @name Initialization */
|
||||
|
||||
/** Create and returns a notification object based on given attribute objects. */
|
||||
- (instancetype)initWithPayload:(NSString *)payload token:(NSString *)token identifier:(NSUInteger)identifier expiration:(NSDate *)date priority:(NSUInteger)priority;
|
||||
|
||||
/** Create and returns a notification object based on given raw attributes. */
|
||||
- (instancetype)initWithPayloadData:(NSData *)payload tokenData:(NSData *)token identifier:(NSUInteger)identifier expirationStamp:(NSUInteger)expirationStamp addExpiration:(BOOL)addExpiration priority:(NSUInteger)priority;
|
||||
|
||||
/** @name Serialization */
|
||||
|
||||
/** Serialize this notification using provided format. */
|
||||
- (NSData *)dataWithType:(NWNotificationType)type;
|
||||
|
||||
/** @name Helpers */
|
||||
|
||||
/** Converts a hex string into binary data. */
|
||||
+ (NSData *)dataFromHex:(NSString *)hex;
|
||||
|
||||
/** Converts binary data into a hex string. */
|
||||
+ (NSString *)hexFromData:(NSData *)data;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// NWNotification.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2014 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWNotification.h"
|
||||
|
||||
|
||||
static NSUInteger const NWDeviceTokenSize = 32;
|
||||
static NSUInteger const NWPayloadMaxSize = 256;
|
||||
|
||||
@implementation NWNotification
|
||||
|
||||
- (instancetype)initWithPayload:(NSString *)payload token:(NSString *)token identifier:(NSUInteger)identifier expiration:(NSDate *)date priority:(NSUInteger)priority
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.payload = payload;
|
||||
self.token = token;
|
||||
_identifier = identifier;
|
||||
self.expiration = date;
|
||||
_priority = priority;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPayloadData:(NSData *)payload tokenData:(NSData *)token identifier:(NSUInteger)identifier expirationStamp:(NSUInteger)expirationStamp addExpiration:(BOOL)addExpiration priority:(NSUInteger)priority
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_payloadData = payload;
|
||||
_tokenData = token;
|
||||
_identifier = identifier;
|
||||
_expirationStamp = expirationStamp;
|
||||
_addExpiration = addExpiration;
|
||||
_priority = priority;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
- (NSString *)payload
|
||||
{
|
||||
return _payloadData ? [[NSString alloc] initWithData:_payloadData encoding:NSUTF8StringEncoding] : nil;
|
||||
}
|
||||
|
||||
- (void)setPayload:(NSString *)payload
|
||||
{
|
||||
_payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
- (NSString *)token
|
||||
{
|
||||
return _tokenData ? [self.class hexFromData:_tokenData] : nil;
|
||||
}
|
||||
|
||||
- (void)setToken:(NSString *)token
|
||||
{
|
||||
if (token) {
|
||||
NSString *normal = [self.class filterHex:token];
|
||||
NSString *trunk = normal.length >= 64 ? [normal substringToIndex:64] : nil;
|
||||
_tokenData = [self.class dataFromHex:trunk];
|
||||
} else {
|
||||
_tokenData = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSDate *)expiration
|
||||
{
|
||||
return _addExpiration ? [NSDate dateWithTimeIntervalSince1970:_expirationStamp] : nil;
|
||||
}
|
||||
|
||||
- (void)setExpiration:(NSDate *)date
|
||||
{
|
||||
_expirationStamp = (NSUInteger)date.timeIntervalSince1970;
|
||||
_addExpiration = !!date;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
+ (NSString *)filterHex:(NSString *)hex
|
||||
{
|
||||
hex = hex.lowercaseString;
|
||||
NSMutableString *result = [[NSMutableString alloc] init];
|
||||
for (NSUInteger i = 0; i < hex.length; i++) {
|
||||
unichar c = [hex characterAtIndex:i];
|
||||
if ((c >= 'a' && c <= 'f') || (c >= '0' && c <= '9')) {
|
||||
[result appendString:[NSString stringWithCharacters:&c length:1]];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (NSData *)dataFromHex:(NSString *)hex
|
||||
{
|
||||
NSMutableData *result = [[NSMutableData alloc] init];
|
||||
char buffer[3] = {'\0','\0','\0'};
|
||||
for (NSUInteger i = 0; i < hex.length / 2; i++) {
|
||||
buffer[0] = [hex characterAtIndex:i * 2];
|
||||
buffer[1] = [hex characterAtIndex:i * 2 + 1];
|
||||
unsigned char b = strtol(buffer, NULL, 16);
|
||||
[result appendBytes:&b length:1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (NSString *)hexFromData:(NSData *)data
|
||||
{
|
||||
NSUInteger length = data.length;
|
||||
NSMutableString *result = [NSMutableString stringWithCapacity:length * 2];
|
||||
for (const unsigned char *b = data.bytes, *end = b + length; b != end; b++) {
|
||||
[result appendFormat:@"%02X", *b];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Types
|
||||
|
||||
- (NSData *)dataWithType:(NWNotificationType)type
|
||||
{
|
||||
switch (type) {
|
||||
case kNWNotificationType0: return [self dataWithType0];
|
||||
case kNWNotificationType1: return [self dataWithType1];
|
||||
case kNWNotificationType2: return [self dataWithType2];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSData *)dataWithType0
|
||||
{
|
||||
char buffer[sizeof(uint8_t) + sizeof(uint32_t) * 2 + sizeof(uint16_t) + NWDeviceTokenSize + sizeof(uint16_t) + NWPayloadMaxSize];
|
||||
char *p = buffer;
|
||||
|
||||
uint8_t command = 0;
|
||||
memcpy(p, &command, sizeof(uint8_t));
|
||||
p += sizeof(uint8_t);
|
||||
|
||||
uint16_t tokenLength = htons(_tokenData.length);
|
||||
memcpy(p, &tokenLength, sizeof(uint16_t));
|
||||
p += sizeof(uint16_t);
|
||||
|
||||
memcpy(p, _tokenData.bytes, _tokenData.length);
|
||||
p += _tokenData.length;
|
||||
|
||||
uint16_t payloadLength = htons(_payloadData.length);
|
||||
memcpy(p, &payloadLength, sizeof(uint16_t));
|
||||
p += sizeof(uint16_t);
|
||||
|
||||
memcpy(p, _payloadData.bytes, _payloadData.length);
|
||||
p += _payloadData.length;
|
||||
|
||||
return [NSData dataWithBytes:buffer length:p - buffer];
|
||||
}
|
||||
|
||||
- (NSData *)dataWithType1
|
||||
{
|
||||
char buffer[sizeof(uint8_t) + sizeof(uint32_t) * 2 + sizeof(uint16_t) + NWDeviceTokenSize + sizeof(uint16_t) + NWPayloadMaxSize];
|
||||
char *p = buffer;
|
||||
|
||||
uint8_t command = 1;
|
||||
memcpy(p, &command, sizeof(uint8_t));
|
||||
p += sizeof(uint8_t);
|
||||
|
||||
uint32_t ID = htonl(_identifier);
|
||||
memcpy(p, &ID, sizeof(uint32_t));
|
||||
p += sizeof(uint32_t);
|
||||
|
||||
uint32_t exp = htonl(_expirationStamp);
|
||||
memcpy(p, &exp, sizeof(uint32_t));
|
||||
p += sizeof(uint32_t);
|
||||
|
||||
uint16_t tokenLength = htons(_tokenData.length);
|
||||
memcpy(p, &tokenLength, sizeof(uint16_t));
|
||||
p += sizeof(uint16_t);
|
||||
|
||||
memcpy(p, _tokenData.bytes, _tokenData.length);
|
||||
p += _tokenData.length;
|
||||
|
||||
uint16_t payloadLength = htons(_payloadData.length);
|
||||
memcpy(p, &payloadLength, sizeof(uint16_t));
|
||||
p += sizeof(uint16_t);
|
||||
|
||||
memcpy(p, _payloadData.bytes, _payloadData.length);
|
||||
p += _payloadData.length;
|
||||
|
||||
return [NSData dataWithBytes:buffer length:p - buffer];
|
||||
}
|
||||
|
||||
- (NSData *)dataWithType2
|
||||
{
|
||||
NSMutableData *result = [[NSMutableData alloc] initWithLength:5];
|
||||
|
||||
if (_tokenData) [self.class appendTo:result identifier:1 bytes:_tokenData.bytes length:_tokenData.length];
|
||||
if (_payloadData) [self.class appendTo:result identifier:2 bytes:_payloadData.bytes length:_payloadData.length];
|
||||
uint32_t identifier = htonl(_identifier);
|
||||
uint32_t expires = htonl(_expirationStamp);
|
||||
uint8_t priority = _priority;
|
||||
if (identifier) [self.class appendTo:result identifier:3 bytes:&identifier length:4];
|
||||
if (_addExpiration) [self.class appendTo:result identifier:4 bytes:&expires length:4];
|
||||
if (priority) [self.class appendTo:result identifier:5 bytes:&priority length:1];
|
||||
uint8_t command = 2;
|
||||
[result replaceBytesInRange:NSMakeRange(0, 1) withBytes:&command];
|
||||
uint32_t length = htonl(result.length - 5);
|
||||
[result replaceBytesInRange:NSMakeRange(1, 4) withBytes:&length];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (void)appendTo:(NSMutableData *)buffer identifier:(NSUInteger)identifier bytes:(const void *)bytes length:(NSUInteger)length
|
||||
{
|
||||
uint8_t i = identifier;
|
||||
uint16_t l = htons(length);
|
||||
[buffer appendBytes:&i length:1];
|
||||
[buffer appendBytes:&l length:2];
|
||||
[buffer appendBytes:bytes length:length];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// NWPushFeedback.h
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWType.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class NWSSLConnection;
|
||||
|
||||
/** Reads tokens and dates from the APNs feedback service.
|
||||
|
||||
The feedback service is a separate server that provides a list of all device tokens that it tried to deliver a notification to, but was unable to. This usually indicates that this device no longer has the app installed. This way, the feedback service provides reliable way of finding out who uninstalled the app, which can be fed back into your database.
|
||||
|
||||
Apple recommends reading from the service once a day. After a device token has been read, it will not be returned again until the next failed delivery. In practice: connect once a day, read all device tokens, and update your own database accordingly.
|
||||
|
||||
Read more in Apple's documentation under *The Feedback Service*.
|
||||
*/
|
||||
@interface NWPushFeedback : NSObject
|
||||
|
||||
/** @name Properties */
|
||||
|
||||
@property (nonatomic, strong) NWSSLConnection *connection;
|
||||
|
||||
/** @name Initialization */
|
||||
|
||||
/** Setup connection with feedback service based on identity. */
|
||||
+ (instancetype)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** Setup connection with feedback service based on PKCS #12 data. */
|
||||
+ (instancetype)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** @name Connecting */
|
||||
|
||||
/** Connect with feedback service based on identity. */
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** Connect with feedback service based on PKCS #12 data. */
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** Disconnect from feedback service. The server will automatically drop the connection after all feedback data has been read. */
|
||||
- (void)disconnect;
|
||||
|
||||
/** @name Reading */
|
||||
|
||||
/** Read a single token-date pair, where token is data. */
|
||||
- (BOOL)readTokenData:(NSData **)token date:(NSDate **)date error:(NSError **)error;
|
||||
|
||||
/** Read a single token-date pair, where token is hex string. */
|
||||
- (BOOL)readToken:(NSString **)token date:(NSDate **)date error:(NSError **)error;
|
||||
|
||||
/** Read all (or max) token-date pairs, where token is hex string. */
|
||||
- (NSArray *)readTokenDatePairsWithMax:(NSUInteger)max error:(NSError **)error;
|
||||
|
||||
// deprecated
|
||||
|
||||
+ (instancetype)connectWithIdentity:(NWIdentityRef)identity error:(NSError **)error __deprecated;
|
||||
+ (instancetype)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError **)error __deprecated;
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity error:(NSError **)error __deprecated;
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError **)error __deprecated;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// NWPushFeedback.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWPushFeedback.h"
|
||||
#import "NWSSLConnection.h"
|
||||
#import "NWSecTools.h"
|
||||
#import "NWNotification.h"
|
||||
|
||||
|
||||
static NSString * const NWSandboxPushHost = @"feedback.sandbox.push.apple.com";
|
||||
static NSString * const NWPushHost = @"feedback.push.apple.com";
|
||||
static NSUInteger const NWPushPort = 2196;
|
||||
static NSUInteger const NWTokenMaxSize = 32;
|
||||
|
||||
@implementation NWPushFeedback
|
||||
|
||||
#pragma mark - Connecting
|
||||
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
if (_connection) [_connection disconnect]; _connection = nil;
|
||||
if (environment == NWEnvironmentAuto) environment = [NWSecTools environmentForIdentity:identity];
|
||||
NSString *host = (environment == NWEnvironmentSandbox) ? NWSandboxPushHost : NWPushHost;
|
||||
NWSSLConnection *connection = [[NWSSLConnection alloc] initWithHost:host port:NWPushPort identity:identity];
|
||||
BOOL connected = [connection connectWithError:error];
|
||||
if (!connected) {
|
||||
return connected;
|
||||
}
|
||||
_connection = connection;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NWIdentityRef identity = [NWSecTools identityWithPKCS12Data:data password:password error:error];
|
||||
if (!identity) {
|
||||
return NO;
|
||||
}
|
||||
return [self connectWithIdentity:identity environment:environment error:error];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
[_connection disconnect]; _connection = nil;
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NWPushFeedback *feedback = [[NWPushFeedback alloc] init];
|
||||
return identity && [feedback connectWithIdentity:identity environment:environment error:error] ? feedback : nil;
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NWPushFeedback *feedback = [[NWPushFeedback alloc] init];
|
||||
return data && [feedback connectWithPKCS12Data:data password:password environment:environment error:error] ? feedback : nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Reading feedback
|
||||
|
||||
- (BOOL)readTokenData:(NSData **)token date:(NSDate **)date error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
*token = nil;
|
||||
*date = nil;
|
||||
NSMutableData *data = [NSMutableData dataWithLength:sizeof(uint32_t) + sizeof(uint16_t) + NWTokenMaxSize];
|
||||
NSUInteger length = 0;
|
||||
BOOL read = [_connection read:data length:&length error:error];
|
||||
if (!read || length == 0) {
|
||||
return read;
|
||||
}
|
||||
if (length != data.length) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorFeedbackLength reason:length error:error];
|
||||
}
|
||||
uint32_t time = 0;
|
||||
[data getBytes:&time range:NSMakeRange(0, 4)];
|
||||
*date = [NSDate dateWithTimeIntervalSince1970:htonl(time)];
|
||||
uint16_t l = 0;
|
||||
[data getBytes:&l range:NSMakeRange(4, 2)];
|
||||
NSUInteger tokenLength = htons(l);
|
||||
if (tokenLength != NWTokenMaxSize) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorFeedbackTokenLength reason:tokenLength error:error];
|
||||
}
|
||||
*token = [data subdataWithRange:NSMakeRange(6, length - 6)];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)readToken:(NSString **)token date:(NSDate **)date error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
*token = nil;
|
||||
NSData *data = nil;
|
||||
BOOL read = [self readTokenData:&data date:date error:error];
|
||||
if (!read) {
|
||||
return read;
|
||||
}
|
||||
if (data) *token = [NWNotification hexFromData:data];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSArray *)readTokenDatePairsWithMax:(NSUInteger)max error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSMutableArray *pairs = @[].mutableCopy;
|
||||
for (NSUInteger i = 0; i < max; i++) {
|
||||
NSString *token = nil;
|
||||
NSDate *date = nil;
|
||||
NSError *e = nil;
|
||||
BOOL read = [self readToken:&token date:&date error:&e];
|
||||
if (!read && e.code == kNWErrorReadClosedGraceful) {
|
||||
break;
|
||||
}
|
||||
if (!read) {
|
||||
if (error) *error = e;
|
||||
return nil;
|
||||
}
|
||||
if (token && date) {
|
||||
[pairs addObject:@[token, date]];
|
||||
}
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
#pragma mark - Deprecated
|
||||
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithIdentity:identity environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithPKCS12Data:data password:password environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithIdentity:(NWIdentityRef)identity error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithIdentity:identity environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithPKCS12Data:data password:password environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// NWPusher.h
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWType.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class NWNotification, NWSSLConnection;
|
||||
|
||||
/** Serializes notification objects and pushes them to the APNs.
|
||||
|
||||
This is the heart of the framework. As the (inconvenient) name suggest, it's also one of the first classes that was added to the framework. This class provides a straightforward interface to the APNs, including connecting, pushing to and reading from the server.
|
||||
|
||||
Connecting is done based on an identity or PKCS #12 data. The identity is an instance of `SecIdentityRef` and contains a certificate and private key. The PKCS #12 data can be deserialized into such an identity. One can reconnect or disconnect at any time, and should if the connection has been dropped by the server. The latter can happen quite easily, for example when there is something wrong with the device token or payload of the notification.
|
||||
|
||||
Notifications are pushed one at a time. It is serialized and sent over the wire. If the server then concludes there is something wrong with that notification, it will write back error data. If you send out multiple notifications in a row, these errors might not match up. Therefore every error contains the identifier of the erroneous notification.
|
||||
|
||||
Make sure to read this error data from the server, so you can lookup the notification that caused it and prevent the issue in the future. As mentioned earlier, the server easily drops the connection if there is something out of the ordinary. NB: if you read right after pushing, it is very unlikely that data about that push already got back from the server.
|
||||
|
||||
Make sure to read Apple's documentation on *Apple Push Notification Service* and *Provider Communication*.
|
||||
*/
|
||||
@interface NWPusher : NSObject
|
||||
|
||||
/** @name Properties */
|
||||
|
||||
/** The SSL connection through which all notifications are pushed. */
|
||||
@property (nonatomic, strong) NWSSLConnection *connection;
|
||||
|
||||
/** @name Initialization */
|
||||
|
||||
/** Creates, connects and returns a pusher object based on the provided identity. */
|
||||
+ (instancetype)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** Creates, connects and returns a pusher object based on the PKCS #12 data. */
|
||||
+ (instancetype)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** @name Connecting */
|
||||
|
||||
/** Connect with the APNs using the identity. */
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** Connect with the APNs using the identity from PKCS #12 data. */
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError **)error;
|
||||
|
||||
/** Reconnect using the same identity, disconnects if necessary. */
|
||||
- (BOOL)reconnectWithError:(NSError **)error;
|
||||
|
||||
/** Disconnect from the server, allows reconnect. */
|
||||
- (void)disconnect;
|
||||
|
||||
/** @name Pushing */
|
||||
|
||||
/** Push a JSON string payload to a device with token string, assign identifier. */
|
||||
- (BOOL)pushPayload:(NSString *)payload token:(NSString *)token identifier:(NSUInteger)identifier error:(NSError **)error;
|
||||
|
||||
/** Push a notification using push type for serialization. */
|
||||
- (BOOL)pushNotification:(NWNotification *)notification type:(NWNotificationType)type error:(NSError **)error;
|
||||
|
||||
/** @name Reading */
|
||||
|
||||
/** Read back from the server the notification identifiers of failed pushes. */
|
||||
- (BOOL)readFailedIdentifier:(NSUInteger *)identifier apnError:(NSError **)apnError error:(NSError **)error;
|
||||
|
||||
/** Read back multiple notification identifiers of, up to max, failed pushes. */
|
||||
- (NSArray *)readFailedIdentifierErrorPairsWithMax:(NSUInteger)max error:(NSError **)error;
|
||||
|
||||
// deprecated
|
||||
|
||||
+ (instancetype)connectWithIdentity:(NWIdentityRef)identity error:(NSError **)error __deprecated;
|
||||
+ (instancetype)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError **)error __deprecated;
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity error:(NSError **)error __deprecated;
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError **)error __deprecated;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,167 @@
|
||||
//
|
||||
// NWPusher.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWPusher.h"
|
||||
#import "NWSSLConnection.h"
|
||||
#import "NWSecTools.h"
|
||||
#import "NWNotification.h"
|
||||
|
||||
|
||||
static NSString * const NWSandboxPushHost = @"gateway.sandbox.push.apple.com";
|
||||
static NSString * const NWPushHost = @"gateway.push.apple.com";
|
||||
static NSUInteger const NWPushPort = 2195;
|
||||
|
||||
@implementation NWPusher
|
||||
|
||||
#pragma mark - Connecting
|
||||
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
if (_connection) [_connection disconnect]; _connection = nil;
|
||||
if (environment == NWEnvironmentAuto) environment = [NWSecTools environmentForIdentity:identity];
|
||||
NSString *host = (environment == NWEnvironmentSandbox) ? NWSandboxPushHost : NWPushHost;
|
||||
NWSSLConnection *connection = [[NWSSLConnection alloc] initWithHost:host port:NWPushPort identity:identity];
|
||||
BOOL connected = [connection connectWithError:error];
|
||||
if (!connected) {
|
||||
return connected;
|
||||
}
|
||||
_connection = connection;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NWIdentityRef identity = [NWSecTools identityWithPKCS12Data:data password:password error:error];
|
||||
if (!identity) {
|
||||
return NO;
|
||||
}
|
||||
return [self connectWithIdentity:identity environment:environment error:error];
|
||||
}
|
||||
|
||||
- (BOOL)reconnectWithError:(NSError *__autoreleasing *)error
|
||||
{
|
||||
if (!_connection) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorPushNotConnected error:error];
|
||||
}
|
||||
return [_connection connectWithError:error];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
[_connection disconnect]; _connection = nil;
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithIdentity:(NWIdentityRef)identity environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NWPusher *pusher = [[NWPusher alloc] init];
|
||||
return identity && [pusher connectWithIdentity:identity environment:environment error:error] ? pusher : nil;
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithPKCS12Data:(NSData *)data password:(NSString *)password environment:(NWEnvironment)environment error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NWPusher *pusher = [[NWPusher alloc] init];
|
||||
return data && [pusher connectWithPKCS12Data:data password:password environment:environment error:error] ? pusher : nil;
|
||||
}
|
||||
|
||||
#pragma mark - Pushing payload
|
||||
|
||||
- (BOOL)pushPayload:(NSString *)payload token:(NSString *)token identifier:(NSUInteger)identifier error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self pushNotification:[[NWNotification alloc] initWithPayload:payload token:token identifier:identifier expiration:nil priority:0] type:kNWNotificationType2 error:error];
|
||||
}
|
||||
|
||||
- (BOOL)pushNotification:(NWNotification *)notification type:(NWNotificationType)type error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSUInteger length = 0;
|
||||
NSData *data = [notification dataWithType:type];
|
||||
BOOL written = [_connection write:data length:&length error:error];
|
||||
if (!written) {
|
||||
return written;
|
||||
}
|
||||
if (length != data.length) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorPushWriteFail reason:length error:error];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Reading failed
|
||||
|
||||
- (BOOL)readFailedIdentifier:(NSUInteger *)identifier apnError:(NSError *__autoreleasing *)apnError error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
*identifier = 0;
|
||||
NSMutableData *data = [NSMutableData dataWithLength:sizeof(uint8_t) * 2 + sizeof(uint32_t)];
|
||||
NSUInteger length = 0;
|
||||
BOOL read = [_connection read:data length:&length error:error];
|
||||
if (!length || !read) {
|
||||
return read;
|
||||
}
|
||||
uint8_t command = 0;
|
||||
[data getBytes:&command range:NSMakeRange(0, 1)];
|
||||
if (command != 8) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorPushResponseCommand reason:command error:error];
|
||||
}
|
||||
uint8_t status = 0;
|
||||
[data getBytes:&status range:NSMakeRange(1, 1)];
|
||||
uint32_t ID = 0;
|
||||
[data getBytes:&ID range:NSMakeRange(2, 4)];
|
||||
*identifier = htonl(ID);
|
||||
switch (status) {
|
||||
case 1: [NWErrorUtil noWithErrorCode:kNWErrorAPNProcessing error:apnError]; break;
|
||||
case 2: [NWErrorUtil noWithErrorCode:kNWErrorAPNMissingDeviceToken error:apnError]; break;
|
||||
case 3: [NWErrorUtil noWithErrorCode:kNWErrorAPNMissingTopic error:apnError]; break;
|
||||
case 4: [NWErrorUtil noWithErrorCode:kNWErrorAPNMissingPayload error:apnError]; break;
|
||||
case 5: [NWErrorUtil noWithErrorCode:kNWErrorAPNInvalidTokenSize error:apnError]; break;
|
||||
case 6: [NWErrorUtil noWithErrorCode:kNWErrorAPNInvalidTopicSize error:apnError]; break;
|
||||
case 7: [NWErrorUtil noWithErrorCode:kNWErrorAPNInvalidPayloadSize error:apnError]; break;
|
||||
case 8: [NWErrorUtil noWithErrorCode:kNWErrorAPNInvalidTokenContent error:apnError]; break;
|
||||
case 10: [NWErrorUtil noWithErrorCode:kNWErrorAPNShutdown error:apnError]; break;
|
||||
default: [NWErrorUtil noWithErrorCode:kNWErrorAPNUnknownErrorCode reason:status error:apnError]; break;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSArray *)readFailedIdentifierErrorPairsWithMax:(NSUInteger)max error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSMutableArray *pairs = @[].mutableCopy;
|
||||
for (NSUInteger i = 0; i < max; i++) {
|
||||
NSUInteger identifier = 0;
|
||||
NSError *apnError = nil;
|
||||
BOOL read = [self readFailedIdentifier:&identifier apnError:&apnError error:error];
|
||||
if (!read) {
|
||||
return nil;
|
||||
}
|
||||
if (!apnError) {
|
||||
break;
|
||||
}
|
||||
[pairs addObject:@[@(identifier), apnError]];
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
#pragma mark - Deprecated
|
||||
|
||||
- (BOOL)connectWithIdentity:(NWIdentityRef)identity error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithIdentity:identity environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
- (BOOL)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithPKCS12Data:data password:password environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithIdentity:(NWIdentityRef)identity error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithIdentity:identity environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
+ (instancetype)connectWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self connectWithPKCS12Data:data password:password environment:NWEnvironmentAuto error:error];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// NWSSLConnection.h
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWType.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
/** An SSL (TLS) connection to the APNs.
|
||||
|
||||
This class is basically an Objective-C wrapper around `SSLContextRef` and `SSLConnectionRef`, which are part of the native Secure Transport framework. This class provides a generic interface for SSL (TLS) connections, independent of NWPusher.
|
||||
|
||||
A SSL connection is set up using the host name, host port and an identity. The host name will be resolved using DNS. The identity is an instance of `SecIdentityRef` and contains both a certificate and a private key. See the *Secure Transport Reference* for more info on that.
|
||||
|
||||
Read more about provider communication in Apple's documentation under *Apple Push Notification Service*.
|
||||
|
||||
Methods return `NO` if an error occurred.
|
||||
*/
|
||||
@interface NWSSLConnection : NSObject
|
||||
|
||||
/** @name Properties */
|
||||
|
||||
/** The host name, which will be resolved using DNS. */
|
||||
@property (nonatomic, strong) NSString *host;
|
||||
|
||||
/** The host TCP port number. */
|
||||
@property (nonatomic, assign) NSUInteger port;
|
||||
|
||||
/** Identity containing a certificate-key pair for setting up the TLS connection. */
|
||||
@property (nonatomic, strong) NWIdentityRef identity;
|
||||
|
||||
/** @name Initialization */
|
||||
|
||||
/** Initialize a connection parameters host name, port, and identity. */
|
||||
- (instancetype)initWithHost:(NSString *)host port:(NSUInteger)port identity:(NWIdentityRef)identity;
|
||||
|
||||
/** @name Connecting */
|
||||
|
||||
/** Connect socket, TLS and perform handshake.
|
||||
Can also be used when already connected, which will then first disconnect. */
|
||||
- (BOOL)connectWithError:(NSError **)error;
|
||||
|
||||
/** Drop connection if connected. */
|
||||
- (void)disconnect;
|
||||
|
||||
/** @name I/O */
|
||||
|
||||
/** Read length number of bytes into mutable data object. */
|
||||
- (BOOL)read:(NSMutableData *)data length:(NSUInteger *)length error:(NSError **)error;
|
||||
|
||||
/** Write length number of bytes from data object. */
|
||||
- (BOOL)write:(NSData *)data length:(NSUInteger *)length error:(NSError **)error;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,239 @@
|
||||
//
|
||||
// NWSSLConnection.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWSSLConnection.h"
|
||||
#include <netdb.h>
|
||||
|
||||
#define NWSSL_HANDSHAKE_TRY_COUNT 1 << 26
|
||||
|
||||
OSStatus NWSSLRead(SSLConnectionRef connection, void *data, size_t *length);
|
||||
OSStatus NWSSLWrite(SSLConnectionRef connection, const void *data, size_t *length);
|
||||
|
||||
|
||||
@implementation NWSSLConnection {
|
||||
int _socket;
|
||||
SSLContextRef _context;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
return [self initWithHost:nil port:0 identity:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host port:(NSUInteger)port identity:(NWIdentityRef)identity
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_host = host;
|
||||
_port = port;
|
||||
_identity = identity;
|
||||
_socket = -1;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self disconnect];
|
||||
}
|
||||
|
||||
#pragma mark - Connecting
|
||||
|
||||
- (BOOL)connectWithError:(NSError *__autoreleasing *)error
|
||||
{
|
||||
[self disconnect];
|
||||
BOOL socket = [self connectSocketWithError:error];
|
||||
if (!socket) {
|
||||
[self disconnect];
|
||||
return socket;
|
||||
}
|
||||
BOOL ssl = [self connectSSLWithError:error];
|
||||
if (!ssl) {
|
||||
[self disconnect];
|
||||
return ssl;
|
||||
}
|
||||
BOOL handshake = [self handshakeSSLWithError:error];
|
||||
if (!handshake) {
|
||||
[self disconnect];
|
||||
return handshake;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)connectSocketWithError:(NSError *__autoreleasing *)error
|
||||
{
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock < 0) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSocketCreate reason:sock error:error];
|
||||
}
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(struct sockaddr_in));
|
||||
struct hostent *entr = gethostbyname(_host.UTF8String);
|
||||
if (!entr) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSocketResolveHostName error:error];
|
||||
}
|
||||
struct in_addr host;
|
||||
memcpy(&host, entr->h_addr, sizeof(struct in_addr));
|
||||
addr.sin_addr = host;
|
||||
addr.sin_port = htons((u_short)_port);
|
||||
addr.sin_family = AF_INET;
|
||||
int conn = connect(sock, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
|
||||
if (conn < 0) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSocketConnect reason:conn error:error];
|
||||
}
|
||||
int cntl = fcntl(sock, F_SETFL, O_NONBLOCK);
|
||||
if (cntl < 0) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSocketFileControl reason:cntl error:error];
|
||||
}
|
||||
int set = 1, sopt = setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
|
||||
if (sopt < 0) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSocketOptions reason:sopt error:error];
|
||||
}
|
||||
_socket = sock;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)connectSSLWithError:(NSError *__autoreleasing *)error
|
||||
{
|
||||
SSLContextRef context = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType);
|
||||
if (!context) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSSLContext error:error];
|
||||
}
|
||||
OSStatus setio = SSLSetIOFuncs(context, NWSSLRead, NWSSLWrite);
|
||||
if (setio != errSecSuccess) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSSLIOFuncs reason:setio error:error];
|
||||
}
|
||||
OSStatus setconn = SSLSetConnection(context, (SSLConnectionRef)(NSInteger)_socket);
|
||||
if (setconn != errSecSuccess) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSSLConnection reason:setconn error:error];
|
||||
}
|
||||
OSStatus setpeer = SSLSetPeerDomainName(context, _host.UTF8String, strlen(_host.UTF8String));
|
||||
if (setpeer != errSecSuccess) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSSLPeerDomainName reason:setpeer error:error];
|
||||
}
|
||||
OSStatus setcert = SSLSetCertificate(context, (__bridge CFArrayRef)@[_identity]);
|
||||
if (setcert != errSecSuccess) {
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSSLCertificate reason:setcert error:error];
|
||||
}
|
||||
_context = context;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)handshakeSSLWithError:(NSError *__autoreleasing *)error
|
||||
{
|
||||
OSStatus status = errSSLWouldBlock;
|
||||
for (NSUInteger i = 0; i < NWSSL_HANDSHAKE_TRY_COUNT && status == errSSLWouldBlock; i++) {
|
||||
status = SSLHandshake(_context);
|
||||
}
|
||||
switch (status) {
|
||||
case errSecSuccess: return YES;
|
||||
case errSSLWouldBlock: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeTimeout error:error];
|
||||
case errSecIO: return [NWErrorUtil noWithErrorCode:kNWErrorSSLDroppedByServer error:error];
|
||||
case errSecAuthFailed: return [NWErrorUtil noWithErrorCode:kNWErrorSSLAuthFailed error:error];
|
||||
case errSSLUnknownRootCert: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeUnknownRootCert error:error];
|
||||
case errSSLNoRootCert: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeNoRootCert error:error];
|
||||
case errSSLCertExpired: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeCertExpired error:error];
|
||||
case errSSLXCertChainInvalid: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeXCertChainInvalid error:error];
|
||||
case errSSLClientCertRequested: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeClientCertRequested error:error];
|
||||
case errSSLServerAuthCompleted: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeServerAuthCompleted error:error];
|
||||
case errSSLPeerCertExpired: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakePeerCertExpired error:error];
|
||||
case errSSLPeerCertRevoked: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakePeerCertRevoked error:error];
|
||||
case errSSLPeerCertUnknown: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakePeerCertUnknown error:error];
|
||||
case errSSLInternal: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeInternalError error:error];
|
||||
#if !TARGET_OS_IPHONE
|
||||
case errSecInDarkWake: return [NWErrorUtil noWithErrorCode:kNWErrorSSLInDarkWake error:error];
|
||||
#endif
|
||||
case errSSLClosedAbort: return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeClosedAbort error:error];
|
||||
}
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorSSLHandshakeFail reason:status error:error];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
if (_context) SSLClose(_context);
|
||||
if (_socket >= 0) close(_socket); _socket = -1;
|
||||
if (_context) CFRelease(_context); _context = NULL;
|
||||
}
|
||||
|
||||
#pragma mark - Read Write
|
||||
|
||||
- (BOOL)read:(NSMutableData *)data length:(NSUInteger *)length error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
*length = 0;
|
||||
size_t processed = 0;
|
||||
OSStatus status = SSLRead(_context, data.mutableBytes, data.length, &processed);
|
||||
*length = processed;
|
||||
switch (status) {
|
||||
case errSecSuccess: return YES;
|
||||
case errSSLWouldBlock: return YES;
|
||||
case errSecIO: return [NWErrorUtil noWithErrorCode:kNWErrorReadDroppedByServer error:error];
|
||||
case errSSLClosedAbort: return [NWErrorUtil noWithErrorCode:kNWErrorReadClosedAbort error:error];
|
||||
case errSSLClosedGraceful: return [NWErrorUtil noWithErrorCode:kNWErrorReadClosedGraceful error:error];
|
||||
}
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorReadFail reason:status error:error];
|
||||
}
|
||||
|
||||
- (BOOL)write:(NSData *)data length:(NSUInteger *)length error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
*length = 0;
|
||||
size_t processed = 0;
|
||||
OSStatus status = SSLWrite(_context, data.bytes, data.length, &processed);
|
||||
*length = processed;
|
||||
switch (status) {
|
||||
case errSecSuccess: return YES;
|
||||
case errSSLWouldBlock: return YES;
|
||||
case errSecIO: return [NWErrorUtil noWithErrorCode:kNWErrorWriteDroppedByServer error:error];
|
||||
case errSSLClosedAbort: return [NWErrorUtil noWithErrorCode:kNWErrorWriteClosedAbort error:error];
|
||||
case errSSLClosedGraceful: return [NWErrorUtil noWithErrorCode:kNWErrorWriteClosedGraceful error:error];
|
||||
}
|
||||
return [NWErrorUtil noWithErrorCode:kNWErrorWriteFail reason:status error:error];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
OSStatus NWSSLRead(SSLConnectionRef connection, void *data, size_t *length) {
|
||||
size_t leng = *length;
|
||||
*length = 0;
|
||||
size_t read = 0;
|
||||
ssize_t rcvd = 0;
|
||||
for(; read < leng; read += rcvd) {
|
||||
rcvd = recv((int)connection, (char *)data + read, leng - read, 0);
|
||||
if (rcvd <= 0) break;
|
||||
}
|
||||
*length = read;
|
||||
if (rcvd > 0 || !leng) {
|
||||
return errSecSuccess;
|
||||
}
|
||||
if (!rcvd) {
|
||||
return errSSLClosedGraceful;
|
||||
}
|
||||
switch (errno) {
|
||||
case EAGAIN: return errSSLWouldBlock;
|
||||
case ECONNRESET: return errSSLClosedAbort;
|
||||
}
|
||||
return errSecIO;
|
||||
}
|
||||
|
||||
OSStatus NWSSLWrite(SSLConnectionRef connection, const void *data, size_t *length) {
|
||||
size_t leng = *length;
|
||||
*length = 0;
|
||||
size_t sent = 0;
|
||||
ssize_t wrtn = 0;
|
||||
for (; sent < leng; sent += wrtn) {
|
||||
wrtn = write((int)connection, (char *)data + sent, leng - sent);
|
||||
if (wrtn <= 0) break;
|
||||
}
|
||||
*length = sent;
|
||||
if (wrtn > 0 || !leng) {
|
||||
return errSecSuccess;
|
||||
}
|
||||
switch (errno) {
|
||||
case EAGAIN: return errSSLWouldBlock;
|
||||
case EPIPE: return errSSLClosedAbort;
|
||||
}
|
||||
return errSecIO;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// NWSecTools.h
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWType.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/** A collection of tools for reading, converting and inspecting Keychain objects and PKCS #12 files.
|
||||
|
||||
This is practically the glue that connects this framework to the Security framework and allows interacting with the OS Keychain and PKCS #12 files. It is mostly an Objective-C around the Security framework, including the benefits of ARC. `NWIdentityRef`, `NWCertificateRef` and `NWKeyRef` represent respectively `SecIdentityRef`, `SecCertificateRef`, `SecKeyRef`. It uses Cocoa-style error handling, so methods return `nil` or `NO` if an error occurred.
|
||||
*/
|
||||
@interface NWSecTools : NSObject
|
||||
|
||||
/** @name Initialization */
|
||||
|
||||
/** Read an identity from a PKCS #12 file (.p12) that contains a single certificate-key pair. */
|
||||
+ (NWIdentityRef)identityWithPKCS12Data:(NSData *)pkcs12 password:(NSString *)password error:(NSError **)error;
|
||||
|
||||
/** Read all identities from a PKCS #12 file (.p12). */
|
||||
+ (NSArray *)identitiesWithPKCS12Data:(NSData *)pkcs12 password:(NSString *)password error:(NSError **)error;
|
||||
|
||||
/** List all push certificates present in the OS Keychain. */
|
||||
+ (NSArray *)keychainCertificatesWithError:(NSError **)error;
|
||||
|
||||
/** @name Sec Wrappers */
|
||||
|
||||
/** Returns the certificate contained by the identity. */
|
||||
+ (NWCertificateRef)certificateWithIdentity:(NWIdentityRef)identity error:(NSError **)error;
|
||||
|
||||
/** Returns the key contained by the identity. */
|
||||
+ (NWKeyRef)keyWithIdentity:(NWIdentityRef)identity error:(NSError **)error;
|
||||
|
||||
/** Reads an X.509 certificate from a DER file. */
|
||||
+ (NWCertificateRef)certificateWithData:(NSData *)data;
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
/** Searches the OS Keychain for an identity (the key) that matches the certificate. (OS X only) */
|
||||
+ (NWIdentityRef)keychainIdentityWithCertificate:(NWCertificateRef)certificate error:(NSError **)error;
|
||||
#endif
|
||||
|
||||
/** @name Inspection */
|
||||
|
||||
/** Extracts the type and summary string. */
|
||||
+ (NWCertType)typeWithCertificate:(NWCertificateRef)certificate summary:(NSString **)summary;
|
||||
|
||||
/** Extracts the summary string. */
|
||||
+ (NSString *)summaryWithCertificate:(NWCertificateRef)certificate;
|
||||
|
||||
/** Tells what environment options can be used with this identity (Development(sandbox)/Production server or both). */
|
||||
+ (NWEnvironmentOptions)environmentOptionsForIdentity:(NWIdentityRef)identity;
|
||||
|
||||
/** Tells what environment options can be used with this certificate (Development(sandbox)/Production server or both). */
|
||||
+ (NWEnvironmentOptions)environmentOptionsForCertificate:(NWCertificateRef)certificate;
|
||||
|
||||
/** Tells if the certificate can be used for connecting with APNs. */
|
||||
+ (BOOL)isPushCertificate:(NWCertificateRef)certificate;
|
||||
|
||||
/** Composes a dictionary describing the characteristics of the identity. */
|
||||
+ (NSDictionary *)inspectIdentity:(NWIdentityRef)identity;
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
/** Extracts the expiration date. */
|
||||
+ (NSDate *)expirationWithCertificate:(NWCertificateRef)certificate;
|
||||
|
||||
/** Extracts given properties of certificate, see `SecCertificateOIDs.h`, use `nil` to get all. */
|
||||
+ (NSDictionary *)valuesWithCertificate:(NWCertificateRef)certificate keys:(NSArray *)keys error:(NSError **)error;
|
||||
#endif
|
||||
|
||||
// deprecated
|
||||
|
||||
+ (BOOL)isSandboxIdentity:(NWIdentityRef)identity __deprecated;
|
||||
+ (BOOL)isSandboxCertificate:(NWCertificateRef)certificate __deprecated;
|
||||
+ (NWEnvironment)environmentForIdentity:(NWIdentityRef)identity;
|
||||
+ (NWEnvironment)environmentForCertificate:(NWCertificateRef)certificate;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,335 @@
|
||||
//
|
||||
// NWSecTools.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWSecTools.h"
|
||||
|
||||
@implementation NWSecTools
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (NWIdentityRef)identityWithPKCS12Data:(NSData *)pkcs12 password:(NSString *)password error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSArray *identities = [self identitiesWithPKCS12Data:pkcs12 password:password error:error];
|
||||
if (!identities) {
|
||||
return nil;
|
||||
}
|
||||
if (identities.count == 0) {
|
||||
return [NWErrorUtil nilWithErrorCode:kNWErrorPKCS12NoItems error:error];
|
||||
}
|
||||
if (identities.count > 1) {
|
||||
return [NWErrorUtil nilWithErrorCode:kNWErrorPKCS12MultipleItems reason:identities.count error:error];
|
||||
}
|
||||
return identities.lastObject;
|
||||
}
|
||||
|
||||
+ (NSArray *)identitiesWithPKCS12Data:(NSData *)pkcs12 password:(NSString *)password error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
if (!pkcs12.length) {
|
||||
return [NWErrorUtil nilWithErrorCode:kNWErrorPKCS12EmptyData error:error];
|
||||
}
|
||||
NSArray *dicts = [self allIdentitiesWithPKCS12Data:pkcs12 password:password error:error];
|
||||
if (!dicts) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray *ids = @[].mutableCopy;
|
||||
for (NSDictionary *dict in dicts) {
|
||||
NWIdentityRef identity = dict[(__bridge id)kSecImportItemIdentity];
|
||||
if (identity) {
|
||||
NWCertificateRef certificate = [self certificateWithIdentity:identity error:error];
|
||||
if (!certificate) {
|
||||
return nil;
|
||||
}
|
||||
if ([self isPushCertificate:certificate]) {
|
||||
NWKeyRef key = [self keyWithIdentity:identity error:error];
|
||||
if (!key) {
|
||||
return nil;
|
||||
}
|
||||
[ids addObject:identity];
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
+ (NSArray *)keychainCertificatesWithError:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSArray *candidates = [self allKeychainCertificatesWithError:error];
|
||||
if (!candidates) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray *certs = [[NSMutableArray alloc] init];
|
||||
for (id certificate in candidates) {
|
||||
if ([self isPushCertificate:certificate]) {
|
||||
[certs addObject:certificate];
|
||||
}
|
||||
}
|
||||
return certs;
|
||||
}
|
||||
|
||||
#pragma mark - Inspection
|
||||
|
||||
+ (NWCertType)typeWithCertificate:(NWCertificateRef)certificate summary:(NSString **)summary
|
||||
{
|
||||
if (summary) *summary = nil;
|
||||
NSString *name = [self plainSummaryWithCertificate:certificate];
|
||||
for (NWCertType t = kNWCertTypeNone; t < kNWCertTypeUnknown; t++) {
|
||||
NSString *prefix = [self prefixWithCertType:t];
|
||||
if (prefix && [name hasPrefix:prefix]) {
|
||||
if (summary) *summary = [name substringFromIndex:prefix.length];
|
||||
return t;
|
||||
}
|
||||
}
|
||||
if (summary) *summary = name;
|
||||
return kNWCertTypeUnknown;
|
||||
}
|
||||
|
||||
+ (NSString *)summaryWithCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
NSString *result = nil;
|
||||
[self typeWithCertificate:certificate summary:&result];
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (NWEnvironmentOptions)environmentOptionsForIdentity:(NWIdentityRef)identity
|
||||
{
|
||||
NWCertificateRef certificate = [self certificateWithIdentity:identity error:nil];
|
||||
return [self environmentOptionsForCertificate:certificate];
|
||||
}
|
||||
|
||||
+ (NWEnvironmentOptions)environmentOptionsForCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
switch ([self typeWithCertificate:certificate summary:nil]) {
|
||||
case kNWCertTypeIOSDevelopment:
|
||||
case kNWCertTypeMacDevelopment:
|
||||
return NWEnvironmentOptionSandbox;
|
||||
|
||||
case kNWCertTypeIOSProduction:
|
||||
case kNWCertTypeMacProduction:
|
||||
return NWEnvironmentOptionProduction;
|
||||
case kNWCertTypeSimplified:
|
||||
case kNWCertTypeWebProduction:
|
||||
case kNWCertTypeVoIPServices:
|
||||
case kNWCertTypeWatchKitServices:
|
||||
case kNWCertTypePasses:
|
||||
return NWEnvironmentOptionAny;
|
||||
case kNWCertTypeNone:
|
||||
case kNWCertTypeUnknown:
|
||||
break;
|
||||
}
|
||||
return NWEnvironmentOptionNone;
|
||||
}
|
||||
|
||||
+ (BOOL)isPushCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
switch ([self typeWithCertificate:certificate summary:nil]) {
|
||||
case kNWCertTypeIOSDevelopment:
|
||||
case kNWCertTypeMacDevelopment:
|
||||
case kNWCertTypeIOSProduction:
|
||||
case kNWCertTypeMacProduction:
|
||||
case kNWCertTypeSimplified:
|
||||
case kNWCertTypeWebProduction:
|
||||
case kNWCertTypeVoIPServices:
|
||||
case kNWCertTypeWatchKitServices:
|
||||
case kNWCertTypePasses:
|
||||
return YES;
|
||||
case kNWCertTypeNone:
|
||||
case kNWCertTypeUnknown:
|
||||
break;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSString *)prefixWithCertType:(NWCertType)type
|
||||
{
|
||||
switch (type) {
|
||||
case kNWCertTypeIOSDevelopment: return @"Apple Development IOS Push Services: ";
|
||||
case kNWCertTypeIOSProduction: return @"Apple Production IOS Push Services: ";
|
||||
case kNWCertTypeMacDevelopment: return @"Apple Development Mac Push Services: ";
|
||||
case kNWCertTypeMacProduction: return @"Apple Production Mac Push Services: ";
|
||||
case kNWCertTypeSimplified: return @"Apple Push Services: ";
|
||||
case kNWCertTypeWebProduction: return @"Website Push ID: ";
|
||||
case kNWCertTypeVoIPServices: return @"VoIP Services: ";
|
||||
case kNWCertTypeWatchKitServices: return @"WatchKit Services: ";
|
||||
case kNWCertTypePasses: return @"Pass Type ID: ";
|
||||
case kNWCertTypeNone:
|
||||
case kNWCertTypeUnknown:
|
||||
break;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (NSDictionary *)inspectIdentity:(NWIdentityRef)identity
|
||||
{
|
||||
if (!identity) return nil;
|
||||
NSMutableDictionary *result = @{}.mutableCopy;
|
||||
SecCertificateRef certificate = NULL;
|
||||
OSStatus certstat = SecIdentityCopyCertificate((__bridge SecIdentityRef)identity, &certificate);
|
||||
result[@"has_certificate"] = @(!!certificate);
|
||||
if (certstat) result[@"certificate_error"] = @(certstat);
|
||||
if (certificate) {
|
||||
result[@"subject_summary"] = CFBridgingRelease(SecCertificateCopySubjectSummary(certificate));
|
||||
result[@"der_data"] = CFBridgingRelease(SecCertificateCopyData(certificate));
|
||||
CFRelease(certificate);
|
||||
}
|
||||
SecKeyRef key = NULL;
|
||||
OSStatus keystat = SecIdentityCopyPrivateKey((__bridge SecIdentityRef)identity, &key);
|
||||
result[@"has_key"] = @(!!key);
|
||||
if (keystat) result[@"key_error"] = @(keystat);
|
||||
if (key) {
|
||||
result[@"block_size"] = @(SecKeyGetBlockSize(key));
|
||||
CFRelease(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma mark - Sec wrappers
|
||||
|
||||
+ (NWCertificateRef)certificateWithData:(NSData *)data
|
||||
{
|
||||
return data ? CFBridgingRelease(SecCertificateCreateWithData(kCFAllocatorDefault, (__bridge CFDataRef)data)) : nil;
|
||||
}
|
||||
|
||||
+ (NSString *)plainSummaryWithCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
return certificate ? CFBridgingRelease(SecCertificateCopySubjectSummary((__bridge SecCertificateRef)certificate)) : nil;
|
||||
}
|
||||
|
||||
+ (NSData *)derDataWithCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
return certificate ? CFBridgingRelease(SecCertificateCopyData((__bridge SecCertificateRef)certificate)) : nil;
|
||||
}
|
||||
|
||||
+ (NWCertificateRef)certificateWithIdentity:(NWIdentityRef)identity error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
SecCertificateRef cert = NULL;
|
||||
OSStatus status = identity ? SecIdentityCopyCertificate((__bridge SecIdentityRef)identity, &cert) : errSecParam;
|
||||
NWCertificateRef certificate = CFBridgingRelease(cert);
|
||||
if (status != errSecSuccess || !cert) {
|
||||
return [NWErrorUtil nilWithErrorCode:kNWErrorIdentityCopyCertificate reason:status error:error];
|
||||
}
|
||||
return certificate;
|
||||
}
|
||||
|
||||
+ (NWKeyRef)keyWithIdentity:(NWIdentityRef)identity error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
SecKeyRef k = NULL;
|
||||
OSStatus status = identity ? SecIdentityCopyPrivateKey((__bridge SecIdentityRef)identity, &k) : errSecParam;
|
||||
NWKeyRef key = CFBridgingRelease(k);
|
||||
if (status != errSecSuccess || !k) {
|
||||
return [NWErrorUtil nilWithErrorCode:kNWErrorIdentityCopyPrivateKey reason:status error:error];
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
+ (NSArray *)allIdentitiesWithPKCS12Data:(NSData *)data password:(NSString *)password error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSDictionary *options = password ? @{(__bridge id)kSecImportExportPassphrase: password} : @{};
|
||||
CFArrayRef items = NULL;
|
||||
OSStatus status = data ? SecPKCS12Import((__bridge CFDataRef)data, (__bridge CFDictionaryRef)options, &items) : errSecParam;
|
||||
NSArray *dicts = CFBridgingRelease(items);
|
||||
if (status != errSecSuccess || !items) {
|
||||
switch (status) {
|
||||
case errSecDecode: return [NWErrorUtil nilWithErrorCode:kNWErrorPKCS12Decode error:error];
|
||||
case errSecAuthFailed: return [NWErrorUtil nilWithErrorCode:kNWErrorPKCS12AuthFailed error:error];
|
||||
#if !TARGET_OS_IPHONE
|
||||
case errSecPkcs12VerifyFailure: return [NWErrorUtil nilWithErrorCode:kNWErrorPKCS12Password error:error];
|
||||
case errSecPassphraseRequired: return [NWErrorUtil nilWithErrorCode:kNWErrorPKCS12PasswordRequired error:error];
|
||||
#endif
|
||||
}
|
||||
return [NWErrorUtil nilWithErrorCode:kNWErrorPKCS12Import reason:status error:error];
|
||||
}
|
||||
return dicts;
|
||||
}
|
||||
|
||||
+ (NSArray *)allKeychainCertificatesWithError:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSDictionary *options = @{(__bridge id)kSecClass: (__bridge id)kSecClassCertificate,
|
||||
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll};
|
||||
CFArrayRef certs = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)options, (CFTypeRef *)&certs);
|
||||
NSArray *certificates = CFBridgingRelease(certs);
|
||||
if (status != errSecSuccess || !certs) {
|
||||
return [NWErrorUtil nilWithErrorCode:kNWErrorKeychainCopyMatching reason:status error:error];
|
||||
}
|
||||
return certificates;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
+ (NWIdentityRef)keychainIdentityWithCertificate:(NWCertificateRef)certificate error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
SecIdentityRef ident = NULL;
|
||||
OSStatus status = certificate ? SecIdentityCreateWithCertificate(NULL, (__bridge SecCertificateRef)certificate, &ident) : errSecParam;
|
||||
NWIdentityRef identity = CFBridgingRelease(ident);
|
||||
if (status != errSecSuccess || !ident) {
|
||||
switch (status) {
|
||||
case errSecItemNotFound: return [NWErrorUtil nilWithErrorCode:kNWErrorKeychainItemNotFound error:error];
|
||||
}
|
||||
return [NWErrorUtil nilWithErrorCode:kNWErrorKeychainCreateIdentity reason:status error:error];
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
+ (NSDate *)expirationWithCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
return [self valueWithCertificate:certificate key:(__bridge id)kSecOIDInvalidityDate];
|
||||
}
|
||||
|
||||
+ (id)valueWithCertificate:(NWCertificateRef)certificate key:(id)key
|
||||
{
|
||||
return [self valuesWithCertificate:certificate keys:@[key] error:nil][key][(__bridge id)kSecPropertyKeyValue];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)valuesWithCertificate:(NWCertificateRef)certificate keys:(NSArray *)keys error:(NSError **)error
|
||||
{
|
||||
CFErrorRef e = NULL;
|
||||
NSDictionary *result = CFBridgingRelease(SecCertificateCopyValues((__bridge SecCertificateRef)certificate, (__bridge CFArrayRef)keys, &e));
|
||||
if (error) *error = CFBridgingRelease(e);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
#pragma mark - Deprecated
|
||||
|
||||
+ (BOOL)isSandboxIdentity:(NWIdentityRef)identity
|
||||
{
|
||||
return [self environmentForIdentity:identity] == NWEnvironmentSandbox;
|
||||
}
|
||||
|
||||
+ (BOOL)isSandboxCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
return [self environmentForCertificate:certificate] == NWEnvironmentSandbox;
|
||||
}
|
||||
|
||||
+ (NWEnvironment)environmentForIdentity:(NWIdentityRef)identity
|
||||
{
|
||||
NWCertificateRef certificate = [self certificateWithIdentity:identity error:nil];
|
||||
return [self environmentForCertificate:certificate];
|
||||
}
|
||||
|
||||
+ (NWEnvironment)environmentForCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
switch ([self typeWithCertificate:certificate summary:nil]) {
|
||||
case kNWCertTypeIOSDevelopment:
|
||||
case kNWCertTypeMacDevelopment:
|
||||
return NWEnvironmentSandbox;
|
||||
|
||||
case kNWCertTypeIOSProduction:
|
||||
case kNWCertTypeMacProduction:
|
||||
return NWEnvironmentProduction;
|
||||
case kNWCertTypeSimplified:
|
||||
case kNWCertTypeWebProduction:
|
||||
case kNWCertTypeVoIPServices:
|
||||
case kNWCertTypeWatchKitServices:
|
||||
case kNWCertTypePasses:
|
||||
case kNWCertTypeNone:
|
||||
case kNWCertTypeUnknown:
|
||||
break;
|
||||
}
|
||||
return NWEnvironmentNone;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// NWType.h
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2014 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/** The current and past data formats supported by APNs. For more information see Apple documentation under 'Legacy Information'. */
|
||||
typedef NS_ENUM(NSInteger, NWNotificationType) {
|
||||
/** The 'Simple Notification Format'. The oldest format, simply concatenates the device token and payload. */
|
||||
kNWNotificationType0 = 0,
|
||||
/** The 'Enhanced Notification Format'. Similar to the previous format, but includes and identifier and expiration date. */
|
||||
kNWNotificationType1 = 1,
|
||||
/** The 'Binary Interface and Notification Format'. The latest, more extensible format that allows for attributes like priority. */
|
||||
kNWNotificationType2 = 2,
|
||||
};
|
||||
|
||||
/** Types of push certificates. */
|
||||
typedef NS_ENUM(NSInteger, NWCertType) {
|
||||
/** None. */
|
||||
kNWCertTypeNone = 0,
|
||||
/** iOS Development. */
|
||||
kNWCertTypeIOSDevelopment = 1,
|
||||
/** iOS Production. */
|
||||
kNWCertTypeIOSProduction = 2,
|
||||
/** OS X Development. */
|
||||
kNWCertTypeMacDevelopment = 3,
|
||||
/** OS X Production. */
|
||||
kNWCertTypeMacProduction = 4,
|
||||
/** Simplified Certificate Handling. */
|
||||
kNWCertTypeSimplified = 5,
|
||||
/** Web Push Production. */
|
||||
kNWCertTypeWebProduction = 6,
|
||||
/** VoIP Services. */
|
||||
kNWCertTypeVoIPServices = 7,
|
||||
/** WatchKit Services. */
|
||||
kNWCertTypeWatchKitServices = 8,
|
||||
/** Pass Type ID. */
|
||||
kNWCertTypePasses = 9,
|
||||
/** Unknown. */
|
||||
kNWCertTypeUnknown = 10,
|
||||
};
|
||||
|
||||
/** An ARC-friendly replacement of SecIdentityRef. */
|
||||
typedef id NWIdentityRef;
|
||||
|
||||
/** An ARC-friendly replacement of SecCertificateRef. */
|
||||
typedef id NWCertificateRef;
|
||||
|
||||
/** An ARC-friendly replacement of SecKeyRef. */
|
||||
typedef id NWKeyRef;
|
||||
|
||||
/** List all error codes. */
|
||||
typedef NS_ENUM(NSInteger, NWError) {
|
||||
/** No error, that's odd. */
|
||||
kNWErrorNone = 0,
|
||||
|
||||
/** APN processing error. */
|
||||
kNWErrorAPNProcessing = -1,
|
||||
/** APN missing device token. */
|
||||
kNWErrorAPNMissingDeviceToken = -2,
|
||||
/** APN missing topic. */
|
||||
kNWErrorAPNMissingTopic = -3,
|
||||
/** APN missing payload. */
|
||||
kNWErrorAPNMissingPayload = -4,
|
||||
/** APN invalid token size. */
|
||||
kNWErrorAPNInvalidTokenSize = -5,
|
||||
/** APN invalid topic size. */
|
||||
kNWErrorAPNInvalidTopicSize = -6,
|
||||
/** APN invalid payload size. */
|
||||
kNWErrorAPNInvalidPayloadSize = -7,
|
||||
/** APN invalid token. */
|
||||
kNWErrorAPNInvalidTokenContent = -8,
|
||||
/** APN unknown reason. */
|
||||
kNWErrorAPNUnknownReason = -9,
|
||||
/** APN shutdown. */
|
||||
kNWErrorAPNShutdown = -10,
|
||||
/** APN unknown error code. */
|
||||
kNWErrorAPNUnknownErrorCode = -11,
|
||||
|
||||
/** Push response command unknown. */
|
||||
kNWErrorPushResponseCommand = -107,
|
||||
/** Push reconnect requires connection. */
|
||||
kNWErrorPushNotConnected = -111,
|
||||
/** Push not fully sent. */
|
||||
kNWErrorPushWriteFail = -112,
|
||||
|
||||
/** Feedback data length unexpected. */
|
||||
kNWErrorFeedbackLength = -108,
|
||||
/** Feedback token length unexpected. */
|
||||
kNWErrorFeedbackTokenLength = -109,
|
||||
|
||||
/** Socket cannot be created. */
|
||||
kNWErrorSocketCreate = -222,
|
||||
/** Socket connecting failed. */
|
||||
kNWErrorSocketConnect = -201,
|
||||
/** Socket host cannot be resolved. */
|
||||
kNWErrorSocketResolveHostName = -219,
|
||||
/** Socket file control failed. */
|
||||
kNWErrorSocketFileControl = -220,
|
||||
/** Socket options cannot be set. */
|
||||
kNWErrorSocketOptions = -221,
|
||||
|
||||
/** SSL connection cannot be set. */
|
||||
kNWErrorSSLConnection = -204,
|
||||
/** SSL context cannot be created. */
|
||||
kNWErrorSSLContext = -202,
|
||||
/** SSL callbacks cannot be set. */
|
||||
kNWErrorSSLIOFuncs = -203,
|
||||
/** SSL peer domain name cannot be set. */
|
||||
kNWErrorSSLPeerDomainName = -205,
|
||||
/** SSL certificate cannot be set. */
|
||||
kNWErrorSSLCertificate = -206,
|
||||
/** SSL handshake dropped by server. */
|
||||
kNWErrorSSLDroppedByServer = -207,
|
||||
/** SSL handshake authentication failed. */
|
||||
kNWErrorSSLAuthFailed = -208,
|
||||
/** SSL handshake failed. */
|
||||
kNWErrorSSLHandshakeFail = -209,
|
||||
/** SSL handshake root not a known anchor. */
|
||||
kNWErrorSSLHandshakeUnknownRootCert = -223,
|
||||
/** SSL handshake chain not verifiable to root. */
|
||||
kNWErrorSSLHandshakeNoRootCert = -224,
|
||||
/** SSL handshake expired certificates. */
|
||||
kNWErrorSSLHandshakeCertExpired = -225,
|
||||
/** SSL handshake invalid certificate chain. */
|
||||
kNWErrorSSLHandshakeXCertChainInvalid = -226,
|
||||
/** SSL handshake expecting client cert. */
|
||||
kNWErrorSSLHandshakeClientCertRequested = -227,
|
||||
/** SSL handshake auth interrupted. */
|
||||
kNWErrorSSLHandshakeServerAuthCompleted = -228,
|
||||
/** SSL handshake certificate expired. */
|
||||
kNWErrorSSLHandshakePeerCertExpired = -229,
|
||||
/** SSL handshake certificate revoked. */
|
||||
kNWErrorSSLHandshakePeerCertRevoked = -230,
|
||||
/** SSL handshake certificate unknown. */
|
||||
kNWErrorSSLHandshakePeerCertUnknown = -233,
|
||||
/** SSL handshake internal error. */
|
||||
kNWErrorSSLHandshakeInternalError = -234,
|
||||
/** SSL handshake in dark wake. */
|
||||
kNWErrorSSLInDarkWake = -231,
|
||||
/** SSL handshake connection closed via error. */
|
||||
kNWErrorSSLHandshakeClosedAbort = -232,
|
||||
/** SSL handshake timeout. */
|
||||
kNWErrorSSLHandshakeTimeout = -218,
|
||||
|
||||
/** Read connection dropped by server. */
|
||||
kNWErrorReadDroppedByServer = -210,
|
||||
/** Read connection error. */
|
||||
kNWErrorReadClosedAbort = -211,
|
||||
/** Read connection closed. */
|
||||
kNWErrorReadClosedGraceful = -212,
|
||||
/** Read failed. */
|
||||
kNWErrorReadFail = -213,
|
||||
|
||||
/** Write connection dropped by server. */
|
||||
kNWErrorWriteDroppedByServer = -214,
|
||||
/** Write connection error. */
|
||||
kNWErrorWriteClosedAbort = -215,
|
||||
/** Write connection closed. */
|
||||
kNWErrorWriteClosedGraceful = -216,
|
||||
/** Write failed. */
|
||||
kNWErrorWriteFail = -217,
|
||||
|
||||
/** Identity does not contain certificate. */
|
||||
kNWErrorIdentityCopyCertificate = -304,
|
||||
/** Identity does not contain private key. */
|
||||
kNWErrorIdentityCopyPrivateKey = -310,
|
||||
|
||||
/** PKCS12 data cannot be imported. */
|
||||
kNWErrorPKCS12Import = -306,
|
||||
/** PKCS12 data is empty. */
|
||||
kNWErrorPKCS12EmptyData = -305,
|
||||
/** PKCS12 data cannot be read or is malformed. */
|
||||
kNWErrorPKCS12Decode = -311,
|
||||
/** PKCS12 data password incorrect. */
|
||||
kNWErrorPKCS12AuthFailed = -312,
|
||||
/** PKCS12 data wrong password. */
|
||||
kNWErrorPKCS12Password = -313,
|
||||
/** PKCS12 data password required. */
|
||||
kNWErrorPKCS12PasswordRequired = -314,
|
||||
/** PKCS12 data contains no identities. */
|
||||
kNWErrorPKCS12NoItems = -307,
|
||||
/** PKCS12 data contains multiple identities. */
|
||||
kNWErrorPKCS12MultipleItems = -309,
|
||||
|
||||
/** Keychain cannot be searched. */
|
||||
kNWErrorKeychainCopyMatching = -401,
|
||||
/** Keychain does not contain private key. */
|
||||
kNWErrorKeychainItemNotFound = -302,
|
||||
/** Keychain does not contain certificate. */
|
||||
kNWErrorKeychainCreateIdentity = -303,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, NWEnvironment) {
|
||||
NWEnvironmentNone = 0,
|
||||
NWEnvironmentSandbox = 1,
|
||||
NWEnvironmentProduction = 2,
|
||||
NWEnvironmentAuto = 3,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, NWEnvironmentOptions) {
|
||||
NWEnvironmentOptionNone = 0,
|
||||
NWEnvironmentOptionSandbox = 1 << NWEnvironmentSandbox,
|
||||
NWEnvironmentOptionProduction = 1 << NWEnvironmentProduction,
|
||||
NWEnvironmentOptionAny = NWEnvironmentOptionSandbox | NWEnvironmentOptionProduction
|
||||
};
|
||||
|
||||
/** NSError dictionary key for integer code that indicates underlying reason. */
|
||||
extern NSString * const NWErrorReasonCodeKey;
|
||||
|
||||
/** A collection of helper methods to support Cocoa-style error handling (`NSError`).
|
||||
|
||||
Most methods in this framework return `NO` or `nil` to indicate an error occurred. In that case an error object will be assigned. This class provides a mapping from codes to description string and some methods to instantiate the `NSError` object.
|
||||
*/
|
||||
|
||||
/** Returns string for given environment, for logging purposes */
|
||||
NSString * descriptionForEnvironentOptions(NWEnvironmentOptions environmentOptions);
|
||||
NSString * descriptionForEnvironent(NWEnvironment environment);
|
||||
NSString * descriptionForCertType(NWCertType type);
|
||||
|
||||
@interface NWErrorUtil : NSObject
|
||||
|
||||
/** @name Helpers */
|
||||
|
||||
/** Assigns the error with provided code and associated description, for returning `NO`. */
|
||||
+ (BOOL)noWithErrorCode:(NWError)code error:(NSError **)error;
|
||||
+ (BOOL)noWithErrorCode:(NWError)code reason:(NSInteger)reason error:(NSError **)error;
|
||||
|
||||
/** Assigns the error with provided code and associated description, for returning `nil`. */
|
||||
+ (id)nilWithErrorCode:(NWError)code error:(NSError **)error;
|
||||
+ (id)nilWithErrorCode:(NWError)code reason:(NSInteger)reason error:(NSError **)error;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// NWType.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2014 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWType.h"
|
||||
|
||||
NSString * const NWErrorReasonCodeKey = @"NWErrorReasonCodeKey";
|
||||
|
||||
NSString * descriptionForEnvironentOptions(NWEnvironmentOptions environmentOptions)
|
||||
{
|
||||
switch (environmentOptions) {
|
||||
case NWEnvironmentOptionNone: return @"No environment";
|
||||
case NWEnvironmentOptionSandbox: return @"Sandbox";
|
||||
case NWEnvironmentOptionProduction: return @"Production";
|
||||
case NWEnvironmentOptionAny: return @"Sandbox|Production";
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString * descriptionForEnvironent(NWEnvironment environment)
|
||||
{
|
||||
switch (environment) {
|
||||
case NWEnvironmentNone: return @"none";
|
||||
case NWEnvironmentProduction: return @"production";
|
||||
case NWEnvironmentSandbox: return @"sandbox";
|
||||
case NWEnvironmentAuto: return @"auto";
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString * descriptionForCertType(NWCertType type)
|
||||
{
|
||||
switch (type) {
|
||||
case kNWCertTypeNone: return @"none";
|
||||
case kNWCertTypeIOSDevelopment:
|
||||
case kNWCertTypeIOSProduction: return @"iOS";
|
||||
case kNWCertTypeMacDevelopment:
|
||||
case kNWCertTypeMacProduction: return @"macOS";
|
||||
case kNWCertTypeSimplified: return @"All";
|
||||
case kNWCertTypeWebProduction: return @"Website";
|
||||
case kNWCertTypeVoIPServices: return @"VoIP";
|
||||
case kNWCertTypeWatchKitServices: return @"WatchKit";
|
||||
case kNWCertTypePasses: return @"Pass";
|
||||
case kNWCertTypeUnknown: return @"unknown";
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@implementation NWErrorUtil
|
||||
|
||||
+ (NSString *)stringWithCode:(NWError)code
|
||||
{
|
||||
switch (code) {
|
||||
case kNWErrorNone : return @"No error, that's odd";
|
||||
|
||||
case kNWErrorAPNProcessing : return @"APN processing error";
|
||||
case kNWErrorAPNMissingDeviceToken : return @"APN missing device token";
|
||||
case kNWErrorAPNMissingTopic : return @"APN missing topic";
|
||||
case kNWErrorAPNMissingPayload : return @"APN missing payload";
|
||||
case kNWErrorAPNInvalidTokenSize : return @"APN invalid token size";
|
||||
case kNWErrorAPNInvalidTopicSize : return @"APN invalid topic size";
|
||||
case kNWErrorAPNInvalidPayloadSize : return @"APN invalid payload size";
|
||||
case kNWErrorAPNInvalidTokenContent : return @"APN invalid token";
|
||||
case kNWErrorAPNUnknownReason : return @"APN unknown reason";
|
||||
case kNWErrorAPNShutdown : return @"APN shutdown";
|
||||
case kNWErrorAPNUnknownErrorCode : return @"APN unknown error code";
|
||||
|
||||
case kNWErrorPushResponseCommand : return @"Push response command unknown";
|
||||
case kNWErrorPushNotConnected : return @"Push reconnect requires connection";
|
||||
case kNWErrorPushWriteFail : return @"Push not fully sent";
|
||||
|
||||
case kNWErrorFeedbackLength : return @"Feedback data length unexpected";
|
||||
case kNWErrorFeedbackTokenLength : return @"Feedback token length unexpected";
|
||||
|
||||
case kNWErrorSocketCreate : return @"Socket cannot be created";
|
||||
case kNWErrorSocketResolveHostName : return @"Socket host cannot be resolved";
|
||||
case kNWErrorSocketConnect : return @"Socket connecting failed";
|
||||
case kNWErrorSocketFileControl : return @"Socket file control failed";
|
||||
case kNWErrorSocketOptions : return @"Socket options cannot be set";
|
||||
|
||||
case kNWErrorSSLConnection : return @"SSL connection cannot be set";
|
||||
case kNWErrorSSLContext : return @"SSL context cannot be created";
|
||||
case kNWErrorSSLIOFuncs : return @"SSL callbacks cannot be set";
|
||||
case kNWErrorSSLPeerDomainName : return @"SSL peer domain name cannot be set";
|
||||
case kNWErrorSSLCertificate : return @"SSL certificate cannot be set";
|
||||
case kNWErrorSSLDroppedByServer : return @"SSL handshake dropped by server";
|
||||
case kNWErrorSSLAuthFailed : return @"SSL handshake authentication failed";
|
||||
case kNWErrorSSLHandshakeFail : return @"SSL handshake failed";
|
||||
case kNWErrorSSLHandshakeUnknownRootCert : return @"SSL handshake root not a known anchor";
|
||||
case kNWErrorSSLHandshakeNoRootCert : return @"SSL handshake chain not verifiable to root";
|
||||
case kNWErrorSSLHandshakeCertExpired : return @"SSL handshake expired certificates";
|
||||
case kNWErrorSSLHandshakeXCertChainInvalid : return @"SSL handshake invalid certificate chain";
|
||||
case kNWErrorSSLHandshakeClientCertRequested : return @"SSL handshake expecting client cert";
|
||||
case kNWErrorSSLHandshakeServerAuthCompleted : return @"SSL handshake auth interrupted";
|
||||
case kNWErrorSSLHandshakePeerCertExpired : return @"SSL handshake certificate expired";
|
||||
case kNWErrorSSLHandshakePeerCertRevoked : return @"SSL handshake certificate revoked";
|
||||
case kNWErrorSSLHandshakePeerCertUnknown : return @"SSL handshake certificate unknown";
|
||||
case kNWErrorSSLHandshakeInternalError : return @"SSL handshake internal error";
|
||||
case kNWErrorSSLInDarkWake : return @"SSL handshake in dark wake";
|
||||
case kNWErrorSSLHandshakeClosedAbort : return @"SSL handshake connection closed via error";
|
||||
case kNWErrorSSLHandshakeTimeout : return @"SSL handshake timeout";
|
||||
|
||||
case kNWErrorReadDroppedByServer : return @"Read connection dropped by server";
|
||||
case kNWErrorReadClosedAbort : return @"Read connection error";
|
||||
case kNWErrorReadClosedGraceful : return @"Read connection closed";
|
||||
case kNWErrorReadFail : return @"Read failed";
|
||||
|
||||
case kNWErrorWriteDroppedByServer : return @"Write connection dropped by server";
|
||||
case kNWErrorWriteClosedAbort : return @"Write connection error";
|
||||
case kNWErrorWriteClosedGraceful : return @"Write connection closed";
|
||||
case kNWErrorWriteFail : return @"Write failed";
|
||||
|
||||
case kNWErrorIdentityCopyCertificate : return @"Identity does not contain certificate";
|
||||
case kNWErrorIdentityCopyPrivateKey : return @"Identity does not contain private key";
|
||||
|
||||
case kNWErrorPKCS12Import : return @"PKCS12 data cannot be imported";
|
||||
case kNWErrorPKCS12EmptyData : return @"PKCS12 data is empty";
|
||||
case kNWErrorPKCS12Decode : return @"PKCS12 data cannot be read or is malformed";
|
||||
case kNWErrorPKCS12AuthFailed : return @"PKCS12 data password incorrect";
|
||||
case kNWErrorPKCS12Password : return @"PKCS12 data wrong password";
|
||||
case kNWErrorPKCS12PasswordRequired : return @"PKCS12 data password required";
|
||||
case kNWErrorPKCS12NoItems : return @"PKCS12 data contains no identities";
|
||||
case kNWErrorPKCS12MultipleItems : return @"PKCS12 data contains multiple identities";
|
||||
|
||||
case kNWErrorKeychainCopyMatching : return @"Keychain cannot be searched";
|
||||
case kNWErrorKeychainItemNotFound : return @"Keychain does not contain private key";
|
||||
case kNWErrorKeychainCreateIdentity : return @"Keychain does not contain certificate";
|
||||
}
|
||||
return @"Unknown";
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
+ (NSError *)errorWithErrorCode:(NWError)code reason:(NSInteger)reason
|
||||
{
|
||||
NSString *description = [self stringWithCode:code];
|
||||
if (reason) description = [NSString stringWithFormat:@"%@ (%i)", description, (int)reason];
|
||||
NSMutableDictionary *info = @{ NSLocalizedDescriptionKey:description }.mutableCopy;
|
||||
if (reason) [info setValue:@(reason) forKey:NWErrorReasonCodeKey];
|
||||
return [NSError errorWithDomain:@"NWPusherErrorDomain" code:code userInfo:info];
|
||||
}
|
||||
|
||||
+ (BOOL)noWithErrorCode:(NWError)code error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self noWithErrorCode:code reason:0 error:error];
|
||||
}
|
||||
|
||||
+ (BOOL)noWithErrorCode:(NWError)code reason:(NSInteger)reason error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSAssert(code != kNWErrorNone, @"code != kNWErrorNone");
|
||||
if (error) *error = [self errorWithErrorCode:code reason:reason];
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (id)nilWithErrorCode:(NWError)code error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
return [self nilWithErrorCode:code reason:0 error:error];
|
||||
}
|
||||
|
||||
+ (id)nilWithErrorCode:(NWError)code reason:(NSInteger)reason error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSAssert(code != kNWErrorNone, @"code != kNWErrorNone");
|
||||
if (error) *error = [self errorWithErrorCode:code reason:reason];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 110 KiB |
@@ -1,328 +0,0 @@
|
||||
//
|
||||
// NWLCore.h
|
||||
// NWLogging
|
||||
//
|
||||
// Created by leonard on 4/25/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#import <CoreFoundation/CFString.h>
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSException.h>
|
||||
#else // __OBJC__
|
||||
#include <assert.h>
|
||||
#endif // __OBJC__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef _NWLOGGING_H_
|
||||
#define _NWLOGGING_H_
|
||||
|
||||
/** Logging active in debug by default. */
|
||||
#ifdef NWL_LIB
|
||||
#define NWL_ACTIVE 1
|
||||
#define NWL_LIB_ NWL_LIB
|
||||
#define NWL_LIB_STR NWL_STR(NWL_LIB)
|
||||
#else
|
||||
#if DEBUG
|
||||
#define NWL_ACTIVE 1
|
||||
#else
|
||||
#define NWL_ACTIVE 0
|
||||
#endif
|
||||
#define NWL_LIB_
|
||||
#define NWL_LIB_STR NULL
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
#define NWL_DEBUG 1
|
||||
#else
|
||||
#define NWL_DEBUG 0
|
||||
#endif
|
||||
|
||||
|
||||
#pragma mark - Convenient logging operations
|
||||
|
||||
#if NWL_ACTIVE
|
||||
|
||||
/** Log directly. */
|
||||
#define NWLog(_format, ...) NWLLogWithoutFilter(, NWL_LIB_, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Log on the 'dbug' tag. */
|
||||
#define NWLogDbug(_format, ...) NWLLogWithFilter(dbug, NWL_LIB_, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Log on the 'info' tag. */
|
||||
#define NWLogInfo(_format, ...) NWLLogWithFilter(info, NWL_LIB_, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Log on the 'warn' tag. */
|
||||
#define NWLogWarn(_format, ...) NWLLogWithFilter(warn, NWL_LIB_, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Log on an 'warn' tag if the condition is false. */
|
||||
#define NWLogWarnIfNot(_condition, _format, ...) do {if (!(_condition)) NWLLogWithFilter(warn, NWL_LIB_, _format, ##__VA_ARGS__);} while (0)
|
||||
#define NWAssert(_condition, _format, ...) NWLogWarnIfNot(_condition, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Log on an error object on the 'warn' tag. */
|
||||
#define NWLogWarnIfError(_error) do {if(_error) NWLLogWithFilter(warn, NWL_LIB_, @"Caught: %@", _error);} while (0)
|
||||
#define NWError(_error) NWLogWarnIfError(_error)
|
||||
|
||||
/** Log on a custom tag. */
|
||||
#define NWLogTag(_tag, _format, ...) NWLLogWithFilter(_tag, NWL_LIB_, _format, ##__VA_ARGS__)
|
||||
|
||||
#else
|
||||
|
||||
#define NWLog(_format, ...)
|
||||
#define NWLogDbug(_format, ...)
|
||||
#define NWLogInfo(_format, ...)
|
||||
#define NWLogWarn(_format, ...)
|
||||
#define NWLogWarnIfNot(_condition, _format, ...) do {break; if (_condition) {}} while (0)
|
||||
#define NWLogWarnIfError(_error)
|
||||
#define NWLogTag(_tag, _format, ...)
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#pragma mark - The Good, the Bad and the Macro
|
||||
|
||||
#define NWL_STR(_a) NWL_STR_(_a)
|
||||
#define NWL_STR_(_a) #_a
|
||||
|
||||
#if __has_feature(objc_arc)
|
||||
#define _NWL_BRIDGE_ __bridge
|
||||
#else
|
||||
#define _NWL_BRIDGE_
|
||||
#endif
|
||||
|
||||
// Objective-C support
|
||||
#ifdef __OBJC__
|
||||
#define _NWL_CFSTRING_(_str) ((_NWL_BRIDGE_ CFStringRef)_str)
|
||||
#define _NWL_EXCEPTION_(_msg) [NSException raise:@"NWLogging" format:@"%@", _msg]
|
||||
#define _NWL_ASSERT_(_msg) NSCAssert1(NO, @"%@", _msg)
|
||||
#define _NWL_LOG_(_msg, _fmt, ...) NSLog(_fmt, ##__VA_ARGS__)
|
||||
#else // __OBJC__
|
||||
#define _NWL_CFSTRING_(_str) CFSTR(_str)
|
||||
#define _NWL_EXCEPTION_(_msg) CFShow(_msg)
|
||||
#define _NWL_ASSERT_(_msg) assert(false)
|
||||
#define _NWL_LOG_(_msg, _fmt, ...) CFShow(_msg)
|
||||
#endif // __OBJC__
|
||||
|
||||
// Misc helper macros
|
||||
#define _NWL_FILE_ (strrchr((__FILE__), '/') + 1)
|
||||
|
||||
/** Combines the format and parameters and prints it to stderr. */
|
||||
#define NWLLogWithoutFilter(_tag, _lib, _fmt, ...) NWLLogWithoutFilter_(_tag, _lib, _fmt, ##__VA_ARGS__)
|
||||
#define NWLLogWithoutFilter_(_tag, _lib, _fmt, ...) do {\
|
||||
NWLContext __context = {(#_tag), (#_lib), _NWL_FILE_, __LINE__, __PRETTY_FUNCTION__};\
|
||||
CFStringRef __message = CFStringCreateWithFormat(NULL, 0, _NWL_CFSTRING_(_fmt), ##__VA_ARGS__);\
|
||||
NWLForwardToPrinters(__context, __message);\
|
||||
CFRelease(__message);\
|
||||
} while (0)
|
||||
|
||||
/** Looks for a match and if so combines the format and parameters and performs the required action. */
|
||||
#define NWLLogWithFilter(_tag, _lib, _fmt, ...) NWLLogWithFilter_(_tag, _lib, _fmt, ##__VA_ARGS__)
|
||||
#define NWLLogWithFilter_(_tag, _lib, _fmt, ...) do {\
|
||||
NWLContext __context = {(#_tag), (#_lib), _NWL_FILE_, __LINE__, __PRETTY_FUNCTION__};\
|
||||
NWLAction __type = NWLMatchingActionForContext(__context);\
|
||||
if (__type) {\
|
||||
CFStringRef __message = CFStringCreateWithFormat(NULL, 0, _NWL_CFSTRING_(_fmt), ##__VA_ARGS__);\
|
||||
switch (__type) {\
|
||||
case kNWLAction_print: NWLForwardToPrinters(__context, __message); break;\
|
||||
case kNWLAction_break: NWLForwardToPrinters(__context, __message); kill(getpid(), SIGINT); break;\
|
||||
case kNWLAction_raise: _NWL_EXCEPTION_(__message); break;\
|
||||
case kNWLAction_assert: _NWL_ASSERT_(__message); break;\
|
||||
default: _NWL_LOG_(__message, _fmt, ##__VA_ARGS__); break;\
|
||||
}\
|
||||
CFRelease(__message);\
|
||||
}\
|
||||
} while (0)
|
||||
|
||||
|
||||
#pragma mark - Type definitions
|
||||
|
||||
/** Kinds of context properties to filter on */
|
||||
typedef enum {
|
||||
kNWLProperty_none = 0,
|
||||
kNWLProperty_tag = 1,
|
||||
kNWLProperty_lib = 2,
|
||||
kNWLProperty_file = 3,
|
||||
kNWLProperty_function = 4,
|
||||
kNWLProperty_count = 5,
|
||||
} NWLProperty;
|
||||
|
||||
/** Kinds of actions to take when a log context matches properties */
|
||||
typedef enum {
|
||||
kNWLAction_none = 0,
|
||||
kNWLAction_print = 1,
|
||||
kNWLAction_break = 2,
|
||||
kNWLAction_raise = 3,
|
||||
kNWLAction_assert = 4,
|
||||
kNWLAction_count = 5,
|
||||
} NWLAction;
|
||||
|
||||
/** The properties of a logging statement. */
|
||||
typedef struct {
|
||||
const char *tag;
|
||||
const char *lib;
|
||||
const char *file;
|
||||
int line;
|
||||
const char *function;
|
||||
} NWLContext;
|
||||
|
||||
|
||||
#pragma mark - Core functions
|
||||
|
||||
/** Sends printing data to all printers. */
|
||||
extern void NWLForwardToPrinters(NWLContext context, CFStringRef message);
|
||||
|
||||
/** Forward printing of line to printers, return true if added. */
|
||||
extern int NWLAddPrinter(const char *name, void(*)(NWLContext, CFStringRef, void *), void *info);
|
||||
|
||||
/** Remove a printer, returns info of the printer. */
|
||||
extern void * NWLRemovePrinter(const char *name);
|
||||
|
||||
/** Clear the printer list. */
|
||||
extern void NWLRemoveAllPrinters(void);
|
||||
|
||||
/** Restore the default stderr printer. */
|
||||
extern void NWLRestoreDefaultPrinters(void);
|
||||
|
||||
/** Add the default stderr printer. */
|
||||
extern void NWLAddDefaultPrinter(void);
|
||||
|
||||
/** Formatter tailored for debugging, with format: "[hr:mn:sc:micros Library File:line] [tag] message", to stderr. */
|
||||
extern void NWLDefaultPrinter(NWLContext context, CFStringRef message, void *info);
|
||||
|
||||
|
||||
/** Tests context (like lib and file name) and returns the matching action. */
|
||||
extern NWLAction NWLMatchingActionForContext(NWLContext context);
|
||||
|
||||
/** Activates and action for these filter properties. */
|
||||
extern int NWLAddFilter(const char *tag, const char *lib, const char *file, const char *function, NWLAction action);
|
||||
|
||||
/** Finds filter that machtes these filter properties and returns its action. */
|
||||
extern NWLAction NWLHasFilter(const char *tag, const char *lib, const char *file, const char *function);
|
||||
|
||||
/** Remove all filters that are included by these filter properties. */
|
||||
extern int NWLRemoveMatchingFilters(const char *tag, const char *lib, const char *file, const char *function);
|
||||
|
||||
/** Remove all actions for all properties. */
|
||||
extern void NWLRemoveAllFilters(void);
|
||||
|
||||
/** Restore the default print-on-warn filter. */
|
||||
extern void NWLRestoreDefaultFilters(void);
|
||||
|
||||
|
||||
/** Reset the clock on log prints to 00:00:00. */
|
||||
extern void NWLResetPrintClock(void);
|
||||
|
||||
/** Offset the clock on log prints with seconds. */
|
||||
extern void NWLOffsetPrintClock(double seconds);
|
||||
|
||||
/** Restore the clock on log prints to UTC time. */
|
||||
extern void NWLRestorePrintClock(void);
|
||||
|
||||
/** Provides clock values, returns time since epoch or since reset. */
|
||||
extern double NWLClock(int *hour, int *minute, int *second, int *micro);
|
||||
|
||||
/** Returns a human-readable summary of this logger, returns the length of the about text excluding the null byte independent of 'size'. */
|
||||
extern int NWLAboutString(char *buffer, int size);
|
||||
|
||||
/** Log the internal state. */
|
||||
extern void NWLogAbout(void);
|
||||
|
||||
|
||||
#pragma mark - Convenient logging configuration
|
||||
|
||||
/** Activate the printing of all info statements. */
|
||||
extern void NWLPrintInfo(void);
|
||||
|
||||
/** Activate the printing of all warn statements. */
|
||||
extern void NWLPrintWarn(void);
|
||||
|
||||
/** Activate the printing of all dbug statements. */
|
||||
extern void NWLPrintDbug(void);
|
||||
|
||||
/** Activate the printing of all statements on a custom tag. */
|
||||
extern void NWLPrintTag(const char *tag);
|
||||
|
||||
/** Activate the printing of all statements. */
|
||||
extern void NWLPrintAll(void);
|
||||
|
||||
|
||||
/** Activate the printing of all info statements in one lib. */
|
||||
extern void NWLPrintInfoInLib(const char *lib);
|
||||
|
||||
/** Activate the printing of all warn statements in one lib. */
|
||||
extern void NWLPrintWarnInLib(const char *lib);
|
||||
|
||||
/** Activate the printing of all dbug statements in one lib. */
|
||||
extern void NWLPrintDbugInLib(const char *lib);
|
||||
|
||||
/** Activate the printing of custom tag statements in one lib. */
|
||||
extern void NWLPrintTagInLib(const char *tag, const char *lib);
|
||||
|
||||
/** Activate the printing of all statements in one lib. */
|
||||
extern void NWLPrintAllInLib(const char *lib);
|
||||
|
||||
|
||||
/** Activate printing of dbug statements in a file. */
|
||||
extern void NWLPrintDbugInFile(const char *file);
|
||||
|
||||
/** Activate printing of dbug statements in a function, of the form: -[CLass parmeter:parmeter:]. */
|
||||
extern void NWLPrintDbugInFunction(const char *function);
|
||||
|
||||
|
||||
/** Activate breaking on all warn statements. */
|
||||
extern void NWLBreakWarn(void);
|
||||
|
||||
/** Activate breaking on all warn statements in one lib. */
|
||||
extern void NWLBreakWarnInLib(const char *lib);
|
||||
|
||||
/** Activate breaking of custom tag statements. */
|
||||
extern void NWLBreakTag(const char *tag);
|
||||
|
||||
/** Activate breaking of custom tag statements in one lib. */
|
||||
extern void NWLBreakTagInLib(const char *tag, const char *lib);
|
||||
|
||||
|
||||
/** Deactivate actions of all info statements. */
|
||||
extern void NWLClearInfo(void);
|
||||
|
||||
/** Deactivate actions of all warn statements. */
|
||||
extern void NWLClearWarn(void);
|
||||
|
||||
/** Deactivate actions of all dbug statements. */
|
||||
extern void NWLClearDbug(void);
|
||||
|
||||
/** Deactivate actions of custom tag statements. */
|
||||
extern void NWLClearTag(const char *tag);
|
||||
|
||||
/** Deactivate actions of all statements in one lib. */
|
||||
extern void NWLClearAllInLib(const char *lib);
|
||||
|
||||
/** Removes all actions for all filters. */
|
||||
extern void NWLClearAll(void);
|
||||
|
||||
|
||||
/** Print internal state info to stderr. */
|
||||
extern void NWLDump(void);
|
||||
extern void NWLDumpFlags(int active, const char *lib, int debug, const char *file, int line, const char *function);
|
||||
extern void NWLDumpConfig();
|
||||
#define NWLDump() do {NWLDumpFlags(NWL_ACTIVE, NWL_LIB_STR, NWL_DEBUG, _NWL_FILE_, __LINE__, __PRETTY_FUNCTION__);if(NWL_ACTIVE){NWLDumpConfig();}} while (0)
|
||||
|
||||
/** Print help info for developers to stderr. */
|
||||
extern void NWLDumpHelp(int active, const char *lib, int debug, const char *file, int line, const char *function);
|
||||
#define NWLHelp() NWLDumpHelp(NWL_ACTIVE, NWL_LIB_STR, NWL_DEBUG, _NWL_FILE_, __LINE__, __PRETTY_FUNCTION__)
|
||||
|
||||
|
||||
#endif // _NWLOGGING_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// NWLFilePrinter.h
|
||||
// NWLogging
|
||||
//
|
||||
// Created by leonard on 6/6/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWLPrinter.h"
|
||||
|
||||
|
||||
@interface NWLFilePrinter : NSObject <NWLPrinter>
|
||||
|
||||
@property (nonatomic, assign) NSUInteger maxLogSize;
|
||||
@property (nonatomic, readonly) NSString *path;
|
||||
@property (nonatomic, readonly) NSString *content;
|
||||
|
||||
- (id)init;
|
||||
- (id)initWithFileName:(NSString *)name;
|
||||
- (void)openPath:(NSString *)path;
|
||||
- (void)sync;
|
||||
- (void)clear;
|
||||
- (void)close;
|
||||
|
||||
+ (NSString *)pathForName:(NSString *)name;
|
||||
|
||||
@end
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// NWLLineLogger.h
|
||||
// NWLogging
|
||||
//
|
||||
// Created by leonard on 6/7/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
extern const char *NWLLineLoggerMessage;
|
||||
extern const char *NWLLineLoggerAscii;
|
||||
|
||||
#ifdef __OBJC__
|
||||
|
||||
@interface NWLLineLogger : NSObject
|
||||
|
||||
+ (void)start:(NSUInteger)info;
|
||||
+ (void)start;
|
||||
+ (void)stop;
|
||||
|
||||
+ (NSString *)tag;
|
||||
+ (NSString *)lib;
|
||||
+ (NSString *)file;
|
||||
+ (NSUInteger)line;
|
||||
+ (NSString *)function;
|
||||
+ (NSString *)message;
|
||||
+ (NSString *)ascii;
|
||||
+ (NSUInteger)info;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// NWLLogView.h
|
||||
// NWLogging
|
||||
//
|
||||
// Created by Leo on 8/22/12.
|
||||
//
|
||||
//
|
||||
|
||||
#import "NWLPrinter.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
@interface NWLLogView : UITextView <NWLPrinter>
|
||||
#else
|
||||
@interface NWLLogView : NSTextView <NWLPrinter>
|
||||
#endif
|
||||
|
||||
@property (nonatomic, assign) NSUInteger maxLogSize;
|
||||
|
||||
- (void)appendAndFollowText:(NSString *)text;
|
||||
- (void)appendAndScrollText:(NSString *)text;
|
||||
- (void)safeAppendAndFollowText:(NSString *)text;
|
||||
|
||||
- (void)scrollDown;
|
||||
|
||||
@end
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// NWLMultiLogger.h
|
||||
// NWLogging
|
||||
//
|
||||
// Created by leonard on 6/7/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
@protocol NWLPrinter;
|
||||
|
||||
@interface NWLMultiLogger : NSObject
|
||||
|
||||
@property (nonatomic, readonly) NSUInteger count;
|
||||
|
||||
- (void)addPrinter:(id<NWLPrinter>)printer;
|
||||
- (void)removePrinter:(id<NWLPrinter>)printer;
|
||||
- (void)removeAllPrinters;
|
||||
|
||||
+ (NWLMultiLogger *)shared;
|
||||
|
||||
@end
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// NWLPrinter.h
|
||||
// NWLogging
|
||||
//
|
||||
// Created by leonard on 6/7/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
@protocol NWLPrinter <NSObject>
|
||||
|
||||
- (void)printWithTag:(NSString *)tag lib:(NSString *)lib file:(NSString *)file line:(NSUInteger)line function:(NSString *)function message:(NSString *)message;
|
||||
- (NSString *)name;
|
||||
|
||||
@end
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// NWLTools.h
|
||||
// NWLogging
|
||||
//
|
||||
// Created by leonard on 6/6/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
@interface NWLTools : NSObject
|
||||
|
||||
+ (NSString *)dateMark;
|
||||
+ (NSString *)bundleInfo;
|
||||
+ (NSString *)formatTag:(NSString *)tag lib:(NSString *)lib file:(NSString *)file line:(NSUInteger)line function:(NSString *)function message:(NSString *)message;
|
||||
|
||||
@end
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// NWLogging.h
|
||||
// NWLogging
|
||||
//
|
||||
// Created by leonard on 6/25/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#include "NWLCore.h"
|
||||
|
||||
#ifdef __OBJC__
|
||||
|
||||
#import "NWLFilePrinter.h"
|
||||
#import "NWLMultiLogger.h"
|
||||
#import "NWLPrinter.h"
|
||||
#import "NWLTools.h"
|
||||
#import "NWLLineLogger.h"
|
||||
|
||||
#import "NWLLogView.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import "NWLLogViewController.h"
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,424 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="13771" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner">
|
||||
<connections>
|
||||
<outlet property="_certificatePopup" destination="aJR-Gv-8Xr" id="Xb3-Fs-MxT"/>
|
||||
<outlet property="_countField" destination="gQs-Aa-IJi" id="SFY-EK-SnL"/>
|
||||
<outlet property="_expiryPopup" destination="imk-VN-mp1" id="mXS-SA-ROT"/>
|
||||
<outlet property="_infoField" destination="M70-t1-JuG" id="9pn-ky-LuB"/>
|
||||
<outlet property="_logField" destination="pi6-RR-ayT" id="FBI-Iz-Vsd"/>
|
||||
<outlet property="_payloadField" destination="ad5-lb-L5u" id="vTt-87-yLF"/>
|
||||
<outlet property="_priorityPopup" destination="c5v-al-Hn5" id="40z-8H-ysO"/>
|
||||
<outlet property="_pushButton" destination="pAB-wf-piT" id="DrL-Oa-n2N"/>
|
||||
<outlet property="_reconnectButton" destination="2E1-cP-egh" id="8Zl-4l-UPd"/>
|
||||
<outlet property="_tokenCombo" destination="xpc-0j-elb" id="hRl-BL-h33"/>
|
||||
<outlet property="window" destination="F0z-JX-Cv5" id="aRl-Rf-XNd"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject">
|
||||
<connections>
|
||||
<outlet property="delegate" destination="kaW-YJ-UA5" id="0Xg-Lu-QoV"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="Pusher" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Pusher" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About Pusher" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide Pusher" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Pusher" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Read Feedback" keyEquivalent="r" id="Rfv-zp-RXJ">
|
||||
<connections>
|
||||
<action selector="readFeedback:" target="kaW-YJ-UA5" id="oBW-Zj-F5X"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="n4w-H3-eqQ"/>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<window title="Pusher" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" animationBehavior="default" id="F0z-JX-Cv5">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="600" height="300"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1177"/>
|
||||
<value key="minSize" type="size" width="500" height="200"/>
|
||||
<view key="contentView" id="se5-gp-TjO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="300"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2E1-cP-egh">
|
||||
<rect key="frame" x="480" y="252" width="106" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="94" id="Rut-jE-8JY"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Reconnect" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="a0n-mi-kdf">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="reconnect:" target="kaW-YJ-UA5" id="VyD-I2-NZH"/>
|
||||
</connections>
|
||||
</button>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aJR-Gv-8Xr">
|
||||
<rect key="frame" x="18" y="256" width="463" height="26"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Select certificate" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="RvW-sH-WOM" id="XcZ-r4-iAc">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" title="OtherViews" id="mk0-rA-hIr">
|
||||
<items>
|
||||
<menuItem title="Select certificate" state="on" id="RvW-sH-WOM"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<connections>
|
||||
<action selector="certificateSelected:" target="kaW-YJ-UA5" id="Asf-IM-UFv"/>
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="imk-VN-mp1">
|
||||
<rect key="frame" x="18" y="180" width="155" height="26"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="150" id="rN0-Vp-M6c"/>
|
||||
</constraints>
|
||||
<popUpButtonCell key="cell" type="push" title="Expiry: None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Mez-Zc-mJo" id="t3L-6a-cAn">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" title="OtherViews" id="EfE-P3-RNJ">
|
||||
<items>
|
||||
<menuItem title="Expiry: None" state="on" id="Mez-Zc-mJo"/>
|
||||
<menuItem title="Immediate (0)" id="lvs-rS-dXx"/>
|
||||
<menuItem title="1 minute" id="VgF-KW-kEC"/>
|
||||
<menuItem title="5 minutes" id="o0I-9q-JWY"/>
|
||||
<menuItem title="1 hour" id="IqN-Sd-Bc5"/>
|
||||
<menuItem title="1 day" id="FeS-Pv-RMs"/>
|
||||
<menuItem title="Far past (1)" id="H5e-Qs-MSH"/>
|
||||
<menuItem title="Far future (2^32-1)" id="kse-6y-BQc"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="c5v-al-Hn5">
|
||||
<rect key="frame" x="176" y="180" width="160" height="26"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="155" id="DV9-wc-su5"/>
|
||||
</constraints>
|
||||
<popUpButtonCell key="cell" type="push" title="Priority: None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="5dF-Og-jd1" id="ijp-4f-mXC">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" title="OtherViews" id="XbO-4x-lw5">
|
||||
<items>
|
||||
<menuItem title="Priority: None" state="on" id="5dF-Og-jd1"/>
|
||||
<menuItem title="Conserve power (5)" id="7DJ-kb-Wf3"/>
|
||||
<menuItem title="Immediately (10)" id="eBG-1S-cUL"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<segmentedControl verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="EnU-aP-5PD">
|
||||
<rect key="frame" x="456" y="177" width="126" height="24"/>
|
||||
<segmentedCell key="cell" borderStyle="border" alignment="left" style="rounded" trackingMode="selectOne" id="Olm-G5-Man">
|
||||
<font key="font" metaFont="system"/>
|
||||
<segments>
|
||||
<segment label="Payload" selected="YES"/>
|
||||
<segment label="Log" tag="1"/>
|
||||
</segments>
|
||||
</segmentedCell>
|
||||
<connections>
|
||||
<action selector="selectOutput:" target="kaW-YJ-UA5" id="xaj-fD-0cJ"/>
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pAB-wf-piT">
|
||||
<rect key="frame" x="504" y="13" width="82" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="70" id="huG-oV-gOx"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Push" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="8rV-6u-5aj">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="push:" target="kaW-YJ-UA5" id="hXv-2c-nZa"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="M70-t1-JuG">
|
||||
<rect key="frame" x="18" y="23" width="486" height="14"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" id="d6h-q1-H0e">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<scrollView horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Q6j-iu-9I9">
|
||||
<rect key="frame" x="20" y="45" width="560" height="130"/>
|
||||
<clipView key="contentView" id="e4e-ov-ymY">
|
||||
<rect key="frame" x="1" y="1" width="558" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" findStyle="panel" allowsCharacterPickerTouchBarItem="NO" allowsUndo="YES" allowsNonContiguousLayout="YES" textCompletion="NO" id="ad5-lb-L5u">
|
||||
<rect key="frame" x="0.0" y="0.0" width="558" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<size key="minSize" width="558" height="128"/>
|
||||
<size key="maxSize" width="573" height="10000000"/>
|
||||
<attributedString key="textStorage">
|
||||
<fragment content="..">
|
||||
<attributes>
|
||||
<font key="NSFont" metaFont="smallSystem"/>
|
||||
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
|
||||
</attributes>
|
||||
</fragment>
|
||||
</attributedString>
|
||||
<color key="insertionPointColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<allowedInputSourceLocales>
|
||||
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
|
||||
</allowedInputSourceLocales>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="kaW-YJ-UA5" id="bUh-Oq-w5B"/>
|
||||
</connections>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="66p-aq-6FU">
|
||||
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="sbz-Qw-fOe">
|
||||
<rect key="frame" x="543" y="1" width="16" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gQs-Aa-IJi">
|
||||
<rect key="frame" x="470" y="49" width="104" height="14"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="100" id="Vlu-lQ-8rQ"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="0" id="kX9-da-dUI">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<scrollView hidden="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z4n-XE-cfI">
|
||||
<rect key="frame" x="20" y="45" width="560" height="130"/>
|
||||
<clipView key="contentView" id="uaF-sl-etn">
|
||||
<rect key="frame" x="1" y="1" width="558" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" usesFontPanel="YES" findStyle="panel" continuousSpellChecking="YES" allowsUndo="YES" usesRuler="YES" allowsNonContiguousLayout="YES" quoteSubstitution="YES" dashSubstitution="YES" spellingCorrection="YES" smartInsertDelete="YES" id="pi6-RR-ayT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="558" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<size key="minSize" width="558" height="128"/>
|
||||
<size key="maxSize" width="573" height="10000000"/>
|
||||
<color key="insertionPointColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="5mH-NO-A0y">
|
||||
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="pny-Of-exT">
|
||||
<rect key="frame" x="543" y="1" width="16" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="hU6-Ym-KR3">
|
||||
<rect key="frame" x="18" y="235" width="223" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Should use sandbox environment" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="ros-Zj-bni">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="sanboxCheckBoxDidPressed:" target="kaW-YJ-UA5" id="kzM-IO-iQv"/>
|
||||
</connections>
|
||||
</button>
|
||||
<comboBox verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xpc-0j-elb">
|
||||
<rect key="frame" x="20" y="208" width="563" height="23"/>
|
||||
<comboBoxCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" enabled="NO" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Device push token (only first 64 hex chars used, other text is ignored)" drawsBackground="YES" completes="NO" numberOfVisibleItems="5" id="Dzy-yi-6QY">
|
||||
<font key="font" size="10" name="Monaco"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</comboBoxCell>
|
||||
<connections>
|
||||
<action selector="tokenSelected:" target="kaW-YJ-UA5" id="p9a-qh-3Nh"/>
|
||||
<outlet property="delegate" destination="kaW-YJ-UA5" id="ZZ9-SB-ZBN"/>
|
||||
</connections>
|
||||
</comboBox>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="pAB-wf-piT" secondAttribute="trailing" constant="20" id="1DK-KM-tMh"/>
|
||||
<constraint firstItem="pAB-wf-piT" firstAttribute="top" secondItem="Q6j-iu-9I9" secondAttribute="bottom" constant="4" id="4hK-j5-BnD"/>
|
||||
<constraint firstItem="EnU-aP-5PD" firstAttribute="top" secondItem="imk-VN-mp1" secondAttribute="top" constant="4" id="52s-fR-SJc"/>
|
||||
<constraint firstItem="aJR-Gv-8Xr" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" id="6JP-pH-jau"/>
|
||||
<constraint firstItem="imk-VN-mp1" firstAttribute="top" secondItem="xpc-0j-elb" secondAttribute="bottom" constant="8" id="9KO-B9-3bt"/>
|
||||
<constraint firstItem="imk-VN-mp1" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="20" id="9ZJ-jz-1To"/>
|
||||
<constraint firstItem="c5v-al-Hn5" firstAttribute="top" secondItem="xpc-0j-elb" secondAttribute="bottom" constant="8" id="A7G-lD-MBE"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Q6j-iu-9I9" secondAttribute="trailing" constant="20" id="A8j-am-oIW"/>
|
||||
<constraint firstItem="Q6j-iu-9I9" firstAttribute="top" secondItem="EnU-aP-5PD" secondAttribute="bottom" constant="4" id="DcC-14-fIg"/>
|
||||
<constraint firstAttribute="trailing" secondItem="2E1-cP-egh" secondAttribute="trailing" constant="20" id="Fb7-EE-6Pl"/>
|
||||
<constraint firstItem="pAB-wf-piT" firstAttribute="leading" secondItem="M70-t1-JuG" secondAttribute="trailing" constant="8" id="GxM-KC-Qel"/>
|
||||
<constraint firstItem="EnU-aP-5PD" firstAttribute="top" secondItem="xpc-0j-elb" secondAttribute="bottom" constant="12" id="J5e-d8-3qc"/>
|
||||
<constraint firstItem="xpc-0j-elb" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="20" id="NPP-1E-QSG"/>
|
||||
<constraint firstAttribute="bottom" secondItem="pAB-wf-piT" secondAttribute="bottom" constant="20" id="PCy-YQ-oFx"/>
|
||||
<constraint firstItem="Q6j-iu-9I9" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="20" id="Q0j-zh-MUZ"/>
|
||||
<constraint firstItem="z4n-XE-cfI" firstAttribute="top" secondItem="EnU-aP-5PD" secondAttribute="bottom" constant="4" id="RTl-r4-RsH"/>
|
||||
<constraint firstItem="c5v-al-Hn5" firstAttribute="leading" secondItem="imk-VN-mp1" secondAttribute="trailing" constant="8" id="Syg-OZ-s0W"/>
|
||||
<constraint firstItem="2E1-cP-egh" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" id="VPW-kd-JRb"/>
|
||||
<constraint firstItem="pAB-wf-piT" firstAttribute="top" secondItem="z4n-XE-cfI" secondAttribute="bottom" constant="4" id="VSi-vV-wpY"/>
|
||||
<constraint firstItem="hU6-Ym-KR3" firstAttribute="top" secondItem="aJR-Gv-8Xr" secondAttribute="bottom" constant="8" symbolic="YES" id="ZTZ-NV-b30"/>
|
||||
<constraint firstItem="EnU-aP-5PD" firstAttribute="top" secondItem="c5v-al-Hn5" secondAttribute="top" constant="4" id="emm-N1-qPy"/>
|
||||
<constraint firstAttribute="trailing" secondItem="z4n-XE-cfI" secondAttribute="trailing" constant="20" id="gM0-t7-RqF"/>
|
||||
<constraint firstItem="hU6-Ym-KR3" firstAttribute="leading" secondItem="aJR-Gv-8Xr" secondAttribute="leading" id="h8B-8W-BTt"/>
|
||||
<constraint firstItem="xpc-0j-elb" firstAttribute="top" secondItem="hU6-Ym-KR3" secondAttribute="bottom" constant="8" symbolic="YES" id="hqj-w5-p6V"/>
|
||||
<constraint firstItem="M70-t1-JuG" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="20" id="ief-Fk-3Mg"/>
|
||||
<constraint firstAttribute="trailing" secondItem="gQs-Aa-IJi" secondAttribute="trailing" constant="28" id="oT0-VS-Mam"/>
|
||||
<constraint firstAttribute="bottom" secondItem="M70-t1-JuG" secondAttribute="bottom" constant="23" id="pWW-7F-Cxq"/>
|
||||
<constraint firstItem="pAB-wf-piT" firstAttribute="top" secondItem="gQs-Aa-IJi" secondAttribute="bottom" constant="8" id="pav-J5-oU6"/>
|
||||
<constraint firstAttribute="trailing" secondItem="EnU-aP-5PD" secondAttribute="trailing" constant="20" id="si6-7v-4p9"/>
|
||||
<constraint firstItem="2E1-cP-egh" firstAttribute="leading" secondItem="aJR-Gv-8Xr" secondAttribute="trailing" constant="8" id="tjT-5W-cvD"/>
|
||||
<constraint firstItem="aJR-Gv-8Xr" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="20" id="u3P-9g-oO1"/>
|
||||
<constraint firstItem="z4n-XE-cfI" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="20" id="vi7-Zb-tSW"/>
|
||||
<constraint firstAttribute="trailing" secondItem="xpc-0j-elb" secondAttribute="trailing" constant="20" id="xlI-65-2lc"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</window>
|
||||
<customObject id="kaW-YJ-UA5" customClass="NWAppDelegate">
|
||||
<connections>
|
||||
<outlet property="_certificatePopup" destination="aJR-Gv-8Xr" id="A43-MU-5mS"/>
|
||||
<outlet property="_countField" destination="gQs-Aa-IJi" id="miX-ao-hNk"/>
|
||||
<outlet property="_expiryPopup" destination="imk-VN-mp1" id="t7w-Mt-DKL"/>
|
||||
<outlet property="_infoField" destination="M70-t1-JuG" id="dcG-qS-Mll"/>
|
||||
<outlet property="_logField" destination="pi6-RR-ayT" id="aq6-2Z-ytl"/>
|
||||
<outlet property="_logScroll" destination="z4n-XE-cfI" id="qve-u7-cAW"/>
|
||||
<outlet property="_payloadField" destination="ad5-lb-L5u" id="qbW-La-sgh"/>
|
||||
<outlet property="_priorityPopup" destination="c5v-al-Hn5" id="6zL-7L-7t3"/>
|
||||
<outlet property="_pushButton" destination="pAB-wf-piT" id="ndY-Uh-dgS"/>
|
||||
<outlet property="_reconnectButton" destination="2E1-cP-egh" id="4CL-8O-MKG"/>
|
||||
<outlet property="_sanboxCheckBox" destination="hU6-Ym-KR3" id="9Zb-UH-d23"/>
|
||||
<outlet property="_tokenCombo" destination="xpc-0j-elb" id="f1o-6I-zGl"/>
|
||||
<outlet property="window" destination="F0z-JX-Cv5" id="a4g-51-daC"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -2,11 +2,13 @@
|
||||
// NWAppDelegate.h
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
@interface NWAppDelegate : NSObject <NSApplicationDelegate, NWLPrinter>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
@interface NWAppDelegate : NSObject <NSApplicationDelegate>
|
||||
|
||||
@property (assign) IBOutlet NSWindow *window;
|
||||
|
||||
@@ -0,0 +1,622 @@
|
||||
//
|
||||
// NWAppDelegate.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWAppDelegate.h"
|
||||
#import <PusherKit/PusherKit.h>
|
||||
|
||||
@interface NWAppDelegate () <NWHubDelegate> @end
|
||||
|
||||
@implementation NWAppDelegate {
|
||||
IBOutlet NSPopUpButton *_certificatePopup;
|
||||
IBOutlet NSComboBox *_tokenCombo;
|
||||
IBOutlet NSTextView *_payloadField;
|
||||
IBOutlet NSTextView *_logField;
|
||||
IBOutlet NSTextField *_countField;
|
||||
IBOutlet NSTextField *_infoField;
|
||||
IBOutlet NSButton *_pushButton;
|
||||
IBOutlet NSButton *_reconnectButton;
|
||||
IBOutlet NSPopUpButton *_expiryPopup;
|
||||
IBOutlet NSPopUpButton *_priorityPopup;
|
||||
IBOutlet NSScrollView *_logScroll;
|
||||
IBOutlet NSButton *_sanboxCheckBox;
|
||||
|
||||
NWHub *_hub;
|
||||
NSDictionary *_config;
|
||||
NSArray *_certificateIdentityPairs;
|
||||
NSUInteger _lastSelectedIndex;
|
||||
NWCertificateRef _selectedCertificate;
|
||||
|
||||
dispatch_queue_t _serial;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Application delegate
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification
|
||||
{
|
||||
NWLogInfo(@"Application did finish launching");
|
||||
NWLAddPrinter("NWPusher", NWPusherPrinter, 0);
|
||||
NWLPrintInfo();
|
||||
_serial = dispatch_queue_create("NWAppDelegate", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_certificateIdentityPairs = @[];
|
||||
[self loadCertificatesFromKeychain];
|
||||
[self migrateOldConfigurationIfNeeded];
|
||||
[self loadConfig];
|
||||
[self updateCertificatePopup];
|
||||
|
||||
NSString *payload = [_config valueForKey:@"payload"];
|
||||
_payloadField.string = payload.length ? payload : @"";
|
||||
_payloadField.font = [NSFont fontWithName:@"Monaco" size:10];
|
||||
_payloadField.enabledTextCheckingTypes = 0;
|
||||
_logField.enabledTextCheckingTypes = 0;
|
||||
[self updatePayloadCounter];
|
||||
NWLogInfo(@"");
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)notification
|
||||
{
|
||||
[self saveConfig];
|
||||
NWLRemovePrinter("NWPusher");
|
||||
[_hub disconnect]; _hub.delegate = nil; _hub = nil;
|
||||
NWLogInfo(@"Application will terminate");
|
||||
}
|
||||
|
||||
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)application
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (IBAction)certificateSelected:(NSPopUpButton *)sender
|
||||
{
|
||||
[self connectWithCertificateAtIndex:_certificatePopup.indexOfSelectedItem];
|
||||
}
|
||||
|
||||
- (IBAction)tokenSelected:(NSComboBox *)sender
|
||||
{
|
||||
[self selectTokenAndUpdateCombo];
|
||||
}
|
||||
|
||||
- (void)textDidChange:(NSNotification *)notification
|
||||
{
|
||||
if (notification.object == _payloadField) [self updatePayloadCounter];
|
||||
}
|
||||
|
||||
- (void)controlTextDidChange:(NSNotification *)notification
|
||||
{
|
||||
// if (notification.object == _tokenCombo) [self something];
|
||||
}
|
||||
|
||||
- (IBAction)push:(NSButton *)sender
|
||||
{
|
||||
[self addTokenAndUpdateCombo];
|
||||
[self push];
|
||||
[self upPayloadTextIndex];
|
||||
}
|
||||
|
||||
- (IBAction)reconnect:(NSButton *)sender
|
||||
{
|
||||
[self reconnect];
|
||||
}
|
||||
|
||||
- (IBAction)sanboxCheckBoxDidPressed:(NSButton *)sender
|
||||
{
|
||||
if (_selectedCertificate)
|
||||
{
|
||||
[self reconnect];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)notification:(NWNotification *)notification didFailWithError:(NSError *)error
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
//NSLog(@"failed notification: %@ %@ %lu %lu %lu", notification.payload, notification.token, notification.identifier, notification.expires, notification.priority);
|
||||
NWLogWarn(@"Notification error: %@", error.localizedDescription);
|
||||
});
|
||||
}
|
||||
|
||||
- (IBAction)selectOutput:(NSSegmentedControl *)sender {
|
||||
_logScroll.hidden = sender.selectedSegment != 1;
|
||||
}
|
||||
|
||||
- (IBAction)readFeedback:(id)sender {
|
||||
[self feedback];
|
||||
}
|
||||
|
||||
#pragma mark - Certificate and Identity
|
||||
|
||||
- (void)loadCertificatesFromKeychain
|
||||
{
|
||||
NSError *error = nil;
|
||||
NSArray *certs = [NWSecTools keychainCertificatesWithError:&error];
|
||||
if (!certs) {
|
||||
NWLogWarn(@"Unable to access keychain: %@", error.localizedDescription);
|
||||
}
|
||||
if (!certs.count) {
|
||||
NWLogWarn(@"No push certificates in keychain.");
|
||||
}
|
||||
certs = [certs sortedArrayUsingComparator:^NSComparisonResult(NWCertificateRef a, NWCertificateRef b) {
|
||||
NWEnvironmentOptions envOptionsA = [NWSecTools environmentOptionsForCertificate:a];
|
||||
NWEnvironmentOptions envOptionsB = [NWSecTools environmentOptionsForCertificate:b];
|
||||
if (envOptionsA != envOptionsB) {
|
||||
return envOptionsA < envOptionsB;
|
||||
}
|
||||
NSString *aname = [NWSecTools summaryWithCertificate:a];
|
||||
NSString *bname = [NWSecTools summaryWithCertificate:b];
|
||||
return [aname compare:bname];
|
||||
}];
|
||||
NSMutableArray *pairs = @[].mutableCopy;
|
||||
for (NWCertificateRef c in certs) {
|
||||
[pairs addObject:@[c, NSNull.null]];
|
||||
}
|
||||
_certificateIdentityPairs = [_certificateIdentityPairs arrayByAddingObjectsFromArray:pairs];
|
||||
}
|
||||
|
||||
- (void)updateCertificatePopup
|
||||
{
|
||||
NSMutableString *suffix = @" ".mutableCopy;
|
||||
[_certificatePopup removeAllItems];
|
||||
[_certificatePopup addItemWithTitle:@"Select Push Certificate"];
|
||||
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
|
||||
[formatter setDateStyle:NSDateFormatterShortStyle];
|
||||
[formatter setTimeStyle:NSDateFormatterShortStyle];
|
||||
for (NSArray *pair in _certificateIdentityPairs) {
|
||||
NWCertificateRef certificate = pair[0];
|
||||
BOOL hasIdentity = (pair[1] != NSNull.null);
|
||||
NWEnvironmentOptions environmentOptions = [NWSecTools environmentOptionsForCertificate:certificate];
|
||||
NSString *summary = nil;
|
||||
NWCertType certType = [NWSecTools typeWithCertificate:certificate summary:&summary];
|
||||
NSString *type = descriptionForCertType(certType);
|
||||
NSDate *date = [NWSecTools expirationWithCertificate:certificate];
|
||||
NSString *expire = [NSString stringWithFormat:@" [%@]", date ? [formatter stringFromDate:date] : @"expired"];
|
||||
// summary = @"com.example.app";
|
||||
[_certificatePopup addItemWithTitle:[NSString stringWithFormat:@"%@%@ (%@ %@)%@%@", hasIdentity ? @"imported: " : @"", summary, type, descriptionForEnvironentOptions(environmentOptions), expire, suffix]];
|
||||
[suffix appendString:@" "];
|
||||
}
|
||||
[_certificatePopup addItemWithTitle:@"Import PKCS #12 file (.p12)..."];
|
||||
}
|
||||
|
||||
- (void)importIdentity
|
||||
{
|
||||
NWLogInfo(@"");
|
||||
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
||||
panel.canChooseFiles = YES;
|
||||
panel.canChooseDirectories = NO;
|
||||
panel.allowsMultipleSelection = YES;
|
||||
panel.allowedFileTypes = @[@"p12"];
|
||||
[panel beginWithCompletionHandler:^(NSInteger result){
|
||||
if (result != NSFileHandlingPanelOKButton) {
|
||||
return;
|
||||
}
|
||||
NSMutableArray *pairs = @[].mutableCopy;
|
||||
for (NSURL *url in panel.URLs) {
|
||||
NSString *text = [NSString stringWithFormat:@"Enter password for %@", url.lastPathComponent];
|
||||
NSAlert *alert = [NSAlert alertWithMessageText:text defaultButton:@"OK" alternateButton:@"Cancel" otherButton:nil informativeTextWithFormat:@""];
|
||||
NSSecureTextField *input = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)];
|
||||
alert.accessoryView = input;
|
||||
NSInteger button = [alert runModal];
|
||||
if (button != NSAlertDefaultReturn) {
|
||||
return;
|
||||
}
|
||||
NSString *password = input.stringValue;
|
||||
NSData *data = [NSData dataWithContentsOfURL:url];
|
||||
NSError *error = nil;
|
||||
NSArray *ids = [NWSecTools identitiesWithPKCS12Data:data password:password error:&error];
|
||||
if (!ids && password.length == 0 && error.code == kNWErrorPKCS12Password) {
|
||||
ids = [NWSecTools identitiesWithPKCS12Data:data password:nil error:&error];
|
||||
}
|
||||
if (!ids) {
|
||||
NWLogWarn(@"Unable to read p12 file: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
for (NWIdentityRef identity in ids) {
|
||||
NSError *error = nil;
|
||||
NWCertificateRef certificate = [NWSecTools certificateWithIdentity:identity error:&error];
|
||||
if (!certificate) {
|
||||
NWLogWarn(@"Unable to import p12 file: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
[pairs addObject:@[certificate, identity]];
|
||||
}
|
||||
}
|
||||
if (!pairs.count) {
|
||||
NWLogWarn(@"Unable to import p12 file: no push certificates found");
|
||||
return;
|
||||
}
|
||||
NWLogInfo(@"Imported %i certificate%@", (int)pairs.count, pairs.count == 1 ? @"" : @"s");
|
||||
NSUInteger index = _certificateIdentityPairs.count;
|
||||
_certificateIdentityPairs = [_certificateIdentityPairs arrayByAddingObjectsFromArray:pairs];
|
||||
[self updateCertificatePopup];
|
||||
[self connectWithCertificateAtIndex:index + 1];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Expiry and Priority
|
||||
|
||||
- (NSDate *)selectedExpiry
|
||||
{
|
||||
switch(_expiryPopup.indexOfSelectedItem) {
|
||||
case 1: return [NSDate dateWithTimeIntervalSince1970:0];
|
||||
case 2: return [NSDate dateWithTimeIntervalSinceNow:60];
|
||||
case 3: return [NSDate dateWithTimeIntervalSince1970:300];
|
||||
case 4: return [NSDate dateWithTimeIntervalSinceNow:3600];
|
||||
case 5: return [NSDate dateWithTimeIntervalSinceNow:86400];
|
||||
case 6: return [NSDate dateWithTimeIntervalSince1970:1];
|
||||
case 7: return [NSDate dateWithTimeIntervalSince1970:UINT32_MAX];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSUInteger)selectedPriority
|
||||
{
|
||||
switch(_priorityPopup.indexOfSelectedItem) {
|
||||
case 1: return 5;
|
||||
case 2: return 10;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#pragma mark - Payload
|
||||
|
||||
- (void)updatePayloadCounter
|
||||
{
|
||||
NSString *payload = _payloadField.string;
|
||||
BOOL isJSON = !![NSJSONSerialization JSONObjectWithData:[payload dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
|
||||
_countField.stringValue = [NSString stringWithFormat:@"%@ %lu", isJSON ? @"" : @"malformed", payload.length];
|
||||
_countField.textColor = payload.length > 256 || !isJSON ? NSColor.redColor : NSColor.darkGrayColor;
|
||||
}
|
||||
|
||||
- (void)upPayloadTextIndex
|
||||
{
|
||||
NSString *payload = _payloadField.string;
|
||||
NSRange range = [payload rangeOfString:@"\\([0-9]+\\)" options:NSRegularExpressionSearch];
|
||||
if (range.location != NSNotFound) {
|
||||
range.location += 1;
|
||||
range.length -= 2;
|
||||
NSString *before = [payload substringToIndex:range.location];
|
||||
NSUInteger value = [payload substringWithRange:range].integerValue + 1;
|
||||
NSString *after = [payload substringFromIndex:range.location + range.length];
|
||||
_payloadField.string = [NSString stringWithFormat:@"%@%lu%@", before, value, after];
|
||||
}
|
||||
}
|
||||
|
||||
- (NWEnvironment)selectedEnvironmentForCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
return (_sanboxCheckBox.state & NSOnState) ? NWEnvironmentSandbox : NWEnvironmentProduction;
|
||||
}
|
||||
|
||||
- (NWEnvironment)preferredEnvironmentForCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
NWEnvironmentOptions environmentOptions = [NWSecTools environmentOptionsForCertificate:certificate];
|
||||
|
||||
return (environmentOptions & NWEnvironmentOptionSandbox) ? NWEnvironmentSandbox : NWEnvironmentProduction;
|
||||
}
|
||||
|
||||
#pragma mark - Connection
|
||||
|
||||
- (void)connectWithCertificateAtIndex:(NSUInteger)index
|
||||
{
|
||||
if (index == 0) {
|
||||
[_certificatePopup selectItemAtIndex:0];
|
||||
_lastSelectedIndex = 0;
|
||||
[self selectCertificate:nil identity:nil environment:NWEnvironmentSandbox];
|
||||
_tokenCombo.enabled = NO;
|
||||
[self loadSelectedToken];
|
||||
} else if (index <= _certificateIdentityPairs.count) {
|
||||
[_certificatePopup selectItemAtIndex:index];
|
||||
_lastSelectedIndex = index;
|
||||
NSArray *pair = [_certificateIdentityPairs objectAtIndex:index - 1];
|
||||
NWCertificateRef certificate = pair[0];
|
||||
NWIdentityRef identity = pair[1];
|
||||
[self selectCertificate:certificate identity:identity == NSNull.null ? nil : identity environment:[self preferredEnvironmentForCertificate:certificate]];
|
||||
_tokenCombo.enabled = YES;
|
||||
[self loadSelectedToken];
|
||||
} else {
|
||||
[_certificatePopup selectItemAtIndex:_lastSelectedIndex];
|
||||
[self importIdentity];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)disableButtons
|
||||
{
|
||||
_pushButton.enabled = NO;
|
||||
_reconnectButton.enabled = NO;
|
||||
_sanboxCheckBox.enabled = NO;
|
||||
}
|
||||
|
||||
- (void)enableButtonsForCertificate:(NWCertificateRef)certificate environment:(NWEnvironment)environment
|
||||
{
|
||||
NWEnvironmentOptions environmentOptions = [NWSecTools environmentOptionsForCertificate:certificate];
|
||||
|
||||
BOOL shouldEnableEnvButton = (environmentOptions == NWEnvironmentOptionAny);
|
||||
BOOL shouldSelectSandboxEnv = (environment == NWEnvironmentSandbox);
|
||||
|
||||
_pushButton.enabled = YES;
|
||||
_reconnectButton.enabled = YES;
|
||||
_sanboxCheckBox.enabled = shouldEnableEnvButton;
|
||||
_sanboxCheckBox.state = shouldSelectSandboxEnv ? NSOnState : NSOffState;
|
||||
}
|
||||
|
||||
- (void)selectCertificate:(NWCertificateRef)certificate identity:(NWIdentityRef)identity environment:(NWEnvironment)environment
|
||||
{
|
||||
if (_hub) {
|
||||
[_hub disconnect]; _hub = nil;
|
||||
|
||||
[self disableButtons];
|
||||
NWLogInfo(@"Disconnected from APN");
|
||||
}
|
||||
|
||||
_selectedCertificate = certificate;
|
||||
[self updateTokenCombo];
|
||||
|
||||
if (certificate) {
|
||||
|
||||
NSString *summary = [NWSecTools summaryWithCertificate:certificate];
|
||||
NWLogInfo(@"Connecting to APN... (%@ %@)", summary, descriptionForEnvironent(environment));
|
||||
|
||||
dispatch_async(_serial, ^{
|
||||
NSError *error = nil;
|
||||
NWIdentityRef ident = identity ?: [NWSecTools keychainIdentityWithCertificate:certificate error:&error];
|
||||
NWHub *hub = [NWHub connectWithDelegate:self identity:ident environment:environment error:&error];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (hub) {
|
||||
NWLogInfo(@"Connected (%@ %@)", summary, descriptionForEnvironent(environment));
|
||||
_hub = hub;
|
||||
|
||||
[self enableButtonsForCertificate:certificate environment:environment];
|
||||
} else {
|
||||
NWLogWarn(@"Unable to connect: %@", error.localizedDescription);
|
||||
[hub disconnect];
|
||||
[_certificatePopup selectItemAtIndex:0];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reconnect
|
||||
{
|
||||
NSString *summary = [NWSecTools summaryWithCertificate:_selectedCertificate];
|
||||
NWEnvironment environment = [self selectedEnvironmentForCertificate:_selectedCertificate];
|
||||
|
||||
NWLogInfo(@"Reconnecting to APN...(%@ %@)", summary, descriptionForEnvironent(environment));
|
||||
|
||||
[self selectCertificate:_selectedCertificate identity:nil environment:environment];
|
||||
}
|
||||
|
||||
- (void)push
|
||||
{
|
||||
NSString *payload = _payloadField.string;
|
||||
NSString *token = _tokenCombo.stringValue;
|
||||
NSDate *expiry = self.selectedExpiry;
|
||||
NSUInteger priority = self.selectedPriority;
|
||||
NWLogInfo(@"Pushing..");
|
||||
dispatch_async(_serial, ^{
|
||||
NWNotification *notification = [[NWNotification alloc] initWithPayload:payload token:token identifier:0 expiration:expiry priority:priority];
|
||||
NSError *error = nil;
|
||||
BOOL pushed = [_hub pushNotification:notification autoReconnect:YES error:&error];
|
||||
if (pushed) {
|
||||
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
|
||||
dispatch_after(popTime, _serial, ^(void){
|
||||
NSError *error = nil;
|
||||
NWNotification *failed = nil;
|
||||
BOOL read = [_hub readFailed:&failed autoReconnect:YES error:&error];
|
||||
if (read) {
|
||||
if (!failed) NWLogInfo(@"Payload has been pushed");
|
||||
} else {
|
||||
NWLogWarn(@"Unable to read: %@", error.localizedDescription);
|
||||
}
|
||||
[_hub trimIdentifiers];
|
||||
});
|
||||
} else {
|
||||
NWLogWarn(@"Unable to push: %@", error.localizedDescription);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)feedback
|
||||
{
|
||||
dispatch_async(_serial, ^{
|
||||
NWCertificateRef certificate = _selectedCertificate;
|
||||
if (!certificate) {
|
||||
NWLogWarn(@"Unable to connect to feedback service: no certificate selected");
|
||||
return;
|
||||
}
|
||||
NWEnvironment environment = [self selectedEnvironmentForCertificate:certificate];
|
||||
NSString *summary = [NWSecTools summaryWithCertificate:certificate];
|
||||
NWLogInfo(@"Connecting to feedback service.. (%@ %@)", summary, descriptionForEnvironent(environment));
|
||||
NSError *error = nil;
|
||||
NWIdentityRef identity = [NWSecTools keychainIdentityWithCertificate:_selectedCertificate error:&error];
|
||||
NWPushFeedback *feedback = [NWPushFeedback connectWithIdentity:identity environment:[self selectedEnvironmentForCertificate:certificate] error:&error];
|
||||
if (!feedback) {
|
||||
NWLogWarn(@"Unable to connect to feedback service: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
NWLogInfo(@"Reading feedback service.. (%@ %@)", summary, descriptionForEnvironent(environment));
|
||||
NSArray *pairs = [feedback readTokenDatePairsWithMax:1000 error:&error];
|
||||
if (!pairs) {
|
||||
NWLogWarn(@"Unable to read feedback: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
for (NSArray *pair in pairs) {
|
||||
NWLogInfo(@"token: %@ date: %@", pair[0], pair[1]);
|
||||
}
|
||||
if (pairs.count) {
|
||||
NWLogInfo(@"Feedback service returned %i device tokens, see logs for details", (int)pairs.count);
|
||||
} else {
|
||||
NWLogInfo(@"Feedback service returned zero device tokens");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Config
|
||||
|
||||
- (NSString *)identifierWithCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
NWEnvironmentOptions environmentOptions = [NWSecTools environmentOptionsForCertificate:certificate];
|
||||
NSString *summary = [NWSecTools summaryWithCertificate:certificate];
|
||||
return summary ? [NSString stringWithFormat:@"%@-%@", summary, descriptionForEnvironentOptions(environmentOptions)] : nil;
|
||||
}
|
||||
|
||||
- (NSMutableArray *)tokensWithCertificate:(NWCertificateRef)certificate create:(BOOL)create
|
||||
{
|
||||
NWEnvironment environment = [self selectedEnvironmentForCertificate:_selectedCertificate];
|
||||
NSString *summary = [NWSecTools summaryWithCertificate:certificate];
|
||||
NSString *identifier = summary ? [NSString stringWithFormat:@"%@%@", summary, environment == NWEnvironmentSandbox ? @"-sandbox" : @""] : nil;
|
||||
if (!identifier) return nil;
|
||||
NSArray *result = _config[@"identifiers"][identifier];
|
||||
if (create && !result) result = (_config[@"identifiers"][identifier] = @[].mutableCopy);
|
||||
if (result && ![result isKindOfClass:NSMutableArray.class]) result = (_config[@"identifiers"][identifier] = result.mutableCopy);
|
||||
return (NSMutableArray *)result;
|
||||
}
|
||||
|
||||
- (BOOL)addToken:(NSString *)token certificate:(NWCertificateRef)certificate
|
||||
{
|
||||
NSMutableArray *tokens = [self tokensWithCertificate:certificate create:YES];
|
||||
if (token.length && ![tokens containsObject:token]) {
|
||||
[tokens addObject:token];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)removeToken:(NSString *)token certificate:(NWCertificateRef)certificate
|
||||
{
|
||||
NSMutableArray *tokens = [self tokensWithCertificate:certificate create:NO];
|
||||
if (token && [tokens containsObject:token]) {
|
||||
[tokens removeObject:token];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)selectToken:(NSString *)token certificate:(NWCertificateRef)certificate
|
||||
{
|
||||
NSMutableArray *tokens = [self tokensWithCertificate:certificate create:YES];
|
||||
if (token && [tokens containsObject:token]) {
|
||||
[tokens removeObject:token];
|
||||
[tokens addObject:token];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)updateTokenCombo
|
||||
{
|
||||
[_tokenCombo removeAllItems];
|
||||
NSArray *tokens = [self tokensWithCertificate:_selectedCertificate create:NO];
|
||||
if (tokens.count) [_tokenCombo addItemsWithObjectValues:tokens.reverseObjectEnumerator.allObjects];
|
||||
}
|
||||
|
||||
- (void)loadSelectedToken
|
||||
{
|
||||
_tokenCombo.stringValue = [[self tokensWithCertificate:_selectedCertificate create:YES] lastObject] ?: @"";
|
||||
// _tokenCombo.stringValue = @"552fff0a65b154eb209e9dc91201025da1a4a413dd2ad6d3b51e9b33b90c977a my iphone";
|
||||
}
|
||||
|
||||
- (void)addTokenAndUpdateCombo
|
||||
{
|
||||
BOOL added = [self addToken:_tokenCombo.stringValue certificate:_selectedCertificate];
|
||||
if (added) [self updateTokenCombo];
|
||||
}
|
||||
|
||||
- (void)selectTokenAndUpdateCombo
|
||||
{
|
||||
BOOL selected = [self selectToken:_tokenCombo.stringValue certificate:_selectedCertificate];
|
||||
if (selected) [self updateTokenCombo];
|
||||
}
|
||||
|
||||
- (NSURL *)configFileURL
|
||||
{
|
||||
NSURL *libraryURL = [[NSFileManager.defaultManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
NSURL *configURL = [libraryURL URLByAppendingPathComponent:@"Pusher" isDirectory:YES];
|
||||
if (!configURL) return nil;
|
||||
NSError *error = nil;
|
||||
BOOL exists = [NSFileManager.defaultManager createDirectoryAtURL:configURL withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
NWLogWarnIfError(error);
|
||||
if (!exists) return nil;
|
||||
NSURL *result = [configURL URLByAppendingPathComponent:@"config.plist"];
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:result.path]){
|
||||
NSURL *defaultURL = [NSBundle.mainBundle URLForResource:@"config" withExtension:@"plist"];
|
||||
[NSFileManager.defaultManager copyItemAtURL:defaultURL toURL:result error:&error];
|
||||
NWLogWarnIfError(error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)loadConfig
|
||||
{
|
||||
NSURL *url = [self configFileURL];
|
||||
_config = [NSDictionary dictionaryWithContentsOfURL:url];
|
||||
NWLogInfo(@"Loaded config from %@", url.path);
|
||||
}
|
||||
|
||||
- (void)saveConfig
|
||||
{
|
||||
if (_config.count) [_config writeToURL:[self configFileURL] atomically:NO];
|
||||
}
|
||||
|
||||
- (void)migrateOldConfigurationIfNeeded
|
||||
{
|
||||
NSURL *libraryURL = [[NSFileManager.defaultManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
NSURL *configURL = [libraryURL URLByAppendingPathComponent:@"Pusher" isDirectory:YES];
|
||||
NSURL *newURL = [configURL URLByAppendingPathComponent:@"config.plist"];
|
||||
NSURL *oldURL = [configURL URLByAppendingPathComponent:@"configuration.plist"];
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:newURL.path]) return;
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:oldURL.path]) return;
|
||||
NWLogInfo(@"Migrating old configuration to new format");
|
||||
NSDictionary *old = [NSDictionary dictionaryWithContentsOfURL:oldURL];
|
||||
NSMutableDictionary *identifiers = @{}.mutableCopy;
|
||||
for (NSDictionary *d in old[@"tokens"]) {
|
||||
for (NSString *identifier in d[@"identifiers"]) {
|
||||
for (NSArray *token in d[@"development"]) {
|
||||
NSString *key = [NSString stringWithFormat:@"%@-sandbox", identifier];
|
||||
if (!identifiers[key]) identifiers[key] = @[].mutableCopy;
|
||||
[identifiers[key] addObject:token];
|
||||
}
|
||||
for (NSArray *token in d[@"production"]) {
|
||||
NSString *key = identifier;
|
||||
if (!identifiers[key]) identifiers[key] = @[].mutableCopy;
|
||||
[identifiers[key] addObject:token];
|
||||
}
|
||||
}
|
||||
}
|
||||
NSMutableDictionary *new = @{}.mutableCopy;
|
||||
new[@"payload"] = old[@"payload"];
|
||||
new[@"identifiers"] = identifiers;
|
||||
[new writeToURL:newURL atomically:NO];
|
||||
NSError *error = nil;
|
||||
[NSFileManager.defaultManager removeItemAtURL:oldURL error:&error];
|
||||
NWLogWarnIfError(error);
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
- (void)log:(NSString *)message warning:(BOOL)warning
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
_infoField.textColor = warning ? NSColor.redColor : NSColor.blackColor;
|
||||
_infoField.stringValue = message;
|
||||
if (message.length) {
|
||||
NSDictionary *attributes = @{NSForegroundColorAttributeName: _infoField.textColor, NSFontAttributeName: [NSFont fontWithName:@"Monaco" size:10]};
|
||||
NSAttributedString *string = [[NSAttributedString alloc] initWithString:message attributes:attributes];
|
||||
[_logField.textStorage appendAttributedString:string];
|
||||
[_logField.textStorage.mutableString appendString:@"\n"];
|
||||
[_logField scrollRangeToVisible:NSMakeRange(_logField.textStorage.length - 1, 1)];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void NWPusherPrinter(NWLContext context, CFStringRef message, void *info) {
|
||||
BOOL warning = context.tag && strncmp(context.tag, "warn", 5) == 0;
|
||||
id delegate = NSApplication.sharedApplication.delegate;
|
||||
[delegate log:(__bridge NSString *)message warning:warning];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,547 @@
|
||||
//
|
||||
// NWLCore.m
|
||||
// NWLogging
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWLCore.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <math.h>
|
||||
#import <CoreFoundation/CFDate.h>
|
||||
|
||||
#pragma mark - Constants and statics
|
||||
|
||||
static const int kNWLFilterListSize = 16;
|
||||
static const int kNWLPrinterListSize = 8;
|
||||
|
||||
typedef struct {
|
||||
const char *properties[kNWLProperty_count];
|
||||
NWLAction action;
|
||||
} NWLFilter;
|
||||
|
||||
typedef struct {
|
||||
int count;
|
||||
NWLFilter elements[kNWLFilterListSize];
|
||||
} NWLFilterList;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
void(*func)(NWLContext, CFStringRef, void *);
|
||||
void *info;
|
||||
} NWLPrinter;
|
||||
|
||||
typedef struct {
|
||||
int count;
|
||||
NWLPrinter elements[kNWLPrinterListSize];
|
||||
} NWLPrinterList;
|
||||
|
||||
#define NWLDefaultPrinterFunction NWLStderrPrinter
|
||||
#define NWLDefaultPrinterName "default"
|
||||
#define NWLDefaultFilterTag "warn"
|
||||
#define NWLDefaultFilterAction kNWLAction_print
|
||||
static NWLFilterList NWLFilters = {1, {NULL, NWLDefaultFilterTag, NULL, NULL, NULL, NWLDefaultFilterAction}};
|
||||
static NWLPrinterList NWLPrinters = {1, {NWLDefaultPrinterName, NWLDefaultPrinterFunction, NULL}};
|
||||
static CFTimeInterval NWLTimeOffset = 0;
|
||||
|
||||
|
||||
#pragma mark - Printing
|
||||
|
||||
void NWLForwardToPrinters(NWLContext context, CFStringRef message) {
|
||||
for (int i = 0; i < NWLPrinters.count; i++) {
|
||||
NWLPrinter *printer = &NWLPrinters.elements[i];
|
||||
void(*func)(NWLContext, CFStringRef, void *) = printer->func;
|
||||
func(context, message, printer->info);
|
||||
}
|
||||
}
|
||||
|
||||
void NWLForwardWithoutFilter(NWLContext context, CFStringRef format, ...) {
|
||||
va_list arglist;
|
||||
va_start(arglist, format);
|
||||
CFStringRef message = CFStringCreateWithFormatAndArguments(NULL, 0, format, arglist);
|
||||
va_end(arglist);
|
||||
NWLForwardToPrinters(context, message);
|
||||
CFRelease(message);
|
||||
}
|
||||
|
||||
void NWLForwardWithFilter(NWLContext context, CFStringRef format, ...) {
|
||||
NWLAction type = NWLMatchingActionForContext(context);
|
||||
if (type) {
|
||||
va_list arglist;
|
||||
va_start(arglist, format);
|
||||
CFStringRef message = CFStringCreateWithFormatAndArguments(NULL, 0, format, arglist);
|
||||
va_end(arglist);
|
||||
switch (type) {
|
||||
case kNWLAction_print: NWLForwardToPrinters(context, message); break;
|
||||
case kNWLAction_break: NWLForwardToPrinters(context, message); NWLBreakInDebugger(); break;
|
||||
default: CFShow(message); break;
|
||||
}
|
||||
CFRelease(message);
|
||||
}
|
||||
}
|
||||
|
||||
int NWLAddPrinter(const char *name, void(*func)(NWLContext, CFStringRef, void *), void *info) {
|
||||
int count = NWLPrinters.count;
|
||||
if (count < kNWLPrinterListSize) {
|
||||
NWLPrinter printer = {name, func, info};
|
||||
NWLPrinters.elements[count] = printer;
|
||||
NWLPrinters.count = count + 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void * NWLRemovePrinter(const char *name) {
|
||||
for (int i = NWLPrinters.count - 1; i >= 0; i--) {
|
||||
NWLPrinter *p = &NWLPrinters.elements[i];
|
||||
const char *n = p->name;
|
||||
if (n == name || (n && name && !strcasecmp(n, name))) {
|
||||
int count = NWLPrinters.count;
|
||||
if (count > 0) {
|
||||
void *info = p->info;
|
||||
NWLPrinters.count = count - 1;
|
||||
NWLPrinters.elements[i] = NWLPrinters.elements[count - 1];
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void NWLRemoveAllPrinters(void) {
|
||||
NWLPrinters.count = 0;
|
||||
}
|
||||
|
||||
void NWLAddDefaultPrinter(void) {
|
||||
NWLAddPrinter(NWLDefaultPrinterName, NWLDefaultPrinterFunction, NULL);
|
||||
}
|
||||
|
||||
void NWLRestoreDefaultPrinters(void) {
|
||||
NWLRemoveAllPrinters();
|
||||
NWLAddDefaultPrinter();
|
||||
}
|
||||
|
||||
void NWLStderrPrinter(NWLContext context, CFStringRef message, void *info) {
|
||||
// init io vector
|
||||
struct iovec iov[16];
|
||||
int i = 0;
|
||||
iov[i].iov_base = "[";
|
||||
iov[i++].iov_len = 1;
|
||||
|
||||
// add time
|
||||
int hour = 0, minute = 0, second = 0, micro = 0;
|
||||
NWLClock(context.time, &hour, &minute, &second, µ);
|
||||
char timeBuffer[16];
|
||||
int timeLength = snprintf(timeBuffer, sizeof(timeBuffer), "%02i:%02i:%02i.%06i", hour, minute, second, micro);
|
||||
iov[i].iov_base = timeBuffer;
|
||||
iov[i++].iov_len = sizeof(timeBuffer) - 1 < timeLength ? sizeof(timeBuffer) - 1 : timeLength;
|
||||
|
||||
// add context
|
||||
if (context.lib && *context.lib) {
|
||||
iov[i].iov_base = " ";
|
||||
iov[i++].iov_len = 1;
|
||||
iov[i].iov_base = (void *)context.lib;
|
||||
iov[i++].iov_len = strnlen(context.lib, 32);
|
||||
}
|
||||
if (context.file && *context.file) {
|
||||
iov[i].iov_base = " ";
|
||||
iov[i++].iov_len = 1;
|
||||
iov[i].iov_base = (void *)context.file;
|
||||
iov[i++].iov_len = strnlen(context.file, 32);
|
||||
iov[i].iov_base = ":";
|
||||
iov[i++].iov_len = 1;
|
||||
char lineBuffer[10];
|
||||
int lineLength = snprintf(lineBuffer, sizeof(lineBuffer), context.line < 1000 ? "%03u" : "%06u", context.line);
|
||||
iov[i].iov_base = lineBuffer;
|
||||
iov[i++].iov_len = sizeof(lineBuffer) - 1 < lineLength ? sizeof(lineBuffer) - 1 : lineLength;
|
||||
}
|
||||
if (context.tag && *context.tag) {
|
||||
iov[i].iov_base = "] [";
|
||||
iov[i++].iov_len = 3;
|
||||
iov[i].iov_base = (void *)context.tag;
|
||||
iov[i++].iov_len = strnlen(context.tag, 32);
|
||||
}
|
||||
|
||||
iov[i].iov_base = "] ";
|
||||
iov[i++].iov_len = 2;
|
||||
|
||||
CFRange range = CFRangeMake(0, message ? CFStringGetLength(message) : 0);
|
||||
if (range.length) {
|
||||
// add message
|
||||
unsigned char messageBuffer[256];
|
||||
CFIndex messageLength = 0;
|
||||
CFIndex length = 1;
|
||||
|
||||
while (length && range.length) {
|
||||
length = CFStringGetBytes(message, range, kCFStringEncodingUTF8, '?', false, messageBuffer, sizeof(messageBuffer), &messageLength);
|
||||
iov[i].iov_base = messageBuffer;
|
||||
iov[i++].iov_len = messageLength;
|
||||
if (length >= range.length) {
|
||||
iov[i].iov_base = "\n";
|
||||
iov[i++].iov_len = 1;
|
||||
} else if (!length) {
|
||||
iov[i].iov_base = "~\n";
|
||||
iov[i++].iov_len = 2;
|
||||
}
|
||||
writev(STDERR_FILENO, iov, i);
|
||||
i = 0;
|
||||
range.location += length;
|
||||
range.length -= length;
|
||||
}
|
||||
} else {
|
||||
iov[i].iov_base = "\n";
|
||||
iov[i++].iov_len = 1;
|
||||
writev(STDERR_FILENO, iov, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Filtering
|
||||
|
||||
NWLAction NWLMatchingActionForContext(NWLContext context) {
|
||||
NWLAction result = kNWLAction_none;
|
||||
int bestScore = 0;
|
||||
for (int i = 0; i < NWLFilters.count; i++) {
|
||||
NWLFilter *filter = &NWLFilters.elements[i];
|
||||
if (result < filter->action) {
|
||||
int score = 0;
|
||||
const char *s = NULL;
|
||||
#define _NWL_FIND_(_prop) s = filter->properties[kNWLProperty_##_prop]; if (s && context._prop) {if (strcasecmp(s, context._prop)) {continue;} else {score++;}}
|
||||
_NWL_FIND_(tag)
|
||||
_NWL_FIND_(lib)
|
||||
_NWL_FIND_(file)
|
||||
_NWL_FIND_(function)
|
||||
if (bestScore <= score) {
|
||||
bestScore = score;
|
||||
result = filter->action;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static int NWLAddFilter1(NWLFilter *filter) {
|
||||
if (filter->action != kNWLAction_none) {
|
||||
int count = NWLFilters.count;
|
||||
if (count < kNWLFilterListSize) {
|
||||
NWLFilters.elements[count] = *filter;
|
||||
NWLFilters.count = count + 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static NWLAction NWLHasFilter1(NWLFilter *filter) {
|
||||
for (int i = 0; i < NWLFilters.count; i++) {
|
||||
NWLFilter *m = &NWLFilters.elements[i];
|
||||
int j = 1;
|
||||
for (; j < kNWLProperty_count; j++) {
|
||||
const char *a = filter->properties[j];
|
||||
const char *b = m->properties[j];
|
||||
if (a != b && (!a || !b || strcasecmp(a, b))) break;
|
||||
}
|
||||
if (j == kNWLProperty_count) {
|
||||
return m->action;
|
||||
}
|
||||
}
|
||||
return kNWLAction_none;
|
||||
}
|
||||
|
||||
static int NWLRemoveFilter1(NWLFilter *filter) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < NWLFilters.count; i++) {
|
||||
NWLFilter *m = &NWLFilters.elements[i];
|
||||
int j = 1;
|
||||
for (; j < kNWLProperty_count; j++) {
|
||||
const char *a = filter->properties[j];
|
||||
const char *b = m->properties[j];
|
||||
if (a != b && (!a || !b || strcasecmp(a, b))) break;
|
||||
}
|
||||
int count = NWLFilters.count;
|
||||
if (j == kNWLProperty_count && count > 0) {
|
||||
NWLFilters.count = count - 1;
|
||||
NWLFilters.elements[i--] = NWLFilters.elements[count - 1];
|
||||
result++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static int NWLRemoveMatchingFilters1(NWLFilter *filter) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < NWLFilters.count; i++) {
|
||||
NWLFilter *m = &NWLFilters.elements[i];
|
||||
int j = 1;
|
||||
for (; j < kNWLProperty_count; j++) {
|
||||
const char *a = filter->properties[j];
|
||||
const char *b = m->properties[j];
|
||||
if (a && (!b || strcasecmp(a, b))) break;
|
||||
}
|
||||
int count = NWLFilters.count;
|
||||
if (j == kNWLProperty_count && count > 0) {
|
||||
NWLFilters.count = count - 1;
|
||||
NWLFilters.elements[i--] = NWLFilters.elements[count - 1];
|
||||
result++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int NWLAddFilter(const char *tag, const char *lib, const char *file, const char *function, NWLAction action) {
|
||||
NWLFilter filter = {NULL, tag, lib, file, function, action};
|
||||
NWLRemoveFilter1(&filter);
|
||||
int result = NWLAddFilter1(&filter);
|
||||
return result;
|
||||
}
|
||||
|
||||
NWLAction NWLHasFilter(const char *tag, const char *lib, const char *file, const char *function) {
|
||||
NWLFilter filter = {NULL, tag, lib, file, function, kNWLAction_none};
|
||||
NWLAction result = NWLHasFilter1(&filter);
|
||||
return result;
|
||||
}
|
||||
|
||||
int NWLRemoveMatchingFilters(const char *tag, const char *lib, const char *file, const char *function) {
|
||||
NWLFilter filter = {NULL, tag, lib, file, function, kNWLAction_none};
|
||||
int result = NWLRemoveMatchingFilters1(&filter);
|
||||
return result;
|
||||
}
|
||||
|
||||
void NWLRemoveAllFilters(void) {
|
||||
NWLFilters.count = 0;
|
||||
}
|
||||
|
||||
void NWLAddDefaultFilter(void) {
|
||||
NWLAddFilter(NWLDefaultFilterTag, NULL, NULL, NULL, NWLDefaultFilterAction);
|
||||
}
|
||||
|
||||
void NWLRestoreDefaultFilters(void) {
|
||||
NWLRemoveAllFilters();
|
||||
NWLAddDefaultFilter();
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Clock
|
||||
|
||||
double NWLTime(void) {
|
||||
return CFAbsoluteTimeGetCurrent() + 978307200;
|
||||
}
|
||||
|
||||
void NWLResetPrintClock(void) {
|
||||
NWLTimeOffset = NWLTime();
|
||||
}
|
||||
|
||||
void NWLOffsetPrintClock(double seconds) {
|
||||
NWLTimeOffset = -seconds;
|
||||
}
|
||||
|
||||
void NWLRestorePrintClock(void) {
|
||||
NWLTimeOffset = 0;
|
||||
}
|
||||
|
||||
void NWLClock(double time, int *hour, int *minute, int *second, int *micro) {
|
||||
double t = time - NWLTimeOffset;
|
||||
*hour = (int)(t / 3600) % 24;
|
||||
*minute = (int)(t / 60) % 60;
|
||||
*second = (int)t % 60;
|
||||
*micro = (int)((t - floor(t)) * 1000000) % 1000000;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - About
|
||||
|
||||
#define _NWL_PRINT_(_buffer, _size, _fmt, ...) do {\
|
||||
int _s = _size > 0 ? _size : 0;\
|
||||
int __p = snprintf(_buffer, _s, _fmt, ##__VA_ARGS__);\
|
||||
if (__p <= _size) _buffer += __p; else buffer += _s;\
|
||||
_size -= __p;\
|
||||
} while (0)
|
||||
|
||||
int NWLAboutString(char *buffer, int size) {
|
||||
int s = size;
|
||||
for (int i = 0; i < NWLFilters.count; i++) {
|
||||
NWLFilter *filter = &NWLFilters.elements[i];
|
||||
#define _NWL_ABOUT_ACTION_(_action) do {if (filter->action == kNWLAction_##_action) {_NWL_PRINT_(buffer, s, " action : "#_action);}} while (0)
|
||||
_NWL_ABOUT_ACTION_(print);
|
||||
_NWL_ABOUT_ACTION_(break);
|
||||
const char *value = NULL;
|
||||
#define _NWL_ABOUT_PROP_(_prop) do {if ((value = filter->properties[kNWLProperty_##_prop])) {_NWL_PRINT_(buffer, s, " "#_prop"=%s", value);}} while (0)
|
||||
_NWL_ABOUT_PROP_(tag);
|
||||
_NWL_ABOUT_PROP_(lib);
|
||||
_NWL_ABOUT_PROP_(file);
|
||||
_NWL_ABOUT_PROP_(function);
|
||||
_NWL_PRINT_(buffer, s, "\n");
|
||||
}
|
||||
for (int i = 0; i < NWLPrinters.count; i++) {
|
||||
NWLPrinter *p = &NWLPrinters.elements[i];
|
||||
_NWL_PRINT_(buffer, s, " printer : %s\n", p->name);
|
||||
}
|
||||
_NWL_PRINT_(buffer, s, " time-offset : %f\n", NWLTimeOffset);
|
||||
return size - s;
|
||||
}
|
||||
|
||||
void NWLogAbout(void) {
|
||||
char buffer[256];
|
||||
int length = NWLAboutString(buffer, sizeof(buffer));
|
||||
NWLContext context = {NULL, "NWLogging", NULL, 0, NULL, NWLTime()};
|
||||
CFStringRef message = CFStringCreateWithFormat(NULL, 0, CFSTR("About NWLogging\n%s%s"), buffer, length <= sizeof(buffer) - 1 ? "" : "\n ...");\
|
||||
NWLForwardToPrinters(context, message);
|
||||
CFRelease(message);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Misc Helpers
|
||||
|
||||
void NWLRestore(void) {
|
||||
NWLRestoreDefaultFilters();
|
||||
NWLRestoreDefaultPrinters();
|
||||
NWLRestorePrintClock();
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Macro wrappers
|
||||
|
||||
void NWLPrintInfo(void) {
|
||||
NWLAddFilter("info", NULL, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintWarn(void) {
|
||||
NWLAddFilter("warn", NULL, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintDbug(void) {
|
||||
NWLAddFilter("dbug", NULL, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintTag(const char *tag) {
|
||||
NWLAddFilter(tag, NULL, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintAll(void) {
|
||||
NWLAddFilter(NULL, NULL, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void NWLPrintInfoInLib(const char *lib) {
|
||||
NWLAddFilter("info", lib, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintWarnInLib(const char *lib) {
|
||||
NWLAddFilter("warn", lib, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintDbugInLib(const char *lib) {
|
||||
NWLAddFilter("dbug", lib, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintTagInLib(const char *tag, const char *lib) {
|
||||
NWLAddFilter(tag, lib, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintAllInLib(const char *lib) {
|
||||
NWLAddFilter(NULL, lib, NULL, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void NWLPrintDbugInFile(const char *file) {
|
||||
NWLAddFilter("dbug", NULL, file, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintAllInFile(const char *file) {
|
||||
NWLAddFilter(NULL, NULL, file, NULL, kNWLAction_print);
|
||||
}
|
||||
|
||||
void NWLPrintDbugInFunction(const char *function) {
|
||||
NWLAddFilter("dbug", NULL, NULL, function, kNWLAction_print);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void NWLBreakWarn(void) {
|
||||
NWLAddFilter("warn", NULL, NULL, NULL, kNWLAction_break);
|
||||
}
|
||||
|
||||
void NWLBreakWarnInLib(const char *lib) {
|
||||
NWLAddFilter("warn", lib, NULL, NULL, kNWLAction_break);
|
||||
}
|
||||
|
||||
void NWLBreakTag(const char *tag) {
|
||||
NWLAddFilter(tag, NULL, NULL, NULL, kNWLAction_break);
|
||||
}
|
||||
|
||||
void NWLBreakTagInLib(const char *tag, const char *lib) {
|
||||
NWLAddFilter(tag, lib, NULL, NULL, kNWLAction_break);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void NWLClearInfo(void) {
|
||||
NWLRemoveMatchingFilters("info", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
void NWLClearWarn(void) {
|
||||
NWLRemoveMatchingFilters("warn", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
void NWLClearDbug(void) {
|
||||
NWLRemoveMatchingFilters("dbug", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
void NWLClearTag(const char *tag) {
|
||||
NWLRemoveMatchingFilters(tag, NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
void NWLClearAllInLib(const char *lib) {
|
||||
NWLRemoveMatchingFilters(NULL, lib, NULL, NULL);
|
||||
}
|
||||
|
||||
void NWLClearAll(void) {
|
||||
NWLRemoveAllFilters();
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Debugging
|
||||
|
||||
void NWLBreakInDebugger(void) {
|
||||
struct kinfo_proc info;
|
||||
info.kp_proc.p_flag = 0;
|
||||
pid_t pid = getpid();
|
||||
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
|
||||
size_t size = sizeof(info);
|
||||
sysctl(mib, 4, &info, &size, NULL, 0);
|
||||
if (info.kp_proc.p_flag & P_TRACED) {
|
||||
kill(pid, SIGINT);
|
||||
}
|
||||
}
|
||||
|
||||
void NWLDumpConfig(void) {
|
||||
char buffer[256];
|
||||
int length = NWLAboutString(buffer, sizeof(buffer));
|
||||
struct iovec iov[2];
|
||||
iov[0].iov_base = buffer;
|
||||
iov[0].iov_len = length <= sizeof(buffer) - 1 ? length : sizeof(buffer) - 1;
|
||||
iov[1].iov_base = " ...\n";
|
||||
iov[1].iov_len = length <= sizeof(buffer) - 1 ? 0 : 7;
|
||||
writev(STDERR_FILENO, iov, 2);
|
||||
}
|
||||
|
||||
#define PRINT(_format, ...) fprintf(stderr, _format"\n", ##__VA_ARGS__)
|
||||
void NWLDumpFlags(int active, const char *lib, int debug, const char *file, int line, const char *function) {
|
||||
PRINT(" file : %s:%i", file, line);
|
||||
PRINT(" function : %s", function);
|
||||
PRINT(" DEBUG : %s", debug ? "YES" : "NO");
|
||||
PRINT(" NWL_LIB : %s", lib && *lib ? lib : (lib ? "<empty>" : "<not set>"));
|
||||
PRINT(" NWLog macros : %s", active ? "YES" : "NO");
|
||||
}
|
||||
|
||||
#undef NWLDump
|
||||
void NWLDump(void) {
|
||||
NWLDumpConfig();
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
//
|
||||
// NWLCore.h
|
||||
// NWLogging
|
||||
//
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#include <string.h>
|
||||
#import <CoreFoundation/CFString.h>
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSException.h>
|
||||
#else // __OBJC__
|
||||
#include <assert.h>
|
||||
#endif // __OBJC__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
|
||||
#ifndef _NWLCORE_H_
|
||||
#define _NWLCORE_H_
|
||||
|
||||
|
||||
#pragma mark - The Good, the Bad and the Macro
|
||||
|
||||
/** Macros for configuration stuff. */
|
||||
#define NWL_STR(_a) NWL_STR_(_a)
|
||||
#define NWL_STR_(_a) #_a
|
||||
|
||||
#ifdef NWL_LIB
|
||||
|
||||
#define NWL_ACTIVE 1
|
||||
#define NWL_LIB_STR NWL_STR(NWL_LIB)
|
||||
|
||||
#else // NWL_LIB
|
||||
|
||||
#if DEBUG
|
||||
#define NWL_ACTIVE 1
|
||||
#else // DEBUG
|
||||
#define NWL_ACTIVE 0
|
||||
#endif // DEBUG
|
||||
#define NWL_LIB_STR NULL
|
||||
|
||||
#endif // NWL_LIB
|
||||
|
||||
|
||||
#pragma mark - Common logging operations
|
||||
|
||||
/** Log directly, bypasses all filter and forwards directly to all printers. */
|
||||
#define NWLog(_format, ...) NWLLogWithoutFilter(NULL, NWL_LIB_STR, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Log on the 'dbug' tag, which can be activated using NWLPrintDbug(). */
|
||||
#define NWLogDbug(_format, ...) NWLLogWithFilter("dbug", NWL_LIB_STR, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Log on the 'info' tag, which can be activated using NWLPrintInfo(). */
|
||||
#define NWLogInfo(_format, ...) NWLLogWithFilter("info", NWL_LIB_STR, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Log on the 'info' tag if the condition is true. */
|
||||
#define NWLogInfoIf(_condition, _format, ...) do {if (_condition) NWLogInfo(_format, ##__VA_ARGS__);} while (0)
|
||||
|
||||
/** Log on the 'warn' tag, which can be activated using NWLPrintWarn(). */
|
||||
#define NWLogWarn(_format, ...) NWLLogWithFilter("warn", NWL_LIB_STR, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Log on an 'warn' tag if the condition is false. */
|
||||
#define NWLogWarnIfNot(_condition, _format, ...) do {if (!(_condition)) NWLogWarn(_format, ##__VA_ARGS__);} while (0)
|
||||
|
||||
/** Log error description on the 'warn' tag if error is not nil. */
|
||||
#define NWLogWarnIfError(_error) NWLogWarnIfNot(!(_error), @"Caught: %@", (_error))
|
||||
|
||||
/** Log on a custom tag, which can be activated using NWLPrintTag(tag). */
|
||||
#define NWLogTag(_tag, _format, ...) NWLLogWithFilter((#_tag), NWL_LIB_STR, _format, ##__VA_ARGS__)
|
||||
|
||||
/** Convenient assert and error macros. */
|
||||
#define NWAssert(_condition) NWLogWarnIfNot((_condition), @"Expected true condition '"#_condition@"' in %s:%i", _NWL_FILE_, __LINE__)
|
||||
#define NWAssertMainThread() NWLogWarnIfNot(_NWL_MAIN_THREAD_, @"Expected running on main thread in %s:%i", _NWL_FILE_, __LINE__)
|
||||
#define NWAssertQueue(_queue,_label) NWLogWarnIfNot(strcmp(dispatch_queue_get_label(_queue)?:"",#_label)==0, @"Expected running on '%s', not on '%s' in %s:%i", #_label, dispatch_queue_get_label(_queue), _NWL_FILE_, __LINE__)
|
||||
#define NWParameterAssert(_condition) NWLogWarnIfNot((_condition), @"Expected parameter: '"#_condition@"' in %s:%i", _NWL_FILE_, __LINE__)
|
||||
#define NWError(_error) do {NWLogWarnIfNot(!(_error), @"Caught: %@", (_error)); _error = nil;} while (0)
|
||||
|
||||
|
||||
#pragma mark - Logging macros
|
||||
|
||||
// ARC helper
|
||||
#if __has_feature(objc_arc)
|
||||
#define _NWL_BRIDGE_ __bridge
|
||||
#else // __has_feature(objc_arc)
|
||||
#define _NWL_BRIDGE_
|
||||
#endif // __has_feature(objc_arc)
|
||||
|
||||
// C/Objective-C support
|
||||
#ifdef __OBJC__
|
||||
#define _NWL_CFSTRING_(_str) ((_NWL_BRIDGE_ CFStringRef)_str)
|
||||
#define _NWL_MAIN_THREAD_ [NSThread isMainThread]
|
||||
#else // __OBJC__
|
||||
#define _NWL_CFSTRING_(_str) CFSTR(_str)
|
||||
#define _NWL_MAIN_THREAD_ (dispatch_get_main_queue() == dispatch_get_current_queue())
|
||||
#endif // __OBJC__
|
||||
|
||||
// Misc helper macros
|
||||
#define _NWL_FILE_ (strrchr((__FILE__), '/') + 1)
|
||||
#define NWL_CALLER ({NSString*__line=NSThread.callStackSymbols[1];NSRange r=[__line rangeOfString:@"0x"];[NSString stringWithFormat:@"<%@>",r.length?[__line substringFromIndex:r.location]:__line];})
|
||||
#define NWL_STACK(__a) ({NSArray*lines=NSThread.callStackSymbols;[lines subarrayWithRange:NSMakeRange(0,__a<lines.count?__a:lines.count)];})
|
||||
|
||||
#define NWLLogWithoutFilter(_tag, _lib, _fmt, ...) NWLLogWithoutFilter_(_tag, _lib, _fmt, ##__VA_ARGS__)
|
||||
#define NWLLogWithFilter(_tag, _lib, _fmt, ...) NWLLogWithFilter_(_tag, _lib, _fmt, ##__VA_ARGS__)
|
||||
|
||||
#if NWL_ACTIVE
|
||||
#define NWLLogWithoutFilter_(_tag, _lib, _fmt, ...) NWLForwardWithoutFilter((NWLContext){_tag, _lib, _NWL_FILE_, __LINE__, __PRETTY_FUNCTION__, NWLTime()}, _NWL_CFSTRING_(_fmt), ##__VA_ARGS__)
|
||||
#define NWLLogWithFilter_(_tag, _lib, _fmt, ...) NWLForwardWithFilter((NWLContext){_tag, _lib, _NWL_FILE_, __LINE__, __PRETTY_FUNCTION__, NWLTime()}, _NWL_CFSTRING_(_fmt), ##__VA_ARGS__)
|
||||
#else // NWL_ACTIVE
|
||||
#define NWLLogWithoutFilter_(_tag, _lib, _fmt, ...) do {} while (0)
|
||||
#define NWLLogWithFilter_(_tag, _lib, _fmt, ...) do {} while (0)
|
||||
#endif // NWL_ACTIVE
|
||||
|
||||
|
||||
#pragma mark - Type definitions
|
||||
|
||||
/** Types of context properties to filter on */
|
||||
typedef enum {
|
||||
kNWLProperty_none = 0,
|
||||
kNWLProperty_tag = 1,
|
||||
kNWLProperty_lib = 2,
|
||||
kNWLProperty_file = 3,
|
||||
kNWLProperty_function = 4,
|
||||
kNWLProperty_count = 5,
|
||||
} NWLProperty;
|
||||
|
||||
/** Types of actions to take when a log context matches properties */
|
||||
typedef enum {
|
||||
kNWLAction_none = 0,
|
||||
kNWLAction_print = 1,
|
||||
kNWLAction_break = 2,
|
||||
kNWLAction_count = 3,
|
||||
} NWLAction;
|
||||
|
||||
/** The properties of a logging statement. */
|
||||
typedef struct {
|
||||
const char *tag;
|
||||
const char *lib;
|
||||
const char *file;
|
||||
int line;
|
||||
const char *function;
|
||||
double time;
|
||||
} NWLContext;
|
||||
|
||||
|
||||
#pragma mark - Configuration
|
||||
|
||||
/** Forwards context and formatted log line to printers. */
|
||||
extern void NWLForwardWithoutFilter(NWLContext context, CFStringRef format, ...) CF_FORMAT_FUNCTION(2,3);
|
||||
|
||||
/** Looks for the best-matching filter and performs the associated action. */
|
||||
extern void NWLForwardWithFilter(NWLContext context, CFStringRef format, ...) CF_FORMAT_FUNCTION(2,3);
|
||||
|
||||
/** Forward printing of line to printers, return true if added. */
|
||||
extern int NWLAddPrinter(const char *name, void(*)(NWLContext, CFStringRef, void *), void *info);
|
||||
|
||||
/** Remove a printer, returns info of the printer. */
|
||||
extern void * NWLRemovePrinter(const char *name);
|
||||
|
||||
/** Clear the printer list. */
|
||||
extern void NWLRemoveAllPrinters(void);
|
||||
|
||||
/** Restore the default stderr printer. */
|
||||
extern void NWLRestoreDefaultPrinters(void);
|
||||
|
||||
/** Add the default stderr printer. */
|
||||
extern void NWLAddDefaultPrinter(void);
|
||||
|
||||
/** Formatter tailored for debugging, with format: "[hr:mn:sc:micros Library File:line] [tag] message", to stderr. */
|
||||
extern void NWLStderrPrinter(NWLContext context, CFStringRef message, void *info);
|
||||
|
||||
|
||||
/** Tests context (like lib and file name) and returns the matching action. */
|
||||
extern NWLAction NWLMatchingActionForContext(NWLContext context);
|
||||
|
||||
/** Activates and action for these filter properties. */
|
||||
extern int NWLAddFilter(const char *tag, const char *lib, const char *file, const char *function, NWLAction action);
|
||||
|
||||
/** Finds filter that machtes these filter properties and returns its action. */
|
||||
extern NWLAction NWLHasFilter(const char *tag, const char *lib, const char *file, const char *function);
|
||||
|
||||
/** Remove all filters that are included by these filter properties. */
|
||||
extern int NWLRemoveMatchingFilters(const char *tag, const char *lib, const char *file, const char *function);
|
||||
|
||||
/** Remove all actions for all properties. */
|
||||
extern void NWLRemoveAllFilters(void);
|
||||
|
||||
/** Restore the default print-on-warn filter. */
|
||||
extern void NWLRestoreDefaultFilters(void);
|
||||
|
||||
/** Add the default print-on-warn filter. */
|
||||
extern void NWLAddDefaultFilter(void);
|
||||
|
||||
|
||||
/** Reset the clock on log prints to 00:00:00. */
|
||||
extern void NWLResetPrintClock(void);
|
||||
|
||||
/** Offset the clock on log prints with seconds. */
|
||||
extern void NWLOffsetPrintClock(double seconds);
|
||||
|
||||
/** Restore the clock on log prints to UTC time. */
|
||||
extern void NWLRestorePrintClock(void);
|
||||
|
||||
/** Seconds since epoch. */
|
||||
extern double NWLTime(void);
|
||||
|
||||
/** Provides clock values, returns time since epoch or since reset. */
|
||||
extern void NWLClock(double time, int *hour, int *minute, int *second, int *micro);
|
||||
|
||||
/** Returns a human-readable summary of this logger, returns the length of the about text excluding the null byte independent of 'size'. */
|
||||
extern int NWLAboutString(char *buffer, int size);
|
||||
|
||||
/** Log the internal state. */
|
||||
extern void NWLogAbout(void);
|
||||
|
||||
|
||||
/** Restore all internal state, including default printers, default filters, and default clock. **/
|
||||
extern void NWLRestore(void);
|
||||
|
||||
|
||||
#pragma mark - Common Configuration
|
||||
|
||||
/** Activate the printing of all log statements. */
|
||||
extern void NWLPrintInfo(void);
|
||||
extern void NWLPrintWarn(void);
|
||||
extern void NWLPrintDbug(void);
|
||||
extern void NWLPrintTag(const char *tag);
|
||||
extern void NWLPrintAll(void);
|
||||
|
||||
/** Activate the printing in one lib. */
|
||||
extern void NWLPrintInfoInLib(const char *lib);
|
||||
extern void NWLPrintWarnInLib(const char *lib);
|
||||
extern void NWLPrintDbugInLib(const char *lib);
|
||||
extern void NWLPrintTagInLib(const char *tag, const char *lib);
|
||||
extern void NWLPrintAllInLib(const char *lib);
|
||||
|
||||
#define NWLPrintInfoInThisLib() NWLPrintInfoInLib(NWL_LIB_STR)
|
||||
#define NWLPrintWarnInThisLib() NWLPrintWarnInLib(NWL_LIB_STR)
|
||||
#define NWLPrintDbugInThisLib() NWLPrintDbugInLib(NWL_LIB_STR)
|
||||
#define NWLPrintTagInThisLib(__tag) NWLPrintTagInLib(__tag, NWL_LIB_STR)
|
||||
#define NWLPrintAllInThisLib() NWLPrintAllInLib(NWL_LIB_STR)
|
||||
#define NWLPrintOnlyInThisLib() do {NWLRemoveAllFilters();NWLPrintAllInLib(NWL_LIB_STR);} while (0)
|
||||
|
||||
/** Activate printing in a file or function. */
|
||||
extern void NWLPrintDbugInFile(const char *file);
|
||||
extern void NWLPrintAllInFile(const char *file);
|
||||
extern void NWLPrintDbugInFunction(const char *function);
|
||||
|
||||
#define NWLPrintDbugInThisFile() NWLPrintDbugInFile(_NWL_FILE_)
|
||||
#define NWLPrintAllInThisFile() NWLPrintAllInFile(_NWL_FILE_)
|
||||
#define NWLPrintDbugInThisFunction() NWLPrintDbugInFunction(__PRETTY_FUNCTION__)
|
||||
#define NWLPrintOnlyInThisFile() do {NWLRemoveAllFilters();NWLPrintAllInFile(_NWL_FILE_);} while (0)
|
||||
#define NWLPrintOnlyInThisFunction() do {NWLRemoveAllFilters();NWLPrintAllInFunction(__PRETTY_FUNCTION__);} while (0)
|
||||
|
||||
/** Activate breaking. */
|
||||
extern void NWLBreakWarn(void);
|
||||
extern void NWLBreakWarnInLib(const char *lib);
|
||||
extern void NWLBreakTag(const char *tag);
|
||||
extern void NWLBreakTagInLib(const char *tag, const char *lib);
|
||||
|
||||
#define NWLBreakWarnInThisLib() NWLBreakWarnInLib(NWL_LIB_STR)
|
||||
#define NWLBreakTagInThisLib(__tag) NWLBreakTagInLib(__tag, NWL_LIB_STR)
|
||||
|
||||
/** Deactivate actions. */
|
||||
extern void NWLClearInfo(void);
|
||||
extern void NWLClearWarn(void);
|
||||
extern void NWLClearDbug(void);
|
||||
extern void NWLClearTag(const char *tag);
|
||||
extern void NWLClearAllInLib(const char *lib);
|
||||
extern void NWLClearAll(void);
|
||||
|
||||
#define NWLClearAllInThisLib() NWLClearAllInLib(NWL_LIB_STR)
|
||||
|
||||
|
||||
#pragma mark - Debugging
|
||||
|
||||
void NWLBreakInDebugger(void);
|
||||
|
||||
/** Print internal state info to stderr. */
|
||||
extern void NWLDump(void);
|
||||
extern void NWLDumpFlags(int active, const char *lib, int debug, const char *file, int line, const char *function);
|
||||
extern void NWLDumpConfig(void);
|
||||
#if DEBUG
|
||||
#define NWL_DEBUG 1
|
||||
#else // DEBUG
|
||||
#define NWL_DEBUG 0
|
||||
#endif // DEBUG
|
||||
#define NWLDump() do {NWLDumpFlags(NWL_ACTIVE, NWL_LIB_STR, NWL_DEBUG, _NWL_FILE_, __LINE__, __PRETTY_FUNCTION__);NWLDumpConfig();} while (0)
|
||||
|
||||
|
||||
#endif // _NWLCORE_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif // __cplusplus
|
||||
@@ -9,7 +9,7 @@
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.noodlewerk.${PRODUCT_NAME:rfc1034identifier}</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
@@ -17,19 +17,19 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>0.7.5</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>19</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.developer-tools</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2012 noodlewerk. All rights reserved.</string>
|
||||
<string>Copyright © 2012 noodlewerk. All rights reserved. https://github.com/noodlewerk/NWPusher</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<string>Application</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
@@ -6,4 +6,4 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#endif
|
||||
|
||||
#import "NWLogging.h"
|
||||
#import "NWLCore.h"
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>payload</key>
|
||||
<string>{"aps":{"alert":"Testing.. (0)","badge":1,"sound":"default"}}</string>
|
||||
<key>identifiers</key>
|
||||
<dict>
|
||||
<key>com.example.app</key>
|
||||
<array>
|
||||
<string>0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF my-iphone</string>
|
||||
</array>
|
||||
<key>com.example.app-sandbox</key>
|
||||
<array>
|
||||
<string>ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789 my-iphone</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -2,10 +2,12 @@
|
||||
// main.m
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
return NSApplicationMain(argc, (const char **)argv);
|
||||
@@ -0,0 +1,15 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'NWPusher'
|
||||
s.version = '0.7.5'
|
||||
s.summary = 'OS X and iOS application and framework to play with the Apple Push Notification service (APNs).'
|
||||
s.homepage = 'https://github.com/noodlewerk/NWPusher'
|
||||
s.license = { :type => 'BSD', :file => 'LICENSE.txt' }
|
||||
s.author = { 'Leonard van Driel' => 'leonardvandriel@gmail.com' }
|
||||
|
||||
s.ios.deployment_target = '6.0'
|
||||
s.osx.deployment_target = '10.8'
|
||||
s.requires_arc = true
|
||||
s.source = { :git => 'https://github.com/noodlewerk/NWPusher.git', :tag => s.version.to_s }
|
||||
s.source_files = 'Classes/*.{h,m,c}'
|
||||
s.framework = 'Security'
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5C7803A51D3C4826002107FB"
|
||||
BuildableName = "PusherKit.framework"
|
||||
BlueprintName = "PusherKit-OSX"
|
||||
ReferencedContainer = "container:NWPusher.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5C7803A51D3C4826002107FB"
|
||||
BuildableName = "PusherKit.framework"
|
||||
BlueprintName = "PusherKit-OSX"
|
||||
ReferencedContainer = "container:NWPusher.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5C7803A51D3C4826002107FB"
|
||||
BuildableName = "PusherKit.framework"
|
||||
BlueprintName = "PusherKit-OSX"
|
||||
ReferencedContainer = "container:NWPusher.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5C7803831D3C4668002107FB"
|
||||
BuildableName = "PusherKit.framework"
|
||||
BlueprintName = "PusherKit-iOS"
|
||||
ReferencedContainer = "container:NWPusher.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5C7803831D3C4668002107FB"
|
||||
BuildableName = "PusherKit.framework"
|
||||
BlueprintName = "PusherKit-iOS"
|
||||
ReferencedContainer = "container:NWPusher.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5C7803831D3C4668002107FB"
|
||||
BuildableName = "PusherKit.framework"
|
||||
BlueprintName = "PusherKit-iOS"
|
||||
ReferencedContainer = "container:NWPusher.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,222 +0,0 @@
|
||||
//
|
||||
// NWAppDelegate.m
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWAppDelegate.h"
|
||||
#import "NWPusher.h"
|
||||
#import "NWSecTools.h"
|
||||
|
||||
|
||||
@implementation NWAppDelegate {
|
||||
IBOutlet NSPopUpButton *certificatePopup;
|
||||
IBOutlet NSComboBox *tokenCombo;
|
||||
IBOutlet NSTextView *payloadField;
|
||||
IBOutlet NSTextField *countField;
|
||||
IBOutlet NSTextField *infoField;
|
||||
IBOutlet NSButton *pushButton;
|
||||
|
||||
NWPusher *pusher;
|
||||
NSArray *configuration;
|
||||
NSArray *certificates;
|
||||
NSUInteger index;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Application delegate
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification
|
||||
{
|
||||
NWLog(@"Application did finish launching");
|
||||
[NWLMultiLogger.shared addPrinter:self];
|
||||
NWLPrintInfo();
|
||||
|
||||
[self loadCertificatesFromKeychain];
|
||||
|
||||
NSURL *plist = [NSBundle.mainBundle URLForResource:@"configuration" withExtension:@"plist"];
|
||||
configuration = [NSArray arrayWithContentsOfURL:plist];
|
||||
|
||||
[self textDidChange:nil];
|
||||
index = 1;
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)notification
|
||||
{
|
||||
NWLog(@"Application will terminate");
|
||||
[pusher disconnect]; pusher = nil;
|
||||
}
|
||||
|
||||
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)application
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UI events
|
||||
|
||||
- (IBAction)certificateSelected:(NSPopUpButton *)sender
|
||||
{
|
||||
if (certificatePopup.indexOfSelectedItem) {
|
||||
id certificate = [certificates objectAtIndex:certificatePopup.indexOfSelectedItem - 1];
|
||||
[self selectCertificate:certificate];
|
||||
} else {
|
||||
[self selectCertificate:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)textDidChange:(NSNotification *)notification
|
||||
{
|
||||
NSUInteger length = payloadField.string.length;
|
||||
countField.stringValue = [NSString stringWithFormat:@"%lu", length];
|
||||
countField.textColor = length > 256 ? NSColor.redColor : NSColor.darkGrayColor;
|
||||
}
|
||||
|
||||
- (IBAction)push:(NSButton *)sender
|
||||
{
|
||||
if (pusher) {
|
||||
[self push];
|
||||
} else {
|
||||
NWLogWarn(@"No certificate selected");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)loadCertificatesFromKeychain
|
||||
{
|
||||
NSArray *certs = nil;
|
||||
BOOL findCerts = [NWSecTools keychainCertificates:&certs];
|
||||
if (!findCerts || !certs.count) {
|
||||
NWLogWarn(@"No push certificates in keychain.");
|
||||
}
|
||||
certs = [certs sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
|
||||
BOOL adev = [NWSecTools isDevelopmentCertificate:(__bridge SecCertificateRef)(a)];
|
||||
BOOL bdev = [NWSecTools isDevelopmentCertificate:(__bridge SecCertificateRef)(b)];
|
||||
if (adev != bdev) {
|
||||
return adev ? NSOrderedAscending : NSOrderedDescending;
|
||||
}
|
||||
NSString *aname = [NWSecTools identifierForCertificate:(__bridge SecCertificateRef)(a)];
|
||||
NSString *bname = [NWSecTools identifierForCertificate:(__bridge SecCertificateRef)(b)];
|
||||
return [aname compare:bname];
|
||||
}];
|
||||
certificates = certs;
|
||||
|
||||
[certificatePopup removeAllItems];
|
||||
[certificatePopup addItemWithTitle:@"Select Push Certificate"];
|
||||
for (id c in certificates) {
|
||||
BOOL development = [NWSecTools isDevelopmentCertificate:(__bridge SecCertificateRef)(c)];
|
||||
NSString *name = [NWSecTools identifierForCertificate:(__bridge SecCertificateRef)(c)];
|
||||
[certificatePopup addItemWithTitle:[NSString stringWithFormat:@"%@ (%@)", name, development ? @"development" : @"production"]];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)tokensForCertificate:(id)certificate
|
||||
{
|
||||
NSMutableArray *result = [[NSMutableArray alloc] init];
|
||||
BOOL development = [NWSecTools isDevelopmentCertificate:(__bridge SecCertificateRef)certificate];
|
||||
NSString *identifier = [NWSecTools identifierForCertificate:(__bridge SecCertificateRef)certificate];
|
||||
for (NSDictionary *dict in configuration) {
|
||||
NSArray *identifiers = [dict valueForKey:@"identifiers"];
|
||||
BOOL match = !identifiers;
|
||||
for (NSString *i in identifiers) {
|
||||
if ([i isEqualToString:identifier]) {
|
||||
match = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
NSArray *tokens = development ? [dict valueForKey:@"development"] : [dict valueForKey:@"production"];
|
||||
if (tokens.count) {
|
||||
[result addObjectsFromArray:tokens];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)selectCertificate:(id)certificate
|
||||
{
|
||||
if (pusher) {
|
||||
[pusher disconnect]; pusher = nil;
|
||||
pushButton.enabled = NO;
|
||||
NWLogInfo(@"Disconnected from APN");
|
||||
}
|
||||
|
||||
NSArray *tokens = [self tokensForCertificate:certificate];
|
||||
[tokenCombo removeAllItems];
|
||||
tokenCombo.stringValue = @"";
|
||||
[tokenCombo addItemsWithObjectValues:tokens];
|
||||
|
||||
if (certificate) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NWPusher *p = [[NWPusher alloc] init];
|
||||
BOOL sandbox = [NWSecTools isDevelopmentCertificate:(__bridge SecCertificateRef)(certificate)];
|
||||
BOOL connected = [p connectWithCertificateRef:(__bridge SecCertificateRef)(certificate) sandbox:sandbox];
|
||||
if (connected) {
|
||||
NWLogInfo(@"Connected established to APN%@", sandbox ? @" (sandbox)" : @"");
|
||||
pusher = p;
|
||||
pushButton.enabled = YES;
|
||||
} else {
|
||||
[p disconnect];
|
||||
[self deselectCombo];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)push
|
||||
{
|
||||
NSString *payload = payloadField.string;
|
||||
NSString *token = tokenCombo.stringValue;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSUInteger i = index++;
|
||||
NSDate *expires = [NSDate dateWithTimeIntervalSinceNow:86400];
|
||||
BOOL pushed = [pusher pushPayloadString:payload token:token identifier:i expires:expires];
|
||||
if (pushed) {
|
||||
NWLogInfo(@"Pushing payload #%i..", (int)i);
|
||||
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
|
||||
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
|
||||
NSUInteger identifier = 0;
|
||||
NSString *reason = nil;
|
||||
BOOL fetched = [pusher fetchFailedIdentifier:&identifier reason:&reason];
|
||||
if (fetched) {
|
||||
if (!reason.length) {
|
||||
NWLogInfo(@"Payload #%i has been pushed", (int)i);
|
||||
} else {
|
||||
NWLogWarn(@"Payload #%i could not be pushed: %@", (int)identifier, reason);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)deselectCombo
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[certificatePopup selectItemAtIndex:0];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - NWLPrinter
|
||||
|
||||
- (void)printWithTag:(NSString *)tag lib:(NSString *)lib file:(NSString *)file line:(NSUInteger)line function:(NSString *)function message:(NSString *)message
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
infoField.textColor = [tag isEqualToString:@"warn"] ? NSColor.redColor : NSColor.blackColor;
|
||||
infoField.stringValue = message;
|
||||
});
|
||||
}
|
||||
|
||||
- (NSString *)name
|
||||
{
|
||||
return NSStringFromClass(self.class);
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// NWPushFeedback.h
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
@interface NWPushFeedback : NSObject
|
||||
|
||||
- (BOOL)connectWithCertificateData:(NSData *)certificate sandbox:(BOOL)sandbox;
|
||||
- (BOOL)connectWithIdentity:(SecIdentityRef)identity sandbox:(BOOL)sandbox;
|
||||
- (BOOL)readDate:(NSDate **)date token:(NSData **)token;
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
@@ -1,92 +0,0 @@
|
||||
//
|
||||
// NWPushFeedback.m
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWPushFeedback.h"
|
||||
#import "NWSSLConnection.h"
|
||||
#import "NWSecTools.h"
|
||||
|
||||
|
||||
// http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html
|
||||
|
||||
static NSString * const NWSandboxPushHost = @"feedback.sandbox.push.apple.com";
|
||||
static NSString * const NWPushHost = @"feedback.push.apple.com";
|
||||
static NSUInteger const NWPushPort = 2196;
|
||||
static NSUInteger const NWTokenMaxSize = 32;
|
||||
|
||||
@implementation NWPushFeedback {
|
||||
NWSSLConnection *connection;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Apple SSL
|
||||
|
||||
- (BOOL)connectWithCertificateData:(NSData *)certificateData sandbox:(BOOL)sandbox
|
||||
{
|
||||
SecIdentityRef identity = NULL;
|
||||
BOOL result = [NWSecTools identityWithCertificateData:certificateData identity:&identity];
|
||||
if (!result) {
|
||||
return NO;
|
||||
}
|
||||
result = [self connectWithIdentity:identity sandbox:sandbox];
|
||||
CFRelease(identity);
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)connectWithIdentity:(SecIdentityRef)identity sandbox:(BOOL)sandbox
|
||||
{
|
||||
NSString *host = sandbox ? NWSandboxPushHost : NWPushHost;
|
||||
|
||||
if (connection) [connection disconnect];
|
||||
connection = [[NWSSLConnection alloc] initWithHost:host port:NWPushPort identity:identity];
|
||||
|
||||
BOOL result = [connection connect];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
[connection disconnect]; connection = nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Apple push
|
||||
|
||||
- (BOOL)readDate:(NSDate **)date token:(NSData **)token
|
||||
{
|
||||
NSMutableData *data = [NSMutableData dataWithLength:sizeof(uint32_t) + sizeof(uint16_t) + NWTokenMaxSize];
|
||||
NSUInteger length = 0;
|
||||
BOOL read = [connection read:data length:&length];
|
||||
if (!read) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (length) {
|
||||
if (length != data.length) {
|
||||
NWLogWarn(@"Unexpected response length: %@", [data subdataWithRange:NSMakeRange(0, length)]);
|
||||
return NO;
|
||||
}
|
||||
|
||||
uint32_t time = 0;
|
||||
[data getBytes:&time range:NSMakeRange(0, 4)];
|
||||
if (date) *date = [NSDate dateWithTimeIntervalSince1970:htonl(time)];
|
||||
|
||||
uint16_t l = 0;
|
||||
[data getBytes:&l range:NSMakeRange(4, 2)];
|
||||
NSUInteger tokenLength = htons(l);
|
||||
if (tokenLength != NWTokenMaxSize) {
|
||||
NWLogWarn(@"Unexpected token length: %i", (int)tokenLength);
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (token) *token = [data subdataWithRange:NSMakeRange(6, length - 6)];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// NWPusher.h
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
@interface NWPusher : NSObject
|
||||
|
||||
- (BOOL)connectWithCertificateRef:(SecCertificateRef)certificate sandbox:(BOOL)sandbox;
|
||||
- (BOOL)connectWithIdentityRef:(SecIdentityRef)identity sandbox:(BOOL)sandbox;
|
||||
- (BOOL)pushPayloadString:(NSString *)payload token:(NSString *)token;
|
||||
- (BOOL)pushPayloadString:(NSString *)payload token:(NSString *)token identifier:(NSUInteger)identifier expires:(NSDate *)expires;
|
||||
- (BOOL)pushPayloadData:(NSData *)payload tokenData:(NSData *)token;
|
||||
- (BOOL)pushPayloadData:(NSData *)payload tokenData:(NSData *)token identifier:(NSUInteger)identifier expires:(NSDate *)expires;
|
||||
- (BOOL)fetchFailedIdentifier:(NSUInteger *)identifier reason:(NSString **)reason;
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
@@ -1,231 +0,0 @@
|
||||
//
|
||||
// NWPusher.m
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWPusher.h"
|
||||
#import "NWSSLConnection.h"
|
||||
#import "NWSecTools.h"
|
||||
|
||||
|
||||
// http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html
|
||||
|
||||
static NSString * const NWSandboxPushHost = @"gateway.sandbox.push.apple.com";
|
||||
static NSString * const NWPushHost = @"gateway.push.apple.com";
|
||||
static NSUInteger const NWPushPort = 2195;
|
||||
static NSUInteger const NWDeviceTokenSize = 32;
|
||||
static NSUInteger const NWPayloadMaxSize = 256;
|
||||
|
||||
@implementation NWPusher {
|
||||
NWSSLConnection *connection;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Apple SSL
|
||||
|
||||
- (BOOL)connectWithCertificateRef:(SecCertificateRef)certificate sandbox:(BOOL)sandbox
|
||||
{
|
||||
SecIdentityRef identity = NULL;
|
||||
BOOL result = [NWSecTools identityWithCertificateRef:certificate identity:&identity];
|
||||
if (!result) {
|
||||
return NO;
|
||||
}
|
||||
result = [self connectWithIdentityRef:identity sandbox:sandbox];
|
||||
CFRelease(identity);
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)connectWithIdentityRef:(SecIdentityRef)identity sandbox:(BOOL)sandbox
|
||||
{
|
||||
NSString *host = sandbox ? NWSandboxPushHost : NWPushHost;
|
||||
|
||||
if (connection) [connection disconnect];
|
||||
connection = [[NWSSLConnection alloc] initWithHost:host port:NWPushPort identity:identity];
|
||||
|
||||
BOOL result = [connection connect];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
[connection disconnect]; connection = nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Apple push
|
||||
|
||||
- (BOOL)pushPayloadString:(NSString *)payload token:(NSString *)token
|
||||
{
|
||||
BOOL result = [self pushPayloadString:payload token:token identifier:0 expires:NULL];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)pushPayloadString:(NSString *)payload token:(NSString *)token identifier:(NSUInteger)identifier expires:(NSDate *)expires
|
||||
{
|
||||
if (!payload.length) {
|
||||
NWLogWarn(@"Payload is empty");
|
||||
return NO;
|
||||
}
|
||||
NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSError *error = nil;
|
||||
NSDictionary *payloadDict = [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:&error];
|
||||
NWLogWarnIfError(error);
|
||||
if (!payloadDict.count) {
|
||||
NWLogWarn(@"Invalid payload format");
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (!token.length) {
|
||||
NWLogWarn(@"Device token is emtpy");
|
||||
return NO;
|
||||
}
|
||||
NSString *normal = [self.class filterHex:token];
|
||||
NSUInteger max = NWDeviceTokenSize * 2;
|
||||
NSString *trunk = normal.length >= max ? [normal substringToIndex:max] : nil;
|
||||
NSData *tokenData = [self.class dataFromHex:trunk];
|
||||
if (!tokenData.length) {
|
||||
NWLogWarn(@"Unable to read device token");
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL result = [self pushPayloadData:payloadData tokenData:tokenData identifier:identifier expires:expires];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)pushPayloadData:(NSData *)payload tokenData:(NSData *)token
|
||||
{
|
||||
BOOL result = [self pushPayloadData:payload tokenData:token enhance:NO identifier:0 expires:nil];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)pushPayloadData:(NSData *)payload tokenData:(NSData *)token identifier:(NSUInteger)identifier expires:(NSDate *)expires
|
||||
{
|
||||
BOOL result = [self pushPayloadData:payload tokenData:token enhance:YES identifier:identifier expires:expires];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)pushPayloadData:(NSData *)payload tokenData:(NSData *)token enhance:(BOOL)enhance identifier:(NSUInteger)identifier expires:(NSDate *)expires
|
||||
{
|
||||
if (token.length != NWDeviceTokenSize) {
|
||||
NWLogWarn(@"Invalid device token specified (%i hex characters)", (int)NWDeviceTokenSize * 2);
|
||||
return NO;
|
||||
}
|
||||
if (payload.length > NWPayloadMaxSize) {
|
||||
NWLogWarn(@"Payload cannot be more than %i bytes (UTF8)", (int)NWPayloadMaxSize);
|
||||
return NO;
|
||||
}
|
||||
|
||||
char buffer[sizeof(uint8_t) + sizeof(uint32_t) * 2 + sizeof(uint16_t) + NWDeviceTokenSize + sizeof(uint16_t) + NWPayloadMaxSize];
|
||||
char *p = buffer;
|
||||
|
||||
uint8_t command = enhance ? 1 : 0;
|
||||
memcpy(p, &command, sizeof(uint8_t));
|
||||
p += sizeof(uint8_t);
|
||||
|
||||
if (enhance) {
|
||||
uint32_t ID = htonl(identifier);
|
||||
memcpy(p, &ID, sizeof(uint32_t));
|
||||
p += sizeof(uint32_t);
|
||||
|
||||
uint32_t exp = htonl((NSUInteger)expires.timeIntervalSince1970);
|
||||
memcpy(p, &exp, sizeof(uint32_t));
|
||||
p += sizeof(uint32_t);
|
||||
}
|
||||
|
||||
uint16_t tokenLength = htons(token.length);
|
||||
memcpy(p, &tokenLength, sizeof(uint16_t));
|
||||
p += sizeof(uint16_t);
|
||||
|
||||
memcpy(p, token.bytes, token.length);
|
||||
p += token.length;
|
||||
|
||||
uint16_t payloadLength = htons(payload.length);
|
||||
memcpy(p, &payloadLength, sizeof(uint16_t));
|
||||
p += sizeof(uint16_t);
|
||||
|
||||
memcpy(p, payload.bytes, payload.length);
|
||||
p += payload.length;
|
||||
|
||||
NSData *data = [NSData dataWithBytes:buffer length:p - buffer];
|
||||
BOOL result = [connection write:data length:NULL];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)fetchFailedIdentifier:(NSUInteger *)identifier reason:(NSString **)reason
|
||||
{
|
||||
NSMutableData *data = [NSMutableData dataWithLength:sizeof(uint8_t) * 2 + sizeof(uint32_t)];
|
||||
NSUInteger length = 0;
|
||||
BOOL read = [connection read:data length:&length];
|
||||
if (!read) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (length) {
|
||||
uint8_t command = 0;
|
||||
[data getBytes:&command range:NSMakeRange(0, 1)];
|
||||
if (command != 8) {
|
||||
NWLogWarn(@"Unexpected response command: %i", command);
|
||||
return NO;
|
||||
}
|
||||
if (length != data.length) {
|
||||
NWLogWarn(@"Unexpected response length: %@", data);
|
||||
return NO;
|
||||
}
|
||||
uint8_t status = 0;
|
||||
[data getBytes:&status range:NSMakeRange(1, 1)];
|
||||
uint32_t ID = 0;
|
||||
[data getBytes:&ID range:NSMakeRange(2, 4)];
|
||||
if (identifier) *identifier = htonl(ID);
|
||||
if (reason) {
|
||||
switch (status) {
|
||||
case 0: *reason = @"No errors encountered"; break;
|
||||
case 1: *reason = @"Processing error"; break;
|
||||
case 2: *reason = @"Missing device token"; break;
|
||||
case 3: *reason = @"Missing topic"; break;
|
||||
case 4: *reason = @"Missing payload"; break;
|
||||
case 5: *reason = @"Invalid token size"; break;
|
||||
case 6: *reason = @"Invalid topic size"; break;
|
||||
case 7: *reason = @"Invalid payload size"; break;
|
||||
case 8: *reason = @"Invalid token"; break;
|
||||
default: *reason = [NSString stringWithFormat:@"Unknown error encountered (%i)", status]; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
+ (NSString *)filterHex:(NSString *)hex
|
||||
{
|
||||
hex = hex.lowercaseString;
|
||||
NSMutableString *result = [[NSMutableString alloc] init];
|
||||
for (NSUInteger i = 0; i < hex.length; i++) {
|
||||
unichar c = [hex characterAtIndex:i];
|
||||
if ((c >= 'a' && c <= 'f') || (c >= '0' && c <= '9')) {
|
||||
[result appendString:[NSString stringWithCharacters:&c length:1]];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (NSData *)dataFromHex:(NSString *)hex
|
||||
{
|
||||
NSMutableData *result = [[NSMutableData alloc] init];
|
||||
char buffer[3] = {'\0','\0','\0'};
|
||||
for (NSUInteger i = 0; i < hex.length / 2; i++) {
|
||||
buffer[0] = [hex characterAtIndex:i * 2];
|
||||
buffer[1] = [hex characterAtIndex:i * 2 + 1];
|
||||
unsigned char b = strtol(buffer, NULL, 16);
|
||||
[result appendBytes:&b length:1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// NWSSLConnection.h
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
@interface NWSSLConnection : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSString *host;
|
||||
@property (nonatomic, assign) NSUInteger port;
|
||||
@property (nonatomic, assign) SecIdentityRef identity;
|
||||
|
||||
- (id)initWithHost:(NSString *)host port:(NSUInteger)port identity:(SecIdentityRef)identity;
|
||||
- (BOOL)connect;
|
||||
- (BOOL)read:(NSMutableData *)data length:(NSUInteger *)length;
|
||||
- (BOOL)write:(NSData *)data length:(NSUInteger *)length;
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
@@ -1,148 +0,0 @@
|
||||
//
|
||||
// NWSSLConnection.m
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWSSLConnection.h"
|
||||
#import "ioSock.h"
|
||||
#include <sys/socket.h>
|
||||
|
||||
@implementation NWSSLConnection {
|
||||
otSocket connection;
|
||||
SSLContextRef context;
|
||||
}
|
||||
|
||||
@synthesize host, port, identity;
|
||||
|
||||
- (id)initWithHost:(NSString *)_host port:(NSUInteger)_port identity:(SecIdentityRef)_identity
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
host = _host;
|
||||
port = _port;
|
||||
identity = _identity;
|
||||
CFRetain(identity);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self disconnect];
|
||||
if (identity) CFRelease(identity); identity = NULL;
|
||||
}
|
||||
|
||||
- (BOOL)connect
|
||||
{
|
||||
PeerSpec spec;
|
||||
OSStatus status = MakeServerConnection(host.UTF8String, (int)port, &connection, &spec);
|
||||
if (status != noErr) {
|
||||
NWLogWarn(@"Unable to create connection to server (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
status = SSLNewContext(false, &context);
|
||||
if (status != noErr) {
|
||||
NWLogWarn(@"Unable create SSL context (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
status = SSLSetIOFuncs(context, SocketRead, SocketWrite);
|
||||
if (status != noErr) {
|
||||
NWLogWarn(@"Unable to set socket callbacks (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
status = SSLSetConnection(context, connection);
|
||||
if (status != noErr) {
|
||||
NWLogWarn(@"Unable to set SSL connection (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
status = SSLSetPeerDomainName(context, host.UTF8String, strlen(host.UTF8String));
|
||||
if (status != noErr) {
|
||||
NWLogWarn(@"Unable to set peer domain (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
CFArrayRef certificates = CFArrayCreate(NULL, (const void **)&identity, 1, NULL);
|
||||
status = SSLSetCertificate(context, certificates);
|
||||
CFRelease(certificates);
|
||||
if (status != noErr) {
|
||||
NWLogWarn(@"Unable to assign certificate (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
do {
|
||||
status = SSLHandshake(context);
|
||||
} while(status == errSSLWouldBlock);
|
||||
if (status != noErr) {
|
||||
switch (status) {
|
||||
case ioErr: NWLogWarn(@"Unable to perform SSL handshake, no connection"); break;
|
||||
case errSecAuthFailed: NWLogWarn(@"Unable to perform SSL handshake, authentication failed"); break;
|
||||
default: NWLogWarn(@"Unable to perform SSL handshake (%i)", status); break;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
int set = 1;
|
||||
setsockopt((int)connection, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)read:(NSMutableData *)data length:(NSUInteger *)length
|
||||
{
|
||||
size_t processed = 0;
|
||||
void *bytes = data.mutableBytes;
|
||||
OSStatus status = errSSLWouldBlock;
|
||||
for (NSUInteger i = 0; i < 4 && status == errSSLWouldBlock; i++) {
|
||||
status = SSLRead(context, bytes, data.length, &processed);
|
||||
}
|
||||
if (status != noErr && status != errSSLWouldBlock) {
|
||||
switch (status) {
|
||||
case ioErr: NWLogWarn(@"Failed to read, connection dropped by server"); break;
|
||||
case errSSLClosedAbort: NWLogWarn(@"Failed to read, connection error"); break;
|
||||
case errSSLClosedGraceful: NWLogWarn(@"Failed to read, connection closed"); break;
|
||||
default: NWLogWarn(@"Failed to read (%i %zu)", status, processed); break;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (length) *length = processed;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)write:(NSData *)data length:(NSUInteger *)length
|
||||
{
|
||||
size_t processed = 0;
|
||||
const void *bytes = data.bytes;
|
||||
OSStatus status = errSSLWouldBlock;
|
||||
for (NSUInteger i = 0; i < 4 && status == errSSLWouldBlock; i++) {
|
||||
status = SSLWrite(context, bytes, data.length, &processed);
|
||||
}
|
||||
if (status != noErr && status != errSSLWouldBlock) {
|
||||
switch (status) {
|
||||
case ioErr: NWLogWarn(@"Failed to write, connection dropped by server"); break;
|
||||
case errSSLClosedAbort: NWLogWarn(@"Failed to write, connection error"); break;
|
||||
case errSSLClosedGraceful: NWLogWarn(@"Failed to write, connection closed"); break;
|
||||
default: NWLogWarn(@"Failed to write (%i %zu)", status, processed); break;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (length) *length = processed;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
SSLClose(context);
|
||||
close((int)connection); connection = NULL;
|
||||
SSLDisposeContext(context); context = NULL;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// NWSecTools.h
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
@interface NWSecTools : NSObject
|
||||
|
||||
+ (BOOL)identityWithCertificateRef:(SecCertificateRef)certificate identity:(SecIdentityRef *)identity;
|
||||
+ (BOOL)identityWithCertificateData:(NSData *)certificate identity:(SecIdentityRef *)identity;
|
||||
+ (BOOL)identityWithPKCS12Data:(NSData *)pkcs12 password:(NSString *)password identity:(SecIdentityRef *)identity;
|
||||
|
||||
+ (BOOL)keychainCertificates:(NSArray **)certificates;
|
||||
+ (BOOL)isDevelopmentCertificate:(SecCertificateRef)certificate;
|
||||
+ (NSString *)identifierForCertificate:(SecCertificateRef)certificate;
|
||||
|
||||
@end
|
||||
@@ -1,140 +0,0 @@
|
||||
//
|
||||
// NWSecTools.m
|
||||
// Pusher
|
||||
//
|
||||
// Created by Leo on 9/9/12.
|
||||
// Copyright (c) 2012 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWSecTools.h"
|
||||
|
||||
static NSString * const NWDevelpmentPrefix = @"Apple Development IOS Push Services: ";
|
||||
static NSString * const NWProductionPrefix = @"Apple Production IOS Push Services: ";
|
||||
|
||||
typedef enum {
|
||||
kNWCertificateTypeNone = 0,
|
||||
kNWCertificateTypeDevelopment = 1,
|
||||
kNWCertificateTypeProduction = 2,
|
||||
} NWCertificateType;
|
||||
|
||||
|
||||
@implementation NWSecTools
|
||||
|
||||
+ (BOOL)identityWithCertificateData:(NSData *)certificate identity:(SecIdentityRef *)identity
|
||||
{
|
||||
SecCertificateRef c = SecCertificateCreateWithData(kCFAllocatorDefault, (__bridge CFDataRef)certificate);
|
||||
if (!c) {
|
||||
NWLogWarn(@"Unable to read certificate");
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL result = [self identityWithCertificateRef:c identity:identity];
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (BOOL)identityWithCertificateRef:(SecCertificateRef)certificate identity:(SecIdentityRef *)identity
|
||||
{
|
||||
OSStatus status = SecIdentityCreateWithCertificate(NULL, certificate, identity);
|
||||
if (status != noErr) {
|
||||
switch (status) {
|
||||
case errSecItemNotFound: NWLogWarn(@"Unable to create identitiy, private key missing"); break;
|
||||
default: NWLogWarn(@"Unable to create identitiy (%i)", status); break;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)identityWithPKCS12Data:(NSData *)pkcs12 password:(NSString *)password identity:(SecIdentityRef *)identity
|
||||
{
|
||||
const void *keys[] = {kSecImportExportPassphrase};
|
||||
const void *values[] = {(__bridge const void *)password};
|
||||
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
|
||||
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
|
||||
OSStatus status = SecPKCS12Import((__bridge CFDataRef)pkcs12, options, &items);
|
||||
CFRelease(options);
|
||||
if (status != noErr) {
|
||||
NWLogWarn(@"Unable to import PKCS12 data (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
CFIndex count = CFArrayGetCount(items);
|
||||
if (!count) {
|
||||
NWLogWarn(@"No items in PKCS12 data (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
CFDictionaryRef dict = CFArrayGetValueAtIndex(items, 0);
|
||||
*identity = (SecIdentityRef)CFDictionaryGetValue(dict, kSecImportItemIdentity);
|
||||
if (!identity) {
|
||||
NWLogWarn(@"No identity in PKCS12 data (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
CFRetain(*identity);
|
||||
CFRelease(items);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)keychainCertificates:(NSArray **)certificates
|
||||
{
|
||||
const void *keys[] = {kSecClass, kSecMatchLimit};
|
||||
int i = 1000;
|
||||
CFNumberRef n = CFNumberCreate(NULL, kCFNumberIntType, &i);
|
||||
const void *values[] = {kSecClassCertificate, n};
|
||||
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 2, NULL, NULL);
|
||||
CFArrayRef results = NULL;
|
||||
|
||||
OSStatus status = SecItemCopyMatching(options, (CFTypeRef *)&results);
|
||||
CFRelease(options);
|
||||
if (status != noErr) {
|
||||
NWLogWarn(@"Unable to find certificate (%i)", status);
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSMutableArray *certs = [[NSMutableArray alloc] init];
|
||||
NSArray *candidates = CFBridgingRelease(results);
|
||||
for (id c in candidates) {
|
||||
SecCertificateRef certificate = (__bridge SecCertificateRef)(c);
|
||||
NWCertificateType type = [self typeForCertificate:certificate identifier:nil];
|
||||
if (type == kNWCertificateTypeDevelopment || type == kNWCertificateTypeProduction) {
|
||||
[certs addObject:c];
|
||||
}
|
||||
}
|
||||
if (certificates) *certificates = certs;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)isDevelopmentCertificate:(SecCertificateRef)certificate
|
||||
{
|
||||
BOOL result = [self typeForCertificate:certificate identifier:nil] == kNWCertificateTypeDevelopment;
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (NSString *)identifierForCertificate:(SecCertificateRef)certificate
|
||||
{
|
||||
NSString *result = nil;
|
||||
[self typeForCertificate:certificate identifier:&result];
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (NWCertificateType)typeForCertificate:(SecCertificateRef)certificate identifier:(NSString **)identifier
|
||||
{
|
||||
CFStringRef ref = NULL;
|
||||
SecCertificateCopyCommonName(certificate, &ref);
|
||||
NSString *name = CFBridgingRelease(ref);
|
||||
if ([name hasPrefix:NWDevelpmentPrefix]) {
|
||||
if (identifier) *identifier = [name substringFromIndex:NWDevelpmentPrefix.length];
|
||||
return kNWCertificateTypeDevelopment;
|
||||
}
|
||||
if ([name hasPrefix:NWProductionPrefix]) {
|
||||
if (identifier) *identifier = [name substringFromIndex:NWProductionPrefix.length];
|
||||
return kNWCertificateTypeProduction;
|
||||
}
|
||||
return kNWCertificateTypeNone;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,453 +0,0 @@
|
||||
/*
|
||||
File: ioSock.h
|
||||
|
||||
Contains: SecureTransport sample I/O module, X sockets version
|
||||
|
||||
Copyright: © Copyright 2002 Apple Computer, Inc. All rights reserved.
|
||||
|
||||
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc.
|
||||
("Apple") in consideration of your agreement to the following terms, and your
|
||||
use, installation, modification or redistribution of this Apple software
|
||||
constitutes acceptance of these terms. If you do not agree with these terms,
|
||||
please do not use, install, modify or redistribute this Apple software.
|
||||
|
||||
In consideration of your agreement to abide by the following terms, and subject
|
||||
to these terms, Apple grants you a personal, non-exclusive license, under AppleÕs
|
||||
copyrights in this original Apple software (the "Apple Software"), to use,
|
||||
reproduce, modify and redistribute the Apple Software, with or without
|
||||
modifications, in source and/or binary forms; provided that if you redistribute
|
||||
the Apple Software in its entirety and without modifications, you must retain
|
||||
this notice and the following text and disclaimers in all such redistributions of
|
||||
the Apple Software. Neither the name, trademarks, service marks or logos of
|
||||
Apple Computer, Inc. may be used to endorse or promote products derived from the
|
||||
Apple Software without specific prior written permission from Apple. Except as
|
||||
expressly stated in this notice, no other rights or licenses, express or implied,
|
||||
are granted by Apple herein, including but not limited to any patent rights that
|
||||
may be infringed by your derivative works or by other works in which the Apple
|
||||
Software may be incorporated.
|
||||
|
||||
The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO
|
||||
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
|
||||
WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
|
||||
COMBINATION WITH YOUR PRODUCTS.
|
||||
|
||||
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
|
||||
OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT
|
||||
(INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Change History (most recent first):
|
||||
11/4/02 1.0d1
|
||||
|
||||
*/
|
||||
|
||||
|
||||
#include "ioSock.h"
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <CoreServices/../Frameworks/CarbonCore.framework/Headers/MacErrors.h>
|
||||
#include <time.h>
|
||||
#include <strings.h>
|
||||
|
||||
/* debugging for this module */
|
||||
#define SSL_OT_DEBUG 1
|
||||
|
||||
/* log errors to stdout */
|
||||
#define SSL_OT_ERRLOG 1
|
||||
|
||||
/* trace all low-level network I/O */
|
||||
#define SSL_OT_IO_TRACE 0
|
||||
|
||||
/* if SSL_OT_IO_TRACE, only log non-zero length transfers */
|
||||
#define SSL_OT_IO_TRACE_NZ 1
|
||||
|
||||
/* pause after each I/O (only meaningful if SSL_OT_IO_TRACE == 1) */
|
||||
#define SSL_OT_IO_PAUSE 0
|
||||
|
||||
/* print a stream of dots while I/O pending */
|
||||
#define SSL_OT_DOT 1
|
||||
|
||||
/* dump some bytes of each I/O (only meaningful if SSL_OT_IO_TRACE == 1) */
|
||||
#define SSL_OT_IO_DUMP 0
|
||||
#define SSL_OT_IO_DUMP_SIZE 256
|
||||
|
||||
/* general, not-too-verbose debugging */
|
||||
#if SSL_OT_DEBUG
|
||||
#define dprintf(s) printf s
|
||||
#else
|
||||
#define dprintf(s)
|
||||
#endif
|
||||
|
||||
/* errors --> stdout */
|
||||
#if SSL_OT_ERRLOG
|
||||
#define eprintf(s) printf s
|
||||
#else
|
||||
#define eprintf(s)
|
||||
#endif
|
||||
|
||||
/* enable nonblocking I/O - maybe should be an arg to MakeServerConnection() */
|
||||
#define NON_BLOCKING 1
|
||||
|
||||
/* trace completion of every r/w */
|
||||
#if SSL_OT_IO_TRACE
|
||||
static void tprintf(
|
||||
const char *str,
|
||||
UInt32 req,
|
||||
UInt32 act,
|
||||
const UInt8 *buf)
|
||||
{
|
||||
#if SSL_OT_IO_TRACE_NZ
|
||||
if(act == 0) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
printf("%s(%d): moved (%d) bytes\n", str, req, act);
|
||||
#if SSL_OT_IO_DUMP
|
||||
{
|
||||
int i;
|
||||
|
||||
for(i=0; i<act; i++) {
|
||||
printf("%02X ", buf[i]);
|
||||
if(i >= (SSL_OT_IO_DUMP_SIZE - 1)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
#endif
|
||||
#if SSL_OT_IO_PAUSE
|
||||
{
|
||||
char instr[20];
|
||||
printf("CR to continue: ");
|
||||
gets(instr);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#else
|
||||
#define tprintf(str, req, act, buf)
|
||||
#endif /* SSL_OT_IO_TRACE */
|
||||
|
||||
/*
|
||||
* If SSL_OT_DOT, output a '.' every so often while waiting for
|
||||
* connection. This gives user a chance to do something else with the
|
||||
* UI.
|
||||
*/
|
||||
|
||||
#if SSL_OT_DOT
|
||||
|
||||
static time_t lastTime = (time_t)0;
|
||||
#define TIME_INTERVAL 3
|
||||
|
||||
static void outputDot()
|
||||
{
|
||||
time_t thisTime = time(0);
|
||||
|
||||
if((thisTime - lastTime) >= TIME_INTERVAL) {
|
||||
printf("."); fflush(stdout);
|
||||
lastTime = thisTime;
|
||||
}
|
||||
}
|
||||
#else
|
||||
#define outputDot()
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
* One-time only init.
|
||||
*/
|
||||
void initSslOt()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Connect to server.
|
||||
*/
|
||||
OSStatus MakeServerConnection(
|
||||
const char *hostName,
|
||||
int port,
|
||||
otSocket *socketNo, // RETURNED
|
||||
PeerSpec *peer) // RETURNED
|
||||
{
|
||||
struct sockaddr_in addr;
|
||||
struct hostent *ent;
|
||||
struct in_addr host;
|
||||
int sock = 0;
|
||||
|
||||
*socketNo = NULL;
|
||||
if (hostName[0] >= '0' && hostName[0] <= '9')
|
||||
{
|
||||
host.s_addr = inet_addr(hostName);
|
||||
}
|
||||
else
|
||||
{ ent = gethostbyname(hostName);
|
||||
if (!ent)
|
||||
{ printf("gethostbyname failed\n");
|
||||
return ioErr;
|
||||
}
|
||||
memcpy(&host, ent->h_addr, sizeof(struct in_addr));
|
||||
}
|
||||
sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
addr.sin_addr = host;
|
||||
addr.sin_port = htons((u_short)port);
|
||||
|
||||
addr.sin_family = AF_INET;
|
||||
if (connect(sock, (struct sockaddr *) &addr, sizeof(struct sockaddr_in)) != 0)
|
||||
{ printf("connect returned error\n");
|
||||
return ioErr;
|
||||
}
|
||||
|
||||
#if NON_BLOCKING
|
||||
/* OK to do this after connect? */
|
||||
{
|
||||
int rtn = fcntl(sock, F_SETFL, O_NONBLOCK);
|
||||
if(rtn == -1) {
|
||||
perror("fctnl(O_NONBLOCK)");
|
||||
return ioErr;
|
||||
}
|
||||
}
|
||||
#endif /* NON_BLOCKING*/
|
||||
|
||||
peer->ipAddr = addr.sin_addr.s_addr;
|
||||
peer->port = htons((u_short)port);
|
||||
*socketNo = (otSocket)sock;
|
||||
return noErr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Set up an otSocket to listen for client connections. Call once, then
|
||||
* use multiple AcceptClientConnection calls.
|
||||
*/
|
||||
OSStatus ListenForClients(
|
||||
int port,
|
||||
otSocket *socketNo) // RETURNED
|
||||
{
|
||||
struct sockaddr_in addr;
|
||||
struct hostent *ent;
|
||||
int len;
|
||||
int sock;
|
||||
|
||||
sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(sock < 1) {
|
||||
perror("socket");
|
||||
return ioErr;
|
||||
}
|
||||
|
||||
ent = gethostbyname("localhost");
|
||||
if (!ent) {
|
||||
perror("gethostbyname");
|
||||
return ioErr;
|
||||
}
|
||||
memcpy(&addr.sin_addr, ent->h_addr, sizeof(struct in_addr));
|
||||
|
||||
addr.sin_port = htons((u_short)port);
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
addr.sin_family = AF_INET;
|
||||
len = sizeof(struct sockaddr_in);
|
||||
if (bind(sock, (struct sockaddr *) &addr, len)) {
|
||||
perror("bind");
|
||||
return ioErr;
|
||||
}
|
||||
if (listen(sock, 1)) {
|
||||
perror("listen");
|
||||
return ioErr;
|
||||
}
|
||||
*socketNo = (otSocket)sock;
|
||||
return noErr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Accept a client connection.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Currently we always get back a different peer port number on successive
|
||||
* connections, no matter what the client is doing. To test for resumable
|
||||
* session support, force peer port = 0.
|
||||
*/
|
||||
#define FORCE_ACCEPT_PEER_PORT_ZERO 1
|
||||
|
||||
OSStatus AcceptClientConnection(
|
||||
otSocket listenSock, // obtained from ListenForClients
|
||||
otSocket *acceptSock, // RETURNED
|
||||
PeerSpec *peer) // RETURNED
|
||||
{
|
||||
struct sockaddr_in addr;
|
||||
int sock;
|
||||
socklen_t len;
|
||||
|
||||
len = sizeof(struct sockaddr_in);
|
||||
sock = accept((int)listenSock, (struct sockaddr *) &addr, &len);
|
||||
if (sock < 0) {
|
||||
perror("accept");
|
||||
return ioErr;
|
||||
}
|
||||
*acceptSock = (otSocket)sock;
|
||||
peer->ipAddr = addr.sin_addr.s_addr;
|
||||
#if FORCE_ACCEPT_PEER_PORT_ZERO
|
||||
peer->port = 0;
|
||||
#else
|
||||
peer->port = ntohs(addr.sin_port);
|
||||
#endif
|
||||
return noErr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Shut down a connection.
|
||||
*/
|
||||
void endpointShutdown(
|
||||
otSocket socket)
|
||||
{
|
||||
close((int)socket);
|
||||
}
|
||||
|
||||
/*
|
||||
* R/W. Called out from SSL.
|
||||
*/
|
||||
OSStatus SocketRead(
|
||||
SSLConnectionRef connection,
|
||||
void *data, /* owned by
|
||||
* caller, data
|
||||
* RETURNED */
|
||||
size_t *dataLength) /* IN/OUT */
|
||||
{
|
||||
UInt32 bytesToGo = (UInt32)*dataLength;
|
||||
UInt32 initLen = bytesToGo;
|
||||
UInt8 *currData = (UInt8 *)data;
|
||||
int sock = (int)connection;
|
||||
OSStatus rtn = noErr;
|
||||
UInt32 bytesRead;
|
||||
int rrtn;
|
||||
|
||||
*dataLength = 0;
|
||||
|
||||
for(;;) {
|
||||
bytesRead = 0;
|
||||
rrtn = (int)read(sock, currData, bytesToGo);
|
||||
if (rrtn <= 0) {
|
||||
/* this is guesswork... */
|
||||
int theErr = errno;
|
||||
// dprintf(("SocketRead: read(%d) error %d\n", (int)bytesToGo, theErr));
|
||||
#if !NON_BLOCKING
|
||||
if((rrtn == 0) && (theErr == 0)) {
|
||||
/* try fix for iSync */
|
||||
rtn = errSSLClosedGraceful;
|
||||
//rtn = errSSLClosedAbort;
|
||||
}
|
||||
else /* do the switch */
|
||||
#endif
|
||||
switch(theErr) {
|
||||
case ENOENT:
|
||||
/* connection closed */
|
||||
rtn = errSSLClosedGraceful;
|
||||
break;
|
||||
case ECONNRESET:
|
||||
rtn = errSSLClosedAbort;
|
||||
break;
|
||||
#if NON_BLOCKING
|
||||
case EAGAIN:
|
||||
#else
|
||||
case 0: /* ??? */
|
||||
#endif
|
||||
rtn = errSSLWouldBlock;
|
||||
break;
|
||||
default:
|
||||
dprintf(("SocketRead: read(%d) error %d\n",
|
||||
(int)bytesToGo, theErr));
|
||||
rtn = ioErr;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
else {
|
||||
bytesRead = rrtn;
|
||||
}
|
||||
bytesToGo -= bytesRead;
|
||||
currData += bytesRead;
|
||||
|
||||
if(bytesToGo == 0) {
|
||||
/* filled buffer with incoming data, done */
|
||||
break;
|
||||
}
|
||||
}
|
||||
*dataLength = initLen - bytesToGo;
|
||||
tprintf("SocketRead", initLen, *dataLength, (UInt8 *)data);
|
||||
|
||||
#if SSL_OT_DOT || (SSL_OT_DEBUG && !SSL_OT_IO_TRACE)
|
||||
if((rtn == 0) && (*dataLength == 0)) {
|
||||
/* keep UI alive */
|
||||
outputDot();
|
||||
}
|
||||
#endif
|
||||
return rtn;
|
||||
}
|
||||
|
||||
int oneAtATime = 0;
|
||||
|
||||
OSStatus SocketWrite(
|
||||
SSLConnectionRef connection,
|
||||
const void *data,
|
||||
size_t *dataLength) /* IN/OUT */
|
||||
{
|
||||
UInt32 bytesSent = 0;
|
||||
int sock = (int)connection;
|
||||
int length;
|
||||
UInt32 dataLen = (UInt32)*dataLength;
|
||||
const UInt8 *dataPtr = (UInt8 *)data;
|
||||
OSStatus ortn;
|
||||
|
||||
if(oneAtATime && (*dataLength > 1)) {
|
||||
UInt32 i;
|
||||
UInt32 outLen;
|
||||
UInt32 thisMove;
|
||||
|
||||
outLen = 0;
|
||||
for(i=0; i<dataLen; i++) {
|
||||
thisMove = 1;
|
||||
ortn = SocketWrite(connection, dataPtr, (size_t *)&thisMove);
|
||||
outLen += thisMove;
|
||||
dataPtr++;
|
||||
if(ortn) {
|
||||
return ortn;
|
||||
}
|
||||
}
|
||||
return noErr;
|
||||
}
|
||||
*dataLength = 0;
|
||||
|
||||
do {
|
||||
length = (int)write(sock,
|
||||
(char*)dataPtr + bytesSent,
|
||||
dataLen - bytesSent);
|
||||
} while ((length > 0) &&
|
||||
( (bytesSent += length) < dataLen) );
|
||||
|
||||
if(length <= 0) {
|
||||
if(errno == EAGAIN) {
|
||||
ortn = errSSLWouldBlock;
|
||||
}
|
||||
else {
|
||||
ortn = ioErr;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ortn = noErr;
|
||||
}
|
||||
tprintf("SocketWrite", dataLen, bytesSent, dataPtr);
|
||||
*dataLength = bytesSent;
|
||||
return ortn;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
File: ioSock.h
|
||||
|
||||
Contains: socket-based I/O routines for SecureTransport tests
|
||||
|
||||
Copyright: © Copyright 2002 Apple Computer, Inc. All rights reserved.
|
||||
|
||||
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc.
|
||||
("Apple") in consideration of your agreement to the following terms, and your
|
||||
use, installation, modification or redistribution of this Apple software
|
||||
constitutes acceptance of these terms. If you do not agree with these terms,
|
||||
please do not use, install, modify or redistribute this Apple software.
|
||||
|
||||
In consideration of your agreement to abide by the following terms, and subject
|
||||
to these terms, Apple grants you a personal, non-exclusive license, under AppleÕs
|
||||
copyrights in this original Apple software (the "Apple Software"), to use,
|
||||
reproduce, modify and redistribute the Apple Software, with or without
|
||||
modifications, in source and/or binary forms; provided that if you redistribute
|
||||
the Apple Software in its entirety and without modifications, you must retain
|
||||
this notice and the following text and disclaimers in all such redistributions of
|
||||
the Apple Software. Neither the name, trademarks, service marks or logos of
|
||||
Apple Computer, Inc. may be used to endorse or promote products derived from the
|
||||
Apple Software without specific prior written permission from Apple. Except as
|
||||
expressly stated in this notice, no other rights or licenses, express or implied,
|
||||
are granted by Apple herein, including but not limited to any patent rights that
|
||||
may be infringed by your derivative works or by other works in which the Apple
|
||||
Software may be incorporated.
|
||||
|
||||
The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO
|
||||
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
|
||||
WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
|
||||
COMBINATION WITH YOUR PRODUCTS.
|
||||
|
||||
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
|
||||
OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT
|
||||
(INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Change History (most recent first):
|
||||
11/4/02 1.0d1
|
||||
|
||||
*/
|
||||
|
||||
|
||||
#ifndef _IO_SOCK_H_
|
||||
#define _IO_SOCK_H_
|
||||
|
||||
#include <CoreServices/../Frameworks/CarbonCore.framework/Headers/MacTypes.h>
|
||||
#include <Security/SecureTransport.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Opaque reference to an Open Transport connection.
|
||||
*/
|
||||
typedef void *otSocket;
|
||||
|
||||
/*
|
||||
* info about a peer returned from MakeServerConnection() and
|
||||
* AcceptClientConnection().
|
||||
*/
|
||||
typedef struct
|
||||
{ UInt32 ipAddr;
|
||||
int port;
|
||||
} PeerSpec;
|
||||
|
||||
/*
|
||||
* Ont-time only init.
|
||||
*/
|
||||
void initSslOt();
|
||||
|
||||
/*
|
||||
* Connect to server.
|
||||
*/
|
||||
extern OSStatus MakeServerConnection(
|
||||
const char *hostName,
|
||||
int port,
|
||||
otSocket *socketNo, // RETURNED
|
||||
PeerSpec *peer); // RETURNED
|
||||
|
||||
/*
|
||||
* Set up an otSocket to listen for client connections. Call once, then
|
||||
* use multiple AcceptClientConnection calls.
|
||||
*/
|
||||
OSStatus ListenForClients(
|
||||
int port,
|
||||
otSocket *socketNo); // RETURNED
|
||||
|
||||
/*
|
||||
* Accept a client connection. Call endpointShutdown() for each successful;
|
||||
* return from this function.
|
||||
*/
|
||||
OSStatus AcceptClientConnection(
|
||||
otSocket listenSock, // obtained from ListenForClients
|
||||
otSocket *acceptSock, // RETURNED
|
||||
PeerSpec *peer); // RETURNED
|
||||
|
||||
/*
|
||||
* Shut down a connection.
|
||||
*/
|
||||
void endpointShutdown(
|
||||
otSocket socket);
|
||||
|
||||
/*
|
||||
* R/W. Called out from SSL.
|
||||
*/
|
||||
OSStatus SocketRead(
|
||||
SSLConnectionRef connection,
|
||||
void *data, /* owned by
|
||||
* caller, data
|
||||
* RETURNED */
|
||||
size_t *dataLength); /* IN/OUT */
|
||||
|
||||
OSStatus SocketWrite(
|
||||
SSLConnectionRef connection,
|
||||
const void *data,
|
||||
size_t *dataLength); /* IN/OUT */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _IO_SOCK_H_ */
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016 noodlewerk. All rights reserved.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// PusherKit-OSX.h
|
||||
// PusherKit-OSX
|
||||
//
|
||||
// Created by Sash Zats on 7/17/16.
|
||||
// Copyright © 2016 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
//! Project version number for PusherKit-OSX.
|
||||
FOUNDATION_EXPORT double PusherKit_OSXVersionNumber;
|
||||
|
||||
//! Project version string for PusherKit-OSX.
|
||||
FOUNDATION_EXPORT const unsigned char PusherKit_OSXVersionString[];
|
||||
|
||||
#import <PusherKit/NWHub.h>
|
||||
#import <PusherKit/NWLCore.h>
|
||||
#import <PusherKit/NWNotification.h>
|
||||
#import <PusherKit/NWPushFeedback.h>
|
||||
#import <PusherKit/NWPusher.h>
|
||||
#import <PusherKit/NWSSLConnection.h>
|
||||
#import <PusherKit/NWSecTools.h>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// PusherKit-iOS.h
|
||||
// PusherKit-iOS
|
||||
//
|
||||
// Created by Sash Zats on 7/17/16.
|
||||
// Copyright © 2016 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for PusherKit-iOS.
|
||||
FOUNDATION_EXPORT double PusherKit_iOSVersionNumber;
|
||||
|
||||
//! Project version string for PusherKit-iOS.
|
||||
FOUNDATION_EXPORT const unsigned char PusherKit_iOSVersionString[];
|
||||
|
||||
#import <PusherKit/NWHub.h>
|
||||
#import <PusherKit/NWLCore.h>
|
||||
#import <PusherKit/NWNotification.h>
|
||||
#import <PusherKit/NWPusher.h>
|
||||
#import <PusherKit/NWSSLConnection.h>
|
||||
#import <PusherKit/NWSecTools.h>
|
||||
#import <PusherKit/NWPushFeedback.h>
|
||||
@@ -1,19 +1,445 @@
|
||||
NWPusher
|
||||
========
|
||||
<img src="icon.png" alt="Pusher Icon" width="72"/>
|
||||
|
||||
*Mac OS X application for playing with Apple's push notification service.*
|
||||
|
||||
Pusher
|
||||
======
|
||||
|
||||
*OS X and iOS application and framework to play with the Apple Push Notification service (APNs)*
|
||||
|
||||
<img src="Docs/osx1.png" alt="Pusher OS X" width="612"/>
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
Install the Mac app using [Homebrew cask](https://github.com/caskroom/homebrew-cask):
|
||||
|
||||
```shell
|
||||
brew cask install pusher
|
||||
```
|
||||
|
||||
Or download the latest `Pusher.app` binary:
|
||||
|
||||
- [Download latest binary](https://github.com/noodlewerk/NWPusher/releases/latest)
|
||||
|
||||
Alternatively, you can include NWPusher as a framework, using [CocoaPods](https://cocoapods.org/):
|
||||
|
||||
```ruby
|
||||
pod 'NWPusher', '~> 0.7.0'
|
||||
```
|
||||
|
||||
or [Carthage](https://github.com/Carthage/Carthage) (iOS 8+ is required to use Cocoa Touch Frameworks)
|
||||
|
||||
```
|
||||
github "noodlewerk/NWPusher"
|
||||
```
|
||||
|
||||
Or simply include the source files you need. NWPusher has a modular architecture and does not have any external dependencies, so use what you like.
|
||||
|
||||
|
||||
About
|
||||
-----
|
||||
Testing push notifications in your app can be difficult, having to set up an SSL connection with Apple or send a payload through some server. NWPusher is an application that allows you to send push notifications from your Mac directly to Apple's servers. It uses your keychain to retreive push certifcates and keys, and provides a convenient configuration file to keep track of your device tokens.
|
||||
Testing push notifications for your iOS or Mac app can be a pain. You might consider setting up your own server or use one of the many push webservices online. Either way it's a lot of work to get all these systems connected properly. When it is all working properly, push notifications come in fast (< 1 sec) and reliably. However when nothing comes in, it can be very hard to find out why.
|
||||
|
||||
That's why I made *Pusher*. It is a Mac and iPhone app for sending push notifications *directly* to the *Apple Push Notification Service*. No need to set up a server or create an account online. You only need the SSL certificate and a device token to start pushing directly from your Mac, or even from an iPhone! Pusher has detailed error reporting and logs, which are very helpful with verifying your setup.
|
||||
|
||||
Pusher comes with a small framework for both OS X and iOS. It provides various tools for sending notifications programmatically. On OS X it can use the keychain to retrieve push certificates and keys. Pusher can also be used without keychain, using a PKCS #12 file. If you want to get a better understanding of how push notifications work, then this framework is a good place to start and play around.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
Mac OS X application for sending push notifications through the APN service:
|
||||
- Takes *certificates and keys* directly from the *keychain*
|
||||
- Fully customizable *payload* with *syntax checking*
|
||||
- Allows setting *expiration* and *priority*
|
||||
- *Stores device tokens* so you don't have to copy-paste them every time
|
||||
- Handles *PKCS #12* files (.p12)
|
||||
- Automatic configuration for *sandbox*
|
||||
- Reports *detailed error messages* returned by APNs
|
||||
- Reads from *feedback service*
|
||||
|
||||
OS X and iOS framework for sending pushes from your own application:
|
||||
- Modular, no dependencies, use what you like
|
||||
- Fully documented source code
|
||||
- Detailed error handling
|
||||
- iOS compatible, so you can also push directly from your iPhone :o
|
||||
- Demo applications for both platforms
|
||||
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
Before you can start sending push notification payloads, there are a few hurdles to take. First you'll need to obtain the *Apple Push Services SSL Certificate* of the app you want to send notifications to. This certificate is used by Pusher to set up the SSL connection through which the payloads will be sent to Apple.
|
||||
|
||||
Second you'll need the *device token* of the device you want to send your payload to. Every device has its own unique token that can only be obtained from within the app. It's a bit complicated, but in the end it all comes down to just a few clicks on Apple's Dev Center website, some gray hairs, and a bit of patience.
|
||||
|
||||
### Certificate
|
||||
Let's start with the SSL certificate. The goal is to get both the certificate *and* the private key into your OS X keychain. If someone else already generated this certificate, you'll need to ask for exporting these into a PKCS12 file. If there is no certificate generated yet, you can generate the certificate and the private key in the following steps:
|
||||
|
||||
1. Log in to [Apple's Dev Center](https://developer.apple.com)
|
||||
2. Go to the *Provisioning Portal* or *Certificates, Identifiers & Profiles*
|
||||
3. Go to *Certificates* and create a *Apple Push Notification service SSL*
|
||||
4. From here on you will be guided through the certificate generation process.
|
||||
|
||||
Keep in mind that you will eventually be downloading a certificate, which you will need to install in your keychain together with the private key. This should look something like this:
|
||||
|
||||
<img src="Docs/keychain1.png" alt="Keychain export" width="690"/>
|
||||
|
||||
NB: There is `Development` and `Production` certificates, which should (generally) correspond to respectively `DEBUG` and `RELEASE` versions of your app. Make sure you get the right one, check *Development (sandbox) or Production*, *iOS or Mac*, and the *bundle identifier*.
|
||||
|
||||
The push certificate should be exported to a PKCS12 file, which allows you to share these with fellow developers:
|
||||
|
||||
<img src="Docs/keychain2.png" alt="PKCS12 file" width="690"/>
|
||||
|
||||
### Device token
|
||||
Now you need to obtain a device token, which is a 64 character hex string (256 bits). This should be done from within the iOS app you're going to push to. Add the following lines to the application delegate (Xcode 6 required):
|
||||
|
||||
```objective-c
|
||||
- (BOOL)application:(UIApplication *)application
|
||||
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
if ([application respondsToSelector:@selector(registerUserNotificationSettings:)]) {
|
||||
NSLog(@"Requesting permission for push notifications..."); // iOS 8
|
||||
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:
|
||||
UIUserNotificationTypeAlert | UIUserNotificationTypeBadge |
|
||||
UIUserNotificationTypeSound categories:nil];
|
||||
[UIApplication.sharedApplication registerUserNotificationSettings:settings];
|
||||
} else {
|
||||
NSLog(@"Registering device for push notifications..."); // iOS 7 and earlier
|
||||
[UIApplication.sharedApplication registerForRemoteNotificationTypes:
|
||||
UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge |
|
||||
UIRemoteNotificationTypeSound];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)application
|
||||
didRegisterUserNotificationSettings:(UIUserNotificationSettings *)settings
|
||||
{
|
||||
NSLog(@"Registering device for push notifications..."); // iOS 8
|
||||
[application registerForRemoteNotifications];
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)application
|
||||
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)token
|
||||
{
|
||||
NSLog(@"Registration successful, bundle identifier: %@, mode: %@, device token: %@",
|
||||
[NSBundle.mainBundle bundleIdentifier], [self modeString], token);
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)application
|
||||
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
|
||||
{
|
||||
NSLog(@"Failed to register: %@", error);
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier
|
||||
forRemoteNotification:(NSDictionary *)notification completionHandler:(void(^)())completionHandler
|
||||
{
|
||||
NSLog(@"Received push notification: %@, identifier: %@", notification, identifier); // iOS 8
|
||||
completionHandler();
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)application
|
||||
didReceiveRemoteNotification:(NSDictionary *)notification
|
||||
{
|
||||
NSLog(@"Received push notification: %@", notification); // iOS 7 and earlier
|
||||
}
|
||||
|
||||
- (NSString *)modeString
|
||||
{
|
||||
#if DEBUG
|
||||
return @"Development (sandbox)";
|
||||
#else
|
||||
return @"Production";
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
Now, when you run the application, the 64 character push string will be logged to the console.
|
||||
|
||||
### Push from OS X
|
||||
With the SSL certificate and private key in the keychain and the device token on the pasteboard, you're ready to send some push notifications. Let's start by sending a notification using the *Pusher app for Mac OS X*. Open the Pusher Xcode project and run the PusherMac target:
|
||||
|
||||
<img src="Docs/osx1.png" alt="Pusher OS X" width="612"/>
|
||||
|
||||
The combo box at the top lists the available SSL certificates in the keychain. Select the certificate you want to use and paste the device token of the device you're pushing to. The text field below shows the JSON formatted payload text that you're sending. Read more about this format in the Apple documentation under *Apple Push Notification Service*.
|
||||
|
||||
Now before you press *Push*, make sure the application you're *sending to* is in the *background*, e.g. by pressing the home button. This way you're sure the app is not going to interfere with the message, yet. Press push, wait a few seconds, and see the notification coming in.
|
||||
|
||||
If things are not working as expected, then take a look at the *Troubleshooting* section below.
|
||||
|
||||
<img src="Docs/osx2.png" alt="Pusher OS X" width="612"/>
|
||||
|
||||
### Push from iOS
|
||||
The ultimate experience is of course pushing from an iPhone to an iPhone, directly. This can be done with the Pusher iOS app. Before you run the PusherTouch target, make sure to include the *certificate, private key, and device token* inside the app. Take the PKCS12 file that you exported earlier and include it in the PusherTouch bundle. Then go to `NWAppDelegate.m` in the `Touch` folder and configure `pkcs12FileName`, `pkcs12Password`, and `deviceToken`. Now run the PusherTouch target:
|
||||
|
||||
<img src="Docs/ios.png" alt="Pusher iOS" width="414"/>
|
||||
|
||||
If everything is set up correctly, you only need to *Connect* and *Push*. Then you should receive the `Testing..` push message on the device.
|
||||
|
||||
Again, if things are not working as expected, take a look at the *Troubleshooting* section below or post an issue on GitHub.
|
||||
|
||||
Consult Apple's documentation for more info on the APNs architecture: [Apple Push Notification Service](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html)
|
||||
|
||||
Pushing from code
|
||||
-----------------
|
||||
Pusher can also be used as a framework to send notifications programmatically. The included Xcode project provides examples for both OS X and iOS. The easiest way to include NWPusher is through CocoaPods:
|
||||
|
||||
```ruby
|
||||
pod 'NWPusher', '~> 0.7.0'
|
||||
```
|
||||
|
||||
CocoaPods also compiles documentation, which can be accessed through [CocoaDocs](http://cocoadocs.org/docsets/NWPusher). Alternatively you can include just the files you need from the `Classes` folder. Make sure you link with `Foundation.framework` and `Security.framework`.
|
||||
|
||||
Before any notification can be sent, you first need to create a connection. When this connection is established, any number of payloads can be sent.
|
||||
|
||||
*Note that Apple doesn't like it when you create a connection for every push.* Therefore be careful to reuse a connection as much as possible in order to prevent Apple from blocking.
|
||||
|
||||
To create a connection directly from a PKCS12 (.p12) file:
|
||||
|
||||
```objective-c
|
||||
NSURL *url = [NSBundle.mainBundle URLForResource:@"pusher.p12" withExtension:nil];
|
||||
NSData *pkcs12 = [NSData dataWithContentsOfURL:url];
|
||||
NSError *error = nil;
|
||||
NWPusher *pusher = [NWPusher connectWithPKCS12Data:pkcs12 password:@"pa$$word" error:&error];
|
||||
if (pusher) {
|
||||
NSLog(@"Connected to APNs");
|
||||
} else {
|
||||
NSLog(@"Unable to connect: %@", error);
|
||||
}
|
||||
```
|
||||
|
||||
When pusher is successfully connected, send a payload to your device:
|
||||
|
||||
```objective-c
|
||||
NSString *payload = @"{\"aps\":{\"alert\":\"Testing..\"}}";
|
||||
NSString *token = @"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
|
||||
NSError *error = nil;
|
||||
BOOL pushed = [pusher pushPayload:payload token:token identifier:rand() error:&error];
|
||||
if (pushed) {
|
||||
NSLog(@"Pushed to APNs");
|
||||
} else {
|
||||
NSLog(@"Unable to push: %@", error);
|
||||
}
|
||||
```
|
||||
|
||||
After a second or so, we can take a look to see if the notification was accepted by Apple:
|
||||
|
||||
```objective-c
|
||||
NSUInteger identifier = 0;
|
||||
NSError *apnError = nil;
|
||||
NSError *error = nil;
|
||||
BOOL read = [pusher readFailedIdentifier:&identifier apnError:&apnError error:&error];
|
||||
if (read && apnError) {
|
||||
NSLog(@"Notification with identifier %i rejected: %@", (int)identifier, apnError);
|
||||
} else if (read) {
|
||||
NSLog(@"Read and none failed");
|
||||
} else {
|
||||
NSLog(@"Unable to read: %@", error);
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively on OS X you can also use the keychain to obtain the SSL certificate. In that case first collect all certificates:
|
||||
|
||||
```objective-c
|
||||
NSError *error = nil;
|
||||
NSArray *certificates = [NWSecTools keychainCertificatesWithError:&error];
|
||||
if (certificates) {
|
||||
NSLog(@"Loaded %i certificates", (int)certificates.count);
|
||||
} else {
|
||||
NSLog(@"Unable to access keychain: %@", error);
|
||||
}
|
||||
```
|
||||
|
||||
After selecting the right certificate, obtain the identity from the keychain:
|
||||
|
||||
```objective-c
|
||||
NSError *error = nil;
|
||||
NWIdentityRef identity = [NWSecTools keychainIdentityWithCertificate:certificate error:&error];
|
||||
if (identity) {
|
||||
NSLog(@"Loaded identity: %@", [NWSecTools inspectIdentity:identity]);
|
||||
} else {
|
||||
NSLog(@"Unable to create identity: %@", error);
|
||||
}
|
||||
```
|
||||
|
||||
Take a look at the example project for variations on this approach.
|
||||
|
||||
Consult Apple's documentation for more info on the client-server communication: [Provider Communication](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html)
|
||||
|
||||
Feedback Service
|
||||
----------------
|
||||
The feedback service is part of the Apple Push Notification service. The feedback service is basically a list containing device tokens that became invalid. Apple recommends that you read from the feedback service once every 24 hours, and no longer send notifications to listed devices. Note that this can be used to find out who removed your app from their device.
|
||||
|
||||
Communication with the feedback service can be done with the `NWPushFeedback` class. First connect using one of the `connect` methods:
|
||||
|
||||
```objective-c
|
||||
NSURL *url = [NSBundle.mainBundle URLForResource:@"pusher.p12" withExtension:nil];
|
||||
NSData *pkcs12 = [NSData dataWithContentsOfURL:url];
|
||||
NSError *error = nil;
|
||||
NWPushFeedback *feedback = [NWPushFeedback connectWithPKCS12Data:pkcs12 password:@"pa$$word" error:&error];
|
||||
if (feedback) {
|
||||
NSLog(@"Connected to feedback service");
|
||||
} else {
|
||||
NSLog(@"Unable to connect to feedback service: %@", error);
|
||||
}
|
||||
```
|
||||
|
||||
When connected read the device token and date of invalidation:
|
||||
|
||||
```objective-c
|
||||
NSError *error = nil;
|
||||
NSArray *pairs = [feedback readTokenDatePairsWithMax:100 error:&error];
|
||||
if (pairs) {
|
||||
NSLog(@"Read token-date pairs: %@", pairs);
|
||||
} else {
|
||||
NSLog(@"Unable to read feedback: %@", error);
|
||||
}
|
||||
```
|
||||
|
||||
Apple closes the connection after the last device token is read.
|
||||
|
||||
Pushing to macOS
|
||||
---------------
|
||||
|
||||
On macOS, you obtain a device token for your app by calling the `registerForRemoteNotificationTypes:` method of the `NSApplication` object. It is recommended that you call this method at launch time as part of your normal startup sequence. The first time your app calls this method, the app object requests the token from APNs. After the initial call, the app object contacts APNs only when the device token changes; otherwise, it returns the existing token quickly.
|
||||
|
||||
The app object notifies its delegate asynchronously upon the successful or unsuccessful retrieval of the device token. You use these delegate callbacks to process the device token or to handle any errors that arose. You must implement the following delegate methods to track whether registration was successful:
|
||||
|
||||
- Use the `application:didRegisterForRemoteNotificationsWithDeviceToken:` to receive the device token and forward it to your server.
|
||||
- Use the `application:didFailToRegisterForRemoteNotificationsWithError:` to respond to errors.
|
||||
|
||||
Note: If the device token changes while your app is running, the app object calls the appropriate delegate method again to notify you of the change.
|
||||
|
||||
The app delegate calls the `registerForRemoteNotificationTypes:` method as part of its regular launch-time setup, passing along the types of interactions that you intend to use. Upon receiving the device token, the `application:didRegisterForRemoteNotificationsWithDeviceToken:` method forwards it to the app’s associated server using a custom method. If an error occurs during registration, the app temporarily disables any features related to remote notifications. Those features are re-enabled when a valid device token is received.
|
||||
|
||||
```objective-c
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
||||
// Configure the user interactions first.
|
||||
[self configureUserInteractions];
|
||||
|
||||
[NSApp registerForRemoteNotificationTypes:(NSRemoteNotificationTypeAlert | NSRemoteNotificationTypeSound)];
|
||||
}
|
||||
```
|
||||
|
||||
```objective-c
|
||||
- (void)application:(NSApplication *)application
|
||||
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
||||
// Forward the token to your server.
|
||||
[self forwardTokenToServer:deviceToken];
|
||||
}
|
||||
```
|
||||
|
||||
```objective-c
|
||||
- (void)application:(NSApplication *)application
|
||||
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
|
||||
NSLog(@"Remote notification support is unavailable due to error: %@", error);
|
||||
[self disableRemoteNotificationFeatures];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Certificate and key files
|
||||
-------------------------
|
||||
Pusher reads certificate and key data from PKCS12 files. This is a binary format that bundles both X.509 certificates and a private key in one file. Conversion from other file formats to and from PKCS12 is provided by the OpenSSL CLI.
|
||||
|
||||
*Inspect PKCS12:*
|
||||
|
||||
openssl pkcs12 -in pusher.p12
|
||||
|
||||
where the output should be something like:
|
||||
|
||||
...
|
||||
friendlyName: Apple Development/Production IOS/Mac Push Services: <your-bundle-identifier>
|
||||
localKeyID: <key-id>
|
||||
...
|
||||
-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
friendlyName: <private-key-used-for-generating-above-certificate>
|
||||
localKeyID: <same-key-id>
|
||||
...
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
...
|
||||
|
||||
Make sure your build matches the `Development/Production`, `iOS/Mac`, and bundle identifier.
|
||||
|
||||
*Inspect PKCS12 structure:*
|
||||
|
||||
openssl pkcs12 -in pusher.p12 -info -noout
|
||||
|
||||
*Inspect PEM:*
|
||||
|
||||
openssl rsa -in pusher.pem -noout -check
|
||||
openssl rsa -in pusher.pem -pubout
|
||||
openssl x509 -in pusher.pem -noout -pubkey
|
||||
|
||||
*PKCS12 to PEM:*
|
||||
|
||||
openssl pkcs12 -in pusher.p12 -out pusher.pem -clcerts -aes256
|
||||
|
||||
Alternatively you can use the command below, which does *not* encrypt the private key (not recommended):
|
||||
|
||||
openssl pkcs12 -in pusher.p12 -out pusher.pem -nodes -clcerts
|
||||
|
||||
*PEM to PKCS12:*
|
||||
|
||||
openssl pkcs12 -export -in pusher.pem -out pusher.p12
|
||||
|
||||
Consult the OpenSSL documentation for more details: [OpenSSL Documents - pkcs12](https://www.openssl.org/docs/apps/pkcs12.html)
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
Apple's Push Notification Service is not very forgiving in nature. If things are done in the wrong order or data is formatted incorrectly the service will refuse to deliver any notification, but generally provides few clues about went wrong and how to fix it. In the worst case, it simply disconnects without even notifying the client.
|
||||
|
||||
Some tips on what to look out for:
|
||||
|
||||
- A device token is unique to both the device, the developer's certificate, and to whether the app was built with a production or development (sandbox) certificate. Therefore make sure that the push certificate matches the app's provisioning profile exactly. This doesn't mean the tokens are always different; device tokens can be the same for different bundle identifiers.
|
||||
|
||||
- There are two channels through which Apple responds to pushed notifications: the notification connection and the feedback connection. Both operate asynchronously, so for example after the second push has been sent, we might get a response to the first push, saying it has an invalid payload. Use a new identifier for every notification so these responses can be linked to the right notification.
|
||||
|
||||
If it fails to connect then check:
|
||||
|
||||
- Are the certificates and keys in order? Use the OpenSSL commands listed above to inspect the certificate. See if there is one push certificate and key present. Also make sure you're online, try `ping www.apple.com`.
|
||||
|
||||
- Is the certificate properly loaded? Try initializing an identity using `[NWSecTools identityWithPKCS12Data:data password:password error:&error]` or `[NWSecTools keychainIdentityWithCertificate:certificate error:&error]`.
|
||||
|
||||
- Are you using the right identity? Use `[NWSecTools inspectIdentity:identity]` to inspect the identity instance. In general `NWSecTools` can be helpful for inspecting certificates, identities and the keychain.
|
||||
|
||||
- Can you connect with the push servers? Try `[NWPusher connectWithIdentity:identity error:&error]` or `[NWPusher connectWithPKCS12Data:pkcs12 password:password error:&error]`.
|
||||
|
||||
- Pusher connects on port `2195` with hosts `gateway.push.apple.com` and `gateway.sandbox.push.apple.com`, and on port `2196` with hosts `feedback.push.apple.com` and `feedback.sandbox.push.apple.com`. Make sure your firewall is configured to allow these connections.
|
||||
|
||||
If nothing is delivered to the device then check:
|
||||
|
||||
- Is the device online? Is it able to receive push notifications from other services? Try to get pushes from other apps, for example a messenger. Many wireless connections work visibly fine, but do not deliver push notifications. Try to switch to another wifi or cellular network.
|
||||
|
||||
- Are you pushing to the right device token? This token should be returned by the OS of the receiving device, in the callback `-application: didRegisterForRemoteNotificationsWithDeviceToken:`. The push certificate should match the provisioning profile of the app, check *Development or Production*, *iOS or Mac*, and the *bundle identifier*. Make sure the receiving app is closed, so it cannot interfere with the delivery.
|
||||
|
||||
- Does the push call succeed? Isn't there any negative response from the push server or feedback server? Both `[pusher pushPayload:payload token:token identifier:rand() error:&error]` and `[pusher readFailedIdentifier:&identifier apnError:&apnError error:&error]` should return `YES`, but wait a second between pushing and reading. Also try to connect to the feedback service to read feedback.
|
||||
|
||||
Consult Apple's documentation for more troubleshooting tips: [Troubleshooting Push Notifications](https://developer.apple.com/library/mac/technotes/tn2265/_index.html)
|
||||
|
||||
Build with Xcode
|
||||
----------------
|
||||
The source comes with an Xcode project file that should take care of building the OS X and iOS demo applications. Alternatively you can also build `Pusher.app` from the commandline with `xcodebuild`:
|
||||
|
||||
xcodebuild -project NWPusher.xcodeproj -target PusherMac -configuration Release clean install
|
||||
|
||||
After a successful build, `Pusher.app` can be found in the `build` folder of the project.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
Documentation generated and installed using *appledoc* by running from the project root:
|
||||
|
||||
appledoc .
|
||||
|
||||
See the [appledoc documentation](http://gentlebytes.com/appledoc/) for more info.
|
||||
|
||||
License
|
||||
-------
|
||||
NWPusher is licensed under the terms of the BSD 2-Clause License, see the included LICENSE file.
|
||||
Pusher is licensed under the terms of the BSD 2-Clause License, see the included LICENSE file.
|
||||
|
||||
|
||||
Authors
|
||||
-------
|
||||
- [Noodlewerk](http://www.noodlewerk.com/)
|
||||
- [Leonard van Driel](http://www.leonardvandriel.nl/)
|
||||
- [Leonard van Driel](http://www.leonardvandriel.nl/)
|
||||
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// NWAppDelegate.h
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2013 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
|
||||
@interface NWPusherViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface NWAppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,257 @@
|
||||
//
|
||||
// NWAppDelegate.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2013 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NWAppDelegate.h"
|
||||
#import <PusherKit/PusherKit.h>
|
||||
|
||||
// TODO: Export your push certificate and key in PKCS12 format to pusher.p12 in the root of the project directory.
|
||||
static NSString * const pkcs12FileName = @"pusher.p12";
|
||||
|
||||
// TODO: Set the password of this .p12 file below, but be careful *not* to commit passwords to a (public) repository.
|
||||
static NSString * const pkcs12Password = @"pa$$word";
|
||||
|
||||
// TODO: Set the device token of the device you want to push to, see
|
||||
// `-application:didRegisterForRemoteNotificationsWithDeviceToken:` for more details.
|
||||
static NSString * const deviceToken = @"ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789";
|
||||
|
||||
static NWPusherViewController *controller = nil;
|
||||
|
||||
@interface NWPusherViewController () <NWHubDelegate> @end
|
||||
|
||||
@implementation NWPusherViewController {
|
||||
UIButton *_connectButton;
|
||||
UITextField *_textField;
|
||||
UIButton *_pushButton;
|
||||
UILabel *_infoLabel;
|
||||
UISwitch *_sanboxSwitch;
|
||||
NWHub *_hub;
|
||||
NSUInteger _index;
|
||||
dispatch_queue_t _serial;
|
||||
|
||||
NWIdentityRef _identity;
|
||||
NWCertificateRef _certificate;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
controller = self;
|
||||
NWLAddPrinter("NWPusher", NWPusherPrinter, 0);
|
||||
NWLPrintInfo();
|
||||
_serial = dispatch_queue_create("NWAppDelegate", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_connectButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
|
||||
_connectButton.frame = CGRectMake(20, 20, (self.view.bounds.size.width - 40)/2, 40);
|
||||
[_connectButton setTitle:@"Connect" forState:UIControlStateNormal];
|
||||
[_connectButton addTarget:self action:@selector(connectButtonPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:_connectButton];
|
||||
|
||||
_sanboxSwitch = [[UISwitch alloc] init];
|
||||
_sanboxSwitch.frame = CGRectMake((self.view.bounds.size.width + 40)/2, 20, 40, 40);
|
||||
[_sanboxSwitch addTarget:self action:@selector(sanboxCheckBoxDidPressed:) forControlEvents:UIControlEventValueChanged];
|
||||
[self.view addSubview:_sanboxSwitch];
|
||||
|
||||
UILabel *sandboxLabel = [[UILabel alloc] init];
|
||||
sandboxLabel.frame = CGRectMake(CGRectGetMaxX(_sanboxSwitch.frame) + 10, 20, 80, 40);
|
||||
sandboxLabel.font = [UIFont systemFontOfSize:12];
|
||||
sandboxLabel.text = @"Use sandbox";
|
||||
[self.view addSubview:sandboxLabel];
|
||||
|
||||
_textField = [[UITextField alloc] init];
|
||||
_textField.frame = CGRectMake(20, 70, self.view.bounds.size.width - 40, 26);
|
||||
_textField.text = @"Testing..";
|
||||
_textField.borderStyle = UITextBorderStyleBezel;
|
||||
[self.view addSubview:_textField];
|
||||
|
||||
_pushButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
|
||||
_pushButton.frame = CGRectMake(20, 106, self.view.bounds.size.width - 40, 40);
|
||||
[_pushButton setTitle:@"Push" forState:UIControlStateNormal];
|
||||
[_pushButton addTarget:self action:@selector(push) forControlEvents:UIControlEventTouchUpInside];
|
||||
_pushButton.enabled = NO;
|
||||
[self.view addSubview:_pushButton];
|
||||
|
||||
_infoLabel = [[UILabel alloc] init];
|
||||
_infoLabel.frame = CGRectMake(20, 156, self.view.bounds.size.width - 40, 60);
|
||||
_infoLabel.font = [UIFont systemFontOfSize:12];
|
||||
_infoLabel.numberOfLines = 0;
|
||||
[self.view addSubview:_infoLabel];
|
||||
|
||||
NWLogInfo(@"Connect with Apple's Push Notification service");
|
||||
|
||||
[self loadCertificate];
|
||||
}
|
||||
|
||||
- (void)loadCertificate
|
||||
{
|
||||
NSURL *url = [NSBundle.mainBundle URLForResource:pkcs12FileName withExtension:nil];
|
||||
NSData *pkcs12 = [NSData dataWithContentsOfURL:url];
|
||||
NSError *error = nil;
|
||||
|
||||
NSArray *ids = [NWSecTools identitiesWithPKCS12Data:pkcs12 password:pkcs12Password error:&error];
|
||||
if (!ids) {
|
||||
NWLogWarn(@"Unable to read p12 file: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
for (NWIdentityRef identity in ids) {
|
||||
NSError *error = nil;
|
||||
NWCertificateRef certificate = [NWSecTools certificateWithIdentity:identity error:&error];
|
||||
if (!certificate) {
|
||||
NWLogWarn(@"Unable to import p12 file: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
_identity = identity;
|
||||
_certificate = certificate;
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)sanboxCheckBoxDidPressed:(UISwitch *)sender
|
||||
{
|
||||
if (_certificate)
|
||||
{
|
||||
[self disconnect];
|
||||
[self connectToEnvironment:[self selectedEnvironmentForCertificate:_certificate]];
|
||||
}
|
||||
}
|
||||
|
||||
- (NWEnvironment)selectedEnvironmentForCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
return _sanboxSwitch.isOn ? NWEnvironmentSandbox : NWEnvironmentProduction;
|
||||
}
|
||||
|
||||
- (NWEnvironment)preferredEnvironmentForCertificate:(NWCertificateRef)certificate
|
||||
{
|
||||
NWEnvironmentOptions environmentOptions = [NWSecTools environmentOptionsForCertificate:certificate];
|
||||
|
||||
return (environmentOptions & NWEnvironmentOptionSandbox) ? NWEnvironmentSandbox : NWEnvironmentProduction;
|
||||
}
|
||||
|
||||
- (void)connectButtonPressed
|
||||
{
|
||||
if (_hub)
|
||||
{
|
||||
[self disconnect];
|
||||
_connectButton.enabled = YES;
|
||||
[_connectButton setTitle:@"Connect" forState:UIControlStateNormal];
|
||||
return;
|
||||
}
|
||||
|
||||
NWEnvironment preferredEnvironment = [self preferredEnvironmentForCertificate:_certificate];
|
||||
|
||||
[self connectToEnvironment:preferredEnvironment];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
[self disableButtons];
|
||||
[_hub disconnect]; _hub = nil;
|
||||
NWLogInfo(@"Disconnected");
|
||||
}
|
||||
|
||||
- (void)connectToEnvironment:(NWEnvironment)environment
|
||||
{
|
||||
[self disableButtons];
|
||||
|
||||
NWLogInfo(@"Connecting..");
|
||||
dispatch_async(_serial, ^{
|
||||
NSError *error = nil;
|
||||
|
||||
NWHub *hub = [NWHub connectWithDelegate:self identity:_identity environment:environment error:&error];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (hub) {
|
||||
NSString *summary = [NWSecTools summaryWithCertificate:_certificate];
|
||||
NWLogInfo(@"Connected to APN: %@ (%@)", summary, descriptionForEnvironent(environment));
|
||||
_hub = hub;
|
||||
|
||||
[_connectButton setTitle:@"Disconnect" forState:UIControlStateNormal];
|
||||
} else {
|
||||
NWLogWarn(@"Unable to connect: %@", error.localizedDescription);
|
||||
}
|
||||
|
||||
[self enableButtonsForCertificate:_certificate environment:environment];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)push
|
||||
{
|
||||
NSString *payload = [NSString stringWithFormat:@"{\"aps\":{\"alert\":\"%@\",\"badge\":1,\"sound\":\"default\"}}", _textField.text];
|
||||
NSString *token = deviceToken;
|
||||
NWLogInfo(@"Pushing..");
|
||||
dispatch_async(_serial, ^{
|
||||
NSUInteger failed = [_hub pushPayload:payload token:token];
|
||||
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
|
||||
dispatch_after(popTime, _serial, ^(void){
|
||||
NSUInteger failed2 = failed + [_hub readFailed];
|
||||
if (!failed2) NWLogInfo(@"Payload has been pushed");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)notification:(NWNotification *)notification didFailWithError:(NSError *)error
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
//NSLog(@"failed notification: %@ %@ %lu %lu %lu", notification.payload, notification.token, notification.identifier, notification.expires, notification.priority);
|
||||
NWLogWarn(@"Notification error: %@", error.localizedDescription);
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - BUtton states
|
||||
|
||||
- (void)disableButtons
|
||||
{
|
||||
_pushButton.enabled = NO;
|
||||
_connectButton.enabled = NO;
|
||||
_sanboxSwitch.enabled = NO;
|
||||
}
|
||||
|
||||
- (void)enableButtonsForCertificate:(NWCertificateRef)certificate environment:(NWEnvironment)environment
|
||||
{
|
||||
NWEnvironmentOptions environmentOptions = [NWSecTools environmentOptionsForCertificate:certificate];
|
||||
|
||||
BOOL shouldEnableEnvButton = (environmentOptions == NWEnvironmentOptionAny);
|
||||
BOOL shouldSelectSandboxEnv = (environment == NWEnvironmentSandbox);
|
||||
|
||||
_pushButton.enabled = YES;
|
||||
_connectButton.enabled = YES;
|
||||
_sanboxSwitch.enabled = shouldEnableEnvButton;
|
||||
_sanboxSwitch.on = shouldSelectSandboxEnv;
|
||||
}
|
||||
|
||||
#pragma mark - NWLogging
|
||||
|
||||
- (void)log:(NSString *)message warning:(BOOL)warning
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
_infoLabel.textColor = warning ? UIColor.redColor : UIColor.blackColor;
|
||||
_infoLabel.text = message;
|
||||
});
|
||||
}
|
||||
|
||||
static void NWPusherPrinter(NWLContext context, CFStringRef message, void *info) {
|
||||
BOOL warning = strncmp(context.tag, "warn", 5) == 0;
|
||||
[controller log:(__bridge NSString *)message warning:warning];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation NWAppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
NWPusherViewController *controller = [[NWPusherViewController alloc] init];
|
||||
self.window.rootViewController = controller;
|
||||
self.window.backgroundColor = [UIColor whiteColor];
|
||||
[self.window makeKeyAndVisible];
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Pusher</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconFiles</key>
|
||||
<array>
|
||||
<string>icon.png</string>
|
||||
<string>Icon@2x.png</string>
|
||||
<string>Icon-72.png</string>
|
||||
<string>Icon-72@2x.png</string>
|
||||
</array>
|
||||
<key>UIPrerenderedIcon</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.7.5</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>19</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIPrerenderedIcon</key>
|
||||
<true/>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Prefix header for all source files of the 'Pusher' target in the 'Pusher' project
|
||||
//
|
||||
|
||||
#import <Availability.h>
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#endif
|
||||
|
||||
#import "NWLCore.h"
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// main.m
|
||||
// Pusher
|
||||
//
|
||||
// Copyright (c) 2013 noodlewerk. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "NWAppDelegate.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([NWAppDelegate class]));
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<array>
|
||||
<dict>
|
||||
<key>identifiers</key>
|
||||
<array>
|
||||
<string>com.example.app.dev</string>
|
||||
</array>
|
||||
<key>development</key>
|
||||
<array>
|
||||
<string>0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF my-iphone</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>identifiers</key>
|
||||
<array>
|
||||
<string>com.example.app</string>
|
||||
</array>
|
||||
<key>production</key>
|
||||
<array>
|
||||
<string>ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789 my-iphone</string>
|
||||
</array>
|
||||
<key>development</key>
|
||||
<array>
|
||||
<string>0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF my-iphone</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>production</key>
|
||||
<array>
|
||||
<string>DEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABC my-ipad</string>
|
||||
</array>
|
||||
<key>development</key>
|
||||
<array>
|
||||
<string>56789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF01234 my-ipad</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</plist>
|
||||