1
votes

I am trying to upload an image from my iPhone app to S3 and then store the S3 url back into my rails app. I am not supposed to embed credentials in the iOS app so the approach i'm taking is to:

I did my best to follow all the directions I found online but it's not working and the result of step 3 returns Error 401 forbidden. Since I am a newbie at this I don't even know what I am doing wrong.

In Step 2, my code looks like this:

def getS3Url
  s3 = AWS::S3.new(
    :access_key_id => "MY S3 KEY",
    :secret_access_key => "MY SECRET ACCESS KEY"
  )
  object = s3.buckets[params["bucket"]].objects[params["path"]]
  @s3url = object.url_for(:write, { :expires => 20.minutes.from_now, :secure => true }).to_s
end

The url returned from step2 looks something like this: https://s3.amazonaws.com/myapp-bucket-name/images/avatar/user1.png?AWSAccessKeyId=[access key id]&Expires=[expiration timestamp]&Signature=[Signature]

And once i get that URL i try to post to it by doing the following:

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager POST:[responseObject valueForKey:@"s3url"] parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
  [formData appendPartWithFileData:jpegData name:@"file" fileName:self.filename mimeType:@"image/png"];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
  NSLog(@"Success: %@", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
  NSLog(@"Error: %@", error);
}];

In this code I extract the url out from the returned object with [responseObject valueForKey:@"s3url"], and then pass that as the URL to post. But it doesn't work. Here's the log when i run it in XCode:

Error: Error Domain=AFNetworkingErrorDomain Code=-1011 "Request failed: forbidden (403)" UserInfo=0x156daaf0 {NSErrorFailingURLKey=https://s3.amazonaws.com/myapp-bucket-name/images/avatar/user1.png?AWSAccessKeyId=[access key id]&Expires=[expiration timestamp]&Signature=[Signature], NSLocalizedDescription=Request failed: forbidden (403), NSUnderlyingError=0x156aef90 "Request failed: unacceptable content-type: application/xml", AFNetworkingOperationFailingURLResponseErrorKey= { URL: https://s3.amazonaws.com/myapp-bucket-name/images/avatar/user1.png?AWSAccessKeyId=[access key id]&Expires=[expiration timestamp]&Signature=[Signature] } { status code: 403, headers { Connection = close; "Content-Type" = "application/xml"; Date = "Mon, 30 Jun 2014 07:21:33 GMT"; Server = AmazonS3; "Transfer-Encoding" = Identity; "x-amz-id-2" = "FJwEeOjV1/osJKgKeHO+/OjXVBEbvW09XxNX2kn1UYIuHswU+LKh0mJODRJDNLXm"; "x-amz-request-id" = 46E84D0967B6D4CD; } }}

At this point I don't even know what I am doing wrong. Maybe I'm not even posting to the correct URL. Maybe I need to do more than just POST. I spent the entire weekend trying to figure this out and failed. Could someone please help? Thanks.

2
@Viad, Did you found any solution to this, I am haveing exact same error, I also found your Thread forums.aws.amazon.com/thread.jspa?threadID=155506Abhishek

2 Answers

6
votes

I faced a similar "challenge". I had to upload with AFNetworking 2.0 an image to an S3 bucket with a pre-signed URL from my server. In one of my many try and error attempts of doing it I got the same 403 error, and what happened to me was that I had to put the right headers in the request:

  • Content-Type with the mime type of the image
  • x-amz-acl as public-read for my bucket configuration

The Content-Length seemed to be optional and note that I haven't uploaded the image in multipart.

So this is what I ended up doing:

+(void) uploadImage:(UIImage *)image atUrl:(NSString *)url withMimeType:(NSString *)mimeType withSuccess:(void (^)(id responseObject))success failure:(void (^)(NSError *error))failure {
    NSData *imageData = UIImageJPEGRepresentation(image, 0.1);
    NSURL *requestURL = [NSURL URLWithString:url];
    AFHTTPSessionManager *client = [[AFHTTPSessionManager alloc] initWithBaseURL:requestURL];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    [request setHTTPMethod:@"PUT"];
    [request setValue:mimeType forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody:imageData];
    [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)[imageData length]] forHTTPHeaderField:@"Content-Length"];
    [request setValue:@"public-read" forHTTPHeaderField:@"x-amz-acl"];
    [request setURL:requestURL];

    NSURLSessionDataTask *task = [client dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
        if (error == nil) {
            if (success) {
                success(responseObject);
            }
        } else {
            if (failure) {
                failure(error);
            }
        }
    }];
    [task resume];
}

where url is the presigned url that I've got from my server. Check the JPEG compression that I have at 0.1 as you might want a different compression. In my case the image quality is not important.

1
votes

Adding to josebama's answer above,

I didnt need to add "x-amz-acl" header field, but instead I added "x-amz-date" and "authorization" headers. Both these headers were returned, from the an API that handled communication with Amazon service, along with a signed URL. The upload to the URL was only successful when I added the two aforementioned header values.

Simply including the "x-amz-acl" header, in my case, would result in a failure to upload.

Perhaps some server side parameters differ or perhaps some setup parameters for amazon vary, needless to say that a solution that works for me might not work for others so it might be good a idea to look at your backend setup a bit..