mirror of
https://github.com/google/santa.git
synced 2026-01-16 17:57:54 -05:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d9af01353 | ||
|
|
9c6af7fc03 | ||
|
|
543b1a29fe | ||
|
|
625ec67789 | ||
|
|
c5696d71e7 | ||
|
|
5f3cef52de | ||
|
|
eeed0b5aa6 | ||
|
|
9ef171e663 | ||
|
|
ad1868a50f | ||
|
|
78643d3c49 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -18,8 +18,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build Userspace
|
||||
run: bazel build --apple_generate_dsym -c opt :release
|
||||
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=ci
|
||||
- name: Build Driver
|
||||
run: bazel build --apple_generate_dsym -c opt :release_driver
|
||||
run: bazel build --apple_generate_dsym -c opt :release_driver --define=SANTA_BUILD_TYPE=ci
|
||||
- name: Test
|
||||
run: bazel test :unit_tests
|
||||
run: bazel test :unit_tests --define=SANTA_BUILD_TYPE=ci
|
||||
|
||||
18
BUILD
18
BUILD
@@ -15,6 +15,20 @@ apple_bundle_version(
|
||||
short_version_string = SANTA_VERSION,
|
||||
)
|
||||
|
||||
# Used to detect release builds
|
||||
config_setting(
|
||||
name = "release_build",
|
||||
values = {"define": "SANTA_BUILD_TYPE=release"},
|
||||
visibility = [":santa_package_group"],
|
||||
)
|
||||
|
||||
# Used to detect CI builds
|
||||
config_setting(
|
||||
name = "ci_build",
|
||||
values = {"define": "SANTA_BUILD_TYPE=ci"},
|
||||
visibility = [":santa_package_group"],
|
||||
)
|
||||
|
||||
# Used to detect optimized builds
|
||||
config_setting(
|
||||
name = "opt_build",
|
||||
@@ -60,9 +74,9 @@ set -e
|
||||
|
||||
rm -rf /tmp/bazel_santa_reload
|
||||
unzip -d /tmp/bazel_santa_reload \
|
||||
$${BUILD_WORKSPACE_DIRECTORY}/bazel-out/*/bin/Source/santa_driver/santa_driver.zip >/dev/null
|
||||
$${BUILD_WORKSPACE_DIRECTORY}/bazel-out/*$(COMPILATION_MODE)*/bin/Source/santa_driver/santa_driver.zip >/dev/null
|
||||
unzip -d /tmp/bazel_santa_reload \
|
||||
$${BUILD_WORKSPACE_DIRECTORY}/bazel-out/*/bin/Source/santa/Santa.zip >/dev/null
|
||||
$${BUILD_WORKSPACE_DIRECTORY}/bazel-out/*$(COMPILATION_MODE)*/bin/Source/santa/Santa.zip >/dev/null
|
||||
echo "You may be asked for your password for sudo"
|
||||
sudo BINARIES=/tmp/bazel_santa_reload CONF=$${BUILD_WORKSPACE_DIRECTORY}/Conf \
|
||||
$${BUILD_WORKSPACE_DIRECTORY}/Conf/install.sh
|
||||
|
||||
@@ -15,6 +15,10 @@ It is named Santa because it keeps track of binaries that are naughty or nice.
|
||||
|
||||
Santa is a project of Google's Macintosh Operations Team.
|
||||
|
||||
# We're hiring!
|
||||
|
||||
Want to work on Santa at Google NYC? [Apply on our Careers page!](https://goo.gle/3tO060z)
|
||||
|
||||
# Docs
|
||||
|
||||
The Santa docs are stored in the
|
||||
|
||||
@@ -102,6 +102,8 @@
|
||||
C7CDA6FE233E73ED0013622B /* SNTXPCNotifierInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = C7658ACF2322B84F00F36578 /* SNTXPCNotifierInterface.m */; };
|
||||
C7CDA6FF233E80160013622B /* SNTLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = C7658AD82322B84F00F36578 /* SNTLogging.m */; };
|
||||
C7CDA700233EB21A0013622B /* SNTXPCBundleServiceInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = C7658ACC2322B84F00F36578 /* SNTXPCBundleServiceInterface.m */; };
|
||||
C7CF38D3262DCD1800B0ABA7 /* SNTCommandSyncFCM.m in Sources */ = {isa = PBXBuildFile; fileRef = C7CF38D1262DCD1800B0ABA7 /* SNTCommandSyncFCM.m */; };
|
||||
C7CF38EB262DD07100B0ABA7 /* SNTCachingEndpointSecurityManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = C7CF38E9262DD07100B0ABA7 /* SNTCachingEndpointSecurityManager.mm */; };
|
||||
C7D35DE42322C99B000C5EB4 /* SantaDecisionManager.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7658B002322B84F00F36578 /* SantaDecisionManager.cc */; };
|
||||
C7D35DE52322C99E000C5EB4 /* SantaDriver.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7658AFF2322B84F00F36578 /* SantaDriver.cc */; };
|
||||
C7D35DE62322C9A1000C5EB4 /* SantaDriverClient.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7658AFA2322B84F00F36578 /* SantaDriverClient.cc */; };
|
||||
@@ -378,6 +380,10 @@
|
||||
C7A8308022F0F81F00F856AC /* com.google.santa.daemon.systemextension */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = com.google.santa.daemon.systemextension; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C7CC458F24B6184F0018C05C /* SNTXPCSyncdInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SNTXPCSyncdInterface.h; sourceTree = "<group>"; };
|
||||
C7CC459024B6184F0018C05C /* SNTXPCSyncdInterface.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SNTXPCSyncdInterface.m; sourceTree = "<group>"; };
|
||||
C7CF38D1262DCD1800B0ABA7 /* SNTCommandSyncFCM.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SNTCommandSyncFCM.m; sourceTree = "<group>"; };
|
||||
C7CF38D2262DCD1800B0ABA7 /* SNTCommandSyncFCM.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SNTCommandSyncFCM.h; sourceTree = "<group>"; };
|
||||
C7CF38E9262DD07100B0ABA7 /* SNTCachingEndpointSecurityManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SNTCachingEndpointSecurityManager.mm; sourceTree = "<group>"; };
|
||||
C7CF38EA262DD07100B0ABA7 /* SNTCachingEndpointSecurityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SNTCachingEndpointSecurityManager.h; sourceTree = "<group>"; };
|
||||
C7D35DDA2322C902000C5EB4 /* santa-driver.kext */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "santa-driver.kext"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C7F5C1A7233E72BC00A3F7FD /* santabundleservice */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = santabundleservice; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C7FA383324B60F3300D192F9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
@@ -468,6 +474,8 @@
|
||||
0D3715F9233E680700BB624A /* EventProviders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C7CF38EA262DD07100B0ABA7 /* SNTCachingEndpointSecurityManager.h */,
|
||||
C7CF38E9262DD07100B0ABA7 /* SNTCachingEndpointSecurityManager.mm */,
|
||||
C7658A632322B84F00F36578 /* SNTDriverManager.h */,
|
||||
C7658A742322B84F00F36578 /* SNTDriverManager.m */,
|
||||
C72ED2B42324962400255555 /* SNTEndpointSecurityManager.h */,
|
||||
@@ -592,6 +600,8 @@
|
||||
C7658AA22322B84F00F36578 /* SNTCommandSyncConstants.m */,
|
||||
C7658A982322B84F00F36578 /* SNTCommandSyncEventUpload.h */,
|
||||
C7658AA12322B84F00F36578 /* SNTCommandSyncEventUpload.m */,
|
||||
C7CF38D2262DCD1800B0ABA7 /* SNTCommandSyncFCM.h */,
|
||||
C7CF38D1262DCD1800B0ABA7 /* SNTCommandSyncFCM.m */,
|
||||
C7658A9E2322B84F00F36578 /* SNTCommandSyncManager.h */,
|
||||
C7658AA92322B84F00F36578 /* SNTCommandSyncManager.m */,
|
||||
C7658A9D2322B84F00F36578 /* SNTCommandSyncPostflight.h */,
|
||||
@@ -1231,6 +1241,7 @@
|
||||
C7658B492322C18500F36578 /* SNTCommandCacheHistogram.m in Sources */,
|
||||
C7658B472322C17D00F36578 /* SNTCommandController.m in Sources */,
|
||||
C7658B5D2322C2B800F36578 /* SNTFileInfo.m in Sources */,
|
||||
C7CF38D3262DCD1800B0ABA7 /* SNTCommandSyncFCM.m in Sources */,
|
||||
C7658B542322C1A400F36578 /* SNTCommandSyncEventUpload.m in Sources */,
|
||||
C7658B5F2322C2C700F36578 /* SNTStoredEvent.m in Sources */,
|
||||
C7658B4F2322C19700F36578 /* SNTCommandStatus.m in Sources */,
|
||||
@@ -1285,6 +1296,7 @@
|
||||
files = (
|
||||
C7CC459224B6188B0018C05C /* SNTXPCSyncdInterface.m in Sources */,
|
||||
C7658B332322C08B00F36578 /* SNTRule.m in Sources */,
|
||||
C7CF38EB262DD07100B0ABA7 /* SNTCachingEndpointSecurityManager.mm in Sources */,
|
||||
C7658B1D2322BFFA00F36578 /* main.m in Sources */,
|
||||
0D9F577C2342650F005D9AA8 /* SNTPrefixTree.cc in Sources */,
|
||||
C7658B282322C02300F36578 /* SNTDriverManager.m in Sources */,
|
||||
|
||||
@@ -349,6 +349,26 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableBackwardsCompatibleContentEncoding;
|
||||
|
||||
///
|
||||
/// Contains the FCM project name.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *fcmProject;
|
||||
|
||||
///
|
||||
/// Contains the FCM project entity.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *fcmEntity;
|
||||
|
||||
///
|
||||
/// Contains the FCM project API key.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *fcmAPIKey;
|
||||
|
||||
///
|
||||
/// True if fcmProject, fcmEntity and fcmAPIKey are all set. Defaults to false.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL fcmEnabled;
|
||||
|
||||
///
|
||||
/// Retrieve an initialized singleton configurator object using the default file path.
|
||||
///
|
||||
|
||||
@@ -87,6 +87,10 @@ static NSString *const kEnableDebugLogging = @"EnableDebugLogging";
|
||||
|
||||
static NSString *const kEnableBackwardsCompatibleContentEncoding = @"EnableBackwardsCompatibleContentEncoding";
|
||||
|
||||
static NSString *const kFCMProject = @"FCMProject";
|
||||
static NSString *const kFCMEntity = @"FCMEntity";
|
||||
static NSString *const kFCMAPIKey = @"FCMAPIKey";
|
||||
|
||||
// The keys managed by a sync server or mobileconfig.
|
||||
static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
|
||||
@@ -163,6 +167,9 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kIgnoreOtherEndpointSecurityClients : number,
|
||||
kEnableDebugLogging : number,
|
||||
kEnableBackwardsCompatibleContentEncoding : number,
|
||||
kFCMProject : string,
|
||||
kFCMEntity : string,
|
||||
kFCMAPIKey : string,
|
||||
};
|
||||
_defaults = [NSUserDefaults standardUserDefaults];
|
||||
[_defaults addSuiteNamed:@"com.google.santa"];
|
||||
@@ -354,6 +361,22 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFcmProject {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFcmEntity {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFcmAPIKey {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFcmEnabled {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (SNTClientMode)clientMode {
|
||||
@@ -614,6 +637,22 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)fcmProject {
|
||||
return self.configState[kFCMProject];
|
||||
}
|
||||
|
||||
- (NSString *)fcmEntity {
|
||||
return self.configState[kFCMEntity];
|
||||
}
|
||||
|
||||
- (NSString *)fcmAPIKey {
|
||||
return self.configState[kFCMAPIKey];
|
||||
}
|
||||
|
||||
- (BOOL)fcmEnabled {
|
||||
return (self.fcmProject.length && self.fcmEntity.length && self.fcmAPIKey.length);
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
///
|
||||
|
||||
@@ -362,7 +362,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
while (pathComponents.count > 1) {
|
||||
NSBundle *bndl = [NSBundle bundleWithPath:[NSString pathWithComponents:pathComponents]];
|
||||
if ([bndl objectForInfoDictionaryKey:@"CFBundleIdentifier"]) {
|
||||
if (!ancestor ||
|
||||
if ((!ancestor && bndl.bundlePath.pathExtension.length) ||
|
||||
[[self allowedAncestorExtensions] containsObject:bndl.bundlePath.pathExtension]) {
|
||||
bundle = bndl;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,10 @@ void logMessage(LogLevel level, FILE *destination, NSString *format, ...) {
|
||||
break;
|
||||
case LOG_LEVEL_DEBUG:
|
||||
levelName = "D";
|
||||
syslogLevel = ASL_LEVEL_DEBUG;
|
||||
// Log debug messages at the same ASL level as INFO.
|
||||
// While it would make sense to use DEBUG, watching debug-level logs
|
||||
// in Console means enabling all debug logs, which is absurdly noisy.
|
||||
syslogLevel = ASL_LEVEL_NOTICE;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ macos_application(
|
||||
bundle_name = "Santa",
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "10.9",
|
||||
provisioning_profile = select({
|
||||
"//:ci_build": None,
|
||||
"//conditions:default": "Santa_Dev.provisionprofile",
|
||||
}),
|
||||
version = "//:version",
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [":SantaGUI_lib"],
|
||||
|
||||
@@ -46,6 +46,7 @@ objc_library(
|
||||
sdk_dylibs = ["libz"],
|
||||
sdk_frameworks = ["IOKit"],
|
||||
deps = [
|
||||
":FCM_lib",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -64,11 +65,20 @@ objc_library(
|
||||
"@FMDB",
|
||||
"@MOLAuthenticatingURLSession",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLFCMClient",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "FCM_lib",
|
||||
srcs = ["Commands/sync/SNTCommandSyncFCM.m"],
|
||||
hdrs = ["Commands/sync/SNTCommandSyncFCM.h"],
|
||||
sdk_frameworks = ["SystemConfiguration"],
|
||||
deps = [
|
||||
"@MOLAuthenticatingURLSession",
|
||||
],
|
||||
)
|
||||
|
||||
macos_command_line_application(
|
||||
name = "santactl",
|
||||
bundle_id = "com.google.santa.ctl",
|
||||
@@ -130,6 +140,7 @@ santa_unit_test(
|
||||
],
|
||||
sdk_dylibs = ["libz"],
|
||||
deps = [
|
||||
":FCM_lib",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDropRootPrivs",
|
||||
@@ -140,7 +151,6 @@ santa_unit_test(
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"//Source/common:SNTXPCSyncdInterface",
|
||||
"@MOLAuthenticatingURLSession",
|
||||
"@MOLFCMClient",
|
||||
"@MOLXPCConnection",
|
||||
"@OCMock",
|
||||
],
|
||||
|
||||
115
Source/santactl/Commands/sync/SNTCommandSyncFCM.h
Normal file
115
Source/santactl/Commands/sync/SNTCommandSyncFCM.h
Normal file
@@ -0,0 +1,115 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/** A block that takes a NSString object as an argument. */
|
||||
typedef void (^SNTCommandSyncFCMTokenHandler)(NSString *);
|
||||
|
||||
/** A block that takes a NSDictionary object as an argument. */
|
||||
typedef void (^SNTCommandSyncFCMMessageHandler)(NSDictionary *);
|
||||
|
||||
/** A block that takes a NSHTTPURLResponse and NSError object as an argument. */
|
||||
typedef void (^SNTCommandSyncFCMConnectionErrorHandler)(NSHTTPURLResponse *, NSError *);
|
||||
|
||||
/** A block that takes a NSDictionary and NSError object as arguments. */
|
||||
typedef void (^SNTCommandSyncFCMAcknowledgeErrorHandler)(NSDictionary *, NSError *);
|
||||
|
||||
@interface SNTCommandSyncFCM : NSObject
|
||||
|
||||
/** Returns YES if connected to FCM. */
|
||||
@property(readonly, nonatomic) BOOL isConnected;
|
||||
|
||||
/** A block to be executed when the FCM token changes */
|
||||
@property(copy) SNTCommandSyncFCMTokenHandler tokenHandler;
|
||||
|
||||
/** A block to be executed when there is an issue with acknowledging a message. */
|
||||
@property(copy) SNTCommandSyncFCMAcknowledgeErrorHandler acknowledgeErrorHandler;
|
||||
|
||||
/** A block to be executed when there is a non-recoverable issue with the FCM Connection. */
|
||||
@property(copy) SNTCommandSyncFCMConnectionErrorHandler connectionErrorHandler;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
* The designated initializer.
|
||||
*
|
||||
* @param project FCM project
|
||||
* @param entity FCM entity
|
||||
* @param apiKey FCM apiKey
|
||||
* @param connectDelayMax Optional, max delay (seconds) when calling connect
|
||||
* @param backoffMax Optional, max backoff (seconds) when the connection is interrupted
|
||||
* @param fatalCodes Optional, do not attempt to reconnect if a fatal code is returned
|
||||
* @param sessionConfiguration Optional, the desired NSURLSessionConfiguration
|
||||
* @param messageHandler The block to be called for every message received
|
||||
*
|
||||
* @note If the host argument is nil, https://fcm-stream.googleapis.com will be used.
|
||||
* @note If the connectDelayMax argument is 0, a default value of 10 will be used.
|
||||
* @note If the backoffMax argument is 0, a default value of 900 will be used.
|
||||
* @note If the fatalCodes argument is nil, @[@302, @400, @403] will be used.
|
||||
* @note If the sessionConfiguration argument is nil, defaultSessionConfiguration will be used.
|
||||
*
|
||||
* @return An initialized SNTCommandSyncFCM object
|
||||
*/
|
||||
- (instancetype)initWithProject:(NSString *)project
|
||||
entity:(NSString *)entity
|
||||
apiKey:(NSString *)apiKey
|
||||
connectDelayMax:(uint32_t)connectDelayMax
|
||||
backoffMax:(uint32_t)backoffMax
|
||||
fatalCodes:(NSArray<NSNumber *> *)fatalCodes
|
||||
sessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration
|
||||
messageHandler:(SNTCommandSyncFCMMessageHandler)messageHandler
|
||||
NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/** A convenience initializer. Optional args will use their zero values. */
|
||||
- (instancetype)initWithProject:(NSString *)project
|
||||
entity:(NSString *)entity
|
||||
apiKey:(NSString *)apiKey
|
||||
sessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration
|
||||
messageHandler:(SNTCommandSyncFCMMessageHandler)messageHandler;
|
||||
|
||||
/** A convenience initializer. Optional args will use their zero values. */
|
||||
- (instancetype)initWithProject:(NSString *)project
|
||||
entity:(NSString *)entity
|
||||
apiKey:(NSString *)apiKey
|
||||
messageHandler:(SNTCommandSyncFCMMessageHandler)messageHandler;
|
||||
|
||||
/**
|
||||
* Opens a connection to FCM and starts listening for messages.
|
||||
*
|
||||
* @note A random delay will occur before the connection is made.
|
||||
* @note If there is a failure in the connection, reconnection will occur once FCM is reachable.
|
||||
* Failed reconnections will backoff exponentially up to the defined max.
|
||||
*/
|
||||
- (void)connect;
|
||||
|
||||
/**
|
||||
* Acknowledges a FCM message. Each message received must be acknowledged.
|
||||
*
|
||||
* @param message A FCM message
|
||||
*
|
||||
* @note Calls the acknowledgeErrorHandler block property when an acknowledge error occurs.
|
||||
*/
|
||||
- (void)acknowledgeMessage:(NSDictionary *)message;
|
||||
|
||||
/**
|
||||
* Closes all FCM connections. Stops Reachability. Outstanding tasks will be canceled.
|
||||
*
|
||||
* @note After disconnect is called the receiver is considered dead. A new MOLFCMClient object
|
||||
* will need to be created to begin listening for messages.
|
||||
* @note After disconnect the receiver can hold a reference to itself for up to 15 minutes.
|
||||
*/
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
480
Source/santactl/Commands/sync/SNTCommandSyncFCM.m
Normal file
480
Source/santactl/Commands/sync/SNTCommandSyncFCM.m
Normal file
@@ -0,0 +1,480 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santactl/Commands/sync/SNTCommandSyncFCM.h"
|
||||
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
|
||||
#import <MOLAuthenticatingURLSession/MOLAuthenticatingURLSession.h>
|
||||
|
||||
#ifdef DEBUG
|
||||
#define LOGD(format, ...) NSLog(format, ##__VA_ARGS__);
|
||||
#else // DEBUG
|
||||
#define LOGD(format, ...)
|
||||
#endif // DEBUG
|
||||
|
||||
/** FCM checkin and register components */
|
||||
static NSString *const kFCMCheckinHost = @"https://android.clients.google.com";
|
||||
static NSString *const kFCMCheckin = @"/checkin";
|
||||
static NSString *const kFCMCheckinBody = @"{'checkin':{}, 'version':3}";
|
||||
static NSString *const kFCMRegister = @"/c2dm/register3";
|
||||
|
||||
/** FCM connect and ack components */
|
||||
static NSString *const kFCMConnectHost = @"https://fcmconnection.googleapis.com";
|
||||
static NSString *const kFCMConnect = @"/v1alpha1:connectDownstream";
|
||||
static NSString *const kFCMAck = @"/v1alpha1:ack";
|
||||
|
||||
/** FCM client keys */
|
||||
static NSString *const kAndroidIDKey = @"android_id";
|
||||
static NSString *const kVersionInfoKey = @"version_info";
|
||||
static NSString *const kSecurityTokenKey = @"security_token";
|
||||
|
||||
/** HTTP Header Constants */
|
||||
static NSString *const kFCMApplicationForm = @"application/x-www-form-urlencoded";
|
||||
static NSString *const kFCMApplicationJSON = @"application/json";
|
||||
static NSString *const kFCMContentType = @"Content-Type";
|
||||
|
||||
/** Default 15 minute backoff maximum */
|
||||
static const uint32_t kDefaultBackoffMaxSeconds = 900;
|
||||
|
||||
/** Default 10 sec connect delay maximum */
|
||||
static const uint32_t kDefaultConnectDelayMaxSeconds = 10;
|
||||
|
||||
#pragma mark MOLFCMClient Extension
|
||||
|
||||
@interface SNTCommandSyncFCM () {
|
||||
/** URL components for client registration, receiving and acknowledging messages. */
|
||||
NSURLComponents *_checkinComponents;
|
||||
NSURLComponents *_registerComponents;
|
||||
NSURLComponents *_connectComponents;
|
||||
NSURLComponents *_ackComponents;
|
||||
|
||||
/** Holds the NSURLSession object generated by the MOLAuthenticatingURLSession object. */
|
||||
NSURLSession *_session;
|
||||
|
||||
/** Holds the current backoff seconds. */
|
||||
uint32_t _backoffSeconds;
|
||||
|
||||
/** Holds the max connect and backoff seconds. */
|
||||
uint32_t _connectDelayMaxSeconds;
|
||||
uint32_t _backoffMaxSeconds;
|
||||
|
||||
NSArray<NSNumber *> *_fatalHTTPStatusCodes;
|
||||
}
|
||||
|
||||
/** NSURLSession wrapper used for https communication with the FCM service. */
|
||||
@property(nonatomic) MOLAuthenticatingURLSession *authSession;
|
||||
|
||||
/** The block to be called for every message. */
|
||||
@property(copy, nonatomic) SNTCommandSyncFCMMessageHandler messageHandler;
|
||||
|
||||
/** Is used throughout the class to reconnect to FCM after a connection loss. */
|
||||
@property SCNetworkReachabilityRef reachability;
|
||||
|
||||
/** FCM client identities. */
|
||||
@property(nonatomic, readonly) NSString *project;
|
||||
@property(nonatomic, readonly) NSString *entity;
|
||||
@property(nonatomic, readonly) NSString *apiKey;
|
||||
|
||||
/** FCM client checkin data */
|
||||
@property NSString *androidID;
|
||||
@property NSString *versionInfo;
|
||||
@property NSString *securityToken;
|
||||
|
||||
/** Called by the reachability handler when the host becomes reachable. */
|
||||
- (void)reachabilityRestored;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark SCNetworkReachabilityCallBack
|
||||
|
||||
/** Called when the network state changes. */
|
||||
static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags,
|
||||
void *info) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (flags & kSCNetworkReachabilityFlagsReachable) {
|
||||
SNTCommandSyncFCM *FCMClient = (__bridge SNTCommandSyncFCM *)info;
|
||||
SEL s = @selector(reachabilityRestored);
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:FCMClient selector:s object:nil];
|
||||
[FCMClient performSelector:s withObject:nil afterDelay:1];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@implementation SNTCommandSyncFCM
|
||||
|
||||
#pragma mark init/dealloc methods
|
||||
|
||||
- (instancetype)initWithProject:(NSString *)project
|
||||
entity:(NSString *)entity
|
||||
apiKey:(NSString *)apiKey
|
||||
connectDelayMax:(uint32_t)connectDelayMax
|
||||
backoffMax:(uint32_t)backoffMax
|
||||
fatalCodes:(NSArray<NSNumber *> *)fatalCodes
|
||||
sessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration
|
||||
messageHandler:(SNTCommandSyncFCMMessageHandler)messageHandler {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_project = project;
|
||||
_entity = entity;
|
||||
_apiKey = apiKey;
|
||||
_checkinComponents = [NSURLComponents componentsWithString:kFCMCheckinHost];
|
||||
_checkinComponents.path = kFCMCheckin;
|
||||
_registerComponents = [NSURLComponents componentsWithString:kFCMCheckinHost];
|
||||
_registerComponents.path = kFCMRegister;
|
||||
_connectComponents = [NSURLComponents componentsWithString:kFCMConnectHost];
|
||||
_connectComponents.path = kFCMConnect;
|
||||
_ackComponents = [NSURLComponents componentsWithString:kFCMConnectHost];
|
||||
_ackComponents.path = kFCMAck;
|
||||
|
||||
_messageHandler = messageHandler;
|
||||
|
||||
_authSession = [[MOLAuthenticatingURLSession alloc]
|
||||
initWithSessionConfiguration:sessionConfiguration
|
||||
?: [NSURLSessionConfiguration
|
||||
defaultSessionConfiguration]];
|
||||
_authSession.dataTaskDidReceiveDataBlock = [self dataTaskDidReceiveDataBlock];
|
||||
_authSession.taskDidCompleteWithErrorBlock = [self taskDidCompleteWithErrorBlock];
|
||||
|
||||
_session = _authSession.session;
|
||||
|
||||
_connectDelayMaxSeconds = connectDelayMax ?: kDefaultConnectDelayMaxSeconds;
|
||||
_backoffMaxSeconds = backoffMax ?: kDefaultBackoffMaxSeconds;
|
||||
_fatalHTTPStatusCodes = fatalCodes ?: @[ @302, @400, @401, @403, @404 ];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithProject:(NSString *)project
|
||||
entity:(NSString *)entity
|
||||
apiKey:(NSString *)apiKey
|
||||
sessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration
|
||||
messageHandler:(SNTCommandSyncFCMMessageHandler)messageHandler {
|
||||
return [self initWithProject:project
|
||||
entity:entity
|
||||
apiKey:apiKey
|
||||
connectDelayMax:0
|
||||
backoffMax:0
|
||||
fatalCodes:nil
|
||||
sessionConfiguration:sessionConfiguration
|
||||
messageHandler:messageHandler];
|
||||
}
|
||||
|
||||
- (instancetype)initWithProject:(NSString *)project
|
||||
entity:(NSString *)entity
|
||||
apiKey:(NSString *)apiKey
|
||||
messageHandler:(SNTCommandSyncFCMMessageHandler)messageHandler {
|
||||
return [self initWithProject:project
|
||||
entity:entity
|
||||
apiKey:apiKey
|
||||
connectDelayMax:0
|
||||
backoffMax:0
|
||||
fatalCodes:nil
|
||||
sessionConfiguration:nil
|
||||
messageHandler:messageHandler];
|
||||
}
|
||||
|
||||
/** Before this object is released ensure reachability release. */
|
||||
- (void)dealloc {
|
||||
[self stopReachability];
|
||||
}
|
||||
|
||||
#pragma mark property methods
|
||||
|
||||
- (BOOL)isConnected {
|
||||
if (!self.androidID || !self.androidID || !self.securityToken) return NO;
|
||||
for (NSURLSessionDataTask *dataTask in [self dataTasks]) {
|
||||
if (dataTask.state == NSURLSessionTaskStateRunning) return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark reachability methods
|
||||
|
||||
- (void)reachabilityRestored {
|
||||
LOGD(@"Reachability restored. Reconnect after a backoff of %i seconds", _backoffSeconds);
|
||||
[self stopReachability];
|
||||
dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, _backoffSeconds * NSEC_PER_SEC);
|
||||
dispatch_after(t, dispatch_get_main_queue(), ^{
|
||||
[self connectHelper];
|
||||
});
|
||||
}
|
||||
|
||||
/** Start listening for network state changes on a background thread. */
|
||||
- (void)startReachability {
|
||||
if (self.reachability) return;
|
||||
LOGD(@"Reachability started.");
|
||||
self.reachability =
|
||||
SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, kFCMConnectHost.UTF8String);
|
||||
SCNetworkReachabilityContext context = {.info = (__bridge void *)self};
|
||||
if (SCNetworkReachabilitySetCallback(self.reachability, reachabilityHandler, &context)) {
|
||||
SCNetworkReachabilitySetDispatchQueue(
|
||||
self.reachability, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop listening for network state changes. */
|
||||
- (void)stopReachability {
|
||||
if (self.reachability) {
|
||||
SCNetworkReachabilitySetDispatchQueue(self.reachability, NULL);
|
||||
if (self.reachability) CFRelease(self.reachability);
|
||||
self.reachability = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark message methods
|
||||
|
||||
- (void)connect {
|
||||
uint32_t ms = arc4random_uniform(_connectDelayMaxSeconds * 1000);
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, ms * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
|
||||
[self connectHelper];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)connectHelper {
|
||||
LOGD(@"Connecting...");
|
||||
[self cancelConnections];
|
||||
|
||||
// Reuse checkin credentials / FCM token if allready registered.
|
||||
if (!self.androidID || !self.androidID || !self.securityToken) return [self checkin];
|
||||
|
||||
NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:_connectComponents.URL];
|
||||
[URLRequest addValue:kFCMApplicationJSON forHTTPHeaderField:kFCMContentType];
|
||||
URLRequest.HTTPMethod = @"GET";
|
||||
[self setCheckinAuthorization:URLRequest];
|
||||
[[_session dataTaskWithRequest:URLRequest] resume];
|
||||
}
|
||||
|
||||
- (void)checkin {
|
||||
NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:_checkinComponents.URL];
|
||||
[URLRequest addValue:kFCMApplicationJSON forHTTPHeaderField:kFCMContentType];
|
||||
URLRequest.HTTPMethod = @"POST";
|
||||
URLRequest.HTTPBody = [kFCMCheckinBody dataUsingEncoding:NSUTF8StringEncoding];
|
||||
[[_session dataTaskWithRequest:URLRequest] resume];
|
||||
}
|
||||
|
||||
- (void)checkinDataHandler:(NSData *)data {
|
||||
id jo = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
|
||||
LOGD(@"checkin: %@", jo);
|
||||
NSDictionary *checkin = [self extractCheckinFrom:jo];
|
||||
if (!checkin) return;
|
||||
self.androidID = [(NSNumber *)checkin[kAndroidIDKey] stringValue];
|
||||
self.versionInfo = checkin[kVersionInfoKey];
|
||||
self.securityToken = [(NSNumber *)checkin[kSecurityTokenKey] stringValue];
|
||||
}
|
||||
|
||||
- (NSDictionary *)extractCheckinFrom:(id)jo {
|
||||
if (!jo) return nil;
|
||||
if (![jo isKindOfClass:[NSDictionary class]]) return nil;
|
||||
if (!jo[kAndroidIDKey]) return nil;
|
||||
if (![jo[kAndroidIDKey] isKindOfClass:[NSNumber class]]) return nil;
|
||||
if (!jo[kVersionInfoKey]) return nil;
|
||||
if (![jo[kVersionInfoKey] isKindOfClass:[NSString class]]) return nil;
|
||||
if (!jo[kSecurityTokenKey]) return nil;
|
||||
if (![jo[kSecurityTokenKey] isKindOfClass:[NSNumber class]]) return nil;
|
||||
return jo;
|
||||
}
|
||||
|
||||
- (void)register {
|
||||
NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:_registerComponents.URL];
|
||||
URLRequest.HTTPMethod = @"POST";
|
||||
URLRequest.HTTPBody = [kFCMCheckinBody dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSURLComponents *params = [[NSURLComponents alloc] init];
|
||||
params.queryItems = @[
|
||||
[NSURLQueryItem queryItemWithName:@"app" value:self.project],
|
||||
[NSURLQueryItem queryItemWithName:@"info" value:self.versionInfo],
|
||||
[NSURLQueryItem queryItemWithName:@"sender" value:self.entity],
|
||||
[NSURLQueryItem queryItemWithName:@"device" value:self.androidID],
|
||||
[NSURLQueryItem queryItemWithName:@"X-scope" value:@"*"],
|
||||
];
|
||||
URLRequest.HTTPBody = [params.query dataUsingEncoding:NSUTF8StringEncoding];
|
||||
[URLRequest addValue:kFCMApplicationForm forHTTPHeaderField:kFCMContentType];
|
||||
NSString *aid = [NSString stringWithFormat:@"AidLogin %@:%@", self.androidID, self.securityToken];
|
||||
[URLRequest addValue:aid forHTTPHeaderField:@"Authorization"];
|
||||
[[_session dataTaskWithRequest:URLRequest] resume];
|
||||
}
|
||||
|
||||
- (void)registerDataHandler:(NSData *)data {
|
||||
NSString *t = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
NSArray *c = [t componentsSeparatedByString:@"="];
|
||||
if (c.count == 2) {
|
||||
NSString *tok = c[1];
|
||||
if ([tok isEqualToString:@"PHONE_REGISTRATION_ERROR"]) {
|
||||
LOGD(@"register: PHONE_REGISTRATION_ERROR - retrying");
|
||||
sleep(1);
|
||||
return [self register];
|
||||
}
|
||||
if (self.tokenHandler) self.tokenHandler(tok);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)acknowledgeMessage:(NSDictionary *)message {
|
||||
if (!message[@"messageId"]) return;
|
||||
NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:_ackComponents.URL];
|
||||
URLRequest.HTTPMethod = @"POST";
|
||||
[URLRequest addValue:kFCMApplicationJSON forHTTPHeaderField:kFCMContentType];
|
||||
[self setCheckinAuthorization:URLRequest];
|
||||
NSDictionary *b = @{@"ack" : @{@"messageId" : message[@"messageId"]}};
|
||||
NSData *body = [NSJSONSerialization dataWithJSONObject:b options:0 error:NULL];
|
||||
URLRequest.HTTPBody = body;
|
||||
[[_session dataTaskWithRequest:URLRequest
|
||||
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
if (((NSHTTPURLResponse *)response).statusCode != 200) {
|
||||
if (self.acknowledgeErrorHandler) {
|
||||
self.acknowledgeErrorHandler(message, error);
|
||||
}
|
||||
}
|
||||
}] resume];
|
||||
}
|
||||
|
||||
- (void)disconnect {
|
||||
[self stopReachability];
|
||||
[_session invalidateAndCancel];
|
||||
_session = nil;
|
||||
}
|
||||
|
||||
- (void)cancelConnections {
|
||||
for (NSURLSessionDataTask *dataTask in [self dataTasks]) {
|
||||
[dataTask cancel];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<NSURLSessionDataTask *> *)dataTasks {
|
||||
__block NSArray *dataTasks;
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
[_session getTasksWithCompletionHandler:^(NSArray *data, NSArray *upload, NSArray *download) {
|
||||
dataTasks = data;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
||||
return dataTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse FCM data; extract and call self.messageHandler for each message.
|
||||
*
|
||||
* Expected format:
|
||||
* [{
|
||||
* "noOp": {}
|
||||
* }
|
||||
* , <-- start of new chunk
|
||||
* {
|
||||
* "noOp": {}
|
||||
* }
|
||||
*
|
||||
*/
|
||||
- (void)processMessagesFromData:(NSData *)data {
|
||||
if (!data) return;
|
||||
NSMutableString *raw = [[NSMutableString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
if (raw.length < 2) return;
|
||||
// Add an opening bracket if this is a message in the middle of the stream.
|
||||
[raw replaceOccurrencesOfString:@",\r" withString:@"[" options:0 range:NSMakeRange(0, 2)];
|
||||
// Always add a closing bracket.
|
||||
[raw appendString:@"]"];
|
||||
NSError *err;
|
||||
id jo = [NSJSONSerialization JSONObjectWithData:[raw dataUsingEncoding:NSUTF8StringEncoding]
|
||||
options:0
|
||||
error:&err];
|
||||
if (!jo) {
|
||||
if (err) LOGD(@"processMessagesFromData err: %@", err);
|
||||
return;
|
||||
}
|
||||
LOGD(@"processMessagesFromData: %@", jo);
|
||||
|
||||
if (![jo isKindOfClass:[NSArray class]]) return;
|
||||
for (id md in jo) {
|
||||
if (![md isKindOfClass:[NSDictionary class]]) continue;
|
||||
NSDictionary *m = md[@"message"];
|
||||
if ([m isKindOfClass:[NSDictionary class]]) self.messageHandler(m);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleHTTPReponse:(NSHTTPURLResponse *)HTTPResponse error:(NSError *)error {
|
||||
if (HTTPResponse.statusCode == 200) {
|
||||
_backoffSeconds = 0;
|
||||
if ([HTTPResponse.URL.path isEqualToString:kFCMCheckin]) {
|
||||
// If checkin was successful, start listening for messages and continue to register.
|
||||
[self connectHelper];
|
||||
return [self register];
|
||||
} else if ([HTTPResponse.URL.path isEqualToString:kFCMConnect]) {
|
||||
// connect will re-connect.
|
||||
return [self connectHelper];
|
||||
} // register may be called more than once, don't do anything in response.
|
||||
} else if ([_fatalHTTPStatusCodes containsObject:@(HTTPResponse.statusCode)]) {
|
||||
if (self.connectionErrorHandler) self.connectionErrorHandler(HTTPResponse, error);
|
||||
} else {
|
||||
// If no backoff is set, start out with 5 - 15 seconds.
|
||||
// If a backoff is already set, double it, with a max of kBackoffMaxSeconds.
|
||||
_backoffSeconds = _backoffSeconds * 2 ?: arc4random_uniform(11) + 5;
|
||||
if (_backoffSeconds > _backoffMaxSeconds) _backoffSeconds = _backoffMaxSeconds;
|
||||
if (error) LOGD(@"handleHTTPReponse err: %@", error);
|
||||
[self startReachability];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setCheckinAuthorization:(NSMutableURLRequest *)URLRequest {
|
||||
NSString *a = [NSString
|
||||
stringWithFormat:@"checkin %@:%@ %@", self.androidID, self.securityToken, self.versionInfo];
|
||||
[URLRequest addValue:a forHTTPHeaderField:@"Authorization"];
|
||||
[URLRequest addValue:self.apiKey forHTTPHeaderField:@"X-Goog-Api-Key"];
|
||||
}
|
||||
|
||||
#pragma mark NSURLSession block property and methods
|
||||
|
||||
/**
|
||||
* MOLAuthenticatingURLSession is the NSURLSessionDelegate. It will call this block every time
|
||||
* the URLSession:task:didCompleteWithError: is called. This allows MOLFCMClient to be notified
|
||||
* when a task ends while using delegate methods.
|
||||
*/
|
||||
- (void (^)(NSURLSession *, NSURLSessionDataTask *, NSData *))dataTaskDidReceiveDataBlock {
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
return ^(NSURLSession *session, NSURLSessionDataTask *dataTask, NSData *data) {
|
||||
__typeof(self) strongSelf = weakSelf;
|
||||
NSString *path = dataTask.originalRequest.URL.path;
|
||||
if ([path isEqualToString:kFCMCheckin]) {
|
||||
return [strongSelf checkinDataHandler:data];
|
||||
} else if ([path isEqualToString:kFCMRegister]) {
|
||||
return [strongSelf registerDataHandler:data];
|
||||
} else if ([dataTask.originalRequest.URL.path isEqualToString:kFCMConnect]) {
|
||||
[strongSelf processMessagesFromData:data];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MOLAuthenticatingURLSession is the NSURLSessionDataDelegate. It will call this block every time
|
||||
* the URLSession:dataTask:didReceiveData: is called. This allows for message data chunks to be
|
||||
* processed as they appear in the FCM buffer. For Content-Type: text/html there is a 512 byte
|
||||
* buffer that must be filled before data is returned. Content-Type: application/json does not use
|
||||
* a buffer and data is returned as soon as it is available.
|
||||
*
|
||||
* TODO:(bur) Follow up with FCM on Content-Type: application/json. Currently FCM returns data with
|
||||
* Content-Type: text/html. Messages under 512 bytes will not be processed until the connection
|
||||
* drains.
|
||||
*/
|
||||
- (void (^)(NSURLSession *, NSURLSessionTask *, NSError *))taskDidCompleteWithErrorBlock {
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
return ^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
|
||||
__typeof(self) strongSelf = weakSelf;
|
||||
// task.response can be nil when an NSURLError* occurs
|
||||
if (task.response && ![task.response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
if (strongSelf.connectionErrorHandler) strongSelf.connectionErrorHandler(nil, error);
|
||||
return;
|
||||
}
|
||||
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)task.response;
|
||||
[strongSelf handleHTTPReponse:HTTPResponse error:error];
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -15,7 +15,6 @@
|
||||
#import "Source/santactl/Commands/sync/SNTCommandSyncManager.h"
|
||||
|
||||
#import <MOLAuthenticatingURLSession/MOLAuthenticatingURLSession.h>
|
||||
#import <MOLFCMClient/MOLFCMClient.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
|
||||
@@ -28,6 +27,7 @@
|
||||
#import "Source/common/SNTXPCSyncdInterface.h"
|
||||
#import "Source/santactl/Commands/sync/SNTCommandSyncConstants.h"
|
||||
#import "Source/santactl/Commands/sync/SNTCommandSyncEventUpload.h"
|
||||
#import "Source/santactl/Commands/sync/SNTCommandSyncFCM.h"
|
||||
#import "Source/santactl/Commands/sync/SNTCommandSyncPostflight.h"
|
||||
#import "Source/santactl/Commands/sync/SNTCommandSyncPreflight.h"
|
||||
#import "Source/santactl/Commands/sync/SNTCommandSyncRuleDownload.h"
|
||||
@@ -61,7 +61,8 @@ static NSString *const kFCMTargetHostIDKey = @"target_host_id";
|
||||
@property NSUInteger FCMGlobalRuleSyncDeadline;
|
||||
@property NSUInteger eventBatchSize;
|
||||
|
||||
@property MOLFCMClient *FCMClient;
|
||||
@property SNTCommandSyncFCM *FCMClient;
|
||||
@property NSString *FCMToken;
|
||||
|
||||
@property(nonatomic) MOLXPCConnection *daemonConn;
|
||||
|
||||
@@ -185,40 +186,46 @@ static void reachabilityHandler(
|
||||
#pragma mark push notification methods
|
||||
|
||||
- (void)listenForPushNotificationsWithSyncState:(SNTCommandSyncState *)syncState {
|
||||
if ([self.FCMClient.FCMToken isEqualToString:syncState.FCMToken]) {
|
||||
LOGD(@"Continue with the current FCMToken");
|
||||
if ([self.FCMToken isEqualToString:syncState.FCMToken]) {
|
||||
LOGD(@"Already listening for push notifications");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGD(@"Start listening for push notifications");
|
||||
|
||||
WEAKIFY(self);
|
||||
|
||||
[self.FCMClient disconnect];
|
||||
NSString *machineID = syncState.machineID;
|
||||
self.FCMClient = [[MOLFCMClient alloc] initWithFCMToken:syncState.FCMToken
|
||||
sessionConfiguration:syncState.session.configuration.copy
|
||||
messageHandler:^(NSDictionary *message) {
|
||||
if (!message || [message isEqual:@{}]) return;
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
self.FCMClient = [[SNTCommandSyncFCM alloc] initWithProject:config.fcmProject
|
||||
entity:config.fcmEntity
|
||||
apiKey:config.fcmAPIKey
|
||||
sessionConfiguration:syncState.session.configuration.copy
|
||||
messageHandler:^(NSDictionary *message) {
|
||||
if (!message || message[@"noOp"]) return;
|
||||
STRONGIFY(self);
|
||||
LOGD(@"%@", message);
|
||||
[self.FCMClient acknowledgeMessage:message];
|
||||
[self processFCMMessage:message withMachineID:machineID];
|
||||
}];
|
||||
|
||||
self.FCMClient.tokenHandler = ^(NSString *t) {
|
||||
STRONGIFY(self);
|
||||
LOGD(@"tokenHandler: %@", t);
|
||||
self.FCMToken = t;
|
||||
[self preflightOnly:YES];
|
||||
};
|
||||
|
||||
self.FCMClient.connectionErrorHandler = ^(NSHTTPURLResponse *response, NSError *error) {
|
||||
STRONGIFY(self);
|
||||
if (response) LOGE(@"FCM fatal response: %@", response);
|
||||
if (error) LOGE(@"FCM fatal error: %@", error);
|
||||
[self.FCMClient disconnect];
|
||||
self.FCMClient = nil;
|
||||
self.FCMToken = nil;
|
||||
[self rescheduleTimerQueue:self.fullSyncTimer secondsFromNow:kDefaultFullSyncInterval];
|
||||
};
|
||||
|
||||
self.FCMClient.loggingBlock = ^(NSString *log) {
|
||||
LOGD(@"FCMClient: %@", log);
|
||||
};
|
||||
|
||||
[self.FCMClient connect];
|
||||
}
|
||||
|
||||
@@ -337,6 +344,10 @@ static void reachabilityHandler(
|
||||
#pragma mark syncing chain
|
||||
|
||||
- (void)preflight {
|
||||
[self preflightOnly:NO];
|
||||
}
|
||||
|
||||
- (void)preflightOnly:(BOOL)preflightOnly {
|
||||
LOGD(@"Preflight starting");
|
||||
SNTCommandSyncState *syncState = [self createSyncState];
|
||||
SNTCommandSyncPreflight *p = [[SNTCommandSyncPreflight alloc] initWithState:syncState];
|
||||
@@ -349,18 +360,19 @@ static void reachabilityHandler(
|
||||
self.eventBatchSize = syncState.eventBatchSize;
|
||||
|
||||
// Start listening for push notifications with a full sync every FCMFullSyncInterval
|
||||
if (syncState.daemon && syncState.FCMToken) {
|
||||
if (syncState.daemon && [SNTConfigurator configurator].fcmEnabled) {
|
||||
self.FCMFullSyncInterval = syncState.FCMFullSyncInterval;
|
||||
self.FCMGlobalRuleSyncDeadline = syncState.FCMGlobalRuleSyncDeadline;
|
||||
[self listenForPushNotificationsWithSyncState:syncState];
|
||||
} else if (syncState.daemon) {
|
||||
LOGD(@"FCMToken not provided. Sync every %lu min.", syncState.fullSyncInterval / 60);
|
||||
LOGD(@"FCM not enabled. Sync every %lu min.", syncState.fullSyncInterval / 60);
|
||||
[self.FCMClient disconnect];
|
||||
self.FCMClient = nil;
|
||||
self.fullSyncInterval = syncState.fullSyncInterval;
|
||||
[self rescheduleTimerQueue:self.fullSyncTimer secondsFromNow:self.fullSyncInterval];
|
||||
}
|
||||
|
||||
if (preflightOnly) return;
|
||||
return [self eventUploadWithSyncState:syncState];
|
||||
} else {
|
||||
if (!syncState.daemon) {
|
||||
@@ -489,6 +501,8 @@ static void reachabilityHandler(
|
||||
syncState.compressedContentEncoding =
|
||||
config.enableBackwardsCompatibleContentEncoding ? @"zlib" : @"deflate";
|
||||
|
||||
syncState.FCMToken = self.FCMToken;
|
||||
|
||||
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
||||
return syncState;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
requestDict[kOSBuild] = [SNTSystemInfo osBuild];
|
||||
requestDict[kSantaVer] = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||||
requestDict[kPrimaryUser] = self.syncState.machineOwner;
|
||||
if (self.syncState.FCMToken) requestDict[kFCMToken] = self.syncState.FCMToken;
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
@@ -102,7 +103,6 @@
|
||||
}];
|
||||
|
||||
self.syncState.eventBatchSize = [resp[kBatchSize] unsignedIntegerValue] ?: kDefaultEventBatchSize;
|
||||
self.syncState.FCMToken = resp[kFCMToken];
|
||||
|
||||
// Don't let these go too low
|
||||
NSUInteger FCMIntervalValue = [resp[kFCMFullSyncInterval] unsignedIntegerValue];
|
||||
|
||||
@@ -80,6 +80,10 @@ macos_bundle(
|
||||
infoplists = ["Info.plist"],
|
||||
linkopts = ["-execute"],
|
||||
minimum_os_version = "10.9",
|
||||
provisioning_profile = select({
|
||||
"//:ci_build": None,
|
||||
"//conditions:default": "Santa_Daemon_Dev.provisionprofile",
|
||||
}),
|
||||
version = "//:version",
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [":santad_lib"],
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#import "Source/common/SantaCache.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
uint64_t GetCurrentUptime() {
|
||||
@@ -28,7 +28,6 @@ template<> uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const& t
|
||||
}
|
||||
|
||||
@implementation SNTCachingEndpointSecurityManager {
|
||||
std::atomic<bool> _compilerPIDs[PID_MAX];
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *_decisionCache;
|
||||
}
|
||||
|
||||
@@ -56,10 +55,20 @@ template<> uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const& t
|
||||
// If item is in cache but hasn't received a response yet, sleep for a bit.
|
||||
// If item is not in cache, break out of loop and forward request to callback.
|
||||
if (RESPONSE_VALID(return_action)) {
|
||||
if (return_action == ACTION_RESPOND_ALLOW) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
} else {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
|
||||
switch (return_action) {
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW_COMPILER: {
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
[self setIsCompilerPID:pid];
|
||||
// Don't let ES cache compilers
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
|
||||
break;
|
||||
}
|
||||
return YES;
|
||||
} else if (return_action == ACTION_REQUEST_BINARY || return_action == ACTION_RESPOND_ACK) {
|
||||
@@ -70,7 +79,7 @@ template<> uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const& t
|
||||
}
|
||||
}
|
||||
|
||||
[self addToCache:vnode_id decision:ACTION_REQUEST_BINARY timeout:0];
|
||||
[self addToCache:vnode_id decision:ACTION_REQUEST_BINARY currentTicks:0];
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -79,25 +88,24 @@ template<> uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const& t
|
||||
es_respond_result_t ret;
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
if (sm.pid >= PID_MAX) {
|
||||
LOGE(@"Unable to watch compiler pid=%d >= pid_max=%d", sm.pid, PID_MAX);
|
||||
} else {
|
||||
LOGD(@"Watching compiler pid=%d path=%s", sm.pid, sm.path);
|
||||
self->_compilerPIDs[sm.pid].store(true);
|
||||
}
|
||||
// Allow the exec, but don't cache the decision so subsequent execs of the compiler get
|
||||
// marked appropriately.
|
||||
[self setIsCompilerPID:sm.pid];
|
||||
|
||||
// Allow the exec and cache in our internal cache but don't let ES cache, because then
|
||||
// we won't see future execs of the compiler in order to record the PID.
|
||||
[self addToCache:sm.vnode_id
|
||||
decision:ACTION_RESPOND_ALLOW_COMPILER
|
||||
currentTicks:GetCurrentUptime()];
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message,
|
||||
ES_AUTH_RESULT_ALLOW, false);
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE:
|
||||
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_ALLOW timeout:GetCurrentUptime()];
|
||||
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_ALLOW currentTicks:GetCurrentUptime()];
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message,
|
||||
ES_AUTH_RESULT_ALLOW, true);
|
||||
break;
|
||||
case ACTION_RESPOND_DENY:
|
||||
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_DENY timeout:GetCurrentUptime()];
|
||||
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_DENY currentTicks:GetCurrentUptime()];
|
||||
OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_TOOLONG:
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message,
|
||||
@@ -114,7 +122,7 @@ template<> uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const& t
|
||||
|
||||
- (void)addToCache:(santa_vnode_id_t)identifier
|
||||
decision:(santa_action_t)decision
|
||||
timeout:(uint64_t)microsecs {
|
||||
currentTicks:(uint64_t)microsecs {
|
||||
switch (decision) {
|
||||
case ACTION_REQUEST_BINARY:
|
||||
_decisionCache->set(identifier, (uint64_t)ACTION_REQUEST_BINARY << 56, 0);
|
||||
|
||||
@@ -25,6 +25,10 @@ const pid_t PID_MAX = 99999;
|
||||
@interface SNTEndpointSecurityManager : NSObject<SNTEventProvider>
|
||||
- (santa_vnode_id_t)vnodeIDForFile:(es_file_t *)file;
|
||||
|
||||
- (BOOL)isCompilerPID:(pid_t)pid;
|
||||
- (void)setIsCompilerPID:(pid_t)pid;
|
||||
- (void)setNotCompilerPID:(pid_t)pid;
|
||||
|
||||
@property(readonly, nonatomic) es_client_t *client;
|
||||
|
||||
@end
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
#include <libproc.h>
|
||||
|
||||
|
||||
@interface SNTEndpointSecurityManager ()
|
||||
@interface SNTEndpointSecurityManager () {
|
||||
std::atomic<bool> _compilerPIDs[PID_MAX];
|
||||
}
|
||||
|
||||
@property(nonatomic) SNTPrefixTree *prefixTree;
|
||||
@property(nonatomic, copy) void (^decisionCallback)(santa_message_t);
|
||||
@@ -36,9 +38,7 @@
|
||||
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityManager {
|
||||
std::atomic<bool> _compilerPIDs[PID_MAX];
|
||||
}
|
||||
@implementation SNTEndpointSecurityManager
|
||||
|
||||
- (instancetype)init API_AVAILABLE(macos(10.15)) {
|
||||
self = [super init];
|
||||
@@ -107,7 +107,7 @@
|
||||
[self removeCacheEntryForVnodeID:[self vnodeIDForFile:m->event.close.target]];
|
||||
|
||||
// Create a transitive rule if the file was modified by a running compiler
|
||||
if (pid && pid < PID_MAX && self->_compilerPIDs[pid].load()) {
|
||||
if ([self isCompilerPID:pid]) {
|
||||
santa_message_t sm = {};
|
||||
BOOL truncated = [self populateBufferFromESFile:m->event.close.target
|
||||
buffer:sm.path
|
||||
@@ -117,6 +117,9 @@
|
||||
sm.path, pid);
|
||||
break;
|
||||
}
|
||||
if ([@(sm.path) hasPrefix:@"/dev/"]) {
|
||||
break;
|
||||
}
|
||||
sm.action = ACTION_NOTIFY_WHITELIST;
|
||||
sm.pid = pid;
|
||||
sm.pidversion = pidversion;
|
||||
@@ -129,7 +132,7 @@
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
// Create a transitive rule if the file was renamed by a running compiler
|
||||
if (pid && pid < PID_MAX && self->_compilerPIDs[pid].load()) {
|
||||
if ([self isCompilerPID:pid]) {
|
||||
santa_message_t sm = {};
|
||||
BOOL truncated = [self populateRenamedNewPathFromESMessage:m->event.rename
|
||||
buffer:sm.path
|
||||
@@ -139,6 +142,9 @@
|
||||
sm.path, pid);
|
||||
break;
|
||||
}
|
||||
if ([@(sm.path) hasPrefix:@"/dev/"]) {
|
||||
break;
|
||||
}
|
||||
sm.action = ACTION_NOTIFY_WHITELIST;
|
||||
sm.pid = pid;
|
||||
sm.pidversion = pidversion;
|
||||
@@ -151,7 +157,7 @@
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT: {
|
||||
// Update the set of running compiler PIDs
|
||||
if (pid && pid < PID_MAX) self->_compilerPIDs[pid].store(false);
|
||||
[self setNotCompilerPID:pid];
|
||||
|
||||
// Skip the standard pipeline and just log.
|
||||
if (![config enableForkAndExitLogging]) return;
|
||||
@@ -191,12 +197,12 @@
|
||||
|
||||
switch (m->action_type) {
|
||||
case ES_ACTION_TYPE_AUTH: {
|
||||
// Create a timer to deny the execution 2 seconds before the deadline,
|
||||
// Create a timer to deny the execution 5 seconds before the deadline,
|
||||
// if a response hasn't already been sent. This block will still be enqueued if
|
||||
// the the deadline - 2 secs is < DISPATCH_TIME_NOW.
|
||||
// As of 10.15.2, a typical deadline is 60 seconds.
|
||||
// the the deadline - 5 secs is < DISPATCH_TIME_NOW.
|
||||
// As of 10.15.5, a typical deadline is 60 seconds.
|
||||
auto responded = std::make_shared<std::atomic<bool>>(false);
|
||||
dispatch_after(dispatch_time(m->deadline, NSEC_PER_SEC * -2), self.esAuthQueue, ^(void) {
|
||||
dispatch_after(dispatch_time(m->deadline, NSEC_PER_SEC * -5), self.esAuthQueue, ^(void) {
|
||||
if (responded->load()) return;
|
||||
LOGE(@"Deadline reached: deny pid=%d ret=%d",
|
||||
pid, es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false));
|
||||
@@ -446,12 +452,8 @@
|
||||
es_respond_result_t ret;
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
if (sm.pid >= PID_MAX) {
|
||||
LOGE(@"Unable to watch compiler pid=%d >= pid_max=%d", sm.pid, PID_MAX);
|
||||
} else {
|
||||
LOGD(@"Watching compiler pid=%d path=%s", sm.pid, sm.path);
|
||||
self->_compilerPIDs[sm.pid].store(true);
|
||||
}
|
||||
[self setIsCompilerPID:sm.pid];
|
||||
|
||||
// Allow the exec, but don't cache the decision so subsequent execs of the compiler get
|
||||
// marked appropriately.
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message,
|
||||
@@ -564,4 +566,23 @@
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)isCompilerPID:(pid_t)pid {
|
||||
return (pid && pid < PID_MAX && self->_compilerPIDs[pid].load());
|
||||
}
|
||||
|
||||
- (void)setIsCompilerPID:(pid_t)pid {
|
||||
if (pid < 1) {
|
||||
LOGE(@"Unable to watch compiler pid=%d", pid);
|
||||
} else if (pid >= PID_MAX) {
|
||||
LOGE(@"Unable to watch compiler pid=%d >= PID_MAX(%d)", pid, PID_MAX);
|
||||
} else {
|
||||
self->_compilerPIDs[pid].store(true);
|
||||
LOGD(@"Watching compiler pid=%d", pid);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setNotCompilerPID:(pid_t)pid {
|
||||
if (pid && pid < PID_MAX) self->_compilerPIDs[pid].store(false);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
17
WORKSPACE
17
WORKSPACE
@@ -14,12 +14,15 @@ git_repository(
|
||||
load("@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies")
|
||||
apple_rules_dependencies()
|
||||
|
||||
load("@build_bazel_apple_support//lib:repositories.bzl", "apple_support_dependencies")
|
||||
apple_support_dependencies()
|
||||
|
||||
# Macops MOL* dependencies
|
||||
|
||||
git_repository(
|
||||
name = "MOLAuthenticatingURLSession",
|
||||
remote = "https://github.com/google/macops-molauthenticatingurlsession.git",
|
||||
tag = "v2.9",
|
||||
tag = "v3.0",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
@@ -34,16 +37,10 @@ git_repository(
|
||||
tag = "v2.2",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "MOLFCMClient",
|
||||
remote = "https://github.com/google/macops-molfcmclient.git",
|
||||
tag = "v2.0",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "MOLXPCConnection",
|
||||
remote = "https://github.com/google/macops-molxpcconnection.git",
|
||||
tag = "v2.0",
|
||||
tag = "v2.1",
|
||||
)
|
||||
|
||||
# FMDB
|
||||
@@ -51,7 +48,7 @@ git_repository(
|
||||
new_git_repository(
|
||||
name = "FMDB",
|
||||
remote = "https://github.com/ccgus/fmdb.git",
|
||||
tag = "v2.7",
|
||||
tag = "2.7.7",
|
||||
build_file_content = """
|
||||
objc_library(
|
||||
name = "FMDB",
|
||||
@@ -69,7 +66,7 @@ objc_library(
|
||||
new_git_repository(
|
||||
name = "OCMock",
|
||||
remote = "https://github.com/erikdoe/ocmock",
|
||||
tag = "v3.4.3",
|
||||
tag = "v3.8.1",
|
||||
build_file_content = """
|
||||
objc_library(
|
||||
name = "OCMock",
|
||||
|
||||
@@ -365,11 +365,11 @@ Recursive lookups of an application or directory is a soon to be added feature
|
||||
|
||||
##### rule
|
||||
|
||||
The rule command is covered in the [rules.md](rules.md) document.
|
||||
The rule command is covered in the [Rules](rules.md) document.
|
||||
|
||||
##### sync
|
||||
|
||||
The sync command is covered in the [syncing.md](syncing.md) document.
|
||||
The sync command is covered in the [Syncing Overview](syncing-overview.md) document.
|
||||
|
||||
##### Debug Commands
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""The version for all Santa components."""
|
||||
|
||||
SANTA_VERSION = "2021.2"
|
||||
SANTA_VERSION = "2021.5"
|
||||
|
||||
Reference in New Issue
Block a user