<?php
// s3helper.php — AWS Signature V4 for MinIO demo (final+paths)
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) session_start();
/* Load or update config (session only) */
function s3_load_config(): array {
$cfg = $_SESSION['s3_demo'] ?? [];
foreach (['endpoint','region','access','secret','bucket'] as $k)
if (isset($_POST[$k])) $cfg[$k] = trim((string)$_POST[$k]);
$cfg['endpoint'] = $cfg['endpoint'] ?? 'https://minio1.webtui.vn:9000';
$cfg['region'] = $cfg['region'] ?? 'vn-1';
if (empty($cfg['bucket'])) {
$u = preg_replace('/[^a-z0-9]/i','', $cfg['access'] ?? 'demo');
$cfg['bucket'] = 'bucket-' . ($u ?: 'demo');
}
$_SESSION['s3_demo'] = $cfg;
return $cfg;
}
/* Path helpers */
function s3_norm_prefix(string $p): string {
$p = trim($p);
$p = ltrim($p, '/'); // không dẫn slash đầu
$p = preg_replace('#/{2,}#','/',$p);// gom //
return ($p === '' ? '' : rtrim($p,'/').'/'); // prefix luôn kết thúc '/'
}
function s3_join(string $prefix, string $name): string {
$prefix = s3_norm_prefix($prefix);
$name = ltrim($name, '/');
return $prefix . $name;
}
/* SigV4 core */
function s3_parse_endpoint(string $endpoint): array {
$p = parse_url($endpoint);
$scheme = $p['scheme'] ?? 'https';
$host = $p['host'] ?? 'minio1.webtui.vn';
$port = $p['port'] ?? ($scheme === 'https' ? 443 : 80);
return ['scheme'=>$scheme,'host'=>$host,'port'=>$port];
}
function hash_sha256_hex(string $d): string { return hash('sha256',$d); }
function hmac_sha256_bin(string $key,string $msg): string { return hash_hmac('sha256',$msg,$key,true); }
function getSignatureKey(string $k,string $date,string $region,string $service): string {
$kDate=hmac_sha256_bin("AWS4".$k,$date);
$kRegion=hmac_sha256_bin($kDate,$region);
$kService=hmac_sha256_bin($kRegion,$service);
return hmac_sha256_bin($kService,"aws4_request");
}
/* Send signed HTTP request */
function s3_send_request(array $cfg,string $method,string $bucket,string $object='',array $qs=[],
array $hdr=[], $body=null): array {
$ep=s3_parse_endpoint($cfg['endpoint']);
$region=$cfg['region']; $access=$cfg['access']; $secret=$cfg['secret'];
$service='s3';
$uri = '/';
if($bucket!=='') {
$uri .= $bucket;
if($object!==''){
$uri .= '/' . ltrim($object, '/');
}
}
$now=new DateTime('UTC');
$amz=$now->format('Ymd\THis\Z');
$date=$now->format('Ymd');
if($body===null)$payloadHash=hash_sha256_hex('');
elseif(is_resource($body)){ $data=stream_get_contents($body); rewind($body); $payloadHash=hash_sha256_hex($data); }
else $payloadHash=hash_sha256_hex((string)$body);
$host=$ep['host'];
if(($ep['scheme']==='https' && $ep['port']!=443)||($ep['scheme']==='http' && $ep['port']!=80))
$host.=':'.$ep['port'];
$headers=['Host'=>$host,'x-amz-date'=>$amz,'x-amz-content-sha256'=>$payloadHash]+$hdr;
ksort($headers);
$canHdr=''; $signed=[];
foreach($headers as $k=>$v){$lk=strtolower($k); $canHdr.="$lk:$v\n"; $signed[]=$lk;}
$signedStr=implode(';',$signed);
ksort($qs);
$qstr=http_build_query($qs,'','&',PHP_QUERY_RFC3986);
$canonical="$method\n$uri\n$qstr\n$canHdr\n$signedStr\n$payloadHash";
$scope="$date/$region/$service/aws4_request";
$stringToSign="AWS4-HMAC-SHA256\n$amz\n$scope\n".hash_sha256_hex($canonical);
$sign=hash_hmac('sha256',$stringToSign,getSignatureKey($secret,$date,$region,$service));
$headers['Authorization']="AWS4-HMAC-SHA256 Credential=$access/$scope, SignedHeaders=$signedStr, Signature=$sign";
$url=$ep['scheme'].'://'.$host.$uri.(empty($qstr)?'':'?'.$qstr);
$ch=curl_init($url);
curl_setopt_array($ch,[
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_HEADER=>true,
CURLOPT_CUSTOMREQUEST=>$method,
CURLOPT_HTTPHEADER=>array_map(fn($k,$v)=>"$k: $v",array_keys($headers),$headers),
CURLOPT_NOBODY=>($method==='HEAD'),
CURLOPT_FOLLOWLOCATION=>false,
]);
if(in_array($method,['PUT','POST'])&&$body!==null){
$data=is_resource($body)?stream_get_contents($body):(string)$body;
curl_setopt($ch,CURLOPT_POSTFIELDS,$data);
}
$resp=curl_exec($ch);
if($resp===false)return['ok'=>false,'http_code'=>0,'error'=>curl_error($ch)];
$hs=curl_getinfo($ch,CURLINFO_HEADER_SIZE);
$code=curl_getinfo($ch,CURLINFO_HTTP_CODE);
curl_close($ch);
$header=substr($resp,0,$hs); $respBody=substr($resp,$hs);
$h=[]; foreach(preg_split('/\r\n/',trim($header)) as $l){
if (strpos($l,':')!==false){ [$n,$v]=explode(':',$l,2); $h[strtolower(trim($n))]=trim($v); }
}
return ['ok'=>$code>=200&&$code<300,'http_code'=>$code,'headers'=>$h,'body'=>$respBody];
}
/* Simple helpers */
function s3_list(array $cfg, string $bucket, string $prefix='', string $delim='/', int $max=1000, string $token=''): array {
$q=['list-type'=>2,'delimiter'=>$delim,'max-keys'=>$max];
if($prefix!=='') $q['prefix']=s3_norm_prefix($prefix);
if($token!=='') $q['continuation-token']=$token;
return s3_send_request($cfg,'GET',$bucket,'',$q,[],null);
}
function s3_delete_prefix(array $cfg, string $bucket, string $prefix): array {
// demo: iterate + DELETE từng object (đơn giản, an toàn)
$deleted=0; $token='';
do{
$r=s3_list($cfg,$bucket,$prefix,'/',1000,$token);
if(!$r['ok']) return ['ok'=>false,'http_code'=>$r['http_code'],'deleted'=>$deleted,'error'=>$r['body']];
$xml=@simplexml_load_string($r['body']); if(!$xml) break;
$batch=[];
foreach($xml->Contents as $c) $batch[]=(string)$c->Key;
foreach($batch as $k){
$d=s3_send_request($cfg,'DELETE',$bucket,$k,[],[],null);
if($d['ok']) $deleted++;
}
$token=(string)($xml->NextContinuationToken ?? '');
} while(!empty($token));
return ['ok'=>true,'deleted'=>$deleted];
}