├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── endpoint-cors.php └── endpoint.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present, Widen Enterprises, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP S3 Server 2 | 3 | [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 4 | [![Packagist](https://img.shields.io/packagist/dt/fineuploader/php-s3-server.svg?maxAge=2592000)](https://packagist.org/packages/fineuploader/php-s3-server) 5 | 6 | PHP-based server-side example for handling signature, delete, etc endpoint requests from Fine Uploader S3. 7 | 8 | You can pull down this from packagist via composer! The package name is `fineuploader/php-s3-server`. 9 | 10 | For a step-by-step getting started guide, have a look at [the "Getting Started" section on our documentation site](http://docs.fineuploader.com/). 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fineuploader/php-s3-server", 3 | "description": "Endpoint handler for Fine Uploader S3's server requests.", 4 | "homepage": "http://fineuploader.com", 5 | "license": "MIT", 6 | "require": { 7 | "aws/aws-sdk-php": "2.*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /endpoint-cors.php: -------------------------------------------------------------------------------- 1 | $serverPublicKey, 119 | 'secret' => $serverPrivateKey 120 | )); 121 | } 122 | 123 | // Only needed if the delete file feature is enabled 124 | function deleteObject() { 125 | getS3Client()->deleteObject(array( 126 | 'Bucket' => $_REQUEST['bucket'], 127 | 'Key' => $_REQUEST['key'] 128 | )); 129 | } 130 | 131 | function signRequest() { 132 | header('Content-Type: application/json'); 133 | 134 | $responseBody = file_get_contents('php://input'); 135 | $contentAsObject = json_decode($responseBody, true); 136 | $jsonContent = json_encode($contentAsObject); 137 | 138 | if (!empty($contentAsObject["headers"])) { 139 | signRestRequest($contentAsObject["headers"]); 140 | } 141 | else { 142 | signPolicy($jsonContent); 143 | } 144 | } 145 | 146 | function signRestRequest($headersStr) { 147 | $version = isset($_REQUEST["v4"]) ? 4 : 2; 148 | if (isValidRestRequest($headersStr, $version)) { 149 | if ($version == 4) { 150 | $response = array('signature' => signV4RestRequest($headersStr)); 151 | } 152 | else { 153 | $response = array('signature' => sign($headersStr)); 154 | } 155 | 156 | echo json_encode($response); 157 | } 158 | else { 159 | echo json_encode(array("invalid" => true)); 160 | } 161 | } 162 | 163 | function isValidRestRequest($headersStr, $version) { 164 | if ($version == 2) { 165 | global $expectedBucketName; 166 | $pattern = "/\/$expectedBucketName\/.+$/"; 167 | } 168 | else { 169 | global $expectedHostName; 170 | $pattern = "/host:$expectedHostName/"; 171 | } 172 | 173 | preg_match($pattern, $headersStr, $matches); 174 | 175 | return count($matches) > 0; 176 | } 177 | 178 | function signPolicy($policyStr) { 179 | $policyObj = json_decode($policyStr, true); 180 | 181 | if (isPolicyValid($policyObj)) { 182 | $encodedPolicy = base64_encode($policyStr); 183 | if (isset($_REQUEST["v4"])) { 184 | $response = array('policy' => $encodedPolicy, 'signature' => signV4Policy($encodedPolicy, $policyObj)); 185 | } 186 | else { 187 | $response = array('policy' => $encodedPolicy, 'signature' => sign($encodedPolicy)); 188 | } 189 | echo json_encode($response); 190 | } 191 | else { 192 | echo json_encode(array("invalid" => true)); 193 | } 194 | } 195 | 196 | function isPolicyValid($policy) { 197 | global $expectedMaxSize, $expectedBucketName; 198 | 199 | $conditions = $policy["conditions"]; 200 | $bucket = null; 201 | $parsedMaxSize = null; 202 | 203 | for ($i = 0; $i < count($conditions); ++$i) { 204 | $condition = $conditions[$i]; 205 | 206 | if (isset($condition["bucket"])) { 207 | $bucket = $condition["bucket"]; 208 | } 209 | else if (isset($condition[0]) && $condition[0] == "content-length-range") { 210 | $parsedMaxSize = $condition[2]; 211 | } 212 | } 213 | 214 | return $bucket == $expectedBucketName && $parsedMaxSize == (string)$expectedMaxSize; 215 | } 216 | 217 | function sign($stringToSign) { 218 | global $clientPrivateKey; 219 | 220 | return base64_encode(hash_hmac( 221 | 'sha1', 222 | $stringToSign, 223 | $clientPrivateKey, 224 | true 225 | )); 226 | } 227 | 228 | function signV4Policy($stringToSign, $policyObj) { 229 | global $clientPrivateKey; 230 | 231 | foreach ($policyObj["conditions"] as $condition) { 232 | if (isset($condition["x-amz-credential"])) { 233 | $credentialCondition = $condition["x-amz-credential"]; 234 | } 235 | } 236 | 237 | $pattern = "/.+\/(.+)\\/(.+)\/s3\/aws4_request/"; 238 | preg_match($pattern, $credentialCondition, $matches); 239 | 240 | $dateKey = hash_hmac('sha256', $matches[1], 'AWS4' . $clientPrivateKey, true); 241 | $dateRegionKey = hash_hmac('sha256', $matches[2], $dateKey, true); 242 | $dateRegionServiceKey = hash_hmac('sha256', 's3', $dateRegionKey, true); 243 | $signingKey = hash_hmac('sha256', 'aws4_request', $dateRegionServiceKey, true); 244 | 245 | return hash_hmac('sha256', $stringToSign, $signingKey); 246 | } 247 | 248 | function signV4RestRequest($rawStringToSign) { 249 | global $clientPrivateKey; 250 | 251 | $pattern = "/.+\\n.+\\n(\\d+)\/(.+)\/s3\/aws4_request\\n(.+)/s"; 252 | preg_match($pattern, $rawStringToSign, $matches); 253 | 254 | $hashedCanonicalRequest = hash('sha256', $matches[3]); 255 | $stringToSign = preg_replace("/^(.+)\/s3\/aws4_request\\n.+$/s", '$1/s3/aws4_request'."\n".$hashedCanonicalRequest, $rawStringToSign); 256 | 257 | $dateKey = hash_hmac('sha256', $matches[1], 'AWS4' . $clientPrivateKey, true); 258 | $dateRegionKey = hash_hmac('sha256', $matches[2], $dateKey, true); 259 | $dateRegionServiceKey = hash_hmac('sha256', 's3', $dateRegionKey, true); 260 | $signingKey = hash_hmac('sha256', 'aws4_request', $dateRegionServiceKey, true); 261 | 262 | return hash_hmac('sha256', $stringToSign, $signingKey); 263 | } 264 | 265 | // This is not needed if you don't require a callback on upload success. 266 | function verifyFileInS3($includeThumbnail) { 267 | global $expectedMaxSize; 268 | 269 | $bucket = $_REQUEST["bucket"]; 270 | $key = $_REQUEST["key"]; 271 | 272 | // If utilizing CORS, we return a 200 response with the error message in the body 273 | // to ensure Fine Uploader can parse the error message in IE9 and IE8, 274 | // since XDomainRequest is used on those browsers for CORS requests. XDomainRequest 275 | // does not allow access to the response body for non-success responses. 276 | if (isset($expectedMaxSize) && getObjectSize($bucket, $key) > $expectedMaxSize) { 277 | // You can safely uncomment this next line if you are not depending on CORS 278 | header("HTTP/1.0 500 Internal Server Error"); 279 | deleteObject(); 280 | echo json_encode(array("error" => "File is too big!", "preventRetry" => true)); 281 | } 282 | else { 283 | $link = getTempLink($bucket, $key); 284 | $response = array("tempLink" => $link); 285 | 286 | if ($includeThumbnail) { 287 | $response["thumbnailUrl"] = $link; 288 | } 289 | 290 | echo json_encode($response); 291 | } 292 | } 293 | 294 | // Provide a time-bombed public link to the file. 295 | function getTempLink($bucket, $key) { 296 | $client = getS3Client(); 297 | $url = "{$bucket}/{$key}"; 298 | $request = $client->get($url); 299 | 300 | return $client->createPresignedUrl($request, '+15 minutes'); 301 | } 302 | 303 | function getObjectSize($bucket, $key) { 304 | $objInfo = getS3Client()->headObject(array( 305 | 'Bucket' => $bucket, 306 | 'Key' => $key 307 | )); 308 | return $objInfo['ContentLength']; 309 | } 310 | 311 | // Return true if it's likely that the associate file is natively 312 | // viewable in a browser. For simplicity, just uses the file extension 313 | // to make this determination, along with an array of extensions that one 314 | // would expect all supported browsers are able to render natively. 315 | function isFileViewableImage($filename) { 316 | $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); 317 | $viewableExtensions = array("jpeg", "jpg", "gif", "png"); 318 | 319 | return in_array($ext, $viewableExtensions); 320 | } 321 | 322 | // Returns true if we should attempt to include a link 323 | // to a thumbnail in the uploadSuccess response. In it's simplest form 324 | // (which is our goal here - keep it simple) we only include a link to 325 | // a viewable image and only if the browser is not capable of generating a client-side preview. 326 | function shouldIncludeThumbnail() { 327 | $filename = $_REQUEST["name"]; 328 | $isPreviewCapable = $_REQUEST["isBrowserPreviewCapable"] == "true"; 329 | $isFileViewableImage = isFileViewableImage($filename); 330 | 331 | return !$isPreviewCapable && $isFileViewableImage; 332 | } 333 | ?> 334 | -------------------------------------------------------------------------------- /endpoint.php: -------------------------------------------------------------------------------- 1 | $serverPublicKey, 90 | 'secret' => $serverPrivateKey 91 | )); 92 | } 93 | 94 | // Only needed if the delete file feature is enabled 95 | function deleteObject() { 96 | getS3Client()->deleteObject(array( 97 | 'Bucket' => $_REQUEST['bucket'], 98 | 'Key' => $_REQUEST['key'] 99 | )); 100 | } 101 | 102 | function signRequest() { 103 | header('Content-Type: application/json'); 104 | 105 | $responseBody = file_get_contents('php://input'); 106 | $contentAsObject = json_decode($responseBody, true); 107 | $jsonContent = json_encode($contentAsObject); 108 | 109 | $headersStr = $contentAsObject["headers"]; 110 | if ($headersStr) { 111 | signRestRequest($headersStr); 112 | } 113 | else { 114 | signPolicy($jsonContent); 115 | } 116 | } 117 | 118 | function signRestRequest($headersStr) { 119 | $version = isset($_REQUEST["v4"]) ? 4 : 2; 120 | if (isValidRestRequest($headersStr, $version)) { 121 | if ($version == 4) { 122 | $response = array('signature' => signV4RestRequest($headersStr)); 123 | } 124 | else { 125 | $response = array('signature' => sign($headersStr)); 126 | } 127 | 128 | echo json_encode($response); 129 | } 130 | else { 131 | echo json_encode(array("invalid" => true)); 132 | } 133 | } 134 | 135 | function isValidRestRequest($headersStr, $version) { 136 | if ($version == 2) { 137 | global $expectedBucketName; 138 | $pattern = "/\/$expectedBucketName\/.+$/"; 139 | } 140 | else { 141 | global $expectedHostName; 142 | $pattern = "/host:$expectedHostName/"; 143 | } 144 | 145 | preg_match($pattern, $headersStr, $matches); 146 | 147 | return count($matches) > 0; 148 | } 149 | 150 | function signPolicy($policyStr) { 151 | $policyObj = json_decode($policyStr, true); 152 | 153 | if (isPolicyValid($policyObj)) { 154 | $encodedPolicy = base64_encode($policyStr); 155 | 156 | if (isset($_REQUEST["v4"])) { 157 | $response = array('policy' => $encodedPolicy, 'signature' => signV4Policy($encodedPolicy, $policyObj)); 158 | } 159 | else { 160 | $response = array('policy' => $encodedPolicy, 'signature' => sign($encodedPolicy)); 161 | } 162 | echo json_encode($response); 163 | 164 | } 165 | else { 166 | echo json_encode(array("invalid" => true)); 167 | } 168 | } 169 | 170 | function isPolicyValid($policy) { 171 | global $expectedMaxSize, $expectedBucketName; 172 | 173 | $conditions = $policy["conditions"]; 174 | $bucket = null; 175 | $parsedMaxSize = null; 176 | 177 | for ($i = 0; $i < count($conditions); ++$i) { 178 | $condition = $conditions[$i]; 179 | 180 | if (isset($condition["bucket"])) { 181 | $bucket = $condition["bucket"]; 182 | } 183 | else if (isset($condition[0]) && $condition[0] == "content-length-range") { 184 | $parsedMaxSize = $condition[2]; 185 | } 186 | } 187 | 188 | return $bucket == $expectedBucketName && $parsedMaxSize == (string)$expectedMaxSize; 189 | } 190 | 191 | function sign($stringToSign) { 192 | global $clientPrivateKey; 193 | 194 | return base64_encode(hash_hmac( 195 | 'sha1', 196 | $stringToSign, 197 | $clientPrivateKey, 198 | true 199 | )); 200 | } 201 | 202 | function signV4Policy($stringToSign, $policyObj) { 203 | global $clientPrivateKey; 204 | 205 | foreach ($policyObj["conditions"] as $condition) { 206 | if (isset($condition["x-amz-credential"])) { 207 | $credentialCondition = $condition["x-amz-credential"]; 208 | } 209 | } 210 | 211 | $pattern = "/.+\/(.+)\\/(.+)\/s3\/aws4_request/"; 212 | preg_match($pattern, $credentialCondition, $matches); 213 | 214 | $dateKey = hash_hmac('sha256', $matches[1], 'AWS4' . $clientPrivateKey, true); 215 | $dateRegionKey = hash_hmac('sha256', $matches[2], $dateKey, true); 216 | $dateRegionServiceKey = hash_hmac('sha256', 's3', $dateRegionKey, true); 217 | $signingKey = hash_hmac('sha256', 'aws4_request', $dateRegionServiceKey, true); 218 | 219 | return hash_hmac('sha256', $stringToSign, $signingKey); 220 | } 221 | 222 | function signV4RestRequest($rawStringToSign) { 223 | global $clientPrivateKey; 224 | 225 | $pattern = "/.+\\n.+\\n(\\d+)\/(.+)\/s3\/aws4_request\\n(.+)/s"; 226 | preg_match($pattern, $rawStringToSign, $matches); 227 | 228 | $hashedCanonicalRequest = hash('sha256', $matches[3]); 229 | $stringToSign = preg_replace("/^(.+)\/s3\/aws4_request\\n.+$/s", '$1/s3/aws4_request'."\n".$hashedCanonicalRequest, $rawStringToSign); 230 | 231 | $dateKey = hash_hmac('sha256', $matches[1], 'AWS4' . $clientPrivateKey, true); 232 | $dateRegionKey = hash_hmac('sha256', $matches[2], $dateKey, true); 233 | $dateRegionServiceKey = hash_hmac('sha256', 's3', $dateRegionKey, true); 234 | $signingKey = hash_hmac('sha256', 'aws4_request', $dateRegionServiceKey, true); 235 | 236 | return hash_hmac('sha256', $stringToSign, $signingKey); 237 | } 238 | 239 | // This is not needed if you don't require a callback on upload success. 240 | function verifyFileInS3($includeThumbnail) { 241 | global $expectedMaxSize; 242 | 243 | $bucket = $_REQUEST["bucket"]; 244 | $key = $_REQUEST["key"]; 245 | 246 | // If utilizing CORS, we return a 200 response with the error message in the body 247 | // to ensure Fine Uploader can parse the error message in IE9 and IE8, 248 | // since XDomainRequest is used on those browsers for CORS requests. XDomainRequest 249 | // does not allow access to the response body for non-success responses. 250 | if (isset($expectedMaxSize) && getObjectSize($bucket, $key) > $expectedMaxSize) { 251 | // You can safely uncomment this next line if you are not depending on CORS 252 | header("HTTP/1.0 500 Internal Server Error"); 253 | deleteObject(); 254 | echo json_encode(array("error" => "File is too big!", "preventRetry" => true)); 255 | } 256 | else { 257 | $link = getTempLink($bucket, $key); 258 | $response = array("tempLink" => $link); 259 | 260 | if ($includeThumbnail) { 261 | $response["thumbnailUrl"] = $link; 262 | } 263 | 264 | echo json_encode($response); 265 | } 266 | } 267 | 268 | // Provide a time-bombed public link to the file. 269 | function getTempLink($bucket, $key) { 270 | $client = getS3Client(); 271 | $url = "{$bucket}/{$key}"; 272 | $request = $client->get($url); 273 | 274 | return $client->createPresignedUrl($request, '+15 minutes'); 275 | } 276 | 277 | function getObjectSize($bucket, $key) { 278 | $objInfo = getS3Client()->headObject(array( 279 | 'Bucket' => $bucket, 280 | 'Key' => $key 281 | )); 282 | return $objInfo['ContentLength']; 283 | } 284 | 285 | // Return true if it's likely that the associate file is natively 286 | // viewable in a browser. For simplicity, just uses the file extension 287 | // to make this determination, along with an array of extensions that one 288 | // would expect all supported browsers are able to render natively. 289 | function isFileViewableImage($filename) { 290 | $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); 291 | $viewableExtensions = array("jpeg", "jpg", "gif", "png"); 292 | 293 | return in_array($ext, $viewableExtensions); 294 | } 295 | 296 | // Returns true if we should attempt to include a link 297 | // to a thumbnail in the uploadSuccess response. In it's simplest form 298 | // (which is our goal here - keep it simple) we only include a link to 299 | // a viewable image and only if the browser is not capable of generating a client-side preview. 300 | function shouldIncludeThumbnail() { 301 | $filename = $_REQUEST["name"]; 302 | $isPreviewCapable = $_REQUEST["isBrowserPreviewCapable"] == "true"; 303 | $isFileViewableImage = isFileViewableImage($filename); 304 | 305 | return !$isPreviewCapable && $isFileViewableImage; 306 | } 307 | ?> 308 | --------------------------------------------------------------------------------