28

I'm using curl to call into a Java ReST API to retrieve a URL. Java then generates a pre-signed URL for S3 upload using my S3 credentials, and returns that in the ReST reply. Curl takes the URL and uses that for upload to S3, but S3 returns 403 "The request signature we calculated does not match the signature you provided. Check your key and signing method."

Here is the code I'm using to generate the pre-signed URL:

public class S3Util {
    static final AmazonS3 s3 = new AmazonS3Client( new AWSCredentials() {
        @Override
        public String getAWSAccessKeyId() {
            return "XXXXXXX";
        }
        @Override
        public String getAWSSecretKey() {
            return "XXXXXXXXXXXXXX";
        }
    });
    static final String BUCKET = "XXXXXXXXXXXXXXXXXXXXXXXXXXX";

    static public URL getMediaChunkURL( MediaChunk mc, HttpMethod method ) {
        String key = ...
        //way in the future (for testing)...
        Date expiration = new Date( System.currentTimeMillis() + CalendarUtil.ONE_MINUTE_IN_MILLISECONDS*60*1000 );

        GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest(BUCKET, key, method);
        req.setExpiration(expiration);
        req.addRequestParameter("Content-Type", "application/octet-stream");

        //this gets passed to the end user:
        return s3.generatePresignedUrl(req);
    }
}

and in curl, run from bash, I execute this:

echo Will try to upload chunk to ${location}
curl -i -X POST \
        -F 'Content-Type=application/octet-stream' \
        -F "file=@${fileName}" \
        ${location} || (echo upload chunk failed. ; exit 1 )

Among other things, I have tried PUT, and I have tried "Content-type" (lowercase T). I realize I'm missing something (or somethings) obvious, but after reading the appropriate docs, googling and looking at lots of similar questions I'm not sure what that is. I see lots of hints about required headers, but I thought the resigned URL was supposed to eliminate those needs. Maybe not?

TIA!

Update:

Just to be clear, I have tested downloads, and that works fine.

Java looks like:

GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest(BUCKET, key, HttpMethod.GET);
req.setExpiration(expiration);

and curl is simply:

curl -i ${location}
Bjorn Roche
  • 11,279
  • 6
  • 36
  • 58

4 Answers4

49

I've been able to generate a pre-signed URL via C# and upload it thereafter via curl as expected. Given my tests I suspect you are indeed not using curl correctly - I've been able to upload a file like so:

curl -v --upload-file ${fileName} ${location}

The parameter -v dumps both request and response headers (as well as the SSL handshake) for debugging and illustration purposes:

> PUT [...] HTTP/1.1
> User-Agent: curl/7.21.0 [...]
> Host: [...]
> Accept: */*
> Content-Length: 12
> Expect: 100-continue

Please note, that --upload-file (or -T) facilitates PUTas expected, but adds more headers as appropriate, yielding a proper response in return:

< HTTP/1.1 100 Continue
< HTTP/1.1 200 OK
< x-amz-id-2: [...]
< x-amz-request-id:  [...]
< Date: Tue, 31 Jan 2012 18:34:56 GMT
< ETag: "253801c0d260f076b0d5db5b62c54824"
< Content-Length: 0
< Server: AmazonS3
Steffen Opel
  • 63,899
  • 11
  • 192
  • 211
  • Thanks for this. I am sticking with my solution because I got it working first, but I am marking this as correct because it addresses the original query, and, honestly, I think it's better. – Bjorn Roche Jan 31 '12 at 20:30
  • 13
    For me the pre-signed urls contained characters that required me to surround the entire url with quotes. Once I did that, your answer worked perfectly. – Kirk Woll Mar 17 '14 at 21:52
  • For me, using pre-signed urls with POST didn't work. Switching just the method (both in GeneratePresignedUrlRequest and in curl) solved the problem. – omnomnom Jan 28 '17 at 16:01
8

when doing this with curl, you need to place the url in single quotes or else half of the query string gets chopped off (the part with the key/signature).

handler
  • 1,463
  • 11
  • 11
4

The way to generate the URL:

private static URL generateRUL(String objectKey, String ACCESS_KEY, String SECRET_KEY, String BUCKET_NAME) {
    AmazonS3 s3Client = new AmazonS3Client(new BasicAWSCredentials(ACCESS_KEY, SECRET_KEY));
    URL url = null;

    try {
        GeneratePresignedUrlRequest request  = new GeneratePresignedUrlRequest(BUCKET_NAME, objectKey);
        request.setMethod(com.amazonaws.HttpMethod.PUT);
        request.setExpiration(new Date( System.currentTimeMillis() + (60 * 60 * 1000)));

        // Very important ! It won't work without adding this! 
        // And request.addRequestParameter("Content-Type", "application/octet-stream") won't work neither
        request.setContentType("application/octet-stream");

        url = s3Client.generatePresignedUrl(request ); 
    } catch (AmazonServiceException exception) { 
    } catch (AmazonClientException ace) { }

    return url;
}

The way to upload the file:

public int upload(byte[] fileBytes, URL url) {
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setDoOutput(true);
    connection.setRequestMethod("PUT");
    connection.setRequestProperty("Content-Type", "application/octet-stream"); // Very important ! It won't work without adding this!
    OutputStream output = connection.getOutputStream();

    InputStream input = new ByteArrayInputStream(fileBytes);
    byte[] buffer = new byte[4096];
    int length;
    while ((length = input.read(buffer)) > 0) {
        output.write(buffer, 0, length);
    }
    output.flush();

    return connection.getResponseCode();
}
Yuwen
  • 963
  • 11
  • 8
  • request.setContentType("application/octet-stream"); it works! I got 403 forbidden error before. I want to know why it works – Jason Jul 24 '17 at 02:17
1

Despite the fact that GeneratePresignedUrlRequest accepts an http method argument (and has a setMethod function), it appears to be unusable for anything but GET.

http://wiki.nercomp.org/wiki/images/0/05/AmazonWebServices.pdf states "The practice of signing a request and giving it to a third-party for execution is suitable only for simple object GET requests." Perhaps setting another method can be used for something, but apparently not this.

So, instead, I had to follow the instructions here:

http://aws.amazon.com/articles/1434?_encoding=UTF8&jiveRedirect=1

This is more complex, because the client is required to post a complete form, rather than just using a URL, and also means all that post info has to be communicated to the client separately, but it does seem to work.

Bjorn Roche
  • 11,279
  • 6
  • 36
  • 58
  • Interesting, I wasn't aware of this (former?) limitation - it has been removed from the [current documentation](http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth) on this subject though, and e.g. [PUT on S3 : Problem](https://forums.aws.amazon.com/thread.jspa?messageID=262822񀊦) illustrates an apparently working example from of using `PUT` as well (despite the side problems discussed there). – Steffen Opel Jan 31 '12 at 16:44
  • 1
    I've just confirmed that using PUT is indeed working just fine as expected by generating a pre-signed URL via C# and downloading thereafter via _curl_ - I'll post more details as an answer after a break. – Steffen Opel Jan 31 '12 at 18:29
  • @SteffenOpel i believe the problem is in generating the put request when using java sdk, more specifically in the signature. An url returned by this can be used to query the server but you will receive a forbidden error, along with some string to sign bytes (The request), If you sign this string with anything else the signature WILL be different. you can even exchange this new signature with the one on the request and the query will work. maybe this is a java bug – Pochi Jul 18 '12 at 06:48