File modules/cms/generator/target/S3Target.class.php

Last commit: Sat Dec 4 14:05:06 2021 +0100	dankert	Fix: Amazon S3 upload is now fully working.
1 <?php 2 // OpenRat Content Management System 3 // Copyright (C) 2002-2012 Jan Dankert, cms@jandankert.de 4 // 5 // This program is free software; you can redistribute it and/or 6 // modify it under the terms of the GNU General Public License 7 // as published by the Free Software Foundation; either version 2 8 // of the License, or (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU General Public License for more details. 14 // 15 // You should have received a copy of the GNU General Public License 16 // along with this program; if not, write to the Free Software 17 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 namespace cms\generator\target; 19 20 use cms\base\Startup; 21 use logger\Logger; 22 use util\exception\PublisherException; 23 use util\FileUtils; 24 25 26 /** 27 * Publishing a file to Amazon S3 "simple storage system". 28 * 29 * Support for: AWS Signature Version 4. 30 * 31 * @author Jan Dankert 32 */ 33 class S3Target extends BaseTarget 34 { 35 const SERVICE = 's3'; 36 const AWS_4_HMAC_SHA_256 = 'AWS4-HMAC-SHA256'; 37 38 /** 39 * @var false|resource 40 */ 41 private $socket; 42 43 public function checkConnection() 44 { 45 $socket = $this->createSocket(); 46 fclose( $socket ); 47 } 48 49 50 /** 51 * @param $source String source file 52 * @param $dest String filename 53 * @param $time int this is ignored, because the date in S3 must be a current date. 54 * @return mixed 55 * @throws PublisherException 56 */ 57 public function put($source, $dest, $time) 58 { 59 $dateIso = date('r',Startup::getStartTime()); 60 $dateShort = date('Ymd'); 61 $timeStamp = date('Ymd\THis\Z',Startup::getStartTime()); 62 //$destPath = FileUtils::unslashifyBegin($this->url->path . '/' . $dest); 63 $destPath = $this->url->path . '/' . $dest; 64 65 $accessKeyId = $this->url->user; 66 $secretAccessKey = $this->url->pass; 67 68 $domain = explode('.',$this->url->host); 69 if ( sizeof($domain)<3 ) 70 throw new PublisherException('S3 Hostname should be <bucket>.s3.<region>...'); 71 72 list($bucket,$service,$region) = $domain; 73 $scope = $dateShort.'/'.$region.'/'.$service.'/aws4_request'; 74 $credential = $accessKeyId.'/'.$scope; 75 76 $headers = []; 77 $hashedPayload = hash_file('SHA256', $source); 78 $headers['x-amz-content-sha256'] = $hashedPayload; 79 $headers['x-amz-date'] = $timeStamp; 80 81 $headers['Host'] = $this->url->host; 82 $headers['Date'] = $dateIso; 83 84 85 // Creating the "signedHeaders" 86 $signedHeaders = $this->getSignedHeaders($headers); 87 88 89 // Creating the "canonicalHeaders" 90 $canonicalHeaders = $this->getCanonicalHeaders($headers); 91 92 93 $canonicalRequestParts = [ 94 'PUT', 95 utf8_encode($destPath), 96 '', 97 $canonicalHeaders, 98 '', 99 $signedHeaders, 100 $hashedPayload 101 ]; 102 $canonicalRequest = implode("\n",$canonicalRequestParts); 103 Logger::debug("S3 Canonical request:\n".$canonicalRequest); 104 105 106 // Create the "StringToSign" 107 $stringToSignParts = [ 108 self::AWS_4_HMAC_SHA_256, 109 $timeStamp, 110 $scope, 111 hash('SHA256',$canonicalRequest ) 112 ]; 113 $stringToSign = implode("\n",$stringToSignParts); 114 Logger::debug("S3 StringToSign:\n".$stringToSign); 115 116 117 // Create the "Signature" 118 $signatureParts = [ 119 $dateShort, 120 $region, 121 's3', 122 'aws4_request', 123 ]; 124 $signingKey = array_reduce($signatureParts,function ($initialKey, $value){ 125 Logger::debug('S3 Signing "'.$value.'"'); 126 return hash_hmac('SHA256',$value,$initialKey,true); 127 },'AWS4'.$secretAccessKey); 128 $signature = hash_hmac( 'SHA256',$stringToSign, $signingKey ); 129 Logger::debug("S3 Signature: ".$signature); 130 131 132 // Create the "Authorization" header 133 $authorizationParts = [ 134 'Credential' => $credential, 135 'SignedHeaders' => $signedHeaders, 136 'Signature' => $signature, 137 ]; 138 array_walk($authorizationParts,function(&$value,$key){$value=$key.'='.$value;}); 139 $authorization = self::AWS_4_HMAC_SHA_256 ." ".implode(",",$authorizationParts); 140 $headers['Authorization' ] = $authorization; 141 Logger::debug("S3 Authorization: ".$authorization); 142 143 144 // Add some extra headers 145 $headers['Connection' ] = 'Close'; 146 $headers['Content-Length'] = filesize($source); 147 148 149 // Creating the HTTP request 150 $content = "PUT $destPath HTTP/1.1\r\n"; 151 152 foreach( $headers as $key=>$value ) 153 $content .= $key.': '.$value."\r\n"; 154 Logger::trace( "S3 Request:\n".$content ); 155 156 fwrite($this->socket, $content."\r\n".file_get_contents($source)); 157 158 $response = ''; 159 if ( !feof($this->socket) ) { 160 $line = fgets($this->socket, 14); 161 $status = substr($line,9,3); 162 }else{ 163 $status = false; 164 } 165 while (!feof($this->socket)) { 166 $line = fgets($this->socket, 1028); 167 $response .= $line; 168 } 169 170 $response .= "\n\n\n\n".$content; 171 172 Logger::debug( "S3 Response status: ".$status ); 173 174 if ( $status != '200' ) 175 // Some server error occured. 176 throw new PublisherException( "Status is ".$status."\n".$response ); 177 } 178 179 180 public function close() 181 { 182 fclose($this->socket); 183 } 184 185 public function open() 186 { 187 $this->socket = $this->createSocket(); 188 } 189 190 /** 191 * @return false|resource 192 */ 193 protected function createSocket() 194 { 195 // Amazon S3 is only working with SSL 196 $socket = fsockopen('ssl://'.$this->url->host, 443, $errno, $errstr, 5); 197 198 if(!$socket) 199 throw new PublisherException("cannot connect to DAV server: $errno -> $errstr"); 200 201 return $socket; 202 203 } 204 205 private function getSignedHeaders( $headers ) 206 { 207 $headers = array_keys( $headers ); 208 209 $header = array_map( 210 function ($header) { 211 return strtolower($header); 212 }, $headers ); 213 214 asort($header ); 215 216 return implode( ';',$header ); 217 } 218 219 220 private function getCanonicalHeaders( $headers ) 221 { 222 // map to lowerkeys header names 223 $header = []; 224 foreach( $headers as $key=>$value ) 225 $header[ strtolower($key) ] = $value; 226 227 ksort($header ); 228 array_walk($header,function(&$value,$key){$value=$key.':'.$value;}); 229 230 return implode( "\n",$header ); 231 } 232 }
Download modules/cms/generator/target/S3Target.class.php
History Sat, 4 Dec 2021 14:05:06 +0100 dankert Fix: Amazon S3 upload is now fully working. Sat, 4 Dec 2021 04:35:42 +0100 dankert New: Amazon S3 as a Publishing-Target, work in progress.