S3 ,verify a signed URL in PHP, Deconstruct signed URL - php

I am using s3 direct uploads along with a database to store the URLS of the files (along with other data like who uploaded etc).
To allow direct upload to s3, I'm creating a presigned URL like :
$s3 = App::make('aws')->createClient('s3', [
'credentials' => [
'key' => 'AAAAAAAAAAAAAAA',
'secret' => 'YYYYYYYYYYYYYYYYYYYY',
]
]);
$command = $s3->getCommand('PutObject', [
'#use_accelerate_endpoint'=>true,
'Bucket' => 'remdev-experimental',
'Key' => "newest newest.txt",
'Metadata' => array(
'foo' => "test",
)
]);
return response()->json(((string)$s3->createPresignedRequest($command, '+1 minutes')->getUri()));
Now, after the file from the client has finished uploading , I want my server to know about it. So I will require the client to send me a request , notifying about the fact that he has finished uploading. For this, I think the simplest(and also secure) way is to just allow the client to send back the signed URL that he just sent back.
Is there a way to parse the URL ?
I am interested in getting the object key , and more importantly , I want to verify that the URL has not been tampered with (meaning, the signature in the URL should match the rest of the contents). How can I do this in php sdk ?

The signed URL is the file's URL with the signature information in the query data. So a signed request for bucket: remdev-experimental file: abc.txt looks like https://s3.amazonaws.com/remdev-experimental/abc.txt?X-Amz-Date=date&X-Amz-Expires=60&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=signature&X-Amz-Credential=SOMEID/20160703/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=Host&x-amz-security-token=some-long-token so all you need to do is get the URL's Path (/remdev-experimental/abc.txt and take everything after the 2nd slash.
Also you should be aware that you can have S3 redirect the browser to a URL using success_action_redirect in an HTTP post policy
Lastly you can have S3 trigger a notification to your server (via SQS, SNS, or Lambda) whenever a file is uploaded.

Related

Dynamic S3 Link for downloading or viewing a PDF

I am storing some customer PDFs in S3 for multiple parties to either view in the browser or download. The trouble is I can only get a single file in S3 to either always download or always view in the browser.
I could just upload the same file twice with each having its own ContentDisposition, but that seems wasteful when ideally it could be as simple as adding something like ?ContentDisposition=inline to the public bucket URL.
My Question: How can dynamically set a ContentDisposition for a single S3 file?
For context, my current code looks something like this:
$s3_object = array(
'ContentDisposition' => sprintf('attachment; filename="%s"', addslashes($basename)),
'ACL' => 'public-read',
'ContentType' => 'pdf',
'StorageClass' => 'REDUCED_REDUNDANCY',
'Bucket' => 'sample',
'Key' => static::build_file_path($path, $filename, $extension),
'Body' => $binary_content,
);
$result = $s3_client->putObject($s3_object);
Also, I did try to search for this elsewhere in SO, but most people seem to just be looking for one or the other, so I didn't find any SO answers that showed how to do this.
I ended up stumbling across the definitive answer for this today (over a month later) while looking at other S3 documentation. Going to the GetObject docs for the S3 API and under the section labeled "Overriding Response Header Values" we find the following:
Note: You must sign the request, either using an Authorization header or a presigned URL, when using these parameters. They cannot be used with an unsigned (anonymous) request.
response-content-language
response-expires
response-cache-control
response-content-disposition
response-content-encoding
This answer's how to dynamically change any S3 object's content-disposition in the URL. However, at least for me, this is an imperfect solution because my intended use case was to store the URL for years as part of an invoicing archive, but signed URLs are only valid for a maximum of 1 week.
I could technically also try to find a way to make the Authorization header work for me or just query the S3 API to get a new signed URL every time I want to link to it, but that has other security, performance, and ROI implications for me that make it not worth it.

php aws sdk cannot put using signed url

I am trying to upload a file using a signed url but I keep getting key too long error. running this via curl on cli works fine.
$result = "hl7/1/2df8c3ef-8eca-4e19-aae0-41b074f37c8e.hl7?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=CREDENTIALS%2F20211206%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20211206T144541Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjELX%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCWV1LXdlc3QtMSJGMEQCIH%2BBvsVggdM0wLpK0Kd7gGVopCVYrDNxrHoJWdJZs%2Bl7AiBwFw1i3LNWsoDsK6SZzz8%2BPdHxYh%2Bn6T4DbHt5Ya5w8Sr6AwiO%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAIaDDc4MTcyNzU3MTE3NyIMK4bkPUK4yBIM3U65Ks4DQAkLuCkrZ%2F7tcoN7ItKGxmSBM0TMYbGq7dhsS1SWSxqffm%2BOM9QzEROrQRpt%2Bm%2BUQ3R5o7vz7Y4x5lxhkvIOyzXxVUqNznCBnWEYwEdK8aLPlZLiFkh2wznL2SeTLZsYO4LD3WXoY3AiVZZYZroNkJcUicNlectdsuoFWov5DIJ7k6PyAoEaRFk00Afa9W8NX5pN4NevvgclgqpceqeLD7L%2FBHBje13F68aJ6sv%2F%2FLeWZ4pvSR5ySgWaUaQ1aHHIsNVhuuZCgqC3R8wlN2AGRQ4h5M%2Byb6Tq6pFJwgI2CeP%2BgAFmMUh0feyJDROLq88i%2BRTSIEsUlH8GQGDg%2FSwIPBlL3Fg8TxDSzL8wa8Quk2h%2BgpNalvdRmIUR6iYem4DoNUxkihU%2F77g0TEOVLIl%2Fk20freTIWSeR8ZNCmIZOhUvm9Ea7TQl88B3jWq3oVWWG3xsKkEvCZ%2FwdnQAI20fUp8EtrnODtjjNNanmgWSafHURLTAm2HjtNbnKri%2B2mQmGQoHo2s2xX%2Fi6CYuEZI5yJBYuUSFZHhSum0h%2Bja643YDOp14TmYO38xFcIThxKDqaEMBjdRo15WvVDFcXSMsUdXLElXvOPMfHBoMdT4meMMiTuI0GOqYBT%2FMubfy97zprO%2FNP8xyb2u8qfDYeO0P7xBXdNFrbfVgX%2Fdr6%2Fhh8WSEz2K9PojxLN3NLGnfiMaUaUpev2kHcThP%2BuMkb%2FurH9Pl9QCFKsgKnhylrIKC0bQ%2B4TaEbJoRwh%2BRQUZGqaJfaPrMR5tAyYZ5kN4w1PXLRjpqi2U27B3qpGfu3jruEtOsuHtxM57tFQgyUHy3J%2FOdV7YVzYX%2B8cHSHkx2ndA%3D%3D&X-Amz-Signature=e653b4bbc96cba74979a087b3bc0f14ea6ff9fcd8eff0bd28234d897833240d1&X-Amz-SignedHeaders=host&x-id=PutObject";
$client = new S3Client( [
'version' => '2006-03-01',
'region' => 'eu-west-1',
'validate' => false
] );
$params = [
'Bucket' => env('RESULTS_UPLOAD_SERVICE_BUCKET'),
'Key' => $result,
'Body' => file_get_contents($fileData)
];
$response = $client->putObject($params);
Here is my curl command which works
curl --request PUT --upload-file 20211008192601_60461040005062.HL7 "https://domain.s3.eu-west-1.amazonaws.com/hl7/1/2df8c3ef-8eca-4e19-aae0-41b074f37c8e.hl7?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIA3MAUYDTUUN444IX6%2F20211206%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20211206T144541Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjELX%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCWV1LXdlc3QtMSJGMEQCIH%2BBvsVggdM0wLpK0Kd7gGVopCVYrDNxrHoJWdJZs%2Bl7AiBwFw1i3LNWsoDsK6SZzz8%2BPdHxYh%2Bn6T4DbHt5Ya5w8Sr6AwiO%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAIaDDc4MTcyNzU3MTE3NyIMK4bkPUK4yBIM3U65Ks4DQAkLuCkrZ%2F7tcoN7ItKGxmSBM0TMYbGq7dhsS1SWSxqffm%2BOM9QzEROrQRpt%2Bm%2BUQ3R5o7vz7Y4x5lxhkvIOyzXxVUqNznCBnWEYwEdK8aLPlZLiFkh2wznL2SeTLZsYO4LD3WXoY3AiVZZYZroNkJcUicNlectdsuoFWov5DIJ7k6PyAoEaRFk00Afa9W8NX5pN4NevvgclgqpceqeLD7L%2FBHBje13F68aJ6sv%2F%2FLeWZ4pvSR5ySgWaUaQ1aHHIsNVhuuZCgqC3R8wlN2AGRQ4h5M%2Byb6Tq6pFJwgI2CeP%2BgAFmMUh0feyJDROLq88i%2BRTSIEsUlH8GQGDg%2FSwIPBlL3Fg8TxDSzL8wa8Quk2h%2BgpNalvdRmIUR6iYem4DoNUxkihU%2F77g0TEOVLIl%2Fk20freTIWSeR8ZNCmIZOhUvm9Ea7TQl88B3jWq3oVWWG3xsKkEvCZ%2FwdnQAI20fUp8EtrnODtjjNNanmgWSafHURLTAm2HjtNbnKri%2B2mQmGQoHo2s2xX%2Fi6CYuEZI5yJBYuUSFZHhSum0h%2Bja643YDOp14TmYO38xFcIThxKDqaEMBjdRo15WvVDFcXSMsUdXLElXvOPMfHBoMdT4meMMiTuI0GOqYBT%2FMubfy97zprO%2FNP8xyb2u8qfDYeO0P7xBXdNFrbfVgX%2Fdr6%2Fhh8WSEz2K9PojxLN3NLGnfiMaUaUpev2kHcThP%2BuMkb%2FurH9Pl9QCFKsgKnhylrIKC0bQ%2B4TaEbJoRwh%2BRQUZGqaJfaPrMR5tAyYZ5kN4w1PXLRjpqi2U27B3qpGfu3jruEtOsuHtxM57tFQgyUHy3J%2FOdV7YVzYX%2B8cHSHkx2ndA%3D%3D&X-Amz-Signature=e653b4bbc96cba74979a087b3bc0f14ea6ff9fcd8eff0bd28234d897833240d1&X-Amz-SignedHeaders=host&x-id=PutObject"
it errors with
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Error><Code>KeyTooLongError</Code><Message>Your key is too long</Message><Size>1 (truncated...)
at /var/www/snapshot/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:113)
[stacktrace]
You are receiving that error because you are passing pre-signed url result in to put request as key, That put request method should actually be used to upload directly with php sdk.
It seems like you want to upload file to a pre-signed url, Once you generate pre-signed url you will need to do a HTTP PUT request, you can do it directly from browser that is the usual way why we generate pre-signed url that expires after certain time so you don't expose your keys to public.
If you are using PHP directly you could simply use your keys to upload using above code or if you still want to upload to a pre-signed url with php see this link
How do I upload a file to an AWS Presigned URL with PHP Curl?

How to create a Slack message containing an uploaded image?

I'd like to create a message in the #general channel of my Slackspace from within a PHP script. The message should contain text and an image which was created locally on-the-fly.
I've already created an App, generated an bearer token and have managed to create an text-only message also as an image-upload.
But i didn't know how to create both in one message, as the procedure above creates two messages, one with text and another one with the image.
There are two different approaches on how to post a Slack message with an image.
A. Upload image directly with message
You can upload an image directly to Slack and share it in a channel. Within that request you can also add a comment that will appear as message above the images. This is the easiest approach, however you comment is limited to one string.
API method: files.upload with these arguments:
channels: ID of one or multiple channel for the image to appear in
initial_comment: Your message
B. Post message with image block / attachment
Alternatively you can add an image to your normal message either as image block or secondary attachment. This only works with a public URL to your image file, so you first need to upload your image to an image hoster (which can be your Slack workspace) to get the public URL.
In our example we will use Slack as image hoster, but you can use any image hoster (e.g. Imgur) even your own webserver, as long as you get a public URL for your image file.
Step 1 - Upload image to Slack
API method: files.upload with no special arguments, but make sure to get the file ID from the response. Don't include the channels argument or the image will be posted visible into those channel.
Step 2 - Create public URL
Next you have to mark the uploaded file as public. Only then will it be accessible through its public_url property
API method: files.sharedPublicURL with the file ID as argument.
Next you need to construct the direct image link from the link to website / permalink_public property of the file.
The website link you get from permalink_public has the format:
https://slack-files.com/{team_id}-{file_id}-{pub_secret}
The direct link to the image has the format:
https://files.slack.com/files-pri/{team_id}-{file_id}/{filename}?pub_secret={pub_secret}
So you just need to extract the pub_secret from permalink_public and you should be able to construct the direct link to the image. The other parameters you can get from your file object.
Step 3 - Send message
Finally compose your message with the image URL either as Image Block or in a secondary attachment and submit it using a method of your choice.
API method: chat.PostMessage or any other method for sending message incl. incoming webhooks.
Answer to OP
If you need to stick with webhooks as it appears from your comments and have no access to the Slack API I would suggest uploading the image to an image hoster (e.g. Imgur) and then use approach B.
See also
Slack bot send an image
Can I upload an image as attachment with Slack API?
How to use the permalink_public URL of an uploaded image to include it in a message?
After much tinkering I found that while I could not use the API to create a message and upload an image simultaneously, I can first upload an image and then, with the timestamp returned, use update message to add text to the original message with the image upload.
This is the flow:
1- Use files_upload method to upload an image to my channel (using the channel name)
response = client.files_upload(
channels=my_channel_name,
file=image_path,
initial_comment='My initial comment'
)
2- Get the response from the files_upload and extract the channel id and timestamp of the message.
channel_id = response['file']['groups'][0]
ts = response['file']['shares']['private'][channel_id][0]['ts']
3- Use chat update to add text or rich content to the message with the uploaded image:
response = client.chat_update(
channel=channel_id,
text="My Message",
ts=ts,
blocks=blocks_list
)
For those who might still need this.. this gist helped me. Its a quick and easier way using GuzzleHttp.
use GuzzleHttp\Client;
/**
* Notes:
*
* Tested with guzzlehttp/guzzle version 7.3
* See https://api.slack.com/methods/files.upload for details on how to generate a token
*
*/
$fileName = '';
$filePath = '';
$slacktoken = ''; // See https://api.slack.com/tokens; The token will need file.read and file.write permissions
$client = new Client();
$apiUrl = 'https://slack.com/api/files.upload';
$client = new Client();
$request = $client->post( $apiUrl, [
'headers' => ['Authorization' => 'auth_trusted_header'],
'multipart' => [
[
'name' => 'token',
'contents' => $slacktoken,
],
[
'name' => 'file',
'contents' => fopen($filePath, 'r'),
'filename' => $fileName
],
[
'name' => 'channels',
'contents' => '#general'
],
[
'name' => 'initial_comment',
'contents' => 'File Uploaded'
]
]
]);
var_dump($request->getBody()->getContents());

AWS S3 PHP Client ListParts returns null

I want to upload large files to S3 through REST API using PHP server for creating signed URLs. In PHP S3 Client, there's a command "listParts" which return an array of multipart uploads and I use them in "completeMultipartUpload" to join all the multipart. Every part of the code is working fine, except "listParts". It returns null sometimes. So the function "completeMultipartUpload" does not executes. I can't figure out the reason why it's returning null sometimes. Here sometimes means when I upload a file, it works well. When I refresh the webpage and re-upload the same file it returns null. Then again refresh and upload, it works.
$partsModel = $client->listParts(array(
'Bucket' => bucket(),
'Key' => $_REQUEST['sendBackData']['key'],
'UploadId' => $_REQUEST['sendBackData']['uploadId'],
));

AWS S3 PHP SDK presigned url restrict content type

I am trying to make a PutObject presigned request using the AWS S3 PHP SDK.
I have gotten the request to work but now I only want to allow my users to be able to only upload video files.I have tried a lot of combinations and searched a lot but I could not get it to work.
Here is the sample code I use:
$cmd = $this->s3client->getCommand('PutObject', [
'Bucket' => 'myBucket',
'Key' => 'inputs/' . $movie->getId(),
'ACL' => 'private',
'Conditions' => ['Starts-With', '$Content-Type', 'video/'], // I have tried other combinations but it seems to not work
]);
$request = $this->s3client->createPresignedRequest($cmd, '+30 minutes');
$movie->setSignedUrl((string)$request->getUri());
The signed url generated does never include the Content-Type in the X-Amz-SignedHeaders query parameter, only the host is included.
The putObject() request has no documented Conditions key.
You appear to be confusing S3's PUT upload interface with the pre-signed POST capability, which supports policy document conditions like ['Starts-With', '$Content-Type', 'video/'],
PUT does not support "starts with". It requires the exact Content-Type and the key for this (which should result in the header appearing in the X-Amz-SignedHeaders query string parameter) is simply ContentType. It goes in the outer parameters array, just like Bucket and Key.
But if you want to support multiple content types without knowing the specific type in advance, you need to use POST uploads.

Categories