iOS - pipe file to HTTP request - ios

I'm trying to upload a large (like in don't fit in memory) file to S3 using presigned request. I finally got it to work with curl
curl -v -T video.mp4 "http://<myBucket>.s3.amazonaws.com/video.mp4?AWSAccessKeyId=<myAccessKey>&Expires=1492187347&Signature=vpcUnvGALlVXju31Qk2nXNmBTgc%3D"
I'm trying to now do that from my app. I first tried with AF (that I'd rather not use):
let videoPath = Bundle.main.path(forResource: "media", ofType: "mov")!
let videoUrl = URL(fileURLWithPath: videoPath)
let presignedUrl = "http://<myBucket>.s3.amazonaws.com/video.mp4?AWSAccessKeyId=<myAccessKey>&Expires=1492187347&Signature=vpcUnvGALlVXju31Qk2nXNmBTgc%3D"
request = Alamofire.upload(videoUrl, to: presignedUrl, method: .put, headers: [:])
request.responseString(completionHandler: { response in
print(response)
})
request.resume()
Which prints an successful response that is actually an error:
The request signature we calculated does not match the signature you provided. Check your key and signing method.
I've read in a few places that there might be issues with headers, and I'd rather not use AF here anyway.
What is in Swift an equivalent of curl -T filePath url ?

When signing the request, pass the content type. In node:
var AWS = require('aws-sdk');
AWS.config.update({
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
region: 'us-east-1',
});
var s3 = new AWS.S3();
var params = {
Bucket: bucketName,
Key: 'path/my-video.mp4',
Expires: 1800000,
ContentType: 'video/mp4',
};
var signedUrl = s3.getSignedUrl('putObject', params)
// returns something like:
// https://<- my bucket ->.s3.amazonaws.com/path/my-video.mp4?AWSAccessKeyId=<- my access key ->&Content-Type=video%2Fmp4&Expires=1492203291&Signature=HeRBUObYQiQ6FIH%2Fg%2FOI0qv57VY%3D
and now in swift:
let request = NSMutableURLRequest(url: presignedUrl)
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
request.httpMethod = "PUT"
request.setValue("video/mp4", forHTTPHeaderField: "Content-Type")
request.setValue("<- my bucket ->.s3.amazonaws.com", forHTTPHeaderField: "host")
let uploadTask = session.uploadTask(with: request as URLRequest, fromFile: videoUrl) { data, response, error in
...
}
uploadTask.resume()

Related

iOS swift post request with binary body

I want to make a POST request from iOS (swift3) which passes a chunk of raw bytes as the body. I had done some experimenting which made me thought the following worked:
let url = URL(string: "https://bla/foo/bar")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = Data(hex: "600DF00D")
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) in
"DATA \(data ?? Data()) RESPONSE \(response) ERROR \(error)".print()
}
task.resume()
Didn't know it was a problem until I tried sending something simple like a single 0xF0. At which point my tornado server started complaining that I was sending it
WARNING:tornado.general:Invalid x-www-form-urlencoded body: 'utf-8' codec can't decode byte 0xf0 in position 2: invalid continuation byte
Am I just supposed to set some header somehow? Or is there something different I need to do?
The two common solutions are:
Your error message tells us that the web service is expecting a x-www-form-urlencoded request (e.g. key=value) and in for the value, you can perform a base-64 encoding of the binary payload.
Unfortunately, base-64 strings still need to be percent escaped (because web servers generally parse + characters as spaces), so you have to do something like:
let base64Encoded = data
.base64EncodedString(options: [])
.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
.data(using: String.Encoding.utf8)!
var body = "key=".data(using: .utf8)!
body.append(base64Encoded)
var request = URLRequest(url: url)
request.httpBody = body
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil else {
print(error!)
return
}
...
}
task.resume()
Where:
extension CharacterSet {
static let urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]#" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
return allowed
}()
}
For more discussion on that character set, see point 2 in this answer: https://stackoverflow.com/a/35912606/1271826.
Anyway, when you receive this on your server, you can retrieve it as and then reverse the base-64 encoding, and you'll have your original binary payload.
Alternatively, you can use multipart/formdata request (in which you can supply binary payload, but you have to wrap it in as part of the broader multipart/formdata format). See https://stackoverflow.com/a/26163136/1271826 if you want to do this yourself.
For both of these approaches, libraries like Alamofire make it even easier, getting you out of the weeds of constructing these requests.

Authenticate Client certificate and get response from server

I am working on get data from client server(ratin24 API). The API basically work after Authentication means I have one certificate and I was authenticate it with NSURLSession "didReceiveChallenge" delegate method. Everything is working fine but now issue is that I Got only header parts as a response not BOTY. so how to get actual data from there. I Pass XML Parameter in request body and the response should be XML but Got only header so please help me how to get BODY data in this situation.
let xmlString = "<?xml version='1.0' encoding='ISO-8859-1'?><TICKETANYWHERE><COUPON VER='1.0'><TEMPLATELIST /></COUPON></TICKETANYWHERE>"
let xmlData = xmlString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
let request = NSMutableURLRequest(URL: NSURL(string: "My URL")!)
request.HTTPMethod = "POST"
request.HTTPBody = xmlData
request.addValue("text/html; charset=ISO-8859-1", forHTTPHeaderField: "Content-Type")
struct SessionProperties {
static let identifier : String! = "url_session_background_download"
}
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let backgroundSession = NSURLSession(configuration: configuration, delegate:self as? NSURLSessionDelegate, delegateQueue: NSOperationQueue.mainQueue())
let downloadTask = backgroundSession.downloadTaskWithRequest(request){ (data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
if (statusCode == 200) {
print("Everyone is fine, file downloaded successfully.")
}
}
downloadTask.resume()
Response Data (only Header) body ? :
status code: 200, headers {
Connection = close;
"Content-Length" = 23113;
"Content-Type" = "text/html; charset=ISO-8859-1";
Date = "Mon, 27 Jun 2016 11:36:12 GMT";
Server = "Apache/2.2.15 (CentOS)";
}
The response object isn't supposed to contain the body data. An NSURLResponse object contains only metadata, such as the status code and headers. The actual body data should be in the NSData object for a data task, or in the provided file for a download task.
Note that for a download task the first parameter is an NSURL, not an NSData object. That NSURL contains the location of a file on disk from which you must immediately read the response data in your completion handler or move the file to a permanent location.

How to set HTTPBody?

In Swift I'm trying to make a post request (using the NSURLSession) to sign in a user to a WebAPI web services.
The Url is www.myurltest.com/Token and I must pass the following string as POST body:
grant_type=password&username=MyUsername&password=MyPassword.
So in Swift I've make:
let session = NSURLSession.sharedSession();
let url = NSURL(string:"www.myurltest.com/Token");
let request = NSMutableURLRequest(URL: url!)
request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.HTTPMethod = "POST"
Now I want to set the POST body (that is a string) but I don't know how:
request.HTTPBody = ?????
Thanks.
You're almost there, you just need to turn the string into an NSData object. If your string is in a variable named body, your code will look like request.HTTPBody = body.dataUsingEncoding(NSUTF8StringEncoding)

JSON NSURLRequest with credentials

I have an api in my swift app that needs some extra authorization. There are some examples provided by the service but none in swift.
My Code:
let request = NSURLRequest(URL: NSURL(string: "https://www.kimonolabs.com/api/au4sl00w?apikey=iFotcJDm95fB6Ua7XiZRDZA0jl3uYWev")!)
Example in Python
import urllib2
request = urllib2.Request("https://www.kimonolabs.com/api/au4sl00w? apikey=iFotcJDm95fB6Ua7XiZRDZA0jl3uYWev", headers={"authorization" : "Bearer A5ve02gq40itf0eoYfT5ny6drZwcysxx"})
contents = urllib2.urlopen(request).read()
Example in Ruby
require 'rest_client'
response = RestClient.get('https://www.kimonolabs.com/api/au4sl00w?apikey=iFotcJDm95fB6Ua7XiZRDZA0jl3uYWev', {'authorization' => 'Bearer A5ve02gq40itf0eoYfT5ny6drZwcysxx'});
print(response);
Example in R
library('RCurl')
library('rjson')
json <- getURL('https://www.kimonolabs.com/api/au4sl00w?apikey=iFotcJDm95fB6Ua7XiZRDZA0jl3uYWuc',
httpheader = c(authorization='Bearer A5ve02gq40itf0eoYfT5ny6drZwcysxx'))
obj <- fromJSON(json)
print(obj)
So, how can I do this in Swift?
Modified answer from: "How to make an HTTP request + basic auth in Swift".
I believe it would look something like this (and assuming your API_ID is au4sl00w) :
let token = "yourToken"
// create the request
let url = NSURL(string: "https://www.kimonolabs.com/api/au4sl00w?apikey=iFotcJDm95fB6Ua7XiZRDZA0jl3uYWev")
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
And be sure to create a new access token, now that this one is public :)

Uploading to azure blob storage from SAS URL returns 404 status

I am using Azure Mobile Services API endpoint to return a private shared access signature URL to my azure storage container like so:
var blobService = azure.createBlobService(accountName, key, host);
blobService.createContainerIfNotExists(containerName, function(err) {
if (err) {
cb(err, null);
return;
}
// Generate a 5 minute access
var expiryDate = minutesFromNow(5);
var sharedAccessPolicy = {
AccessPolicy: {
Permissions: azure.Constants.BlobConstants.SharedAccessPermissions.WRITE,
Expiry: expiryDate
}
};
// Generate the URL with read access token
var sasURL = blobService.generateSharedAccessSignature(containerName, blobName, sharedAccessPolicy);
var urlForDownloading = sasURL.baseUrl + sasURL.path + '?' + qs.stringify(sasURL.queryString);
cb(null, urlForDownloading);
});
function minutesFromNow(minutes) {
var date = new Date();
date.setMinutes(date.getMinutes() + minutes);
return date;
};
I then return this URL to my iOS client to upload the file and process it as:
client.invokeAPI("document/\(document.idValue).\(document.fileExtension)",
body: nil,
HTTPMethod: "PUT",
parameters: nil,
headers: nil) { result, response, error in
if let dictResult = result as? NSDictionary {
// Get the SAS URL to write directly to the blob storage
if let location = dictResult["location"] as? String {
let url = NSURL(string: location)
let request = NSURLRequest(URL: url)
let uploadTask = session.uploadTaskWithRequest(request, fromFile: localFile) { data, response, error in
if completionBlock != nil {
let success = (error == nil && httpResponse.statusCode == 200)
completionBlock!(success)
}
}
}
}
}
uploadTask.resume()
The iOS client gets a 404 response with a message of
<?xml version="1.0" encoding="utf-8"?><Error><Code>ResourceNotFound</Code><Message>The specified resource does not exist.
The container does exist in the storage account and requests to get blobs from the container with the access keys are successful. This new blob won't exist as it is a new upload, but why am I getting a 404 for a write request to the container?
Found the solution...
let request = NSURLRequest(URL: url) produces a GET request and even passing it to uploadTaskWithRequest persists this request type, whereas I thought this call would change it to a PUT or POST request signifying the upload.
Defining the iOS request as
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "PUT"
was successful. And the response returned was a 201 created.

Resources