aboutsummaryrefslogtreecommitdiffstats
path: root/Frameworks/SPMySQLFramework/Source
diff options
context:
space:
mode:
Diffstat (limited to 'Frameworks/SPMySQLFramework/Source')
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h14
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m14
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m38
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m9
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m87
5 files changed, 128 insertions, 34 deletions
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h
index 6f7b1a9a..77b70bf9 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h
@@ -34,6 +34,7 @@
@interface SPMySQLConnection (Conversion)
+ (const char *)_cStringForString:(NSString *)aString usingEncoding:(NSStringEncoding)anEncoding returningLengthAs:(NSUInteger *)cStringLengthPointer;
++ (NSString *)_stringForCString:(const char *)cString usingEncoding:(NSStringEncoding)encoding;
- (const char *)_cStringForString:(NSString *)aString;
- (NSString *)_stringForCString:(const char *)cString;
@@ -56,3 +57,16 @@ static inline const char* _cStringForStringWithEncoding(NSString* aString, NSStr
return (const char *)(*cachedMethodPointer)(cachedClass, cachedSelector, aString, anEncoding, cStringLengthPointer);
}
+
+/**
+ * Converts a C string (NUL-terminated) to an NSString using the supplied encoding.
+ *
+ * Unlike +[NSString stringWithCString:encoding:] which will crash on a NULL pointer, this method will return nil instead.
+ */
+static inline NSString * _stringForCStringWithEncoding(const char *aString, NSStringEncoding inputEncoding)
+{
+ //This implementation is smaller than the cached selector voodoo above, so let's do it inline
+
+ //NSString will crash on NULL ptr
+ return (aString == NULL)? nil : [NSString stringWithCString:aString encoding:inputEncoding];
+}
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m
index 676684ca..a7d293ea 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m
@@ -79,7 +79,7 @@
}
/**
- * Converts a C string to an NSString using the supplied encoding.
+ * Converts a C string to an NSString using the current connection encoding.
* This method *will not* correctly preserve nul characters within c strings; instead
* the first nul character within the string will be treated as the line ending. This
* is unavoidable without supplying a string length, so this method should not be widely
@@ -87,11 +87,15 @@
*/
- (NSString *)_stringForCString:(const char *)cString
{
+ return _stringForCStringWithEncoding(cString, stringEncoding);
+}
- // Don't try and convert null strings
- if (cString == NULL) return nil;
-
- return [NSString stringWithCString:cString encoding:stringEncoding];
+/**
+ * @see _stringForCStringWithEncoding()
+ */
++ (NSString *)_stringForCString:(const char *)cString usingEncoding:(NSStringEncoding)encoding
+{
+ return _stringForCStringWithEncoding(cString, encoding);
}
@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m
index d46b5552..65a7ae02 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m
@@ -85,9 +85,9 @@
if(keepAliveThread) {
NSLog(@"warning: overwriting existing keepAliveThread: %@, results may be unpredictable!",keepAliveThread);
}
+ keepAliveThread = [NSThread currentThread];
}
- keepAliveThread = [NSThread currentThread];
[keepAliveThread setName:[NSString stringWithFormat:@"SPMySQL connection keepalive monitor thread (id=%p)", self]];
// If the maximum number of ping failures has been reached, determine whether to reconnect.
@@ -159,11 +159,13 @@ end_cleanup:
if (timeout > 0) pingTimeout = timeout;
// Set up a struct containing details the ping task will need
- SPMySQLConnectionPingDetails *pingDetails = malloc(sizeof(SPMySQLConnectionPingDetails));
- pingDetails->mySQLConnection = mySQLConnection;
- pingDetails->keepAliveLastPingSuccessPointer = &keepAliveLastPingSuccess;
- pingDetails->keepAlivePingThreadActivePointer = &keepAlivePingThreadActive;
- pingDetails->parentId = self;
+ // we can do this on the stack since this method makes sure to outlive the ping thread
+ SPMySQLConnectionPingDetails pingDetails = {
+ .mySQLConnection = mySQLConnection,
+ .keepAliveLastPingSuccessPointer = &keepAliveLastPingSuccess,
+ .keepAlivePingThreadActivePointer = &keepAlivePingThreadActive,
+ .parentId = self
+ };
// Create a pthread for the ping
pthread_t keepAlivePingThread_t;
@@ -171,7 +173,7 @@ end_cleanup:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
- pthread_create(&keepAlivePingThread_t, &attr, (void *)&_backgroundPingTask, pingDetails);
+ pthread_create(&keepAlivePingThread_t, &attr, (void *)&_backgroundPingTask, &pingDetails);
// Record the ping start time
pingStartTime_t = mach_absolute_time();
@@ -200,13 +202,12 @@ end_cleanup:
}
} while (keepAlivePingThreadActive);
- //wait for thread to go away, otherwise our free() below might run before _pingThreadCleanup()
+ //wait for thread to go away, otherwise pingDetails may go away before _pingThreadCleanup() finishes
pthread_join(keepAlivePingThread_t, NULL);
// Clean up
keepAlivePingThread_t = NULL;
pthread_attr_destroy(&attr);
- free(pingDetails);
// Unlock the connection
[self _unlockConnection];
@@ -282,7 +283,24 @@ void _pingThreadCleanup(void *pingDetails)
if (keepAliveThread) {
// Mark the thread as cancelled
- [keepAliveThread cancel];
+ @synchronized(self) {
+ // the synchronized is neccesary here, because we don't retain keepAliveThread.
+ // If it were ommitted, for example this could happen:
+ //
+ // this thread keepalive thread
+ // -------------- -----------------
+ // 1 fetch value of keepAliveThread to register
+ // 2 keepAliveThread = nil
+ // 3 [[NSThread currentThread] release]
+ // 4 objc_msgSend() <-- invalid memory accessed
+ //
+ // With synchronized we are guaranteed to either message nil or block the keepAliveThread from exiting
+ // (and thus releasing the NSThread object) until this call finishes.
+ //
+ // We can omit it in the other 2 cases, since keepAliveThread is already volatile and we are only
+ // checking for NULL, not dereferencing it.
+ [keepAliveThread cancel];
+ }
// Wait inside a time limit of ten seconds for it to exit
uint64_t threadCancelStartTime_t = mach_absolute_time();
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m
index 594756be..ef98a21c 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m
@@ -323,7 +323,8 @@
// Store the error state
theErrorMessage = [self _stringForCString:mysql_error(mySQLConnection)];
theErrorID = mysql_errno(mySQLConnection);
- theSqlstate = [self _stringForCString:mysql_sqlstate(mySQLConnection)];
+ // sqlstate is always an ASCII string, regardless of charset (but use latin1 anyway as that is less picky about invalid bytes)
+ theSqlstate = _stringForCStringWithEncoding(mysql_sqlstate(mySQLConnection), NSISOLatin1StringEncoding);
// Prevent retries if the query was cancelled or not a connection error
if (lastQueryWasCancelled || ![SPMySQLConnection isErrorIDConnectionError:theErrorID]) {
@@ -382,7 +383,8 @@
// Update the error message, if appropriate, to reflect result store errors or overall success
theErrorMessage = [self _stringForCString:mysql_error(mySQLConnection)];
theErrorID = mysql_errno(mySQLConnection);
- theSqlstate = [self _stringForCString:mysql_sqlstate(mySQLConnection)];
+ // sqlstate is always an ASCII string, regardless of charset (but use latin1 anyway as that is less picky about invalid bytes)
+ theSqlstate = _stringForCStringWithEncoding(mysql_sqlstate(mySQLConnection), NSISOLatin1StringEncoding);
} else {
theResult = [[SPMySQLEmptyResult alloc] init];
}
@@ -735,7 +737,8 @@
{
// If a SQLSTATE wasn't supplied, select one from the connection
if(!theSqlstate) {
- theSqlstate = [self _stringForCString:mysql_sqlstate(mySQLConnection)];
+ // sqlstate is always an ASCII string, regardless of charset (but use latin1 anyway as that is less picky about invalid bytes)
+ theSqlstate = _stringForCStringWithEncoding(mysql_sqlstate(mySQLConnection), NSISOLatin1StringEncoding);
}
// Clear the last SQLSTATE stored on the instance
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m
index e8692895..f581c03c 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m
@@ -606,6 +606,7 @@ static uint64_t _elapsedMicroSecondsSinceAbsoluteTime(uint64_t comparisonTime)
mysql_options(theConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&timeout);
// Set the connection encoding
+ NSStringEncoding connectEncodingNS = [SPMySQLConnection stringEncodingForMySQLCharset:[encodingName UTF8String]];
mysql_options(theConnection, MYSQL_SET_CHARSET_NAME, [encodingName UTF8String]);
// Set up the connection variables in the format MySQL needs, from the class-wide variables
@@ -614,22 +615,36 @@ static uint64_t _elapsedMicroSecondsSinceAbsoluteTime(uint64_t comparisonTime)
const char *thePassword = NULL;
const char *theSocket = NULL;
- if (host) theHost = [self _cStringForString:host];
- if (username) theUsername = [self _cStringForString:username];
+ if (host) theHost = [host UTF8String]; //mysql calls getaddrinfo on the hostname. Apples code uses -UTF8String in that situation.
+ if (username) theUsername = _cStringForStringWithEncoding(username, connectEncodingNS, NULL); //during connect this is in MYSQL_SET_CHARSET_NAME encoding
- // If a password was supplied, use it; otherwise ask the delegate if appropriate
+ // If a password was supplied, use it; otherwise ask the delegate if appropriate.
+ //
+ // Note that password has no charset in mysql: If a user password is set to 'ΓΌ' on a latin1 connection
+ // and you later try to connect on an UTF-8 terminal (or vice versa) it will fail. The MySQL (5.5) manual wrongly states that
+ // MYSQL_SET_CHARSET_NAME has influence over that, but it does not and could not, since the password is hashed by the client
+ // before transmitting it to the server and the (5.5) client has no charset support, effectively treating password as
+ // a NUL-terminated byte array.
+ // There is one exception, though: The "mysql_clear_password" auth plugin sends the password in plaintext and the server side
+ // MAY choose to do a charset conversion as appropriate before handing it to whatever backend is used.
+ // Since we don't know which auth plugin server and client will agree upon, we'll do as the manual says...
if (password) {
- thePassword = [self _cStringForString:password];
+ thePassword = _cStringForStringWithEncoding(password, connectEncodingNS, NULL);
} else if ([delegate respondsToSelector:@selector(keychainPasswordForConnection:)]) {
- thePassword = [self _cStringForString:[delegate keychainPasswordForConnection:self]];
+ thePassword = _cStringForStringWithEncoding([delegate keychainPasswordForConnection:self], connectEncodingNS, NULL);
}
// If set to use a socket and a socket was supplied, use it; otherwise, search for a socket to use
if (useSocket) {
- if ([socketPath length]) {
- theSocket = [self _cStringForString:socketPath];
- } else {
- theSocket = [self _cStringForString:[SPMySQLConnection findSocketPath]];
+ //default to user supplied path
+ NSString *mySocketPath = socketPath;
+ //if none was given, search in the default locations instead
+ if (![mySocketPath length]) {
+ mySocketPath = [SPMySQLConnection findSocketPath];
+ }
+ //get C string if we have a path (danger: method will throw on empty/nil string!)
+ if([mySocketPath length]) {
+ theSocket = [mySocketPath fileSystemRepresentation];
}
}
@@ -640,20 +655,39 @@ static uint64_t _elapsedMicroSecondsSinceAbsoluteTime(uint64_t comparisonTime)
const char *theCACertificatePath = NULL;
const char *theSSLCiphers = SPMySQLSSLPermissibleCiphers;
- if (sslKeyFilePath) {
- theSSLKeyFilePath = [[sslKeyFilePath stringByExpandingTildeInPath] UTF8String];
+ if ([sslKeyFilePath length]) {
+ theSSLKeyFilePath = [[sslKeyFilePath stringByExpandingTildeInPath] fileSystemRepresentation];
}
- if (sslCertificatePath) {
- theSSLCertificatePath = [[sslCertificatePath stringByExpandingTildeInPath] UTF8String];
+ if ([sslCertificatePath length]) {
+ theSSLCertificatePath = [[sslCertificatePath stringByExpandingTildeInPath] fileSystemRepresentation];
}
- if (sslCACertificatePath) {
- theCACertificatePath = [[sslCACertificatePath stringByExpandingTildeInPath] UTF8String];
+ if ([sslCACertificatePath length]) {
+ theCACertificatePath = [[sslCACertificatePath stringByExpandingTildeInPath] fileSystemRepresentation];
}
if(sslCipherList) {
theSSLCiphers = [sslCipherList UTF8String];
}
+ // Calling mysql_ssl_set() to libmysqlclient only means that connecting with SSL would be nice.
+ // If the server doesn't support SSL though, it will *silently* fall back to plaintext and in the worst case even transmit
+ // the password in cleartext.
+ //
+ // Setting MYSQL_OPT_SSL_MODE is required, to actually make it abort the connection if the server doesn't signal SSL support.
+ //
+ // mysql 5.5.55+
+ // mysql 5.6.36+
+ // mysql 5.7.11+ (5.7.3 - 5.7.10 with a different name)
+ // mysql 8.0+
mysql_ssl_set(theConnection, theSSLKeyFilePath, theSSLCertificatePath, theCACertificatePath, NULL, theSSLCiphers);
+ enum mysql_ssl_mode opt_ssl_mode = SSL_MODE_REQUIRED;
+ if(mysql_options(theConnection, MYSQL_OPT_SSL_MODE, (void *)&opt_ssl_mode)) {
+ if(isMaster) {
+ [self _updateLastErrorMessage:@"libmysqlclient is missing support for MYSQL_OPT_SSL_MODE"];
+ [self _updateLastSqlstate:@"HY000"];
+ [self _updateLastErrorID:2026];
+ }
+ return NULL;
+ }
}
MYSQL *connectionStatus = mysql_real_connect(theConnection, theHost, theUsername, thePassword, NULL, (unsigned int)port, theSocket, [self clientFlags]);
@@ -663,9 +697,30 @@ static uint64_t _elapsedMicroSecondsSinceAbsoluteTime(uint64_t comparisonTime)
// If the connection is the master connection, record the error state
if (isMaster) {
+ // <TODO>
+ // this is tricky: mysql_error() is supposed to return data encoded in character_set_results (in mysql 5.5+),
+ // yet the whole API treats it as if it were a plain C string.
+ // So if the charset is e.g. utf16 the mysql server will itself fall over that and return an empty error message
+ // (5.5, 5.7: the message is really missing at the network layer).
+ // (Side Note: There is a workaround for server generated error messages: "show warnings" will also include errors
+ // and because it uses a regular results table it can contain the actual error message)
+ //
+ // Before 5.5 things are much worse, because the charset of the message depends on the language of the error messages
+ // (which can be changed at runtime per session (or at launch time in 4.1)) plus all arguments in the template string
+ // will retain their original encoding.
+ // So if you connect with utf8 to a server with russian locale the error message will be in koi8r and contain the name of
+ // an erroneus value in utf8...
+ //
+ // On the other hand mysql_error() may also return errors generated by the client locally.
+ // The client has no charset support and simply assumes the local charset is ASCII-compatible.
+ // The english messages are compiled into the client (see libmysql/errmsg.c and include/errmsg.h).
+ // We could use a little trick, though: client errors are in the exclusive range 2000 to 2999 (CR_MIN_ERROR/CR_MAX_ERROR)
+ // and all their string arguments are either hostnames or file system paths, which on OS X use UTF-8.
[self _updateLastErrorMessage:[self _stringForCString:mysql_error(theConnection)]];
+ // </TODO>
[self _updateLastErrorID:mysql_errno(theConnection)];
- [self _updateLastSqlstate:[self _stringForCString:mysql_sqlstate(theConnection)]];
+ // sqlstate is always an ASCII string, regardless of charset (but use latin1 anyway as that is less picky about invalid bytes)
+ [self _updateLastSqlstate:_stringForCStringWithEncoding(mysql_sqlstate(theConnection),NSISOLatin1StringEncoding)];
}
return NULL;