Private
Public Access
1
0

Add 'server/' from commit '800090542d91beae40bc81fc41b67ba61c47da77'

git-subtree-dir: server
git-subtree-mainline: 6a4054c15a
git-subtree-split: 800090542d
This commit is contained in:
2025-09-06 19:36:27 -07:00
77 changed files with 13705 additions and 0 deletions

3
server/.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "CocoaHTTPServer"]
path = CocoaHTTPServer
url = https://github.com/robbiehanson/CocoaHTTPServer.git

Submodule server/CocoaHTTPServer added at 52b2a64e9c

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

13
server/Makefile Normal file
View File

@@ -0,0 +1,13 @@
INSTALL_PATH := /usr/share/kordophone
.PHONY: build/Release/kordophoned
build/Release/kordophoned:
xcodebuild
.PHONY: install
install: build/Release/kordophoned
install -d $(INSTALL_PATH)
install build/Release/kordophoned $(INSTALL_PATH)
clean:
rm -Rf build

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CDF62331219A895D00690038"
BuildableName = "kordophoned"
BlueprintName = "kordophoned"
ReferencedContainer = "container:MessagesBridge.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CDF62331219A895D00690038"
BuildableName = "kordophoned"
BlueprintName = "kordophoned"
ReferencedContainer = "container:MessagesBridge.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</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">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CDF62331219A895D00690038"
BuildableName = "kordophoned"
BlueprintName = "kordophoned"
ReferencedContainer = "container:MessagesBridge.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "-s -c certificate.p12 -a access.asc"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CDF62331219A895D00690038"
BuildableName = "kordophoned"
BlueprintName = "kordophoned"
ReferencedContainer = "container:MessagesBridge.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

56
server/README.md Normal file
View File

@@ -0,0 +1,56 @@
# Entitlements
You might to enable this default to use private entitlements
```
sudo defaults write /Library/Preferences/com.apple.security.coderequirements Entitlements -string always
```
Maybe a better thing to do is to DYLD_PRELOAD `imagent` and swizzle `IMDAuditTokenTaskHasEntitlement` to always return YES.
Included in the project is "kordophoned-RestrictedEntitlements.plist", which contains all necessary restricted entitlements.
On production macOS builds, the kernel will kill kordophoned immediately if it's signed using restricted entitlements, so agent hook is a
better option when running on prod machines. By default, the project is configured to ignore kordophoned-RestrictedEntitlements.plist when building.
## Building/linking
If you get dyld errors running from the command line, use `install_name_tool` to update the @rpath (where @rpath points to where linked Frameworks like GCDWebServer is).
`install_name_tool -add_rpath . ./kordophoned`
## Running
You need to hook imagent first to bypass entitlements check. Look at `hookAgent.sh`
## SSL
If you want to run with SSL, you have to generate a self-signed certificate, and have the Mac trust the root cert.
### Generate a root cert
1. Generate root key
`openssl genrsa -out Kordophone-root.key 4096`
2. Generate root certificate
`openssl req -x509 -new -nodes -key Kordophone-root.key -sha256 -days 1024 -out Kordophone-root.crt`
3. Add this certificate to the Mac's trust store via Keychain Access. Set to "Always Trust"
### Create signing certificate by signing a new cert with the root cert
1. Generate signing key
`openssl genrsa -out kp.localhost.key 2048`
2. Create certificate signing request
`openssl req -new -key kp.localhost.key -out kp.localhost.csr`
3. Sign the cert with the root cert
`openssl x509 -req -in kp.localhost.csr -CA Kordophone-root.crt -CAkey Kordophone-root.key -CAcreateserial -out kp.localhost.crt -days 365 -sha256`
4. kordophoned works with a signing cert in PKCS12 format. Convert the cert and the privkey to PKCS12
`openssl pkcs12 -export -in kp.localhost.crt -inkey kp.localhost.key -out certificate.p12 -name "Kordophone"`
### Start kordophone with the SSL options and provide the p12
`kordophoned -s -c certificate.p12`
## Authentication
Basic Authentication is also optional, but requires SSL to be enabled as well. To configure basic authentication, create a file containing your username and password on two separate lines encrypted with your GPG key.
`echo "username\npassword" > password.txt"`
`gpg -e -r (your email) -o password.asc password.txt`
Then run kordophoned with the following option
`kordophone -s -c certificate.p12 -a password.asc`
You may need to unlock your GPG keyring (via gpg-agent) when running kordophoned the first time.

View File

@@ -0,0 +1,53 @@
<?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>EnvironmentVariables</key>
<dict>
<key>NSRunningFromLaunchd</key>
<string>1</string>
<key>DYLD_INSERT_LIBRARIES</key>
<string>/usr/share/kordophone/libagentHook.dylib</string>
</dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>Label</key>
<string>com.apple.imagent</string>
<key>LaunchEvents</key>
<dict>
<key>com.apple.distnoted.matching</key>
<dict>
<key>com.apple.authkit.user-info-changed</key>
<dict>
<key>Name</key>
<string>com.apple.authkit.user-info-changed</string>
</dict>
<key>com.apple.private.IMCore.LoggedIntoHSA2</key>
<dict>
<key>Name</key>
<string>com.apple.private.IMCore.LoggedIntoHSA2</string>
</dict>
</dict>
<key>com.apple.xpc.activity</key>
<dict/>
</dict>
<key>MachServices</key>
<dict>
<key>com.apple.imagent.cache-delete</key>
<true/>
<key>com.apple.imagent.desktop.auth</key>
<true/>
<key>com.apple.incoming-call-filter-server</key>
<true/>
</dict>
<key>POSIXSpawnType</key>
<string>Interactive</string>
<key>ProgramArguments</key>
<array>
<string>/System/Library/PrivateFrameworks/IMCore.framework/imagent.app/Contents/MacOS/imagent</string>
</array>
</dict>
</plist>

View File

@@ -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>EnvironmentVariables</key>
<dict>
<key>NSRunningFromLaunchd</key>
<string>1</string>
</dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>Label</key>
<string>net.buzzert.kordophoned</string>
<key>POSIXSpawnType</key>
<string>Interactive</string>
<key>ProgramArguments</key>
<array>
<string>/usr/share/kordophone/bootstrap_kordophone.sh</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,48 @@
//
// Tests.m
// Tests
//
// Created by James Magahern on 11/15/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import <XCTest/XCTest.h>
#import "NSData+AES.h"
// base64 encoded
static NSString *const kTestKey = @"QMeDmiHj8eCFVfrQWQfDpw==";
@interface Tests : XCTestCase
@property (nonatomic, strong) NSString *commonPayload;
@property (nonatomic, strong) NSData *commonIVData;
@property (nonatomic, strong) NSData *symmetricKeyData;
@end
@implementation Tests
- (void)setUp {
self.commonPayload = @"Hey this is a test";
NSString *IVDataString = [[NSUUID UUID] UUIDString];
self.commonIVData = [IVDataString dataUsingEncoding:NSUTF8StringEncoding];
self.symmetricKeyData = [[NSData alloc] initWithBase64EncodedString:kTestKey options:0];
XCTAssert(self.commonIVData && self.symmetricKeyData);
}
- (void)testEncryptionAndDecryption
{
NSData *inputData = [self.commonPayload dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
NSData *encryptedData = [inputData encryptedDataWithKey:self.symmetricKeyData iv:self.commonIVData error:&error];
XCTAssert(!error);
NSData *decryptedData = [encryptedData decryptedDataWithKey:self.symmetricKeyData iv:self.commonIVData error:&error];
XCTAssert(!error);
XCTAssert([decryptedData isEqualToData:inputData]);
}
@end

22
server/Tests/Info.plist Normal file
View File

@@ -0,0 +1,22 @@
<?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>$(DEVELOPMENT_LANGUAGE)</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>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -0,0 +1,19 @@
#import <mach/message.h>
#import <Foundation/Foundation.h>
#include <dlfcn.h>
#define DYLD_INTERPOSE(_replacment,_replacee) \
__attribute__((used)) static struct{ const void* replacment; const void* replacee; } _interpose_##_replacee \
__attribute__ ((section ("__DATA,__interpose"))) = { (const void*)(unsigned long)&_replacment, (const void*)(unsigned long)&_replacee };
BOOL IMDAuditTokenTaskHasEntitlement(audit_token_t *auditToken, NSString *entitlement);
BOOL replacement__IMDAuditTokenTaskHasEntitlement(audit_token_t *auditToken, NSString *entitlement)
{
// Bypass all entitlement checks
return YES;
}
DYLD_INTERPOSE(replacement__IMDAuditTokenTaskHasEntitlement, IMDAuditTokenTaskHasEntitlement);

20
server/agentHook/hookAgent.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
# This script is necessary to circumvent the entitlements check in imagent.
# Might want to wrap this script up in a startup script or something so we make sure this
# happens every time.
if [[ $# -lt 1 ]]; then
echo "Usage: hookAgent.sh libagentHook.dylib"
exit 0
fi
LIB_PATH=$(python -c "import os; print(os.path.realpath('$1'))")
echo "Library path: $LIB_PATH"
echo "Telling imagent to launch with inserted libraries for uid $EUID"
sudo launchctl debug gui/$EUID/com.apple.imagent --environment DYLD_INSERT_LIBRARIES=$LIB_PATH
launchctl kill SIGKILL gui/501/com.apple.imagent
echo "\nYou might have to kill imagent for changes to take effect (killall imagent)"

View File

@@ -0,0 +1,26 @@
//
// MBIMAuthToken.h
// MBIMAuthToken
//
// Created by James Magahern on 7/6/21.
// Copyright © 2021 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MBIMAuthToken : NSObject
@property (nonatomic, readonly) NSString *username;
@property (nonatomic, readonly) NSString *jwtToken;
@property (nonatomic, readonly) NSDate *expirationDate;
- (instancetype)initWithUsername:(NSString *)username NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTokenString:(NSString *)tokenString NS_DESIGNATED_INITIALIZER;
- (BOOL)isValid;
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,149 @@
//
// MBIMAuthToken.m
// MBIMAuthToken
//
// Created by James Magahern on 7/6/21.
// Copyright © 2021 James Magahern. All rights reserved.
//
#import "MBIMAuthToken.h"
#import <CommonCrypto/CommonHMAC.h>
#define HOUR 3600
#define DAY (24*HOUR)
static const NSTimeInterval ExpirationTime = 15 * DAY;
static const char *SecretKey = "709E7CD8-4983-4D5F-B7BF-8B1C6341D2DB";
static NSString *const kUsernamePayloadKey = @"user";
static NSString *const kIssuerPayloadKey = @"iss";
static NSString *const kExpirationDatePayloadKey = @"exp";
static NSString *const kIssuerPayloadValue = @"kordophone";
@interface MBIMAuthToken ()
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *jwtToken;
@property (nonatomic, copy) NSDate *expirationDate;
// JWT Payload data
@property (nonatomic, copy) NSString *headerString;
@property (nonatomic, copy) NSString *payloadString;
@property (nonatomic, copy) NSData *signatureData;
@end
@implementation MBIMAuthToken
- (instancetype)initWithUsername:(NSString *)username
{
self = [super init];
if (self) {
self.username = username;
self.expirationDate = [NSDate dateWithTimeIntervalSinceNow:ExpirationTime];
}
return self;
}
- (instancetype)initWithTokenString:(NSString *)tokenString
{
NSArray<NSString *> *components = [tokenString componentsSeparatedByString:@"."];
if (components.count != 3) {
return nil;
}
NSString *header = components[0];
NSString *payload = components[1];
NSString *signature = components[2];
NSData *payloadData = [[NSData alloc] initWithBase64EncodedString:payload options:0];
NSDictionary *decodedPayload = [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:nil];
if (!decodedPayload) {
return nil;
}
if (![decodedPayload[kIssuerPayloadKey] isEqualToString:@"kordophone"]) {
return nil;
}
self = [super init];
if (self) {
_headerString = header;
_payloadString = payload;
_signatureData = [[NSData alloc] initWithBase64EncodedString:signature options:0];
_username = decodedPayload[kUsernamePayloadKey];
NSTimeInterval expirationDate = [decodedPayload[kExpirationDatePayloadKey] floatValue];
_expirationDate = [NSDate dateWithTimeIntervalSince1970:expirationDate];
}
return self;
}
- (NSUInteger)hash
{
return (_username.hash ^ _expirationDate.hash);
}
- (NSString *)jwtToken
{
if (!_jwtToken) {
NSDictionary *header = @{
@"alg" : @"HS256",
@"typ" : @"jwt"
};
NSData *headerData = [NSJSONSerialization dataWithJSONObject:header options:0 error:nil];
NSString *headerStr = [headerData base64EncodedStringWithOptions:0];
NSInteger expirationDate = [_expirationDate timeIntervalSince1970];
NSDictionary *payload = @{
kUsernamePayloadKey : _username,
kIssuerPayloadKey : kIssuerPayloadValue,
kExpirationDatePayloadKey : [NSString stringWithFormat:@"%ld", expirationDate]
};
NSData *payloadData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:nil];
NSString *payloadStr = [payloadData base64EncodedStringWithOptions:0];
NSString *jwtDataStr = [NSString stringWithFormat:@"%@.%@", headerStr, payloadStr];
NSData *jwtData = [jwtDataStr dataUsingEncoding:NSASCIIStringEncoding];
unsigned char signature[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, SecretKey, sizeof(SecretKey), jwtData.bytes, jwtData.length, signature);
NSData *signatureData = [NSData dataWithBytes:signature length:CC_SHA256_DIGEST_LENGTH];
NSString *signatureStr = [signatureData base64EncodedStringWithOptions:0];
_jwtToken = [NSString stringWithFormat:@"%@.%@", jwtDataStr, signatureStr];
}
return _jwtToken;
}
- (BOOL)isValid
{
// Verify expiration date
BOOL expirationDateValid = [_expirationDate timeIntervalSinceNow] > 0;
if (!expirationDateValid) {
MBIMLogInfo(@"Auth token expired.");
return NO;
}
// Verify signature
NSString *verificationDataStr = [NSString stringWithFormat:@"%@.%@", _headerString, _payloadString];
NSData *verificationData = [verificationDataStr dataUsingEncoding:NSASCIIStringEncoding];
unsigned char computedSignature[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, SecretKey, sizeof(SecretKey), verificationData.bytes, verificationData.length, computedSignature);
NSData *computedSignatureData = [NSData dataWithBytes:computedSignature length:CC_SHA256_DIGEST_LENGTH];
if (![computedSignatureData isEqualToData:_signatureData]) {
MBIMLogInfo(@"Auth token signature verification failed.");
return NO;
}
return YES;
}
@end

View File

@@ -0,0 +1,37 @@
//
// MBIMBridge.h
// MessagesBridge
//
// Created by James Magahern on 11/12/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
// See note in hooking.m about why this was a bad idea
#define HOOK_IMAGENT 0
NS_ASSUME_NONNULL_BEGIN
@interface MBIMBridge : NSObject
@property (nonatomic, strong) NSString *dylibPath;
@property (nonatomic, assign) UInt16 port;
@property (nonatomic, readonly) NSOperationQueue *operationQueue;
@property (nonatomic, assign) BOOL usesAccessControl;
@property (nonatomic, strong) NSString *authUsername;
@property (nonatomic, strong) NSString *authPassword;
@property (nonatomic, assign) BOOL usesSSL;
@property (nonatomic, strong) NSString *sslCertPath;
+ (instancetype)sharedInstance;
- (instancetype)init NS_UNAVAILABLE;
- (void)connect;
- (void)disconnect;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,260 @@
//
// MBIMBridge.m
// MessagesBridge
//
// Created by James Magahern on 11/12/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMBridge.h"
#import "MBIMBridge_Private.h"
#import "MBIMBridgeOperation.h"
#import "MBIMConcurrentHTTPServer.h"
#import "MBIMHTTPConnection.h"
#import "MBIMUpdateQueue.h"
#import "hooking.h"
#import "HTTPServer.h"
#import "IMCore_ClassDump.h"
#import "IMFoundation_ClassDump.h"
static const UInt16 kDefaultPort = 5738;
static NSString *const MBIMBridgeToken = @"net.buzzert.kordophone";
@interface MBIMBridge (/* INTERNAL */) {
__strong NSArray *_sslCertificateAndIdentity;
}
@property (nonatomic, strong) MBIMConcurrentHTTPServer *httpServer;
@property (nonatomic, strong) NSOperationQueue *operationQueue;
- (instancetype)_init;
@end
@implementation MBIMBridge
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
static __strong MBIMBridge *sharedBridge = nil;
dispatch_once(&onceToken, ^{
sharedBridge = [[MBIMBridge alloc] _init];
});
return sharedBridge;
}
- (instancetype)_init
{
self = [super init];
if (self) {
self.port = kDefaultPort;
_operationQueue = [[NSOperationQueue alloc] init];
_operationQueue.maxConcurrentOperationCount = 5;
}
return self;
}
- (void)_terminate
{
// *shrug*
exit(1);
}
- (NSArray *)sslCertificateAndIdentity
{
if (!_sslCertificateAndIdentity && self.sslCertPath) {
// Get the p12
NSError *error = nil;
NSData *certData = [NSData dataWithContentsOfFile:self.sslCertPath options:0 error:&error];
if (!certData || error) {
MBIMLogError(@"Unable to load SSL certificate from file: %@", [error localizedDescription]);
return nil;
}
CFArrayRef items = nil;
OSStatus status = SecPKCS12Import(
(__bridge CFDataRef)certData,
(__bridge CFDictionaryRef) @{
(__bridge id)kSecImportExportPassphrase : @"xNAq3vn)^PNu}[&gyQ4MZeV?J"
},
&items
);
if (status != noErr) {
MBIMLogError(@"Error importing PKCS12: SecPKCS12Import status: %d", status);
return nil;
}
CFDictionaryRef certDict = CFArrayGetValueAtIndex(items, 0);
if (!certDict) {
MBIMLogError(@"Error parsing the SSL certificate");
return nil;
}
SecIdentityRef identity = (SecIdentityRef)CFDictionaryGetValue(certDict, kSecImportItemIdentity);
_sslCertificateAndIdentity = @[ (__bridge id)identity ];
}
return _sslCertificateAndIdentity;
}
- (void)checkSSLCertificate
{
if (self.usesSSL) {
NSArray *certAndIdentity = [self sslCertificateAndIdentity];
if ([certAndIdentity count]) {
MBIMLogInfo(@"SSL Certificate looks okay");
} else {
MBIMLogFatal(@"Wasn't able to load SSL certificate. Bailing...");
[self _terminate];
}
}
}
#pragma mark -
#pragma mark Connection
- (void)connect
{
#if HOOK_IMAGENT
char *errorString = nil;
BOOL hooked = HookIMAgent(self.dylibPath, &errorString);
if (!hooked) {
NSString *errorNSString = [NSString stringWithUTF8String:errorString];
MBIMLogInfo(@"Error hooking imagent: %@", errorNSString);
return;
}
#endif
[self registerForNotifications];
[[IMDaemonController sharedInstance] setDelegate:(id)self];
[[[IMDaemonController sharedInstance] listener] addHandler:self];
if (![[IMDaemonController sharedInstance] hasListenerForID:MBIMBridgeToken]) {
if (![[IMDaemonController sharedInstance] addListenerID:MBIMBridgeToken capabilities:(kFZListenerCapFileTransfers | kFZListenerCapManageStatus | kFZListenerCapChats | kFZListenerCapMessageHistory | kFZListenerCapIDQueries | kFZListenerCapSendMessages)]) {
MBIMLogFatal(@"Failed to connect to imagent");
[self _terminate];
}
}
[self checkSSLCertificate];
[self startWebServer];
}
- (void)disconnect
{
[[IMDaemonController sharedInstance] removeListenerID:MBIMBridgeToken];
}
#pragma mark -
#pragma mark Notifications
- (void)registerForNotifications
{
(void)[IMChatRegistry sharedInstance];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_messageReceived:) name:IMChatMessageReceivedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_chatRegistryDidLoad:) name:IMChatRegistryDidLoadNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_chatItemsDidChange:) name:IMChatItemsDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_unreadCountChanged:) name:IMChatRegistryUnreadCountChangedNotification object:nil];
}
- (void)_unreadCountChanged:(NSNotification *)notification
{
// Not a lot of useful information plumbed here...
}
- (void)_messageReceived:(NSNotification *)notification
{
MBIMLogInfo(@"Received message from chat with GUID: %@", [[notification object] guid]);
IMChat *chat = [notification object];
IMMessage *message = [[notification userInfo] objectForKey:IMChatValueKey];
if (chat && message) {
if (![message isFromMe]) {
MBIMUpdateItem *updateItem = [[MBIMUpdateItem alloc] init];
updateItem.changedChat = chat;
updateItem.addedMessage = message;
[[MBIMUpdateQueue sharedInstance] enqueueUpdateItem:updateItem];
} else {
// TODO: care about messages from me?
}
}
}
- (void)_chatRegistryDidLoad:(NSNotification *)notification
{
MBIMLogInfo(@"Loaded chat registry. %lu existing chats", (unsigned long)[[IMChatRegistry sharedInstance] numberOfExistingChats]);
}
- (void)_chatItemsDidChange:(NSNotification *)notification
{
IMChat *chat = [notification object];
if (chat) {
MBIMLogInfo(@"Chat items change for GUID: %@", [chat guid]);
MBIMUpdateItem *updateItem = [[MBIMUpdateItem alloc] init];
updateItem.changedChat = chat;
[[MBIMUpdateQueue sharedInstance] enqueueUpdateItem:updateItem];
}
}
#pragma mark -
#pragma mark Web Server initialization
- (void)startWebServer
{
self.httpServer = [[MBIMConcurrentHTTPServer alloc] init];
[self.httpServer setConnectionClass:[MBIMHTTPConnection class]];
[self.httpServer setPort:self.port];
NSError *error = nil;
if (![self.httpServer start:&error]) {
MBIMLogError(@"Error starting HTTP server: %@", [error localizedDescription]);
} else {
MBIMLogNotify(@"Started Kordophone HTTP server on port %u", self.port);
}
}
#pragma mark -
#pragma mark Daemon lifecycle
- (void)daemonControllerWillConnect
{
MBIMLogInfo(@"Connecting to imagent...");
}
- (void)daemonControllerDidConnect
{
MBIMLogInfo(@"imagent responded.");
IMAccount *iMessageAccount = [[IMAccountController sharedInstance] bestAccountForService:[IMServiceImpl iMessageService]];
if (iMessageAccount) {
MBIMLogInfo(@"Successfully got accounts from imagent");
MBIMLogInfo(@"iMessage account connected: %@", iMessageAccount);
} else {
MBIMLogFatal(@"ERROR: imagent returned no accounts (not entitled? speak with Agent Hook)");
[self _terminate];
}
}
- (void)daemonControllerDidDisconnect
{
MBIMLogInfo(@"Disconnected from imagent");
}
- (void)daemonConnectionLost
{
MBIMLogError(@"Connection lost to imagent");
}
@end

View File

@@ -0,0 +1,14 @@
//
// MBIMBridge_Private.h
// MessagesBridge
//
// Created by James Magahern on 1/22/19.
// Copyright © 2019 James Magahern. All rights reserved.
//
#import "MBIMBridge.h"
@interface MBIMBridge (/*PRIVATE*/)
- (NSArray *)sslCertificateAndIdentity;
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMConcurrentHTTPServer.h
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "HTTPServer.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMConcurrentHTTPServer : HTTPServer
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,23 @@
//
// MBIMConcurrentHTTPServer.m
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMConcurrentHTTPServer.h"
@implementation MBIMConcurrentHTTPServer
- (id)init
{
self = [super init];
if (self) {
connectionQueue = dispatch_queue_create("net.buzzert.MBIMConcurrentHTTPConnectionQueue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
@end

View File

@@ -0,0 +1,13 @@
//
// MBIMHTTPConnection.h
// CocoaHTTPServer
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "HTTPConnection.h"
@interface MBIMHTTPConnection : HTTPConnection
@end

View File

@@ -0,0 +1,186 @@
//
// MBIMHTTPConnection.m
// CocoaHTTPServer
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMHTTPConnection.h"
#import "MBIMBridge.h"
#import "MBIMBridge_Private.h"
#import "MBIMBridgeOperation.h"
#import "MBIMAuthToken.h"
#import "MBIMUpdateQueue.h"
#import "MBIMURLUtilities.h"
#import <Security/Security.h>
#import "HTTPMessage.h"
#import "GCDAsyncSocket.h"
@interface HTTPConnection (/* INTERNAL */)
- (BOOL)isAuthenticated;
@end
@implementation MBIMHTTPConnection {
NSMutableData *_bodyData;
MBIMBridgeOperation *_currentOperation;
}
- (BOOL)isSecureServer
{
return [[MBIMBridge sharedInstance] usesSSL];
}
- (NSArray *)sslIdentityAndCertificates
{
return [[MBIMBridge sharedInstance] sslCertificateAndIdentity];
}
- (BOOL)isPasswordProtected:(NSString *)path
{
if ([[MBIMBridge sharedInstance] usesAccessControl]) {
NSURL *url = [NSURL URLWithString:path];
NSString *endpointName = [url lastPathComponent];
Class operationClass = [MBIMBridgeOperation operationClassForEndpointName:endpointName];
return [operationClass requiresAuthentication];
}
return NO;
}
- (NSString *)passwordForUser:(NSString *)username
{
MBIMBridge *bridge = [MBIMBridge sharedInstance];
if ([username isEqualToString:bridge.authUsername]) {
return bridge.authPassword;
}
// nil means "user not in system"
return nil;
}
- (BOOL)isAuthenticated
{
NSString *authInfo = [request headerField:@"Authorization"];
if ([authInfo hasPrefix:@"Bearer"]) {
NSArray *bearerAuthTuple = [authInfo componentsSeparatedByString:@" "];
if ([bearerAuthTuple count] == 2) {
NSString *jwtToken = [bearerAuthTuple objectAtIndex:1];
MBIMAuthToken *authToken = [[MBIMAuthToken alloc] initWithTokenString:jwtToken];
return [authToken isValid];
}
}
return [super isAuthenticated];
}
- (BOOL)useDigestAccessAuthentication
{
// TODO: should use digest all the time, but it's a bit more complicated. Allow this to be NO if
// SSL is on, because then at least basic auth is encrypted
return ![[MBIMBridge sharedInstance] usesSSL];
}
- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
{
if ([method isEqualToString:@"GET"] || [method isEqualToString:@"POST"]) {
return YES;
}
return NO;
}
- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
__block NSObject<HTTPResponse> *response = nil;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
MBIMBridgeOperationCompletionBlock completion = ^(NSObject<HTTPResponse> *incomingResponse) {
response = incomingResponse;
dispatch_semaphore_signal(sema);
};
NSURL *url = [NSURL URLWithString:path];
NSString *endpointName = [url lastPathComponent];
BOOL requestTimedOut = NO;
Class operationClass = [MBIMBridgeOperation operationClassForEndpointName:endpointName];
if (operationClass != nil) {
_currentOperation = [[operationClass alloc] initWithRequestURL:url completion:completion];
_currentOperation.requestBodyData = _bodyData;
_currentOperation.request = self->request;
[[[MBIMBridge sharedInstance] operationQueue] addOperation:_currentOperation];
long status = dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(60.0 * NSEC_PER_SEC)));
if (status != 0) {
requestTimedOut = YES;
}
} else {
response = [[HTTPErrorResponse alloc] initWithErrorCode:404];
}
if (requestTimedOut) {
response = [_currentOperation cancelAndReturnTimeoutResponse];
}
return response;
}
- (WebSocket *)webSocketForURI:(NSString *)path
{
NSURL *url = [NSURL URLWithString:path];
NSString *endpointName = [url lastPathComponent];
NSString *authTokenString = [url valueForQueryItemWithName:@"token"];
MBIMAuthToken *queryAuthToken = [[MBIMAuthToken alloc] initWithTokenString:authTokenString];
NSLog(@"Websocket for URI: %@ | authenticated request: %@", path, [self isAuthenticated] ? @"YES" : @"NO");
if ([endpointName isEqualToString:@"updates"]) {
if (![self isAuthenticated] && ![queryAuthToken isValid]) {
NSLog(@"Websocket: auth invalid, rejecting.");
NSLog(@"Query Token: %@, raw: %@", queryAuthToken, authTokenString);
// Respond with 401 unauthorized
HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:401 description:nil version:HTTPVersion1_1];
[response setHeaderField:@"Content-Length" value:@"0"];
NSData *responseData = [self preprocessErrorResponse:response];
[asyncSocket writeData:responseData withTimeout:30 tag:90];
return nil;
}
NSLog(@"Vending websocket for consumer");
return [[MBIMUpdateQueue sharedInstance] vendUpdateWebSocketConsumerForRequest:request socket:asyncSocket];
}
return [super webSocketForURI:path];
}
- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path
{
if ([method isEqualToString:@"POST"]) {
return YES;
}
return NO;
}
- (void)prepareForBodyWithSize:(UInt64)contentLength
{
_bodyData = [[NSMutableData alloc] initWithCapacity:contentLength];
}
- (void)processBodyData:(NSData *)postDataChunk
{
[_bodyData appendData:postDataChunk];
}
- (void)die
{
[_currentOperation cancel];
[super die];
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMPingPongWebSocket.h
// kordophoned
//
// Created by James Magahern on 6/13/25.
// Copyright © 2025 James Magahern. All rights reserved.
//
#import "WebSocket.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMPingPongWebSocket : WebSocket
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,216 @@
//
// MBIMPingPongWebSocket.m
// kordophoned
//
// Created by James Magahern on 6/13/25.
// Copyright © 2025 James Magahern. All rights reserved.
//
#import "MBIMPingPongWebSocket.h"
#import "GCDAsyncSocket.h"
// WebSocket opcodes (from the base class)
#define TIMEOUT_NONE -1
#define TIMEOUT_REQUEST_BODY 10
#define TAG_HTTP_REQUEST_BODY 100
#define TAG_HTTP_RESPONSE_HEADERS 200
#define TAG_HTTP_RESPONSE_BODY 201
#define TAG_PREFIX 300
#define TAG_MSG_PLUS_SUFFIX 301
#define TAG_MSG_WITH_LENGTH 302
#define TAG_MSG_MASKING_KEY 303
#define TAG_PAYLOAD_PREFIX 304
#define TAG_PAYLOAD_LENGTH 305
#define TAG_PAYLOAD_LENGTH16 306
#define TAG_PAYLOAD_LENGTH64 307
#define WS_OP_CONTINUATION_FRAME 0
#define WS_OP_TEXT_FRAME 1
#define WS_OP_BINARY_FRAME 2
#define WS_OP_CONNECTION_CLOSE 8
#define WS_OP_PING 9
#define WS_OP_PONG 10
// Additional tags for ping/pong handling
#define TAG_PING_PAYLOAD 400
#define TAG_PONG_SENT 401
@interface WebSocket ()
- (void)socket:(GCDAsyncSocket *)asyncSocket didReadData:(NSData *)data withTag:(long)tag;
@end
@implementation MBIMPingPongWebSocket
{
NSUInteger currentPayloadLength;
}
#pragma mark - Ping/Pong Frame Construction
- (NSData *)createPongFrameWithPayload:(NSData *)pingPayload {
NSMutableData *frame = [NSMutableData data];
// First byte: FIN (1) + RSV (000) + Opcode (1010 = 0xA for Pong)
uint8_t firstByte = 0x8A; // 10001010 in binary
[frame appendBytes:&firstByte length:1];
// Second byte: MASK (0) + Payload Length
NSUInteger payloadLength = [pingPayload length];
if (payloadLength <= 125) {
// Short payload length (0-125 bytes)
uint8_t secondByte = (uint8_t)payloadLength; // MASK bit is 0 for server-to-client
[frame appendBytes:&secondByte length:1];
}
else if (payloadLength <= 65535) {
// Medium payload length (126-65535 bytes)
uint8_t secondByte = 126;
[frame appendBytes:&secondByte length:1];
// Add 16-bit length in network byte order (big-endian)
uint16_t extendedLength = htons((uint16_t)payloadLength);
[frame appendBytes:&extendedLength length:2];
}
else {
// Large payload length (65536+ bytes) - rarely needed for pings
uint8_t secondByte = 127;
[frame appendBytes:&secondByte length:1];
// Add 64-bit length in network byte order (big-endian)
uint64_t extendedLength = OSSwapHostToBigInt64(payloadLength);
[frame appendBytes:&extendedLength length:8];
}
// Append the payload (copied exactly from ping)
if (payloadLength > 0) {
[frame appendData:pingPayload];
}
return [frame copy];
}
- (void)sendPongWithPayload:(NSData *)payload {
NSData *pongFrame = [self createPongFrameWithPayload:payload];
// Send the pong frame directly through the socket
[asyncSocket writeData:pongFrame withTimeout:TIMEOUT_NONE tag:TAG_PONG_SENT];
}
#pragma mark - Override AsyncSocket Delegate
- (void)socket:(GCDAsyncSocket *)asyncSocket didReadData:(NSData *)data withTag:(long)tag {
// xxx: sheesh: really need to get off of CococaHTTPServer.
NSData *maskingKey = [self valueForKey:@"maskingKey"];
BOOL nextFrameMasked = [[self valueForKey:@"nextFrameMasked"] boolValue];
NSUInteger nextOpCode = [[self valueForKey:@"nextOpCode"] unsignedIntValue];
// Handle our custom ping payload tag
if (tag == TAG_PING_PAYLOAD) {
// We've read the ping payload, now send the pong response
NSData *payload = data;
// If the frame was masked, we need to unmask it
if (nextFrameMasked && maskingKey) {
NSMutableData *unmaskedPayload = [data mutableCopy];
UInt8 *payloadBytes = [unmaskedPayload mutableBytes];
UInt8 *maskBytes = (UInt8 *)[maskingKey bytes];
for (NSUInteger i = 0; i < [data length]; i++) {
payloadBytes[i] ^= maskBytes[i % 4];
}
payload = [unmaskedPayload copy];
}
[self sendPongWithPayload:payload];
// Continue reading the next frame
[asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX];
return;
}
// Let parent handle TAG_PAYLOAD_PREFIX first to set nextOpCode
if (tag == TAG_PAYLOAD_PREFIX) {
[super socket:asyncSocket didReadData:data withTag:tag];
return;
}
// Now intercept ping/pong handling after nextOpCode is set
if (nextOpCode == WS_OP_PING || nextOpCode == WS_OP_PONG) {
if (tag == TAG_PAYLOAD_LENGTH) {
UInt8 frame = *(UInt8 *)[data bytes];
BOOL masked = (frame & 0x80) != 0;
NSUInteger length = frame & 0x7F;
nextFrameMasked = masked;
currentPayloadLength = length;
if (length <= 125) {
if (nextFrameMasked) {
[asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY];
} else if (length > 0) {
[asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_PING_PAYLOAD];
} else {
// Empty payload, no masking - handle immediately
[self sendPongWithPayload:[NSData data]];
[asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX];
}
}
else if (length == 126) {
[asyncSocket readDataToLength:2 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH16];
}
else {
[asyncSocket readDataToLength:8 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH64];
}
return;
}
if (tag == TAG_MSG_MASKING_KEY) {
// Store the masking key
maskingKey = [data copy];
// Now read the payload (or handle empty payload)
if (currentPayloadLength > 0) {
[asyncSocket readDataToLength:currentPayloadLength withTimeout:TIMEOUT_NONE tag:TAG_PING_PAYLOAD];
} else {
// Empty payload - send pong immediately
[self sendPongWithPayload:[NSData data]];
[asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX];
}
return;
}
if (tag == TAG_PAYLOAD_LENGTH16) {
UInt8 *pFrame = (UInt8 *)[data bytes];
NSUInteger length = ((NSUInteger)pFrame[0] << 8) | (NSUInteger)pFrame[1];
if (nextFrameMasked) {
[asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY];
}
[asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_PING_PAYLOAD];
return;
}
if (tag == TAG_PAYLOAD_LENGTH64) {
// For 64-bit length, this is too complex for ping frames - just close
[self didClose];
return;
}
}
// For all other cases, call the parent implementation
[super socket:asyncSocket didReadData:data withTag:tag];
}
#pragma mark - Helper Methods
// Expose the isValidWebSocketFrame method since it's private in the parent
- (BOOL)isValidWebSocketFrame:(UInt8)frame {
NSUInteger rsv = frame & 0x70;
NSUInteger opcode = frame & 0x0F;
if (rsv || (3 <= opcode && opcode <= 7) || (0xB <= opcode && opcode <= 0xF)) {
return NO;
}
return YES;
}
@end

View File

@@ -0,0 +1,39 @@
//
// MBIMUpdateQueue.h
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "IMCore_ClassDump.h"
#import "IMFoundation_ClassDump.h"
NS_ASSUME_NONNULL_BEGIN
@class GCDAsyncSocket;
@class HTTPMessage;
@class WebSocket;
@interface MBIMUpdateItem : NSObject
@property (nonatomic, strong) IMChat *changedChat;
@property (nonatomic, strong) IMMessage *addedMessage;
- (NSDictionary *)dictionaryRepresentation;
@end
typedef void (^MBIMUpdateConsumer)(NSArray<MBIMUpdateItem *> *items);
@interface MBIMUpdateQueue : NSObject
+ (instancetype)sharedInstance;
- (void)addPollingConsumer:(MBIMUpdateConsumer)consumer withLastSyncedMessageSeq:(NSInteger)messageSeq;
- (void)removePollingConsumer:(MBIMUpdateConsumer)consumer;
- (void)enqueueUpdateItem:(MBIMUpdateItem *)item;
- (WebSocket *)vendUpdateWebSocketConsumerForRequest:(HTTPMessage *)request socket:(GCDAsyncSocket *)socket;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,203 @@
//
// MBIMUpdateQueue.m
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMUpdateQueue.h"
#import "IMMessageItem+Encoded.h"
#import "IMChat+Encoded.h"
#import "MBIMHTTPConnection.h"
#import "MBIMURLUtilities.h"
#import "MBIMPingPongWebSocket.h"
#import "GCDAsyncSocket.h"
#import "HTTPMessage.h"
#import "WebSocket.h"
static const NSUInteger kUpdateItemsCullingLength = 100;
@interface MBIMUpdateItem (/*INTERNAL*/) <WebSocketDelegate>
@property (nonatomic, assign) NSUInteger messageSequenceNumber;
@end
@implementation MBIMUpdateQueue {
NSUInteger _messageSequenceNumber;
dispatch_queue_t _accessQueue;
NSMutableArray *_longPollConsumers;
// Maps message sequence number to update item
NSMutableDictionary<NSNumber *, MBIMUpdateItem *> *_updateItemHistory;
// WebSocket consumers
NSMutableDictionary<NSString *, MBIMUpdateConsumer> *_websocketConsumers;
}
+ (instancetype)sharedInstance
{
static MBIMUpdateQueue *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (instancetype)init
{
self = [super init];
if (self) {
_accessQueue = dispatch_queue_create("net.buzzert.MBIMUpdateQueue", DISPATCH_QUEUE_SERIAL);
_messageSequenceNumber = 0;
_updateItemHistory = [[NSMutableDictionary alloc] init];
_websocketConsumers = [[NSMutableDictionary alloc] init];
_longPollConsumers = [[NSMutableArray alloc] init];
}
return self;
}
- (void)addPollingConsumer:(MBIMUpdateConsumer)consumer withLastSyncedMessageSeq:(NSInteger)messageSeq
{
if (![self _syncConsumer:consumer fromLastMessageSeq:messageSeq]) {
__weak NSMutableArray *consumers = _longPollConsumers;
dispatch_async(_accessQueue, ^{
[consumers addObject:consumer];
});
}
}
- (void)removePollingConsumer:(MBIMUpdateConsumer)consumer
{
__weak NSMutableArray *consumers = _longPollConsumers;
dispatch_async(_accessQueue, ^{
[consumers removeObject:consumer];
});
}
- (void)enqueueUpdateItem:(MBIMUpdateItem *)item
{
__weak __auto_type pollingConsumers = _longPollConsumers;
__weak __auto_type websocketConsumers = _websocketConsumers;
__weak NSMutableDictionary *updateItemHistory = _updateItemHistory;
dispatch_async(_accessQueue, ^{
self->_messageSequenceNumber++;
item.messageSequenceNumber = self->_messageSequenceNumber;
// Notify polling consumers
for (MBIMUpdateConsumer consumer in pollingConsumers) {
consumer(@[ item ]);
}
[pollingConsumers removeAllObjects];
// Notify websocket consumers
for (MBIMUpdateConsumer consumer in [websocketConsumers allValues]) {
consumer(@[ item ]);
}
[updateItemHistory setObject:item forKey:@(item.messageSequenceNumber)];
[self _cullUpdateItems];
});
}
- (WebSocket *)vendUpdateWebSocketConsumerForRequest:(HTTPMessage *)request socket:(GCDAsyncSocket *)gcdSocket
{
WebSocket *socket = [[MBIMPingPongWebSocket alloc] initWithRequest:request socket:gcdSocket];
socket.delegate = self;
MBIMUpdateConsumer consumer = ^(NSArray<MBIMUpdateItem *> *updates) {
NSMutableArray *encodedUpdates = [NSMutableArray array];
for (MBIMUpdateItem *item in updates) {
NSDictionary *updateDict = [item dictionaryRepresentation];
[encodedUpdates addObject:updateDict];
}
NSData *data = [NSJSONSerialization dataWithJSONObject:encodedUpdates options:0 error:NULL];
[socket sendData:data];
};
NSString *messageSeqString = [[request url] valueForQueryItemWithName:@"seq"];
[self _syncConsumer:consumer fromLastMessageSeq:(messageSeqString ? [messageSeqString integerValue] : -1)];
__weak __auto_type websocketConsumers = _websocketConsumers;
dispatch_async(_accessQueue, ^{
NSString *websocketKey = [socket description];
[websocketConsumers setObject:consumer forKey:websocketKey];
});
return socket;
}
- (BOOL)_syncConsumer:(MBIMUpdateConsumer)consumer fromLastMessageSeq:(NSInteger)messageSeq
{
const BOOL needsSync = (messageSeq >= 0) && messageSeq < self->_messageSequenceNumber;
if (needsSync) {
__weak NSMutableDictionary *updateItemHistory = _updateItemHistory;
dispatch_async(_accessQueue, ^{
NSMutableArray *batchedUpdates = [NSMutableArray array];
for (NSUInteger seq = messageSeq + 1; seq <= self->_messageSequenceNumber; seq++) {
MBIMUpdateItem *item = [updateItemHistory objectForKey:@(seq)];
if (item) {
[batchedUpdates addObject:item];
}
}
consumer(batchedUpdates);
});
}
return needsSync;
}
- (void)_cullUpdateItems
{
__weak NSMutableDictionary *updateItemHistory = _updateItemHistory;
dispatch_async(_accessQueue, ^{
if ([updateItemHistory count] > kUpdateItemsCullingLength) {
NSArray *sortedKeys = [[updateItemHistory allKeys] sortedArrayUsingSelector:@selector(compare:)];
for (NSValue *key in sortedKeys) {
[updateItemHistory removeObjectForKey:key];
if ([updateItemHistory count] <= kUpdateItemsCullingLength) {
break;
}
}
}
});
}
#pragma mark - <WebSocketDelegate>
- (void)webSocketDidClose:(WebSocket *)ws
{
// xxx: not great, but works.
NSString *websocketKey = [ws description];
__weak __auto_type websocketConsumers = _websocketConsumers;
dispatch_async(_accessQueue, ^{
[websocketConsumers removeObjectForKey:websocketKey];
});
}
@end
@implementation MBIMUpdateItem
- (NSDictionary *)dictionaryRepresentation
{
NSMutableDictionary *updateDict = [NSMutableDictionary dictionary];
updateDict[@"messageSequenceNumber"] = @(self.messageSequenceNumber);
if ([self changedChat]) {
updateDict[@"conversation"] = [[self changedChat] mbim_dictionaryRepresentation];
}
if ([self addedMessage]) {
updateDict[@"message"] = [[self addedMessage] mbim_dictionaryRepresentation];
}
return updateDict;
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMAuthenticateOperation.h
// MBIMAuthenticateOperation
//
// Created by James Magahern on 7/6/21.
// Copyright © 2021 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMAuthenticateOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,77 @@
//
// MBIMAuthenticateOperation.m
// MBIMAuthenticateOperation
//
// Created by James Magahern on 7/6/21.
// Copyright © 2021 James Magahern. All rights reserved.
//
#import "MBIMAuthenticateOperation.h"
#import "MBIMBridge.h"
#import "MBIMAuthToken.h"
@implementation MBIMAuthenticateOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"authenticate";
}
+ (BOOL)requiresAuthentication
{
return NO;
}
- (void)main
{
NSObject<HTTPResponse> *response = nil;
if (self.requestBodyData.length == 0) {
self.serverCompletionBlock([[HTTPErrorResponse alloc] initWithErrorCode:400]);
return;
}
NSError *error = nil;
NSDictionary *args = [NSJSONSerialization JSONObjectWithData:self.requestBodyData options:0 error:&error];
if (error || args.count == 0) {
response = [[HTTPErrorResponse alloc] initWithErrorCode:400];
} else {
do {
NSString *username = [args objectForKey:@"username"];
NSString *password = [args objectForKey:@"password"];
if (!username || !password) {
response = [[HTTPErrorResponse alloc] initWithErrorCode:400];
break;
}
if (![MBIMBridge.sharedInstance.authUsername isEqualToString:username]) {
response = [[HTTPErrorResponse alloc] initWithErrorCode:401];
break;
}
if (![MBIMBridge.sharedInstance.authPassword isEqualToString:password]) {
response = [[HTTPErrorResponse alloc] initWithErrorCode:401];
break;
}
MBIMAuthToken *token = [[MBIMAuthToken alloc] initWithUsername:username];
// All systems go
MBIMJSONDataResponse *dataResponse = [MBIMJSONDataResponse responseWithJSONObject:@{
@"jwt" : token.jwtToken
}];
// Send a cookie down so we can use httpOnly cookies
dataResponse.httpHeaders[@"Set-Cookie"] = [NSString stringWithFormat:@"auth_token=%@", token.jwtToken];
response = dataResponse;
} while (NO);
}
self.serverCompletionBlock(response);
}
@end

View File

@@ -0,0 +1,41 @@
//
// MBIMBridgeOperation.h
// kordophoned
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "HTTPMessage.h"
#import "HTTPResponse.h"
#import "HTTPErrorResponse.h"
#import "MBIMJSONDataResponse.h"
NS_ASSUME_NONNULL_BEGIN
typedef void (^MBIMBridgeOperationCompletionBlock)(NSObject<HTTPResponse> * _Nullable response);
@interface MBIMBridgeOperation : NSOperation
@property (class, nonatomic, readonly) NSString *endpointName;
@property (class, nonatomic, readonly) BOOL requiresAuthentication; // default YES
@property (nonatomic, strong) HTTPMessage *request;
@property (nonatomic, strong) NSData *requestBodyData;
@property (nonatomic, readonly) NSURL *requestURL;
@property (nonatomic, readonly) MBIMBridgeOperationCompletionBlock serverCompletionBlock;
+ (dispatch_queue_t)sharedIMAccessQueue;
+ (nullable Class)operationClassForEndpointName:(NSString *)endpointName;
- (instancetype)initWithRequestURL:(NSURL *)requestURL completion:(MBIMBridgeOperationCompletionBlock)completionBlock;
- (NSObject<HTTPResponse> *)cancelAndReturnTimeoutResponse;
// convenience
- (nullable NSString *)valueForQueryItemWithName:(NSString *)queryItemName;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,86 @@
//
// MBIMBridgeOperation.m
// kordophoned
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
#import "MBIMURLUtilities.h"
@interface MBIMBridgeOperation (/*INTERNAL*/)
@property (nonatomic, strong) NSURL *requestURL;
@property (nonatomic, copy) MBIMBridgeOperationCompletionBlock serverCompletionBlock;
@end
@implementation MBIMBridgeOperation
+ (NSString *)endpointName
{
// To be inplemented by subclasses
return @"__unimplemented__";
}
+ (NSMutableDictionary *)_operationClassMapping
{
static dispatch_once_t onceToken;
static NSMutableDictionary *operationClassMapping = nil;
dispatch_once(&onceToken, ^{
operationClassMapping = [[NSMutableDictionary alloc] init];
});
return operationClassMapping;
}
+ (dispatch_queue_t)sharedIMAccessQueue
{
static dispatch_once_t onceToken;
static dispatch_queue_t accessQueue = nil;
dispatch_once(&onceToken, ^{
accessQueue = dispatch_queue_create("IMAccessQueue", DISPATCH_QUEUE_SERIAL);
});
return accessQueue;
}
+ (void)load
{
if ([self class] != [MBIMBridgeOperation class]) {
[[self _operationClassMapping] setObject:[self class] forKey:[self endpointName]];
}
}
+ (nullable Class)operationClassForEndpointName:(NSString *)endpointName
{
return [[self _operationClassMapping] objectForKey:endpointName];
}
+ (BOOL)requiresAuthentication
{
return YES;
}
- (instancetype)initWithRequestURL:(NSURL *)requestURL completion:(MBIMBridgeOperationCompletionBlock)completionBlock
{
self = [super init];
if (self) {
self.requestURL = requestURL;
self.serverCompletionBlock = completionBlock;
}
return self;
}
- (NSObject<HTTPResponse> *)cancelAndReturnTimeoutResponse
{
[self cancel];
return [[HTTPErrorResponse alloc] initWithErrorCode:500];
}
- (NSString *)valueForQueryItemWithName:(NSString *)queryItemName
{
return [[self requestURL] valueForQueryItemWithName:queryItemName];
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMConversationListOperation.h
// kordophoned
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMConversationListOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,40 @@
//
// MBIMConversationListOperation.m
// kordophoned
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMConversationListOperation.h"
#import "MBIMHTTPUtilities.h"
#import "IMChat+Encoded.h"
#import "IMCore_ClassDump.h"
@implementation MBIMConversationListOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"conversations";
}
- (void)main
{
__block NSMutableArray *conversations = [NSMutableArray array];
dispatch_sync([[self class] sharedIMAccessQueue], ^{
NSArray<IMChat *> *chats = [[IMChatRegistry sharedInstance] allExistingChats];
for (IMChat *chat in chats) {
NSDictionary *chatDict = [chat mbim_dictionaryRepresentation];
[conversations addObject:chatDict];
}
});
MBIMJSONDataResponse *response = [MBIMJSONDataResponse responseWithJSONObject:conversations];
self.serverCompletionBlock(response);
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMDeleteConversationOperation.h
// kordophoned
//
// Created by James Magahern on 5/25/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMDeleteConversationOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,49 @@
//
// MBIMDeleteConversationOperation.m
// kordophoned
//
// Created by James Magahern on 5/25/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import "MBIMDeleteConversationOperation.h"
#import "IMChat+Encoded.h"
#import "IMCore_ClassDump.h"
@implementation MBIMDeleteConversationOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"delete";
}
- (void)main
{
__block NSObject<HTTPResponse> *response = nil;
do {
NSString *guid = [self valueForQueryItemWithName:@"guid"];
if (!guid) {
MBIMLogInfo(@"No conversation GUID provided.");
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
dispatch_sync([[self class] sharedIMAccessQueue], ^{
IMChat *chat = [[IMChatRegistry sharedInstance] existingChatWithGUID:guid];
if (!chat) {
MBIMLogInfo(@"Chat with guid: %@ not found", guid);
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
} else {
[chat remove];
}
});
response = [[HTTPErrorResponse alloc] initWithErrorCode:200];
} while (0);
self.serverCompletionBlock(response);
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMFetchAttachmentOperation.h
// kordophoned
//
// Created by James Magahern on 11/20/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMFetchAttachmentOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,135 @@
//
// MBIMFetchAttachmentOperation.m
// kordophoned
//
// Created by James Magahern on 11/20/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMFetchAttachmentOperation.h"
#import "MBIMDataResponse.h"
#import "MBIMImageUtils.h"
#import "IMCore_ClassDump.h"
#import "IMSharedUtilities_ClassDump.h"
#import <CoreGraphics/CoreGraphics.h>
@implementation MBIMFetchAttachmentOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"attachment";
}
- (void)main
{
NSObject<HTTPResponse> *response = nil;
do {
BOOL preview = [[self valueForQueryItemWithName:@"preview"] boolValue];
NSString *guid = [self valueForQueryItemWithName:@"guid"];
if (!guid) {
MBIMLogInfo(@"No query item provided");
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
IMFileTransfer *transfer = [[IMFileTransferCenter sharedInstance] transferForGUID:guid];
if (!transfer) {
MBIMLogInfo(@"No transfer found for guid: %@", guid);
response = [[HTTPErrorResponse alloc] initWithErrorCode:404];
break;
}
if (![transfer existsAtLocalPath]) {
MBIMLogInfo(@"We don't have the file for this yet (still downloading to server?)");
response = [[HTTPErrorResponse alloc] initWithErrorCode:404];
break;
}
NSData *responseData = nil;
NSURL *localURL = [transfer localURL];
NSString *extension = [[localURL pathExtension] lowercaseString];
if (preview) {
IMPreviewConstraints constraints = IMPreviewConstraintsZero();
// Fetch preview constraints from transfer
NSDictionary *previewConstraintsDict = [[transfer attributionInfo] objectForKey:@"pgenszc"];
if (previewConstraintsDict) {
constraints = IMPreviewConstraintsFromDictionary(previewConstraintsDict);
} else {
// Or, make a guess.
constraints.maxPxWidth = 500.0;
constraints.scale = 1.0;
}
NSURL *previewURL = IMAttachmentPreviewFileURL(localURL, extension, YES);
if (!previewURL) {
// I'm not sure why this sometimes returns nil...
MBIMLogInfo(@"Unable to generate attachment preview cache URL for %@, making one up.", localURL);
NSURL *temporaryAttachmentCache = [[[NSFileManager defaultManager] temporaryDirectory] URLByAppendingPathComponent:@"kordophone_attachment_cache"];
temporaryAttachmentCache = [temporaryAttachmentCache URLByAppendingPathComponent:guid];
[[NSFileManager defaultManager] createDirectoryAtURL:temporaryAttachmentCache withIntermediateDirectories:YES attributes:nil error:nil];
previewURL = [temporaryAttachmentCache URLByAppendingPathComponent:[localURL lastPathComponent]];
}
if (![[NSFileManager defaultManager] fileExistsAtPath:[previewURL path]]) {
MBIMLogInfo(@"Generating preview image for guid: %@ at %@", guid, [previewURL path]);
// Generate preview using preview generator manager
NSError *error = nil;
IMPreviewGeneratorManager *generator = [IMPreviewGeneratorManager sharedInstance];
CGImageRef previewImage = [generator newPreviewFromSourceURL:localURL withPreviewConstraints:constraints error:&error];
if (error) {
MBIMLogInfo(@"Unable to generate preview for attachment guid: %@", guid);
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
// Convert to JPEG.
responseData = MBIMCGImageJPEGRepresentation(previewImage);
// Persist JPEG preview to disk
if (previewURL) {
[responseData writeToURL:previewURL atomically:YES];
}
} else {
// File exists
MBIMLogInfo(@"Using cached preview image for guid: %@ at %@", guid, [previewURL path]);
responseData = [NSData dataWithContentsOfURL:previewURL];
}
} else {
responseData = [NSData dataWithContentsOfURL:localURL];
}
if (!responseData) {
MBIMLogInfo(@"Wasn't able to load data for guid: %@", guid);
response = [[HTTPErrorResponse alloc] initWithErrorCode:404];
break;
}
NSString *mimeType = [transfer mimeType];
if ([mimeType isEqualToString:@"image/heic"]) {
// TODO: We should convert this to JPEG here. I don't want clients to have to deal with HEIC.
MBIMLogInfo(@"WARNING: Returning HEIC data for attachment %@", guid);
}
// It's unusual, but if this is nil, try to guess the MIME type based on the filename
if (!mimeType) {
// XXX: REALLY hacky
mimeType = [NSString stringWithFormat:@"image/%@", extension];
}
MBIMDataResponse *dataResponse = [[MBIMDataResponse alloc] initWithData:responseData contentType:mimeType];
dataResponse.httpHeaders[@"Cache-Control"] = @"public, immutable, max-age=31536000";
response = dataResponse;
} while (0);
self.serverCompletionBlock(response);
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMMarkOperation.h
// kordophoned
//
// Created by James Magahern on 11/19/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMMarkOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,53 @@
//
// MBIMMarkOperation.m
// kordophoned
//
// Created by James Magahern on 11/19/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMMarkOperation.h"
#import "IMCore_ClassDump.h"
@implementation MBIMMarkOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"markConversation";
}
- (void)main
{
__block NSObject<HTTPResponse> *response = nil;
do {
NSString *guid = [self valueForQueryItemWithName:@"guid"];
if (!guid) {
MBIMLogInfo(@"No query item provided");
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
dispatch_sync([[self class] sharedIMAccessQueue], ^{
IMChat *chat = [[IMChatRegistry sharedInstance] existingChatWithGUID:guid];
if (!chat) {
MBIMLogInfo(@"Chat with guid: %@ not found", guid);
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
} else {
// TODO: be smarter about this and mark individual messages as read? Could lead
// to a race condition
if ([chat unreadMessageCount] > 0) {
[chat markAllMessagesAsRead];
}
}
});
response = [[HTTPErrorResponse alloc] initWithErrorCode:200];
} while (0);
self.serverCompletionBlock(response);
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMMessagesListOperation.h
// kordophoned
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMMessagesListOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,149 @@
//
// MBIMMessagesListOperation.m
// kordophoned
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMMessagesListOperation.h"
#import "MBIMHTTPUtilities.h"
#import "IMMessageItem+Encoded.h"
#import "MBIMErrorResponse.h"
#import "IMCore_ClassDump.h"
#define kDefaultMessagesLimit 75
@interface IMChat (MBIMSafeMessagesLoading)
- (void)load:(NSUInteger)number messagesBeforeGUID:(NSString *)beforeMessageGUID;
- (void)load:(NSUInteger)number messagesAfterGUID:(NSString *)afterMessageGUID;
@end
@implementation MBIMMessagesListOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"messages";
}
- (void)main
{
__block NSObject<HTTPResponse> *response = nil;
do {
// Required parameters
NSString *guid = [self valueForQueryItemWithName:@"guid"];
// Optional
NSString *limitValue = [self valueForQueryItemWithName:@"limit"];
NSDate *beforeDate = nil;
NSString *beforeDateValue = [self valueForQueryItemWithName:@"beforeDate"];
if (beforeDateValue) {
beforeDate = [beforeDateValue ISO8601Date];
if (!beforeDate) {
response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"Unable to decode ISO8601 beforeDate value"];
break;
}
}
NSString *beforeMessageGUID = [self valueForQueryItemWithName:@"beforeMessageGUID"];
NSString *afterMessageGUID = [self valueForQueryItemWithName:@"afterMessageGUID"];
if (beforeMessageGUID && afterMessageGUID) {
response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"Cannot provide both beforeMessageGUID and afterMessageGUID params."];
break;
}
if (!guid) {
response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"No GUID provided."];
break;
}
__block NSMutableArray *messages = [NSMutableArray array];
dispatch_sync([[self class] sharedIMAccessQueue], ^{
IMChat *chat = [[IMChatRegistry sharedInstance] existingChatWithGUID:guid];
if (!chat) {
MBIMLogInfo(@"Chat with guid: %@ not found", guid);
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
} else {
// Load messages
// (Must be done on main queue for some reason)
dispatch_sync(dispatch_get_main_queue(), ^{
NSUInteger limit = kDefaultMessagesLimit;
if (limitValue) {
limit = [limitValue integerValue];
}
if (beforeMessageGUID) {
[chat load:limit messagesBeforeGUID:beforeMessageGUID];
} else if (afterMessageGUID) {
[chat load:limit messagesAfterGUID:afterMessageGUID];
} else {
[chat loadMessagesBeforeDate:beforeDate limit:limit loadImmediately:YES];
}
IMMessage *beforeMessage = beforeMessageGUID ? [chat messageForGUID:beforeMessageGUID] : nil;
IMMessage *afterMessage = afterMessageGUID ? [chat messageForGUID:afterMessageGUID] : nil;
[[chat chatItems] enumerateMessagesWithOptions:0 usingBlock:^(IMMessage *message, BOOL *stop) {
BOOL includeMessage = YES;
NSDate *messageDate = [message time];
if (beforeMessage && [[beforeMessage time] compare:messageDate] != NSOrderedDescending) {
includeMessage = NO;
}
if (afterMessage && [[afterMessage time] compare:messageDate] != NSOrderedAscending) {
includeMessage = NO;
}
if (includeMessage) {
NSDictionary *messageDict = [message mbim_dictionaryRepresentation];
[messages addObject:messageDict];
}
}];
});
}
});
response = [MBIMJSONDataResponse responseWithJSONObject:messages];
} while (0);
self.serverCompletionBlock(response);
}
@end
@implementation IMChat (MBIMSafeMessagesLoading)
- (id)_safe_loadMessagesBeforeAndAfterGUID:(NSString *)messagesGUID numberOfMessagesToLoadBeforeGUID:(NSUInteger)limitBefore numberOfMessagesToLoadAfterGUID:(NSUInteger)limitAfter loadImmediately:(BOOL)loadImmediately
{
if ([self respondsToSelector:@selector(loadMessagesBeforeAndAfterGUID:
numberOfMessagesToLoadBeforeGUID:
numberOfMessagesToLoadAfterGUID:
loadImmediately:threadIdentifier:)]) {
return [self loadMessagesBeforeAndAfterGUID:messagesGUID
numberOfMessagesToLoadBeforeGUID:limitBefore
numberOfMessagesToLoadAfterGUID:limitAfter
loadImmediately:YES threadIdentifier:nil];
} else {
return [self loadMessagesBeforeAndAfterGUID:messagesGUID
numberOfMessagesToLoadBeforeGUID:limitBefore
numberOfMessagesToLoadAfterGUID:limitAfter
loadImmediately:YES];
}
}
- (void)load:(NSUInteger)number messagesBeforeGUID:(NSString *)beforeMessageGUID
{
[self _safe_loadMessagesBeforeAndAfterGUID:beforeMessageGUID numberOfMessagesToLoadBeforeGUID:number numberOfMessagesToLoadAfterGUID:0 loadImmediately:YES];
}
- (void)load:(NSUInteger)number messagesAfterGUID:(NSString *)afterMessageGUID
{
[self _safe_loadMessagesBeforeAndAfterGUID:afterMessageGUID numberOfMessagesToLoadBeforeGUID:0 numberOfMessagesToLoadAfterGUID:number loadImmediately:YES];
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMResolveHandleOperation.h
// kordophoned
//
// Created by James Magahern on 10/1/24.
// Copyright © 2024 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMResolveHandleOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,134 @@
//
// MBIMResolveHandleOperation.m
// kordophoned
//
// Created by James Magahern on 10/1/24.
// Copyright © 2024 James Magahern. All rights reserved.
//
#import "MBIMResolveHandleOperation.h"
#import "IMCore_ClassDump.h"
#import "IMMessageItem+Encoded.h"
/*
# Response:
Dictionary {
"resolvedHandle": <Encoded IMHandle>
"status": valid | invalid | unknown,
"existingChat": chatGUID | null
}
# IMHandle:
Dictionary {
"id" : resolvedID
"name" : fullName | null
}
*/
@interface NSNumber (IDSStatusUtility)
- (NSString *)_idsStatusDescription;
@end
@implementation NSNumber (IDSStatusUtility)
- (NSString *)_idsStatusDescription
{
switch ([self unsignedIntegerValue]) {
case 1: return @"valid";
case 2: return @"invalid";
default:
return @"unknown";
}
}
@end
@interface IMHandle (Encoded)
- (NSDictionary *)mbim_dictionaryRepresentation;
@end
@implementation IMHandle (Encoded)
- (NSDictionary *)mbim_dictionaryRepresentation
{
return @{
@"id" : [self ID],
@"name" : [self name] ?: [NSNull null],
};
}
@end
@implementation MBIMResolveHandleOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"resolveHandle";
}
- (void)main
{
NSString *specifiedIdentifier = [[self valueForQueryItemWithName:@"id"] _stripFZIDPrefix];
if (!specifiedIdentifier) {
MBIMLogError(@"No handle ID provided.");
HTTPErrorResponse *response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
self.serverCompletionBlock(response);
return;
}
if (IMStringIsPhoneNumber(specifiedIdentifier)) {
// Phone numbers will require a country code guess here.
// Passing nil, I presume, will make some kind of guess for the country code (useNetworkCountryCode).
specifiedIdentifier = IMCopyIDForPhoneNumber(specifiedIdentifier, nil, YES);
}
NSString *canonicalAddress = [specifiedIdentifier _bestGuessURI];
IMAccount *iMessageAccount = [[IMAccountController sharedInstance] bestAccountForService:[IMServiceImpl iMessageService]];
IMHandle *loginHandle = [iMessageAccount loginIMHandle];
NSString *lastAddressedHandle = [[[loginHandle ID] _stripFZIDPrefix] _bestGuessURI];
IMChatCalculateServiceForSendingNewComposeMaybeForce(@[ canonicalAddress ], lastAddressedHandle, nil, NO, IMStringIsEmail(canonicalAddress), NO, NO, NO, [iMessageAccount service], ^(BOOL allAddressesiMessageCapable, NSDictionary * _Nullable perRecipientAvailability, BOOL checkedServer, IMChatServiceForSendingAvailabilityError error)
{
NSError *encodingError = nil;
NSObject<HTTPResponse> *response = nil;
do {
// Assume we have returned just one key here.
NSString *resolvedHandleID = [[perRecipientAvailability allKeys] firstObject];
if (!resolvedHandleID) {
MBIMLogError(@"Unexpected missing handle id");
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
IMHandle *resolvedHandle = [iMessageAccount imHandleWithID:resolvedHandleID];
if (!resolvedHandle) {
MBIMLogError(@"Couldn't resolve handle");
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
NSString *handleStatus = [[perRecipientAvailability objectForKey:resolvedHandleID] _idsStatusDescription];
IMChat *existingChat = [[IMChatRegistry sharedInstance] existingChatForIMHandle:resolvedHandle];
NSDictionary *responseDict = @{
@"resolvedHandle" : [resolvedHandle mbim_dictionaryRepresentation],
@"status" : handleStatus,
@"existingChat" : [existingChat guid] ?: [NSNull null],
};
NSData *data = [NSJSONSerialization dataWithJSONObject:responseDict options:0 error:&encodingError];
if (encodingError) {
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
} else {
response = [[MBIMDataResponse alloc] initWithData:data contentType:@"application/json"];
}
} while (0);
self.serverCompletionBlock(response);
});
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMSendMessageOperation.h
// kordophoned
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMSendMessageOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,122 @@
//
// MBIMSendMessageOperation.m
// kordophoned
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMSendMessageOperation.h"
#import "IMCore_ClassDump.h"
#import "IMMessageItem+Encoded.h"
@implementation MBIMSendMessageOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"sendMessage";
}
- (IMMessage *)_sendMessage:(NSString *)messageBody toChatWithGUID:(NSString *)chatGUID attachmentGUIDs:(NSArray<NSString *> *)guids
{
__block IMMessage *result = nil;
dispatch_sync([[self class] sharedIMAccessQueue], ^{
IMChat *chat = [[IMChatRegistry sharedInstance] existingChatWithGUID:chatGUID];
// TODO: chat might not be an iMessage chat!
IMAccount *iMessageAccount = [[IMAccountController sharedInstance] bestAccountForService:[IMServiceImpl iMessageService]];
IMHandle *senderHandle = [iMessageAccount loginIMHandle];
NSAttributedString *replyAttrString = [[NSAttributedString alloc] initWithString:messageBody];
NSAttributedString *attrStringWithFileTransfers = IMCreateSuperFormatStringWithAppendedFileTransfers(replyAttrString, guids);
IMMessage *reply = [IMMessage fromMeIMHandle:senderHandle
withText:attrStringWithFileTransfers
fileTransferGUIDs:guids
flags:(kIMMessageFinished | kIMMessageIsFromMe)];
for (NSString *guid in [reply fileTransferGUIDs]) {
[[IMFileTransferCenter sharedInstance] assignTransfer:guid toHandle:chat.recipient];
}
if (!chat) {
MBIMLogInfo(@"Chat does not exist: %@", chatGUID);
} else {
result = reply;
dispatch_async(dispatch_get_main_queue(), ^{
[chat sendMessage:reply];
});
}
});
return result;
}
#if 0
- (NSDictionary *)adjustMessageSummaryInfoForSending:(NSDictionary *)messageSummaryInfo
{
NSMutableDictionary *adjustedInfo = [messageSummaryInfo mutableCopy];
if (!adjustedInfo) {
adjustedInfo = [NSMutableDictionary dictionary];
}
if ([fullText length] > 50) {
summary = [[summary substringToIndex:[summary rangeOfComposedCharacterSequenceAtIndex:kMaxSummaryLength].location] stringByAppendingString:@"…"];
adjustedInfo[IMMessageSummaryInfoSummary] = summary;
}
adjustedInfo[IMMessageSummaryInfoTapbackRepresentationKey] = @"Loved";
return adjustedInfo;
}
#endif
- (void)main
{
NSObject<HTTPResponse> *response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
NSError *error = nil;
NSDictionary *args = [NSJSONSerialization JSONObjectWithData:self.requestBodyData options:0 error:&error];
if (error || args.count == 0) {
self.serverCompletionBlock(response);
return;
}
NSString *guid = [args objectForKey:@"guid"];
NSString *messageBody = [args objectForKey:@"body"];
if (!guid || !messageBody) {
self.serverCompletionBlock(response);
return;
}
// tapbacks
#if 0
IMMessage *acknowledgment = [IMMessage instantMessageWithAssociatedMessageContent: /* [NSString stringWithFormat:@"%@ \"%%@\"", tapbackAction] */
flags:0
associatedMessageGUID:guid
associatedMessageType:IMAssociatedMessageTypeAcknowledgmentHeart
associatedMessageRange:[imMessage messagePartRange]
messageSummaryInfo:[self adjustMessageSummaryInfoForSending:message]
threadIdentifier:[imMessage threadIdentifier]];
#endif
NSArray *transferGUIDs = [args objectForKey:@"fileTransferGUIDs"];
if (!transferGUIDs) {
transferGUIDs = @[];
}
IMMessage *result = [self _sendMessage:messageBody toChatWithGUID:guid attachmentGUIDs:transferGUIDs];
if (result) {
response = [MBIMJSONDataResponse responseWithJSONObject:[result mbim_dictionaryRepresentation]];
}
self.serverCompletionBlock(response);
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMStatusOperation.h
// kordophoned
//
// Created by James Magahern on 8/3/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMStatusOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,25 @@
//
// MBIMStatusOperation.m
// kordophoned
//
// Created by James Magahern on 8/3/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import "MBIMStatusOperation.h"
@implementation MBIMStatusOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"status";
}
- (void)main
{
self.serverCompletionBlock([[MBIMDataResponse alloc] initWithData:[@"OK" dataUsingEncoding:NSUTF8StringEncoding]]);
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMUpdatePollOperation.h
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMUpdatePollOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,58 @@
//
// MBIMUpdatePollOperation.m
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMUpdatePollOperation.h"
#import "MBIMUpdateQueue.h"
@implementation MBIMUpdatePollOperation {
__strong MBIMUpdateConsumer _updateConsumer;
}
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"pollUpdates";
}
- (void)main
{
NSInteger messageSeq = -1;
NSString *messageSeqString = [self valueForQueryItemWithName:@"seq"];
if (messageSeqString) {
messageSeq = [messageSeqString integerValue];
}
__weak __auto_type weakSelf = self;
_updateConsumer = ^(NSArray<MBIMUpdateItem *> *updates) {
NSMutableArray *encodedUpdates = [NSMutableArray array];
for (MBIMUpdateItem *item in updates) {
NSDictionary *updateDict = [item dictionaryRepresentation];
[encodedUpdates addObject:updateDict];
}
MBIMJSONDataResponse *response = [MBIMJSONDataResponse responseWithJSONObject:encodedUpdates];
weakSelf.serverCompletionBlock(response);
};
[[MBIMUpdateQueue sharedInstance] addPollingConsumer:_updateConsumer withLastSyncedMessageSeq:messageSeq];
}
- (void)cancel
{
[super cancel];
[[MBIMUpdateQueue sharedInstance] removePollingConsumer:_updateConsumer];
}
- (NSObject<HTTPResponse> *)cancelAndReturnTimeoutResponse
{
[self cancel];
return [[HTTPErrorResponse alloc] initWithErrorCode:205]; // 205 = nothing to report
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMUploadAttachmentOperation.h
// kordophoned
//
// Created by James Magahern on 1/16/19.
// Copyright © 2019 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMUploadAttachmentOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,73 @@
//
// MBIMUploadAttachmentOperation.m
// kordophoned
//
// Created by James Magahern on 1/16/19.
// Copyright © 2019 James Magahern. All rights reserved.
//
#import "MBIMUploadAttachmentOperation.h"
#import "MBIMDataResponse.h"
#import "IMCore_ClassDump.h"
@implementation MBIMUploadAttachmentOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"uploadAttachment";
}
- (void)main
{
NSObject<HTTPResponse> *response = nil;
do {
NSString *filename = [self valueForQueryItemWithName:@"filename"];
if ([filename length] == 0) {
MBIMLogInfo(@"No filename provided");
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
NSData *attachmentData = self.requestBodyData;
if ([attachmentData length] == 0) {
MBIMLogInfo(@"No attachment data in request");
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
// Sanitize filename
NSCharacterSet *dotCharacter = [NSCharacterSet characterSetWithCharactersInString:@"."];
NSCharacterSet *illegalFileNameCharacters = [NSCharacterSet characterSetWithCharactersInString:@"/\\?%*|\"<>"];
NSString *sanitizedFilename = [[[filename componentsSeparatedByCharactersInSet:illegalFileNameCharacters]
componentsJoinedByString:@"-"]
stringByTrimmingCharactersInSet:dotCharacter];
NSString *localPath = [NSTemporaryDirectory() stringByAppendingPathComponent:sanitizedFilename];
NSURL *localURL = [NSURL fileURLWithPath:localPath];
BOOL success = [attachmentData writeToURL:localURL atomically:NO];
if (!success) {
MBIMLogInfo(@"Error writing attachment to temporary directory");
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
NSString *guid = [[IMFileTransferCenter sharedInstance] guidForNewOutgoingTransferWithLocalURL:localURL];
if (!guid) {
MBIMLogInfo(@"There was some problem shuttling the file to IMCore");
response = [[HTTPErrorResponse alloc] initWithErrorCode:500];
break;
}
NSDictionary *responseDict = @{
@"fileTransferGUID" : guid
};
response = [MBIMJSONDataResponse responseWithJSONObject:responseDict];
} while (0);
self.serverCompletionBlock(response);
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMVersionOperation.h
// kordophoned
//
// Created by James Magahern on 8/3/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMVersionOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,40 @@
//
// MBIMVersionOperation.m
// kordophoned
//
// Created by James Magahern on 8/3/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import "MBIMVersionOperation.h"
#import "MBIMErrorResponse.h"
#ifdef __clang_analyzer__
const char* MBIMVersion() {
return "UNKNOWN";
}
#else
#import "MBIMVersion.c"
#endif
@implementation MBIMVersionOperation
+ (void)load { [super load]; }
+ (BOOL)requiresAuthentication
{
return NO;
}
+ (NSString *)endpointName
{
return @"version";
}
- (void)main
{
NSString *versionString = [NSString stringWithUTF8String:MBIMVersion()];
self.serverCompletionBlock([[MBIMDataResponse alloc] initWithData:[versionString dataUsingEncoding:NSUTF8StringEncoding]]);
}
@end

View File

@@ -0,0 +1,19 @@
//
// MBIMDataResponse.h
// kordophoned
//
// Created by James Magahern on 11/20/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "HTTPDataResponse.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMDataResponse : HTTPDataResponse
@property (nonatomic, readonly) NSMutableDictionary *httpHeaders;
- (instancetype)initWithData:(NSData *)data contentType:(NSString *)contentType;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,36 @@
//
// MBIMDataResponse.m
// kordophoned
//
// Created by James Magahern on 11/20/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMDataResponse.h"
@implementation MBIMDataResponse {
NSString *_contentType;
NSMutableDictionary *_httpHeaders;
}
- (instancetype)initWithData:(NSData *)data contentType:(NSString *)contentType
{
self = [super initWithData:data];
if (self) {
_contentType = contentType;
_httpHeaders = [@{
@"Content-Type" : _contentType ?: @"application/octet-stream",
@"Access-Control-Allow-Origin" : @"*", // CORS
@"Access-Control-Allow-Credentials" : @"true"
} mutableCopy];
}
return self;
}
- (NSDictionary *)httpHeaders
{
return _httpHeaders;
}
@end

View File

@@ -0,0 +1,18 @@
//
// MBIMErrorResponse.h
// kordophoned
//
// Created by James Magahern on 8/3/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import "HTTPDataResponse.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMErrorResponse : HTTPDataResponse
- (instancetype)initWithErrorCode:(int)httpErrorCode;
- (instancetype)initWithErrorCode:(int)httpErrorCode message:(NSString *)message;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,38 @@
//
// MBIMErrorResponse.m
// kordophoned
//
// Created by James Magahern on 8/3/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import "MBIMErrorResponse.h"
@implementation MBIMErrorResponse {
int _status;
}
- (instancetype)initWithErrorCode:(int)httpErrorCode
{
if (self = [super initWithData:nil]) {
_status = httpErrorCode;
}
return self;
}
- (instancetype)initWithErrorCode:(int)httpErrorCode message:(NSString *)message
{
if (self = [super initWithData:[message dataUsingEncoding:NSUTF8StringEncoding]]) {
_status = httpErrorCode;
}
return self;
}
- (NSInteger)status
{
return _status;
}
@end

View File

@@ -0,0 +1,22 @@
//
// MBIMHTTPUtilities.h
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
NSString* MBIMWebServerFormatRFC822(NSDate *date);
NSString* MBIMWebServerFormatISO8601(NSDate *date);
@interface NSDate (MBIMWebServerFormat)
- (NSString *)RFC822StringValue;
- (NSString *)ISO8601StringValue;
@end
@interface NSString (MBIMWebServerFormat)
- (NSDate *)RFC822Date;
- (NSDate *)ISO8601Date;
@end

View File

@@ -0,0 +1,77 @@
//
// MBIMHTTPUtilities.c
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#include "MBIMHTTPUtilities.h"
static NSDateFormatter* _dateFormatterRFC822 = nil;
static NSDateFormatter* _dateFormatterISO8601 = nil;
static dispatch_queue_t _dateFormatterQueue = NULL;
__attribute__((constructor))
static void __InitializeDateFormatter()
{
_dateFormatterQueue = dispatch_queue_create("dateFormatter", DISPATCH_QUEUE_SERIAL);
_dateFormatterRFC822 = [[NSDateFormatter alloc] init];
_dateFormatterRFC822.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
_dateFormatterRFC822.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
_dateFormatterRFC822.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
_dateFormatterISO8601 = [[NSDateFormatter alloc] init];
_dateFormatterISO8601.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
_dateFormatterISO8601.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'";
_dateFormatterISO8601.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
}
NSString* MBIMWebServerFormatRFC822(NSDate *date)
{
__block NSString *string = nil;
dispatch_sync(_dateFormatterQueue, ^{
string = [_dateFormatterRFC822 stringFromDate:date];
});
return string;
}
NSString* MBIMWebServerFormatISO8601(NSDate *date)
{
__block NSString *string = nil;
dispatch_sync(_dateFormatterQueue, ^{
string = [_dateFormatterISO8601 stringFromDate:date];
});
return string;
}
@implementation NSDate (MBIMWebServerFormat)
- (NSString *)RFC822StringValue
{
return MBIMWebServerFormatRFC822(self);
}
- (NSString *)ISO8601StringValue
{
return MBIMWebServerFormatISO8601(self);
}
@end
@implementation NSString (MBIMWebServerFormat)
- (NSDate *)RFC822Date
{
return [_dateFormatterRFC822 dateFromString:self];
}
- (NSDate *)ISO8601Date
{
return [_dateFormatterISO8601 dateFromString:self];
}
@end

View File

@@ -0,0 +1,12 @@
//
// MBIMImageUtils.h
// kordophoned
//
// Created by James Magahern on 12/20/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
extern NSData* MBIMCGImageJPEGRepresentation(CGImageRef imageRef);

View File

@@ -0,0 +1,37 @@
//
// MBIMImageUtils.m
// kordophoned
//
// Created by James Magahern on 12/20/22.
// Copyright © 2022 James Magahern. All rights reserved.
//
#import "MBIMImageUtils.h"
#import <ImageIO/ImageIO.h>
NSData* MBIMCGImageJPEGRepresentation(CGImageRef imageRef)
{
if (imageRef == NULL) return nil;
NSNumber *const DPI = @72.0;
NSNumber *const compressionQuality = @0.9;
NSDictionary *properties = @{
(__bridge NSString *)kCGImagePropertyDPIWidth : DPI,
(__bridge NSString *)kCGImagePropertyDPIHeight : DPI,
(__bridge NSString *)kCGImageDestinationLossyCompressionQuality : compressionQuality,
};
bool success = false;
NSMutableData *data = [NSMutableData data];
if (data) {
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((CFMutableDataRef)data, CFSTR("public.jpeg"), 1/*count*/, NULL/*options*/);
if (imageDestination != NULL) {
CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
success = CGImageDestinationFinalize(imageDestination);
CFRelease(imageDestination);
}
}
return success ? data : nil;
}

View File

@@ -0,0 +1,16 @@
//
// MBIMJSONDataResponse.h
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MBIMDataResponse.h"
@interface MBIMJSONDataResponse : MBIMDataResponse
+ (instancetype)responseWithJSONObject:(id)object;
@end

View File

@@ -0,0 +1,26 @@
//
// MBIMJSONDataResponse.m
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "MBIMJSONDataResponse.h"
@implementation MBIMJSONDataResponse
+ (instancetype)responseWithJSONObject:(id)object
{
NSError *error = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error];
if (data == nil) {
NSLog(@"JSON encoding error: %@", error);
return nil;
}
MBIMJSONDataResponse *response = [[self alloc] initWithData:data contentType:@"application/json; charset=utf-8"];
return response;
}
@end

View File

@@ -0,0 +1,17 @@
//
// MBIMURLUtilities.h
// kordophoned
//
// Created by James Magahern on 1/17/23.
// Copyright © 2023 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (MBIMURLUtilities)
- (nullable NSString *)valueForQueryItemWithName:(NSString *)queryItemName;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,28 @@
//
// MBIMURLUtilities.m
// kordophoned
//
// Created by James Magahern on 1/17/23.
// Copyright © 2023 James Magahern. All rights reserved.
//
#import "MBIMURLUtilities.h"
@implementation NSURL (MBIMURLUtilities)
- (nullable NSString *)valueForQueryItemWithName:(NSString *)queryItemName
{
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:self resolvingAgainstBaseURL:NO];
NSString *value = nil;
for (NSURLQueryItem *queryItem in [urlComponents queryItems]) {
if ([[queryItem name] isEqualToString:queryItemName]) {
value = [queryItem value];
break;
}
}
return value;
}
@end

View File

@@ -0,0 +1,17 @@
//
// IMChat+Encoded.h
// kordophoned
//
// Created by James Magahern on 11/19/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "IMCore_ClassDump.h"
NS_ASSUME_NONNULL_BEGIN
@interface IMChat (Encoded)
- (NSDictionary *)mbim_dictionaryRepresentation;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,50 @@
//
// IMChat+Encoded.m
// kordophoned
//
// Created by James Magahern on 11/19/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "IMChat+Encoded.h"
#import "IMMessageItem+Encoded.h"
#import "MBIMHTTPUtilities.h"
@implementation IMChat (Encoded)
- (NSDictionary *)mbim_dictionaryRepresentation
{
NSMutableDictionary *chatDict = [NSMutableDictionary dictionary];
chatDict[@"guid"] = [self guid];
chatDict[@"displayName"] = [self displayName];
chatDict[@"date"] = MBIMWebServerFormatISO8601([self lastFinishedMessageDate]);
chatDict[@"unreadCount"] = @([self unreadMessageCount]);
// NOTE: -[IMChat lastMessage] != -[[IMChat chatItems] lastMessage].
// This means the messages we get back from the message list operation might be different from this here, which means we
// can't reliably use `lastMessage.guid` as a sync anchor.
//
// Ideally, everything should move to use chat items instead of messages so we can handle associated message items (reactions).
// When that happens, we should use `lastFinishedMessageItem` here instead of `lastMessage`, and avoid the db hit.
//
IMMessage *lastMessage = [self lastMessage];
if (lastMessage) {
NSString *lastMessagePreview = [lastMessage descriptionForPurpose:IMMessageDescriptionConversationList];
chatDict[@"lastMessagePreview"] = [lastMessagePreview substringToIndex:MIN(128, [lastMessagePreview length])];
chatDict[@"lastMessage"] = [lastMessage mbim_dictionaryRepresentation];
}
NSMutableArray *participantStrings = [NSMutableArray array];
for (IMHandle *participantHandle in self.participants) {
NSString *participantString = [participantHandle displayNameForChat:self];
if (participantString) {
[participantStrings addObject:participantString];
}
}
chatDict[@"participantDisplayNames"] = participantStrings;
return chatDict;
}
@end

View File

@@ -0,0 +1,18 @@
//
// IMMessageItem+Encoded.h
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "IMCore_ClassDump.h"
NS_ASSUME_NONNULL_BEGIN
@interface IMMessage (Encoded)
- (NSDictionary *)mbim_dictionaryRepresentation;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,95 @@
//
// IMMessageItem+Encoded.m
// kordophoned
//
// Created by James Magahern on 11/16/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import "IMMessageItem+Encoded.h"
#import "MBIMHTTPUtilities.h"
static NSString* IMAssociatedMessageTypeValue(IMAssociatedMessageType type) {
switch (type) {
case IMAssociatedMessageTypeAcknowledgmentHeart:
return @"heart";
case IMAssociatedMessageTypeAcknowledgmentThumbsUp:
return @"thumbsUp";
case IMAssociatedMessageTypeAcknowledgmentThumbsDown:
return @"thumbsDown";
case IMAssociatedMessageTypeAcknowledgmentHa:
return @"ha";
case IMAssociatedMessageTypeAcknowledgmentExclamation:
return @"exclamation";
case IMAssociatedMessageTypeAcknowledgmentQuestionMark:
return @"questionMark";
case IMAssociatedMessageTypeAcknowledgmentHeartRemoved:
return @"removed heart";
case IMAssociatedMessageTypeAcknowledgmentThumbsUpRemoved:
return @"removed thumbsUp";
case IMAssociatedMessageTypeAcknowledgmentThumbsDownRemoved:
return @"removed thumbsDown";
case IMAssociatedMessageTypeAcknowledgmentHaRemoved:
return @"removed ha";
case IMAssociatedMessageTypeAcknowledgmentExclamationRemoved:
return @"removed exclamation";
case IMAssociatedMessageTypeAcknowledgmentQuestionMarkRemoved:
return @"removed questionMark";
case IMAssociatedMessageTypeUnspecified:
default:
return @"unknown";
}
}
@implementation IMMessage (Encoded)
- (NSDictionary *)mbim_dictionaryRepresentation
{
NSMutableDictionary *messageDict = [NSMutableDictionary dictionary];
messageDict[@"text"] = [[self text] string];
messageDict[@"date"] = MBIMWebServerFormatISO8601([self time]);
messageDict[@"sender"] = ([self isFromMe] ? nil : [[self sender] displayID]); // TODO: nil sender is still a weird way to represent this...
messageDict[@"guid"] = [self guid];
if ([self associatedMessageGUID]) {
NSString *encodedAssociatedMessageGUID = [self associatedMessageGUID];
messageDict[@"associatedMessageGUID"] = encodedAssociatedMessageGUID;
messageDict[@"associatedMessageType"] = IMAssociatedMessageTypeValue([self associatedMessageType]);
NSString *decodedAssociatedChatItemGUID = IMAssociatedMessageDecodeGUID(encodedAssociatedMessageGUID);
if (decodedAssociatedChatItemGUID) messageDict[@"associatedChatItemGUID"] = decodedAssociatedChatItemGUID;
}
if ([self fileTransferGUIDs]) {
// Support only images right now
NSMutableDictionary *attachmentMetadatas = [NSMutableDictionary dictionary];
NSMutableArray *filteredFileTransferGUIDs = [NSMutableArray array];
for (NSString *guid in self.fileTransferGUIDs) {
NSMutableDictionary *metadata = [NSMutableDictionary dictionary];
IMFileTransfer *transfer = [[IMFileTransferCenter sharedInstance] transferForGUID:guid];
if ([[transfer mimeType] containsString:@"image"]) {
[filteredFileTransferGUIDs addObject:guid];
if ([transfer attributionInfo] != nil) {
metadata[@"attributionInfo"] = [transfer attributionInfo];
}
}
if (metadata.count) {
attachmentMetadatas[guid] = metadata;
}
}
if ([filteredFileTransferGUIDs count]) {
messageDict[@"fileTransferGUIDs"] = filteredFileTransferGUIDs;
messageDict[@"attachmentMetadata"] = attachmentMetadatas;
}
}
return messageDict;
}
@end

View File

@@ -0,0 +1,12 @@
//
// hooking.h
// MessagesBridge
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
// Returns success and a populated errorString on error.
BOOL HookIMAgent(const char *hookDylibPath, char **errorString);

View File

@@ -0,0 +1,75 @@
//
// hooking.c
// kordophoned
//
// Created by James Magahern on 11/13/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#include "hooking.h"
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
BOOL HookIMAgent(const char *relativeDylibPath, char **errorString)
{
MBIMLogInfo(@"Hooking imagent");
const char *hookDylibPath = realpath(relativeDylibPath, NULL);
// See if file is there.
int succ = access(hookDylibPath, R_OK);
if (succ != 0) {
*errorString = "Unable to access hook dylib. Does file exist?";
return NO;
}
// Make sure we can load the dylib (filters out codesigning issues)
void *handle = dlopen(hookDylibPath, RTLD_NOW);
if (!handle) {
*errorString = dlerror();
return NO;
}
/*********
***********
PROBABLY DON'T DO THIS
If other processes start and load agentHook, then they will crash because dyld tries to
interpose a function that doesn't exist.
A better way (maybe put this in a script or something):
( But launchctl debug needs to run as root :( )
$ launchctl debug gui/501/com.apple.imagent --environment DYLD_INSERT_LIBRARIES=(path to libagentHook.dylib)
$ launchctl kill SIGKILL gui/501/com.apple.imagent
// then let it restart...
**/
// Set launchd DYLD_INSERT_LIBRARIES environment variable
const char *systemCommandFormatString = "/bin/launchctl setenv DYLD_INSERT_LIBRARIES %s";
size_t bufferSize = strlen(systemCommandFormatString) + strlen(hookDylibPath) + 2;
char *systemCommand = (char *)malloc(sizeof(char) * bufferSize);
sprintf(systemCommand, "/bin/launchctl setenv DYLD_INSERT_LIBRARIES %s", hookDylibPath);
int setEnvSucc = system(systemCommand);
if (setEnvSucc != 0) {
*errorString = "Unable to set launchd environment variable.";
return NO;
}
MBIMLogInfo(@"Successfully setup environment variables");
// Kill imagent so the new one has the loaded bundle
MBIMLogInfo(@"Killing imagent...");
int killAgentSuccess = system("killall imagent");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
system("/bin/launchctl unsetenv DYLD_INSERT_LIBRARIES");
});
return (killAgentSuccess == 0);
}

View File

@@ -0,0 +1,14 @@
//
// KPServer.pch
// MessagesBridge
//
// Created by James Magahern on 1/22/19.
// Copyright © 2019 James Magahern. All rights reserved.
//
#ifndef KPServer_h
#define KPServer_h
#include "MBIMLogging.h"
#endif /* KPServer_h */

View File

@@ -0,0 +1,34 @@
//
// MBIMLogging.h
// kordophoned
//
// Created by James Magahern on 1/22/19.
// Copyright © 2019 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef enum {
ML_INFO,
ML_NOTIFY,
ML_ERROR,
ML_FATAL
} MBIMLogLevel;
extern void __MBIMLogCommon(MBIMLogLevel level, NSString *format, ...);
#define MBIMLogInfo(format, ...) \
__MBIMLogCommon(ML_INFO, format, ##__VA_ARGS__)
#define MBIMLogNotify(format, ...) \
__MBIMLogCommon(ML_NOTIFY, format, ##__VA_ARGS__)
#define MBIMLogError(format, ...) \
__MBIMLogCommon(ML_ERROR, format, ##__VA_ARGS__)
#define MBIMLogFatal(format, ...) \
__MBIMLogCommon(ML_FATAL, format, ##__VA_ARGS__)
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,75 @@
//
// MBIMLogging.m
// kordophoned
//
// Created by James Magahern on 1/22/19.
// Copyright © 2019 James Magahern. All rights reserved.
//
#import "MBIMLogging.h"
#import <os/log.h>
#define ESC(inner) "\033[" inner "m"
#define CLR ESC("0")
#define BLD "1;"
#define RED "31;"
static os_log_t MBIMOSLog(void)
{
static dispatch_once_t onceToken;
static os_log_t customLog = NULL;
dispatch_once(&onceToken, ^{
customLog = os_log_create("net.buzzert.kordophoned", "General");
});
return customLog;
}
extern void __MBIMLogCommon(MBIMLogLevel level, NSString *format, ...)
{
va_list args;
va_start(args, format);
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
if (getenv("NSRunningFromLaunchd") != NULL) {
// Running with launchd, use oslog.
os_log_t mbimlog = MBIMOSLog();
switch (level) {
case ML_INFO:
os_log_debug(mbimlog, "%{public}@", message);
break;
case ML_NOTIFY:
os_log_info(mbimlog, "%{public}@", message);
break;
case ML_FATAL:
case ML_ERROR:
os_log_error(mbimlog, "%{public}@", message);
break;
}
} else {
// Otherwise, write to stdout.
static dispatch_once_t onceToken;
static NSDateFormatter *dateFormatter = nil;
dispatch_once(&onceToken, ^{
dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"Y-MM-d HH:mm:ss";
});
const char *c_fmt = "%s";
if (level == ML_NOTIFY) {
// BOLD
c_fmt = ESC(BLD) "%s";
} else if (level == ML_ERROR) {
c_fmt = ESC(RED) "%s";
} else if (level == ML_FATAL) {
c_fmt = ESC(BLD RED) "%s";
}
NSString *dateStr = [dateFormatter stringFromDate:[NSDate date]];
fprintf(stdout, "%s: ", [dateStr UTF8String]);
fprintf(stdout, c_fmt, [message UTF8String]);
fprintf(stdout, CLR "\n");
}
}

View File

@@ -0,0 +1,16 @@
<?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>com.apple.imagent</key>
<true/>
<key>com.apple.private.imagent</key>
<true/>
<key>com.apple.private.imcore.imagent</key>
<true/>
<key>com.apple.imagent.av</key>
<true/>
<key>com.apple.imagent.chat</key>
<true/>
</dict>
</plist>

202
server/kordophone/main.m Normal file
View File

@@ -0,0 +1,202 @@
//
// main.m
// kordophone
//
// Created by James Magahern on 11/12/18.
// Copyright © 2018 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MBIMBridge.h"
void printUsage()
{
fprintf(stderr, "Usage: kordophoned [-h] [-s | -c (certificate.p12)] [-a (access control file)\n");
fprintf(stderr, "\t-h \t Show this help message\n");
fprintf(stderr, "\t-s \t Use SSL (requires -c option)\n");
fprintf(stderr, "\t-c \t SSL certificate path encoded as pkcs12\n");
fprintf(stderr, "\t-a \t Optional access control file\n");
fprintf(stderr, "\t-p \t Specify port number (default: 5738)\n");
}
BOOL acquireCredentials(bool encrypted, const char *accessFile, NSString **out_username, NSString **out_password)
{
BOOL success = NO;
NSString *asString = nil;
NSError *launchError = nil;
NSString *accessFilePath = [NSString stringWithUTF8String:accessFile];
if (encrypted) {
NSPipe *stdoutPipe = [NSPipe pipe];
NSPipe *stderrPipe = [NSPipe pipe];
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/local/bin/gpg";
task.arguments = @[ @"-q", @"-d", accessFilePath ];
task.standardOutput = stdoutPipe;
task.standardError = stderrPipe;
success = [task launchAndReturnError:&launchError];
[task waitUntilExit];
if (success) {
NSFileHandle *stdoutFile = stdoutPipe.fileHandleForReading;
NSData *data = [stdoutFile readDataToEndOfFile]; // blocks
[stdoutFile closeFile];
if ([task terminationStatus] != 0) {
NSData *stderrData = [[stderrPipe fileHandleForReading] readDataToEndOfFile];
MBIMLogFatal(@"GPG error when decrypting access file: %@", [[NSString alloc] initWithData:stderrData encoding:NSUTF8StringEncoding]);
return NO;
}
asString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
} else {
NSError *fileReadError = nil;
asString = [NSString stringWithContentsOfFile:accessFilePath
encoding:NSASCIIStringEncoding
error:&fileReadError];
if (fileReadError != nil) {
MBIMLogFatal(@"File open error when opening access file: %@", fileReadError.localizedDescription);
return NO;
}
success = (asString.length > 0);
}
if (success) {
NSScanner *scanner = [NSScanner scannerWithString:asString];
BOOL scannerSuccess = NO;
NSString *username = nil;
scannerSuccess = [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet]
intoString:&username];
if (!scannerSuccess) {
MBIMLogFatal(@"Error parsing username from access file");
return NO;
}
NSString *password = nil;
scannerSuccess = [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet]
intoString:&password];
if (!scannerSuccess) {
MBIMLogFatal(@"Error parsing password from access file");
return NO;
}
if ([username length] && [password length]) {
*out_username = username;
*out_password = password;
} else {
MBIMLogFatal(@"Error parsing username or password from access file");
return NO;
}
} else {
MBIMLogFatal(@"Unable to launch GPG executable to decrypt access file: %@", [launchError localizedDescription]);
return NO;
}
return YES;
}
int main(int argc, char *const argv[]) {
@autoreleasepool {
BOOL usesSSL = NO;
BOOL showHelp = NO;
BOOL usesAccessControl = NO;
long portNumber = -1;
const char *certPath = NULL;
const char *accessFilePath = NULL;
int c = -1;
while ( (c = getopt(argc, argv, "hsc:a:p:")) != -1 ) {
switch (c) {
case 's':
usesSSL = YES;
break;
case 'c':
certPath = optarg;
break;
case 'a':
usesAccessControl = YES;
accessFilePath = optarg;
break;
case 'p':
portNumber = strtol(optarg, NULL, 10);
break;
case 'h':
showHelp = YES;
break;
case '?':
if (optopt == 'c') {
fprintf (stderr, "Option -%c requires an argument.\n", optopt);
} else if (isprint(optopt)) {
fprintf (stderr, "Unknown option `-%c'.\n", optopt);
} else {
fprintf (stderr, "Unknown option character `\\x%x'.\n", optopt);
return 1;
}
default:
abort ();
}
}
if (showHelp) {
printUsage();
return 1;
}
if (usesSSL && certPath == NULL) {
fprintf(stderr, "Error: wants SSL (-s) but no ssl certificate path (-c) provided\n");
return 1;
}
MBIMBridge *bridge = [MBIMBridge sharedInstance];
if (usesAccessControl) {
NSString *username = nil;
NSString *password = nil;
BOOL success = acquireCredentials(false, accessFilePath, &username, &password);
if (!success) {
MBIMLogInfo(
@"Access file must be a GPG encrypted file (encrypted with your private key, to your pub key) "
"with the follwing format: \n"
"(username)\n"
"(password)"
);
return 1;
} else {
const ssize_t ast_len = 55;
const unichar asterisks[ast_len] = u"*******************************************************";
NSString *obscuredPassword = [NSString stringWithCharacters:asterisks
length:MIN([password length], ast_len)];
MBIMLogNotify(@"Using access control credentials: username(%@) password(%@)", username, obscuredPassword);
bridge.usesAccessControl = YES;
bridge.authUsername = username;
bridge.authPassword = password;
}
}
if (usesSSL && certPath != NULL) {
bridge.usesSSL = YES;
bridge.sslCertPath = [NSString stringWithCString:certPath encoding:NSASCIIStringEncoding];
}
if (portNumber > 0) {
bridge.port = portNumber;
}
[bridge connect];
BOOL running = YES;
while (running) {
[[NSRunLoop currentRunLoop] run];
}
}
return 0;
}