Please forgive my clumsiness, I'm new to Stackoverflow, C#, and Objective C.
In a nutshell, I'm trying to do what is answered in this question, but in PHP:
How to authenticate the GKLocalPlayer on my 'third party server'?
Hopefully this will also help other PHP devs working on the same thing.
I'm using Unity (Unity3D) and PHP server-side. I've got Objective C properly connecting to GameCenter and returning data via a call to generateIdentityVerificationSignatureWithCompletionHandler.
Unfortunately, I can't figure out what I'm doing wrong to validate the SHA1 hash. I've been working on this for the past week, trying all sorts of things, but with no luck.
I'm trying three different ways of making the SHA1 hash (shown below). Once in Objective C, another in Unity's C#, and finally a third time on my server in PHP. The Objective C and C# SHA1 hashes end up identical. However, the PHP one does not match them. And none validate against Apple's public cert and signature.
Admittedly, I could be misunderstanding something fundamental. It would be a huge step to at least get the Objective C and C# hashes to validate.
Thanks.
Objective C code:
[localPlayer generateIdentityVerificationSignatureWithCompletionHandler:^(NSURL *publicKeyUrl, NSData *signature, NSData *salt, uint64_t timestamp, NSError *error) {
NSDictionary *params = #{#"public_key_url": [publicKeyUrl absoluteString],
#"timestamp": [NSString stringWithFormat:#"%llu", timestamp],
#"signature": [signature base64EncodedStringWithOptions:0],
#"salt": [salt base64EncodedStringWithOptions:0],
#"player_id": [GKLocalPlayer localPlayer].playerID,
#"app_bundle_id": [[NSBundle mainBundle] bundleIdentifier]};
// Build hash using iOS...
NSMutableData *payload = [[NSMutableData alloc] init];
[payload appendData:[[GKLocalPlayer localPlayer].playerID dataUsingEncoding:NSASCIIStringEncoding]];
[payload appendData:[[[NSBundle mainBundle] bundleIdentifier] dataUsingEncoding:NSASCIIStringEncoding]];
uint64_t timestampBE = CFSwapInt64HostToBig(timestamp);
[payload appendBytes:×tampBE length:sizeof(timestampBE)];
[payload appendData:salt];
uint8_t sha1HashDigest[CC_SHA1_DIGEST_LENGTH];
CC_SHA1([payload bytes], [payload length], sha1HashDigest);
// Convert to hex string so it can be sent to Unity's C# then to the PHP webserver...
NSString *sIOSHash = [self stringFromDigest:sha1HashDigest length:CC_SHA1_DIGEST_LENGTH];
// END - Build hash using iOS
// Build string to send to Unity's C#...
NSMutableString * data = [[NSMutableString alloc] init];
[data appendString:params[#"public_key_url"]];
[data appendString:#","];
[data appendString:params[#"timestamp"]];
[data appendString:#","];
[data appendString:params[#"signature"]];
[data appendString:#","];
[data appendString:params[#"salt"]];
[data appendString:#","];
[data appendString:params[#"player_id"]];
[data appendString:#","];
[data appendString:params[#"app_bundle_id"]];
[data appendString:#","];
[data appendString:sIOSHash];
// END - Build string to send to Unity's C#.
// Send string to Unity's C# for parsing and sending off to PHP webserver.
NSString *str = [[data copy] autorelease];
UnitySendMessage("GameCenterManager", "onAuthenticateLocalPlayer", [ISNDataConvertor NSStringToChar:str]);
}];
// Helper method to convert uint8_t into a hex string for sending to the webserver.
- (NSString *)stringFromDigest:(uint8_t *)digest length:(int)length {
NSMutableString *ms = [[NSMutableString alloc] initWithCapacity:length * 2];
for (int i = 0; i < length; i++) {
[ms appendFormat: #"%02x", (int)digest[i]];
}
return [ms copy];
}
What follows is the C# code (within Unity3D) to generate the second version of the SHA1 hash.
These variables are all sent to Unity from the iOS code (above), and came in as strings: player_id, app_bundle_id, timestamp, salt.
(I'm not showing any Unity3D C# code to send to my server. But I'm using the WWWForm and AddField to send it. Nor am I showing the "bridge" code to move data from Objective C to C#.)
var sha1 = new SHA1Managed();
var data = new List<byte>();
data.AddRange(Encoding.UTF8.GetBytes(player_id));
data.AddRange(Encoding.UTF8.GetBytes(app_bundle_id));
data.AddRange(ToBigEndian(Convert.ToUInt64(timestamp)));
data.AddRange(Convert.FromBase64String(salt));
var sig = data.ToArray();
public static string CSharpHash = ToHex(sha1.ComputeHash(sig), false);
This last code block is my server-side PHP that receives the data from the client, validates the public cert, and then tries to verify the hash against it and the signature. That last part is where I am stuck.
/*
Sample data as received within the PHP (all strings):
$public_cert_url eg: https://sandbox.gc.apple.com/public-key/gc-sb.cer
$timestamp eg: 00-00-01-47-12-9C-16-D4 [derived from: 1404766525140]
$signature eg: EGc8J9D7SdZ0qq2xl2XLz2[lots more...]
$salt eg: LDfyIQ==
$player_id eg: G:[#########]
$app_bundle_id eg: com.[mydomain].[myapp]
$sIOSHash eg: 00032b9416315c8298b5a6e7f5d9dec71bd5ace2 [The C# and Objective C code both generate the same hash.]
$CSharpHash eg: 00032b9416315c8298b5a6e7f5d9dec71bd5ace2
*/
// Verify the public cert.
// As far as I understand, PHP's openssl_pkey_get_public() cannot read raw
// cer data, so I download and convert to PEM. Optimize later.
$fp = fopen("temp.cer", "w"); // Open file for writing.
$header[] = "Content-Type: application/pkix-cert";
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_URL, $public_cert_url);
curl_setopt($curl, CURLOPT_BINARYTRANSFER, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($curl, CURLOPT_FILE, $fp);
curl_exec($curl);
curl_close($curl);
fclose($fp);
shell_exec("openssl x509 -inform der -in temp.cer -out temp.pem"); // Convert to PEM.
$pub_cert = file_get_contents("temp.pem");
$sKey = openssl_pkey_get_public($pub_cert); // Validate PEM file here.
If( $sKey === False ) echo "pkey bad";
// This ^^ works.
// This is where I am stuck:
// Verify the data from the client against the signature from the client
// and the downloaded public key.
// First, try to verify against a hash created within PHP:
$iResult = openssl_verify(
sha1($player_id . $app_bundle_id . $timestamp . $salt),
$signature,
$pub_cert,
OPENSSL_ALGO_SHA1);
If( $iResult != 1 ) echo "not valid PHP hash!\n";
// Second, see if it will verify by using the hash created in.
$iResult = openssl_verify($sIOSHash, $signature, $pub_cert, OPENSSL_ALGO_SHA1);
If( $iResult != 1 ) echo "not valid sIOSHash hash!\n";
// Finally, does the C# has verify?
$iResult = openssl_verify($CSharpHash, $signature, $pub_cert, OPENSSL_ALGO_SHA1);
If( $iResult != 1 ) echo "not valid CSharpHash hash!\n";
// None of these ^^ ever validate.
Update: Jul 9 2014
I got it to validate the data by not doing SHA1 on it. I was confused by the Apple documentation (https://developer.apple.com/library/prerelease/ios/documentation/GameKit/Reference/GKLocalPlayer_Ref/index.html#//apple_ref/occ/instm/GKLocalPlayer/generateIdentityVerificationSignatureWithCompletionHandler:). Specifically #7 which says: "Generate a SHA-1 hash value for the buffer."
I removed ALL the C# code (to try and generate the payload) and now only use the Objective C.
Modified as follows:
NSMutableData *payload = [[NSMutableData alloc] init];
[payload appendData:[[GKLocalPlayer localPlayer].playerID dataUsingEncoding:NSUTF8StringEncoding]];
[payload appendData:[[[NSBundle mainBundle] bundleIdentifier] dataUsingEncoding:NSUTF8StringEncoding]];
uint64_t timestampBE = CFSwapInt64HostToBig(timestamp);
[payload appendBytes:×tampBE length:sizeof(timestampBE)];
[payload appendData:salt];
NSString *siOSData = [payload base64EncodedStringWithOptions:0];
Notice the removal of SHA1.
I gave up on trying to create the payload in PHP. I tried many variations of pack, conversions, upgrading my server to 64 bit, etc. But I think (please correct me if I am wrong) that since I am transmitting the exact same data from the client as makes up the payload, it should be ok.
Note to Apple:
PLEASE implement OAuth 2.0.
I also figured out how to validate the Apple cer file without wasting processing on saving to a file. As follows:
// Get data from client. I urlencoded it before sending. So need to urldecode now.
// The payload is in "iosdata" and it, along with the signature, both need to be
// base64_decoded.
$sIOSData = ( isset($_REQUEST["iosdata"]) ) ? urldecode(Trim($_REQUEST["iosdata"])) : "";
$sIOSData = base64_decode($sIOSData);
$sSignature = ( isset($_REQUEST["signature"]) ) ? urldecode(Trim($_REQUEST["signature"])) : "";
$sSignature = base64_decode($sSignature);
// Here is where I download Apple's cert (DER format), save it as raw bits
// to a variable, convert it to PEM format (the ONLY format PHP's OpenSSL
// works with apparently...?) and then validate it.
// TODO: figure out if Apple live returns different results each time, and/or if
// this can be cached. Apple sandbox returns the same each time.
$header[0] = "Content-Type: application/pkix-cert";
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_URL, $sPublicKeyUrl);
curl_setopt($curl, CURLOPT_BINARYTRANSFER, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
$der_data = curl_exec($curl);
curl_close($curl);
$sPublicKey = chunk_split(base64_encode($der_data), 64, "\n");
$sPublicKey = "-----BEGIN CERTIFICATE-----\n".$sPublicKey."-----END CERTIFICATE-----\n";
$sKey = openssl_pkey_get_public($sPublicKey);
If( $sKey === False ) Return "pkey bad";
// Here I use the package ($sIOSData) and signature to validate against Apple's
// public certificate.
$iResult = openssl_verify($sIOSData, $sSignature, $sKey, OPENSSL_ALGO_SHA1);
If( $iResult != 1 ) {
echo "BAD!\n";
echo "error: ".openssl_error_string()."\n";
}else{
echo "WORKED!\n";
}
Feedback is welcome. I'm sure there are tons of things that can be improved on. But hopefully this will help save someone a week of work.
I had a heck of a time with this. Garraeth's code was helpful, but there were a few other helpful hints scattered around SO, plus the php docs, plus some lucky guessing, and I finally arrived at this:
On the iOS side:
Main verify-user code:
// Don't bother verifying not-authenticated players
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
if (localPlayer.authenticated)
{
// __weak copy for use within code-block
__weak GKLocalPlayer *useLocalPlayer = localPlayer;
[useLocalPlayer generateIdentityVerificationSignatureWithCompletionHandler: ^(NSURL * _Nullable publicKeyUrl,
NSData * _Nullable signature,
NSData * _Nullable salt,
uint64_t timestamp,
NSError * _Nullable error) {
if (error == nil)
{
[self verifyPlayer: useLocalPlayer.playerID // our verify routine: below
publicKeyUrl: publicKeyUrl
signature: signature
salt: salt
timestamp: timestamp];
}
else
{
// GameCenter returned an error; deal with it here.
}
}];
}
else
{
// User is not authenticated; it makes no sense to try to verify them.
}
My verifyPlayer: routine:
-(void)verifyPlayer: (NSString*) playerID
publicKeyUrl: (NSURL*) publicKeyUrl
signature: (NSData*) signature
salt: (NSData*) salt
timestamp: (uint64_t) timestamp
{
NSDictionary *paramsDict = #{ #"publicKeyUrl": [publicKeyUrl absoluteString],
#"timestamp" : [NSString stringWithFormat: #"%llu", timestamp],
#"signature" : [signature base64EncodedStringWithOptions: 0],
#"salt" : [salt base64EncodedStringWithOptions: 0],
#"playerID" : playerID,
#"bundleID" : [[NSBundle mainBundle] bundleIdentifier]
};
// NOTE: A lot of the code below was cribbed from another SO answer for which I have lost the URL.
// FIXME: <When found, insert other-SO-answer URL here>
// build payload
NSMutableData *payload = [NSMutableData new];
[payload appendData: [playerID dataUsingEncoding: NSASCIIStringEncoding]];
[payload appendData: [[[NSBundle mainBundle] bundleIdentifier] dataUsingEncoding: NSASCIIStringEncoding]];
uint64_t timestampBE = CFSwapInt64HostToBig(timestamp);
[payload appendBytes: ×tampBE length: sizeof(timestampBE)];
[payload appendData: salt];
// Verify with server
[self verifyPlayerOnServer: payload withSignature: signature publicKeyURL: publicKeyUrl];
#if 0 // verify locally (for testing)
//get certificate
NSData *certificateData = [NSData dataWithContentsOfURL: publicKeyUrl];
//sign
SecCertificateRef certificateFromFile = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData); // load the certificate
SecPolicyRef secPolicy = SecPolicyCreateBasicX509();
SecTrustRef trust;
OSStatus statusTrust = SecTrustCreateWithCertificates(certificateFromFile, secPolicy, &trust);
if (statusTrust != errSecSuccess)
{
NSLog(#"%s ***** Could not create trust certificate", __PRETTY_FUNCTION__);
return;
}
SecTrustResultType resultType;
OSStatus statusTrustEval = SecTrustEvaluate(trust, &resultType);
if (statusTrustEval != errSecSuccess)
{
NSLog(#"%s ***** Could not evaluate trust", __PRETTY_FUNCTION__);
return;
}
if ((resultType != kSecTrustResultProceed)
&& (resultType != kSecTrustResultRecoverableTrustFailure) )
{
NSLog(#"%s ***** Server can not be trusted", __PRETTY_FUNCTION__);
return;
}
SecKeyRef publicKey = SecTrustCopyPublicKey(trust);
uint8_t sha256HashDigest[CC_SHA256_DIGEST_LENGTH];
CC_SHA256([payload bytes], (CC_LONG)[payload length], sha256HashDigest);
NSLog(#"%s [DEBUG] sha256HashDigest: %#", __PRETTY_FUNCTION__, [NSData dataWithBytes: sha256HashDigest length: CC_SHA256_DIGEST_LENGTH]);
//check to see if its a match
OSStatus verficationResult = SecKeyRawVerify(publicKey, kSecPaddingPKCS1SHA256, sha256HashDigest, CC_SHA256_DIGEST_LENGTH, [signature bytes], [signature length]);
CFRelease(publicKey);
CFRelease(trust);
CFRelease(secPolicy);
CFRelease(certificateFromFile);
if (verficationResult == errSecSuccess)
{
NSLog(#"%s [DEBUG] Verified", __PRETTY_FUNCTION__);
dispatch_async(dispatch_get_main_queue(), ^{
[self updateGameCenterUI];
});
}
else
{
NSLog(#"%s ***** Danger!!!", __PRETTY_FUNCTION__);
}
#endif
}
My routine that passes code to the server (Cribbed from this question):
- (void) verifyPlayerOnServer: (NSData*) payload withSignature: signature publicKeyURL: (NSURL*) publicKeyUrl
{
// hint courtesy of: http://stackoverflow.com/questions/24621839/how-to-authenticate-the-gklocalplayer-on-my-third-party-server-using-php
NSDictionary *jsonDict = #{ #"data" : [payload base64EncodedStringWithOptions: 0] };
//NSLog(#"%s [DEBUG] jsonDict: %#", __PRETTY_FUNCTION__, jsonDict);
NSError *error = nil;
NSData *bodyData = [NSJSONSerialization dataWithJSONObject: jsonDict options: 0 error: &error];
if (error != nil)
{
NSLog(#"%s ***** dataWithJson error: %#", __PRETTY_FUNCTION__, error);
}
// To validate at server end:
// http://stackoverflow.com/questions/21570700/how-to-authenticate-game-center-user-from-3rd-party-node-js-server
// NOTE: MFURLConnection is my subclass of NSURLConnection.
// .. this routine just builds an NSMutableURLRequest, then
// .. kicks it off, tracking a tag and calling back to delegate
// .. when the request is complete.
[MFURLConnection connectionWitURL: [self serverURLWithSuffix: #"gameCenter.php"]
headers: #{ #"Content-Type" : #"application/json",
#"Publickeyurl" : [publicKeyUrl absoluteString],
#"Signature" : [signature base64EncodedStringWithOptions: 0],
}
bodyData: bodyData
delegate: self
tag: worfc2_gameCenterVerifyConnection
userInfo: nil];
}
On the server side:
Somewhat cribbed from this question, and others, and php docs and...
$publicKeyURL = filter_var($headers['Publickeyurl'], FILTER_SANITIZE_URL);
$pkURL = urlencode($publicKeyURL);
if (empty($pkURL))
{
$response->addparameters(array('msg' => "no pku"));
$response->addparameters(array("DEBUG-headers" => $headers));
$response->addparameters(array('DEBUG-publicKeyURL' => $publicKeyURL));
$response->addparameters(array('DEBUG-pkURL' => $pkURL));
$response->setStatusCode(400); // bad request
}
else
{
$sslCertificate = file_get_contents($publicKeyURL);
if ($sslCertificate === false)
{
// invalid read
$response->addparameters(array('msg' => "no certificate"));
$response->setStatusCode(400); // bad request
}
else
{
// Example code from http://php.net/manual/en/function.openssl-verify.php
try
{
// According to: http://stackoverflow.com/questions/10944071/parsing-x509-certificate
$pemData = der2pem($sslCertificate);
// fetch public key from certificate and ready it
$pubkeyid = openssl_pkey_get_public($pemData);
if ($pubkeyid === false)
{
$response->addparameters(array('msg' => "public key error"));
$response->setStatusCode(400); // bad request
}
else
{
// According to: http://stackoverflow.com/questions/24621839/how-to-authenticate-the-gklocalplayer-on-my-third-party-server-using-php
// .. we use differently-formatted parameters
$sIOSData = $body['data'];
$sIOSData = base64_decode($sIOSData);
$sSignature = $headers['Signature'];
$sSignature = base64_decode($sSignature);
//$iResult = openssl_verify($sIOSData, $sSignature, $sKey, OPENSSL_ALGO_SHA1);
$dataToUse = $sIOSData;
$signatureToUse = $sSignature;
// state whether signature is okay or not
$ok = openssl_verify($dataToUse, $signatureToUse, $pubkeyid, OPENSSL_ALGO_SHA256);
if ($ok == 1)
{
//* echo "good";
$response->addparameters(array('msg' => "user validated"));
}
elseif ($ok == 0)
{
//* echo "bad";
$response->addparameters(array('msg' => "INVALID USER SIGNATURE"));
$response->addparameters(array("DEBUG-$dataToUse" => $dataToUse));
$response->addparameters(array("DEBUG-$signatureToUse" => $signatureToUse));
$response->addparameters(array("DEBUG-body" => $body));
$response->setStatusCode(401); // unauthorized
}
else
{
//* echo "ugly, error checking signature";
$response->addparameters(array('msg' => "***** ERROR checking signature"));
$response->setStatusCode(500); // server error
}
// free the key from memory
openssl_free_key($pubkeyid);
}
}
catch (Exception $ex)
{
$response->addparameters(array('msg' => "verification error"));
$response->addparameters(array("DEBUG-headers" => $headers));
$response->addparameters(array('DEBUG-Exception' => $ex));
$response->setStatusCode(400); // bad request
}
}
// NODE.js code at http://stackoverflow.com/questions/21570700/how-to-authenticate-game-center-user-from-3rd-party-node-js-server
}
Don't forget the handy utility routine:
function der2pem($der_data)
{
$pem = chunk_split(base64_encode($der_data), 64, "\n");
$pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
return $pem;
}
Using all of this, I was finally able to get "user validated" back from my server. Yay! :)
NOTE: This method seems very open to hacking, as anyone could sign whatever they want with their own certificate then pass the server the data, signature and URL to their certificate and get back a "that's a valid GameCenter login" answer so, while this code "works" in the sense that it implements the GC algorithm, the algorithm itself seems flawed. Ideally, we would also check that the certificate came from a trusted source. Extra-paranoia to check that it is Apple's Game Center certificate would be good, too.
Thank you #garraeth, your code helped me implement the logic.
From the C# code, concat a payload data on server side is working fine for me.
When using openssl_verify we needn't do the hash ourselves.
Also, I think validate the publicKeyUrl is form HTTPS and apple.com is required.
Some pseudo code here (Note that Apple has change the algorithm to OPENSSL_ALGO_SHA256 in 2015).
// do some urls, input params validate...
// do the signature validate
$payload = concatPayload($playerId, $bundleId, $timestamp, $salt);
$pubkeyId = openssl_pkey_get_public($pem);
$isValid = openssl_verify($payload, base64_decode($signature),
$pubkeyId, OPENSSL_ALGO_SHA256);
function concatPayload($playerId, $bundleId, $timestamp, $salt) {
$bytes = array_merge(
unpack('C*', $playerId),
unpack('C*', $bundleId),
int64ToBigEndianArray($timestamp),
base64ToByteArray($salt)
);
$payload = '';
foreach ($bytes as $byte) {
$payload .= chr($byte);
}
return $payload;
}
function int64ToBigEndianArray() {
//... follow the C# code
}
function base64ToByteArray() {
//...
}
Related
I'm currently trying to send an array, which contains text and images, from my PHP file to my iOS application using NSURLSession. Initially, I've tested with a text-only array which I've converted in JSON format before sending to my application: everything worked fine, but now I need to send an array with text and images, so I've done something like this:
Here is the code:
- PHP (sorry for non - english comments and variable names)
<?php
// Connessione al server mediante le credenziali.
$con = mysql_connect("localhost", "mobdev2015", "Pistacchio1");
if(!$con){
die("Server non trovato" . mysql_error());
}
// Seleziono il database.
$db = mysql_select_db("gandalf", $con);
if(!$db){
die("Database non trovato" . mysql_error());
}
// Mi metto in ascolto per ricevere l'user dall'utente.
$user = file_get_contents('php://input');
// Prelevo i dati dello studente, controllando se tutto va bene.
$sql = "SELECT * FROM Studente WHERE nomeUtente = '$user' ";
if(!mysql_query($sql, $con))
die ("Errore nella ricerca dello studente" . mysql_error());
// Prelevo i valori delle colonne del result set.
$result = mysql_query($sql, $con);
$resultSet = mysql_fetch_row($result);
// Prelevo il percorso dell'immagine dell'università dello studente, dato l'id nel risultato,
// Facendo sempre i vari controlli del caso.
$queryImmagineUni = "SELECT immagine FROM Universita WHERE id = '$result[5]'";
if(!mysql_query($queryImmagineUni, $con))
die ("Errore nella ricerca dell'università" . mysql_error());
$result = mysql_query($queryImmagineUni, $con);
$pathImmagine = mysql_result($result, 0);
//Inserisco tutti i dati nell'array, ottenendo le immagini mediante file_get_contents.
$datiutente = array(
"nome" => $resultSet[1],
"cognome" => $resultSet[2],
"email" => $resultSet[4],
"nomeUtente" => $resultset[6],
"immagineProfilo" => file_get_contents($resultSet[3]),
"immagineUni" => file_get_contents($pathImmagine)
);
//Mando in output il risultato e chiudo la connessione.
echo $datiutente;
mysql_close($con);
?>
immagineProfilo and (aka profileImage) and immagineUni (aka universityImage) are two paths retrieved from database (like "./folder/image.jpg").
iOS:
// Setting up the url of the request (we will call a php file).
NSURL *url = [[NSURL alloc]initWithString:#"http://inserturlhere.com/userdata.php"];
// Creating the NSMutableRequest object, which will contain the HTML headers and the nickname needed to retrieve data.
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url];
// Converting the NSString into NSData to append at MutableURLRequest.
NSData *postData = [user dataUsingEncoding:NSASCIIStringEncoding];
//Setting the method to post
[request setHTTPMethod:#"POST"];
// Setting the body of the post to the reqeust
[request setHTTPBody:postData];
//
/*
NSURLSession needs a NSURLSessionConfiguration to run, so we instiate a NSURLSessionConfiguration object saying
we want to use a default Session, then we create a session with that configuration
*/
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
// Starting a dataTask with the request previously defined. The completion handler will be used to manage the response
// from the server, stored in a NSURLResponse object.
[[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSArray *datiUtente = [NSKeyedUnarchiver unarchiveObjectWithData:data];
NSLog(#"%#", datiUtente);
}]resume];
The problem in this solution is I can't print the content of the array which should contain the contents of the PHP array, but using the debugger I can see data is not NULL, so it seems like something is sent.
Your PHP line says:
echo $datiutente;
Instead, you want to return JSON, which can be easily parsed by the client. So, you should specify that the response will be JSON (and do this before you echo anything):
header('Content-type: application/json');
And then, the echoing of the response data would be:
echo json_encode($datiutente);
And then to parse it on the client side, you want:
[[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(#"%#", error);
}
if (!data) {
return;
}
NSError *parseError;
NSArray *datiUtente = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
if (datiUtente) {
NSLog(#"responseObject = %#", datiUtente);
} else {
NSLog(#"parseError = %#", parseError);
NSLog(#"responseString = %#", [[NSString alloc] initWithData:data encoding: NSUTF8StringEncoding]);
}
}] resume];
By the way, when you build a JSON response, you cannot include binary data (namely, the image payload). So if you're going to include the image(s) in the JSON response, make sure to base64_encode them (and then decode them on the client side):
$datiutente = array(
"nome" => $resultSet[1],
"cognome" => $resultSet[2],
"email" => $resultSet[4],
"nomeUtente" => $resultset[6],
"immagineProfilo" => base64_encode(file_get_contents($resultSet[3])),
"immagineUni" => base64_encode(file_get_contents($pathImmagine)),
"success" => true
);
Personally, I would not be inclined to include the image payload in the JSON ar all (because it increases the size of the response by several orders of magnitude, slowing it down). I might prefer to just include a URL for the image in the response, and let the client request the image itself, if and when it needs it. You can make the app more responsive with that sort of design. But that's up to you.
Note, in addition to the above change, I also added a success code. This can be useful so that the client can quickly determine whether the response was successful or not.
Obviously, you want to JSON encode failures, too. For example, if the MySQL connection failed, you should indicate that in a JSON response (and include the appropriate information provided by MySQL):
if (!$con) {
$response = array(
"success" => false,
"message" => "Server non trovato",
"sqlerror" => mysql_error(),
"sqlerrno" => mysql_errno()
);
echo json_encode($response);
exit();
}
Once you get this working, a few other observations:
Do not just take the posted data and use it in a query. That exposes you to SQL injection attacks. Remember to mysql_real_escape_string that input before using it in a query.
I'd probably change the request created by the client code to be a application/x-www-form-urlencoded request (e.g., user=...) or a application/json request (e.g. use NSJSONSerialization dataWithJSONObject to build request that looks like {"user": "..."}). and then parse it on the server side.
Note this MySQL interface is deprecated. As the docs say:
This extension was deprecated in PHP 5.5.0, and it was removed in PHP 7.0.0. Instead, the MySQLi or PDO_MySQL extension should be used. See also MySQL: choosing an API guide and related FAQ for more information.
I'm having an issue sending data to my online database. Nothing seems to post when I check the database. I performed an NSLog on the received response, and it's blank.
Here is the .php:
<?php
$db_host="someurl.com";
$db_username="some_user";
$db_pass="some_passwd";
$db_name="some_db";
$conn = mysql_connect($db_host, $db_username, $db_pass) or die ("Could not connect to
MySQL");
mysql_select_db("$db_name") or die ("No database");
// array for JSON response
$json = $_SERVER['HTTP_JSON'];
$data = json_decode($json);
$some1_id = $data->some1_id;
$imei = $data->imei;
//does the imei exist?
$result = mysql_query("SELECT * FROM usr_go WHERE imei = '".$imei."'");
if (mysql_num_rows($result) == 0){
if(isset($some1_id))
$result = mysql_query("INSERT INTO usr_go(some1_id, imei) VALUES('".$some1_id."','".$imei."')");
}
else{
if(isset($some1_id))
$result = mysql_query("UPDATE usr_go SET some1_id = '".$some1_id."' WHERE imei = '". $imei ." AND some1_id IS NULL ");
}
mysql_close($conn);
header('Content-type: application/json');
$response = $result;
echo json_encode($response);
?>
However, if I hard-code the $response to be some string value, and NSLog the received response, it receives the appropriate string value.
Here is my code:
NSDictionary *dict = #{#"some1_id" : [NSNumber numberWithInt:self.cellIndex]};
NSError *error = nil;
NSData *json = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&error];
if (json)
{
NSURL *url = [NSURL URLWithString:#"someurl.com"];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
[req setHTTPMethod:#"POST"];
[req setValue:#"application/json; charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[req setHTTPBody:json];
NSURLResponse *res = nil;
NSData *ret = [NSURLConnection sendSynchronousRequest:req returningResponse:&res error:&error];
NSString *resString = [[NSString alloc] initWithData:ret encoding:NSUTF8StringEncoding];
NSLog(#"response String: %#",resString);
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSLog(#"JSON Output: %#", jsonString);
}
else
{
NSLog(#"Unable to serialize the data %#: %#", dictionary, error);
}
Is it the fact that it's not possible to insert the IMEI, which is why it's not posting, or some other issue?
Thanks for your assistance.
A couple of observations:
You should use msqli interface rather than the deprecated mysql interface.
You should never take input and just use it in SQL statement. Either use mysqli_real_escape_string or bind values (as shown below). This is critical to ensure you aren't susceptible to SQL injection attacks. It also protects you against innocent errors that can arise if the inserted value just happens to contain a reserved character.
Rather than trying to just json_encode the result of mysqli_query result, you should build a more meaningful associative array. For example, you might check the result of the mysqli call and return one JSON if it was successful, and another on failure. I might suggest having the failure rendition return the error message.
You should test your PHP either in a web browser, or test it from a device using something like Charles. Make sure you're getting back the JSON you expected before you go too far with your client code. Bottom line, see if you can test the client code and the server code in isolation of each other (or keeping it as simple as possible at first).
I'm not familiar with this $_SERVER['HTTP_JSON']; construct. If that works for you, great, but it doesn't work on my server. I've historically done fopen of php://input as illustrated below.
For example, this is a different database/table, but it might illustrate the idea of what the PHP code might look like:
// read JSON input
$handle = fopen("php://input", "rb");
$raw_post_data = '';
while (!feof($handle)) {
$raw_post_data .= fread($handle, 8192);
}
fclose($handle);
$request_data = json_decode($raw_post_data, true);
// prepare header for reply
header("Content-Type: application/json");
// open database
$mysqli = new mysqli($host, $userid, $password, $database);
// check connection
if ($mysqli->connect_errno) {
echo json_encode(array("success" => false, "message" => $mysqli->connect_error, "sqlerrno" => $mysqli->connect_errno));
exit();
}
// perform the insert
$sql = "INSERT INTO locations (message, device, longitude, latitude) VALUES (?, ?, ?, ?)";
if ($stmt = $mysqli->prepare($sql)) {
$stmt->bind_param("ssdd", $request_data["message"], $request_data["device"], $request_data["latitude"], $request_data["longitude"]);
if (!$stmt->execute())
$response = array("success" => false, "message" => $mysqli->error, "sqlerrno" => $mysqli->errno, "sqlstate" => $mysqli->sqlstate);
else
$response = array("success" => true);
$stmt->close();
} else {
$response = array("success" => false, "message" => $mysqli->error, "sqlerrno" => $mysqli->errno, "sqlstate" => $mysqli->sqlstate);
}
$mysqli->close();
echo json_encode($response);
Obviously, change this for your tables, but it illustrates some of the above concepts. I would generally add more error checking (e.g. the Content-Type of the request, test to make sure variables were set before I tried to use them, etc.), but you probably get the idea.
On the client side, there are also a few more minor observations:
The most serious problem is the use of sendSynchronousRequest. Use sendAsynchronousRequest instead (or any of a myriad of other, asynchronous techniques). Never issue synchronous requests from the main thread.
When parsing the response, resString will contain the raw JSON. I don't know what the jsonData variable you reference when building jsonString, but that doesn't look right.
If you want to parse the response, it would be:
NSError *parseError;
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
By the way, the above assumes you return a JSON dictionary in your response, like I do in my example, rather than what your original JSON did.
Hello i'm trying to use mutiple json objects as shown in example below.
[
{
"DateandTime" : "1025",
"LoggingLevel" : "ERROR",
"Description" : "Test"
}
]
[
{
"DateandTime" : "1025",
"LoggingLevel" : "ERROR",
"Description" : "Test"
}
]
This is how it's created from the iOS side because i only create one of the objects at a time because it's for a report logging system and only need a message to be passed when it's needed. So the Json objects are created at separate times and appended to a file.
I know a valid Json string would look like this below.
[
{
"DateandTime" : "1025",
"LoggingLevel" : "ERROR",
"Description" : "Test"
},
{
"DateandTime" : "1025",
"LoggingLevel" : "ERROR",
"Description" : "Test"
}
]
However that's not what i need. Is there a way of using the two separate Json Objects?
iOS
NSString *DataString = [NSString stringWithFormat:#"{ \"DateandTime\":\"%#\", \"Description\":\"%#\", \"LoggingLevel\":\"%#\" }", #"1025", logString, [self getLogTypeName:(LOGS)level]];
NSMutableArray *CurrentData = [NSJSONSerialization JSONObjectWithData:[DataString dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil];
NSMutableArray *ArrayOfData = [[NSMutableArray alloc] init];
[ArrayOfData addObject:CurrentData];
NSData *JsonObject = [NSJSONSerialization dataWithJSONObject:ArrayOfData options:0 error:nil];
NSString *jsonString = [[NSString alloc] initWithData:JsonObject encoding:NSUTF8StringEncoding];
post = [NSString stringWithFormat:#"Message=%#", jsonString];
PHP
$file = 'testingFile.txt';
// Open the file to get existing content
$current = file_get_contents($file);
if (isset($_POST['Message'])) {
// Append a new person to the file
$current .= $_POST['Message'] . PHP_EOL;
// Write the contents back to the file
file_put_contents($file, $current);
} else {
$Contents = file_get_contents($file);
echo $Contents;
}
Javascript
function GetLoggingData() {
$.get("../ISOSEC/logging/TestPHP.php", function(data){
$.each($.parseJSON(data), function(idx, obj) {
console.log(obj.DateandTime);
console.log(obj.LoggingLevel);
console.log(obj.Description);
AddLog(obj.DateandTime, obj.LoggingLevel, obj.Description);
});
});
}
Could anyone show me how i could merge the objects together if there is already a json object in the file or is there any other work around?
Thanks.
As I mentioned in my comment, you would be better off generating a file with valid JSON rather than trying to parse your own JSON syntax.
You can do this by parsing the existing JSON and appending the objects as needed.
Conceptual (not tested) example:
NSMutableArray *currentData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers errors:&errors];
[currentData addObject:newObject];
NSData *updatedJSONData = [NSJSONSerialization dataWithJSONObject:currentData options:0 errors:&errors];
Update
Okay, so as far as I see your question, you have two environments. You have the client and the server. What I didn't catch is that the individual log messages are handled by your PHP script.
Here's what I would do in your app:
NSError *error;
// Simplify the generation of your dictionary
NSDictionary *logLine = #{
#"DateandTime": #"1024"
#"Description": logString,
#"LoggingLevel": [self getLogTypeName:(LOGS)level]
};
// Create JSON string from dictionary
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:logLine options:0 error:&error];
NSString *jsonStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// Now post jsonStr as the HTTP body to your PHP script
And this is what I would do in your script:
<?php
$logFileName = 'testingFile.txt';
// Read your current log file contents
$currentLogJSON = file_get_contents($logFileName);
// Check the log for any contents
if (empty($currentLogJSON)) {
$currentLog = [];
} else {
$currentLog = json_decode($currentLogJSON);
// Ensure that the contents of the log file is an array
if (is_array($currentLog)) {
$currentLog = [];
}
}
// Get the new log line from HTTP body
$newLogLineJSON = file_get_contents('php://input');
// Decode the log line which is passed as JSON string
$newLogLine = json_decode($newLogLine);
if (json_last_error() == JSON_ERROR_NONE) {
// Append the log line to the current log
$currentLog[] = $newLogLine;
// Write the updated log contents to log file
file_put_contents($logFileName, json_encode($currentLog));
}
If your file will always look like the first example you could very well only make a preg_replace
This is only a subjestion it is probably a bad way of doing it but I think it could work.
Edit
Try this instead
$NewSingleJsonString = preg_replace("/.\].\[.\s+/s",',',$MultipleJsonString);
I'm trying to learn AFNetworking, and I've taken a sample from their Github. I don't know what parameters to set in dataToPostusing the the php I have. I'm new to Objective-C and php. Can someone take a look at my snippet and my php to see what I'm missing. It's hard to find "upload" tutorials for AFNetworking, but there are TONS of JSON tutorials out there.
I want to use a NSMutableURLRequest because eventually I would like to upload from an array or UITableview; here is mt code so far:
PHP:
<?php
header("Content-Type: application/json");
$uploaddir = './'; //Uploading to same directory as PHP file
$file = basename($_FILES['userfile']['name']);
$uploadFile = $file;
$randomNumber = rand(0, 99999);
$newName = $uploadDir . $randomNumber . $uploadFile;
if (!is_uploaded_file($_FILES['userfile']['tmp_name'])) {
$result = array("success" => false);
echo json_encode($result);
exit();
}
if ($_FILES['userfile']['size']> 300000) {
$result = array("success" => false, "message" =>"the uploaded file is too big");
echo json_encode($result);
exit();
}
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $newName)) {
$postsize = ini_get('post_max_size'); //Not necessary, I was using these
$canupload = ini_get('file_uploads'); //server variables to see what was
$tempdir = ini_get('upload_tmp_dir'); //going wrong.
$maxsize = ini_get('upload_max_filesize');
//echo "http://localhost:8888/upload/{$file}" . "\r\n" . $_FILES['userfile']['size'] . "\r\n" . $_FILES['userfile']['type'] ;
$result = array("success" => true,
"code" => 0,
"message" => "success",
"postsize" => $postsize,
"canupload" => $canupload,
"tempdir" => $tempdir,
"maxsize" => $maxsize);
echo json_encode($result);
}
?>
Xcode:
// 1. Create AFHTTPRequestSerializer which will create your request.
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
// 2. Create an NSMutableURLRequest.
NSMutableURLRequest *request =
[serializer multipartFormRequestWithMethod:#"POST" URLString:#"http://my.com/upload/upload.php"
parameters:nil
constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileData:imageData
name:#"userfile"
fileName:#"myimage.jpg"
mimeType:#"image/jpeg"];
}];
// 3. Create and use AFHTTPRequestOperationManager to create an AFHTTPRequestOperation from the NSMutableURLRequest that we just created.
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
AFHTTPRequestOperation *operation =
[manager HTTPRequestOperationWithRequest:request
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failure %#", error.description);
}];
// 4. Set the progress block of the operation.
[operation setUploadProgressBlock:^(NSUInteger __unused bytesWritten,
NSInteger totalBytesWritten,
NSInteger totalBytesExpectedToWrite) {
NSLog(#"Wrote %ld/%ld", (long)totalBytesWritten, (long)totalBytesExpectedToWrite);
}];
// 5. Begin!
[operation start];
ERROR (resolved):
2014-03-31 15:37:46.921 TestingUpload[7190:60b] Wrote 32768/59063
2014-03-31 15:37:46.922 TestingUpload[7190:60b] Wrote 59063/59063
2014-03-31 15:37:46.923 TestingUpload[7190:60b] Wrote 32768/59063
2014-03-31 15:37:46.923 TestingUpload[7190:60b] Wrote 59063/59063
2014-03-31 15:37:46.925 TestingUpload[7190:60b] Success {
canupload = 1;
code = 0;
maxsize = 32M;
message = success;
postsize = 32M;
success = 1;
tempdir = "/Applications/MAMP/tmp/php";
}
2014-03-31 15:37:46.927 TestingUpload[7190:60b] Success {
canupload = 1;
code = 0;
maxsize = 32M;
message = success;
postsize = 32M;
success = 1;
tempdir = "/Applications/MAMP/tmp/php";
}
Thanks for any help and explanation. Thank you!
Your PHP is looking for the field name userfile, but your Objective-C is using attachment. You must use the same field name on both platforms. I also assume the "<$php" was just a typo and that you intended "<?php".
A couple of other improvements you might want to consider:
I would suggest that you might want to change your PHP to return JSON rather than just writing text strings. It will be easier for your Objective-C code to parse the responses and differentiate between various errors and success.
For example, your if successful, your PHP might do the following:
$result = array("success" => true, "code" => 0, "message" => "success");
Or if you wanted to log those additional values, as in your existing code sample, you could:
$result = array("success" => true,
"code" => 0,
"message" => "success",
"postsize" => $postsize,
"canupload" => $canupload,
"tempdir" => $tempdir,
"maxsize" => $maxsize);
If unsuccessful, you might do:
$result = array("success" => false, "code" => 1, "message" => "file not found");
or
$result = array("success" => false, "code" => 2, "message" => "file too large");
Regardless of which $result is chosen, when done, you should JSON encode it and echo it (rather than echoing the simple text string):
echo json_encode($result);
Clearly, use whatever codes and messages you want, but the idea is to return JSON, which can be easily parsed to determine if the upload request was successful, and if not, then why. Trying to parse simple text responses from the server will be inherently fragile.
Anyway, your Objective-C can then parse this response and just check the success or code values and handle these scenarios appropriately.
I would not suggest having the PHP save the uploads in the same folder as the PHP, itself. At the very least, I'd create a dedicated subdirectory for the uploads. I'd personally choose a directory completely outside the web server's directory structure.
On the basis of your updated code sample and reported warnings/errors, I have a few observations:
You are receiving the message about multipartFormRequestWithMethod being deprecated because you're using the rendition without the error parameter, but that's been replaced with another rendition with this additional parameter. See the declaration of this method in AFURLRequestSerialization.h for more information.
Your error about the text/html is a result of the fact that PHP is sending a response with a header that reports a Content-type of text/html. If your PHP script is not sending JSON back in your PHP, you have to change your your Objective-C code so it knows to expect an HTTP response (by default, it expects JSON back):
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
That tells the AFHTTPRequestOperationManager to accept any HTTP response back from the server.
Alternatively, if you change your PHP to return JSON (and you should, IMHO), you need to not only change the PHP code to echo the json_encode of the results, but also inform PHP to specify the appropriate Content-Type in the header. So before you echo any JSON in your PHP), add the following line to the PHP code:
header("Content-Type: application/json");
You said:
I thought the content type was defined in mimetype
The mimetype defines the Content-type for that part of the multipart request. But the whole request has its own Content-type header, too. The response also bears a Content-type setting in its header. This error, is telling you that the response bore a Content-type of text/html, and it expected application/json. (See my prior point regarding fixing that.)
Now I'm trying to POST jpeg files to MySQL via PHP and GET from MySQL to iOS.
In GET method I encode jpeg(as NSData) and some related data(ex. caption, timestamp) to JSON on PHP script using json_encode().
{"caption":"(captiondata)","img":"(imagedata)","timestamp":"(timestampdata)"}
Then I set the datas into array like,
[{"caption":"(captiondata)","img":"(imagedata)","timestamp":"(timestampdata)"},
{"caption":"(captiondata)","img":"(imagedata)","timestamp":"(timestampdata)"},
.......,
{"caption":"(captiondata)","img":"(imagedata)","timestamp":"(timestampdata)"}]
I believe I can parse and get this JSON by echo (json_encode ()) on PHP and SBJsonParser on iOS but nothing returns to my App. Here's my code in iOS.(Also I use TTURLRequest by Three20)
TTURLRequest* request = [TTURLRequest requestWithURL:url delegate:self];
request.cachePolicy = cachePolicy;
TTURLJSONResponse* response = [[[TTURLJSONResponse alloc] init] autorelease];
request.response = response;
[request send];
- (void)requestDidFinishLoad:(TTURLRequest*)request {
TTURLJSONResponse* response = request.response;
NSLog(#"%#",response);
}
Can you print all the logs in requestDidFinishLoad:?
The response is the rootObject of TTURLJSONResponse.
- (void)requestDidFinishLoad:(TTURLRequest*)request {
TTURLJSONResponse *response = request.response;
NSDictionary *dict = response.rootObject;
NSLog(#"dict : %#",dict);
}
In your case,
- (void)requestDidFinishLoad:(TTURLRequest*)request {
TTURLJSONResponse* response = request.response;
NSLog(#"%#",response);
}
The response may look like <TTURLJSONResponse: 0x125398c0>
If still nothing returns, you may check the requestDidFinishLoad: is been called and not the cache issue.