为什么 NSURLSessionUploadTask 向具有基本身份验证的 PHP 端点发送数据两次?
Why does NSURLSessionUploadTask to PHP endpoint with basic authentication send data twice?
我在 macOS 10.12 Sierra 上通过 NSURLSession
使用 NSURLSessionUploadTask
和自定义委托,使用 PHP
脚本将本地文件上传到 Apache
服务器请求基本身份验证。除了上传任务似乎发送完整的文件数据 然后服务器提示 NSURLAuthenticationChallenge
并且在发送正确的凭据后,上传任务发送整个数据有效载荷。我希望基本身份验证挑战会在上传之前出现,或者如果它确实在上传之后出现,一旦确认,已经上传的数据将被接受并且不会再次上传。如果您能帮助我们仅发布一次上传的数据,我们将不胜感激。
端点脚本uploader.php:
<?php
$u = $_SERVER['PHP_AUTH_USER'];
$p = $_SERVER['PHP_AUTH_PW'];
if (($u !== 'user') || ($p !== 'password')) {
header('WWW-Authenticate: Basic realm="Restricted Area"');
header('HTTP/1.0 401 Unauthorized');
die('<h1>401 Unauthorized</h1>Access Denied.');
}
$response = 'file upload failed: upload not specified';
if (isset($_FILES['upload'])) {
$file_tmp_name = $_FILES['upload']['tmp_name'];
$file_name = $_FILES['upload']['name'];
$file_name_new = ('uploads/' . stripslashes($file_name));
if (!is_writable(dirname($file_name_new))) {
$response = 'file upload failed: directory is not writable.';
} else {
if (!move_uploaded_file($file_tmp_name, $file_name_new)) {
$response = 'file upload failed: couldn\'t move file to ' . $new_name;
} else {
$response = $file_name_new;
}
}
}
echo($response);
?>
FileUploader.m:
- (void)startUpload {
NSLog(@"starting upload");
NSURL *url = [NSURL URLWithString:@"https://www.domain.com/uploader.php"];
NSString *localPath = @"/path/to/file.ext";
NSString *inputName = @"upload";
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30.0];
request.HTTPMethod = @"POST";
NSString *boundary = [NSString stringWithFormat:@"x-mime-boundary://%@", [NSUUID UUID].UUIDString];
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
[request setValue:[NSBundle mainBundle].bundleIdentifier forHTTPHeaderField:@"User-Agent"];
NSMutableData *postData = [NSMutableData data];
[postData appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; type=\"file\"; filename=\"%@\"\r\n\r\n", inputName, localPath.lastPathComponent] dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[NSData dataWithContentsOfFile:localPath]];
[postData appendData:[[NSString stringWithFormat:@"\r\n\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[request setValue:[NSString stringWithFormat:@"%ld", postData.length] forHTTPHeaderField:@"Content-Length"];
NSURLSessionConfiguration *defaultConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:defaultConfiguration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
[[self.session uploadTaskWithRequest:request fromData:[NSData dataWithData:postData]] resume];
}
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSLog(@"URLSession didReceiveChallenge: %@", challenge.protectionSpace.authenticationMethod);
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(((credential) ? NSURLSessionAuthChallengePerformDefaultHandling : NSURLSessionAuthChallengeUseCredential), credential);
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSLog(@"NSURLSessionTask didReceiveChallenge: %@", challenge.protectionSpace.authenticationMethod);
NSString *username = @"user";
NSString *password = @"password";
NSURLCredential *credential = [NSURLCredential credentialWithUser:username password:password persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
NSLog(@"sent %ld b of %ld b (%.1f%%)", (long)totalBytesSent, (long)totalBytesExpectedToSend, (((float)totalBytesSent / (float)totalBytesExpectedToSend) * 100.0));
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"upload complete");
if (error) [NSApp presentError:error];
[self.session invalidateAndCancel];
self.session = nil;
}
简化的控制台输出:
2016-10-11 12:14:34.323485 FileUploader[23624:5580925] starting upload
2016-10-11 12:14:34.429419 FileUploader[23624:5580925] URLSession didReceiveChallenge: NSURLAuthenticationMethodServerTrust
2016-10-11 12:14:34.459239 FileUploader[23624:5580925] sent 32768 b of 10616647 b (0.3%)
2016-10-11 12:14:34.459351 FileUploader[23624:5580925] sent 65536 b of 10616647 b (0.6%)
...
2016-10-11 12:14:42.849080 FileUploader[23624:5580925] sent 10584064 b of 10616647 b (99.7%)
2016-10-11 12:14:42.849179 FileUploader[23624:5580925] sent 10616647 b of 10616647 b (100.0%)
2016-10-11 12:14:43.038092 FileUploader[23624:5580925] NSURLSessionTask didReceiveChallenge: NSURLAuthenticationMethodHTTPBasic
2016-10-11 12:14:43.040085 FileUploader[23624:5580925] sent 10649415 b of 21233294 b (50.2%)
2016-10-11 12:14:43.040141 FileUploader[23624:5580925] sent 10682183 b of 21233294 b (50.3%)
...
2016-10-11 12:14:46.508339 FileUploader[23624:5580925] sent 21200711 b of 21233294 b (99.8%)
2016-10-11 12:14:46.594864 FileUploader[23624:5580925] sent 21233294 b of 21233294 b (100.0%)
2016-10-11 12:14:46.757213 FileUploader[23624:5580925] upload complete
有几种方法可以解决这个问题。想到的前两个是:
- 在发送 POST 请求之前发送明确的 HEAD 或 GET 请求以验证凭据。这将在 >99% 的时间起作用。
- 不发送身份验证错误代码,而是发送您的应用可以识别为错误的 JSON blob,并在该 JSON blob 中为您之前上传的文件提供一个 UUID然后应用程序可以在新请求中提供将上传与用户帐户相关联的请求。这将在 100% 的时间内工作,但您需要在服务器上添加一个 cron 作业以定期删除旧文件。
这两种方法都涉及客户端和服务器端的更改。
我在 macOS 10.12 Sierra 上通过 NSURLSession
使用 NSURLSessionUploadTask
和自定义委托,使用 PHP
脚本将本地文件上传到 Apache
服务器请求基本身份验证。除了上传任务似乎发送完整的文件数据 然后服务器提示 NSURLAuthenticationChallenge
并且在发送正确的凭据后,上传任务发送整个数据有效载荷。我希望基本身份验证挑战会在上传之前出现,或者如果它确实在上传之后出现,一旦确认,已经上传的数据将被接受并且不会再次上传。如果您能帮助我们仅发布一次上传的数据,我们将不胜感激。
端点脚本uploader.php:
<?php
$u = $_SERVER['PHP_AUTH_USER'];
$p = $_SERVER['PHP_AUTH_PW'];
if (($u !== 'user') || ($p !== 'password')) {
header('WWW-Authenticate: Basic realm="Restricted Area"');
header('HTTP/1.0 401 Unauthorized');
die('<h1>401 Unauthorized</h1>Access Denied.');
}
$response = 'file upload failed: upload not specified';
if (isset($_FILES['upload'])) {
$file_tmp_name = $_FILES['upload']['tmp_name'];
$file_name = $_FILES['upload']['name'];
$file_name_new = ('uploads/' . stripslashes($file_name));
if (!is_writable(dirname($file_name_new))) {
$response = 'file upload failed: directory is not writable.';
} else {
if (!move_uploaded_file($file_tmp_name, $file_name_new)) {
$response = 'file upload failed: couldn\'t move file to ' . $new_name;
} else {
$response = $file_name_new;
}
}
}
echo($response);
?>
FileUploader.m:
- (void)startUpload {
NSLog(@"starting upload");
NSURL *url = [NSURL URLWithString:@"https://www.domain.com/uploader.php"];
NSString *localPath = @"/path/to/file.ext";
NSString *inputName = @"upload";
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30.0];
request.HTTPMethod = @"POST";
NSString *boundary = [NSString stringWithFormat:@"x-mime-boundary://%@", [NSUUID UUID].UUIDString];
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
[request setValue:[NSBundle mainBundle].bundleIdentifier forHTTPHeaderField:@"User-Agent"];
NSMutableData *postData = [NSMutableData data];
[postData appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; type=\"file\"; filename=\"%@\"\r\n\r\n", inputName, localPath.lastPathComponent] dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[NSData dataWithContentsOfFile:localPath]];
[postData appendData:[[NSString stringWithFormat:@"\r\n\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[request setValue:[NSString stringWithFormat:@"%ld", postData.length] forHTTPHeaderField:@"Content-Length"];
NSURLSessionConfiguration *defaultConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:defaultConfiguration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
[[self.session uploadTaskWithRequest:request fromData:[NSData dataWithData:postData]] resume];
}
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSLog(@"URLSession didReceiveChallenge: %@", challenge.protectionSpace.authenticationMethod);
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(((credential) ? NSURLSessionAuthChallengePerformDefaultHandling : NSURLSessionAuthChallengeUseCredential), credential);
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSLog(@"NSURLSessionTask didReceiveChallenge: %@", challenge.protectionSpace.authenticationMethod);
NSString *username = @"user";
NSString *password = @"password";
NSURLCredential *credential = [NSURLCredential credentialWithUser:username password:password persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
NSLog(@"sent %ld b of %ld b (%.1f%%)", (long)totalBytesSent, (long)totalBytesExpectedToSend, (((float)totalBytesSent / (float)totalBytesExpectedToSend) * 100.0));
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"upload complete");
if (error) [NSApp presentError:error];
[self.session invalidateAndCancel];
self.session = nil;
}
简化的控制台输出:
2016-10-11 12:14:34.323485 FileUploader[23624:5580925] starting upload
2016-10-11 12:14:34.429419 FileUploader[23624:5580925] URLSession didReceiveChallenge: NSURLAuthenticationMethodServerTrust
2016-10-11 12:14:34.459239 FileUploader[23624:5580925] sent 32768 b of 10616647 b (0.3%)
2016-10-11 12:14:34.459351 FileUploader[23624:5580925] sent 65536 b of 10616647 b (0.6%)
...
2016-10-11 12:14:42.849080 FileUploader[23624:5580925] sent 10584064 b of 10616647 b (99.7%)
2016-10-11 12:14:42.849179 FileUploader[23624:5580925] sent 10616647 b of 10616647 b (100.0%)
2016-10-11 12:14:43.038092 FileUploader[23624:5580925] NSURLSessionTask didReceiveChallenge: NSURLAuthenticationMethodHTTPBasic
2016-10-11 12:14:43.040085 FileUploader[23624:5580925] sent 10649415 b of 21233294 b (50.2%)
2016-10-11 12:14:43.040141 FileUploader[23624:5580925] sent 10682183 b of 21233294 b (50.3%)
...
2016-10-11 12:14:46.508339 FileUploader[23624:5580925] sent 21200711 b of 21233294 b (99.8%)
2016-10-11 12:14:46.594864 FileUploader[23624:5580925] sent 21233294 b of 21233294 b (100.0%)
2016-10-11 12:14:46.757213 FileUploader[23624:5580925] upload complete
有几种方法可以解决这个问题。想到的前两个是:
- 在发送 POST 请求之前发送明确的 HEAD 或 GET 请求以验证凭据。这将在 >99% 的时间起作用。
- 不发送身份验证错误代码,而是发送您的应用可以识别为错误的 JSON blob,并在该 JSON blob 中为您之前上传的文件提供一个 UUID然后应用程序可以在新请求中提供将上传与用户帐户相关联的请求。这将在 100% 的时间内工作,但您需要在服务器上添加一个 cron 作业以定期删除旧文件。
这两种方法都涉及客户端和服务器端的更改。