diff --git a/public_html/api/index.php b/public_html/api/index.php index 31063b5ec3020186b873c77161bc7b24198e886a..15c0b25ad5c8cb4889cc86851f94ec17d18d976b 100644 --- a/public_html/api/index.php +++ b/public_html/api/index.php @@ -11,245 +11,271 @@ * ---------------------------------------------------------------------- * 2020-06-16 v0.9 <axel.hahn@iml.unibe.ch> * 2021-03-29 v1.2 <axel.hahn@iml.unibe.ch> support slashes in branch names + * 2024-09-02 v1.3 <axel.hahn@unibe.ch> php8 only; added variable types; short array syntax * ====================================================================== */ - $bDebug=false; - ini_set('display_errors', 1); - ini_set('display_startup_errors', 1); - error_reporting(E_ALL); - - require_once("../../config/inc_projects_config.php"); - - $sDirClasses=__DIR__.'/../deployment/classes/'; - require_once($sDirClasses.'/project.class.php'); - require_once($sDirClasses.'logger.class.php'); - - $iMaxAge=60; - - // ---------------------------------------------------------------------- - // FUNCTIONS - // ---------------------------------------------------------------------- - /** - * write debug text (if enabled) - * @global boolean $bDebug - * @param string $s message - * @param string $sLevel level; one of info| - * @return boolean - */ - function _wd($s, $sLevel='info'){ - global $bDebug; - if ($bDebug){ - echo '<div class="debug debug-'.$sLevel.'">DEBUG: '.$s.'</div>'; - } - return true; +$bDebug = false; +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); + +/** + * Path to deployment classes + * @var string + */ + $sDirClasses = __DIR__ . '/../deployment/classes/'; + + /** + * Allowed time delta for client or server + * @var integer + */ +$iMaxAge = 60; + +require_once("../../config/inc_projects_config.php"); + +require_once($sDirClasses . '/project.class.php'); +require_once($sDirClasses . 'logger.class.php'); + +// ---------------------------------------------------------------------- +// FUNCTIONS +// ---------------------------------------------------------------------- +/** + * Write debug text (if enabled) + * + * @global boolean $bDebug + * + * @param string $s message + * @param string $sLevel level; one of info| + * @return boolean + */ +function _wd(string $s, string $sLevel = 'info'): bool +{ + global $bDebug; + if ($bDebug) { + echo "<div class=\"debug debug-$sLevel\">DEBUG: $s</div>"; } - - /** - * abort execution with error - * @param string $s message - * @param integer $iStatus http status code to send - */ - function _quit($s, $iStatus=400){ - $aStatus=array( - 400=>'HTTP/1.0 400 Bad Request', - 403=>'HTTP/1.0 403 Access denied', - 404=>'HTTP/1.0 404 Not found', - ); - header($aStatus[$iStatus]); - _done(array('status'=>$iStatus, 'info'=>$aStatus[$iStatus], 'message'=>$s)); + return true; +} + +/** + * Abort execution of API requestwith error + * + * @param string $s message + * @param integer $iStatus http status code to send + */ +function _quit(string $s, int $iStatus = 400): void +{ + $aStatus = [ + 400 => 'HTTP/1.0 400 Bad Request', + 403 => 'HTTP/1.0 403 Access denied', + 404 => 'HTTP/1.0 404 Not found', + ]; + header($aStatus[$iStatus]); + _done(['status' => $iStatus, 'info' => $aStatus[$iStatus], 'message' => $s]); +} + +/** + * End with OK output + * + * @param array $Data array data to show as JSON + * @return void + */ +function _done(array $Data): void +{ + echo is_array($Data) + ? json_encode($Data, JSON_PRETTY_PRINT) + : $Data + ; + die(); +} + +/** + * Check authorization in the http request header and age of timestamp + * On a failed check the request will be terminated + * + * @global int $iMaxAge max allowed age + * + * @param string $sProjectSecret + * @return boolean + */ +function _checkAuth(string $sProjectSecret): bool +{ + global $iMaxAge; + $aReqHeaders = apache_request_headers(); + _wd('<pre>' . print_r($aReqHeaders, 1) . '</pre>'); + if (!isset($aReqHeaders['Authorization'])) { + _quit('Access denied. Missing authorization.', 403); } - /** - * end with OK output - * @param type $Data - */ - function _done($Data){ - echo is_array($Data) - ? json_encode($Data, JSON_PRETTY_PRINT) - : $Data - ; - die(); + if (!isset($aReqHeaders['Date'])) { + _quit('Access denied. Missing field "Date:" in the request header.', 403); } - - /** - * Check authorization in the http request header and age of timestamp - * On a failed check the request will be terminated - * @global int $iMaxAge max allowed age - * @param string $sProjectSecret - * @return boolean - */ - function _checkAuth($sProjectSecret){ - global $iMaxAge; - $aReqHeaders=apache_request_headers(); - _wd('<pre>'.print_r($aReqHeaders, 1).'</pre>'); - if(!isset($aReqHeaders['Authorization'])){ - _quit('Access denied. Missing authorization.', 403); - } - if(!isset($aReqHeaders['Date'])){ - _quit('Access denied. Missing field "Date:" in the request header.', 403); - } - - $sGotHash= preg_replace('/^.*\:/', '', $aReqHeaders['Authorization']); - $sGotDate= $aReqHeaders['Date']; - $sGotMethod=$_SERVER['REQUEST_METHOD']; - $sGotReq=$_SERVER['REQUEST_URI']; - - - $sMyData="$sGotMethod\n$sGotReq\n$sGotDate\n"; - $sMyHash= base64_encode(hash_hmac("sha1", $sMyData, $sProjectSecret)); - - _wd('Hash: '.$sGotHash.' -- from header'); - _wd('Hash: '.$sMyHash.' -- rebuilt'); - if($sGotHash!==$sMyHash){ - _quit('Access denied. Invalid hash.', 403); - } - $iAge=date('U')-date('U', strtotime($sGotDate)); - _wd('Date: '.$sGotDate.' - age: '.$iAge.' sec'); - if($iAge>$iMaxAge){ - _quit('Access denied. Hash is out of date: '.$iAge. ' sec is older '.$iMaxAge.' sec. Maybe client or server is out of sync.', 403); - } - if($iAge<-$iMaxAge){ - _quit('Access denied. Hash is '.$iAge. ' sec in future but only '.$iMaxAge.' sec are allowed. Maybe client or server is out of sync.', 403); - } - return true; + $sGotHash = preg_replace('/^.*\:/', '', $aReqHeaders['Authorization']); + $sGotDate = $aReqHeaders['Date']; + $sGotMethod = $_SERVER['REQUEST_METHOD']; + $sGotReq = $_SERVER['REQUEST_URI']; + + + $sMyData = "$sGotMethod\n$sGotReq\n$sGotDate\n"; + $sMyHash = base64_encode(hash_hmac("sha1", $sMyData, $sProjectSecret)); + + _wd('Hash: ' . $sGotHash . ' -- from header'); + _wd('Hash: ' . $sMyHash . ' -- rebuilt'); + if ($sGotHash !== $sMyHash) { + _quit('Access denied. Invalid hash.', 403); + } + + $iAge = date('U') - date('U', strtotime($sGotDate)); + _wd('Date: ' . $sGotDate . ' - age: ' . $iAge . ' sec'); + if ($iAge > $iMaxAge) { + _quit('Access denied. Hash is out of date: ' . $iAge . ' sec is older ' . $iMaxAge . ' sec. Maybe client or server is out of sync.', 403); } - // ---------------------------------------------------------------------- - // MAIN - // ---------------------------------------------------------------------- - if (!$bDebug){ - header('Content-Type: application/json'); + if ($iAge < -$iMaxAge) { + _quit('Access denied. Hash is ' . $iAge . ' sec in future but only ' . $iMaxAge . ' sec are allowed. Maybe client or server is out of sync.', 403); } - _wd('Start: '.date('Y-m-d H:i:s').'<style>body{background:#eee; color:#456;} + return true; +} +// ---------------------------------------------------------------------- +// MAIN +// ---------------------------------------------------------------------- +if (!$bDebug) { + header('Content-Type: application/json'); +} +_wd('Start: ' . date('Y-m-d H:i:s') . '<style>body{background:#eee; color:#456;} .debug{background:#ddd; margin-bottom: 2px;} </style>'); - _wd('request uri is '.$_SERVER["REQUEST_URI"]); - _wd('<pre>GET: '.print_r($_GET, 1).'</pre>'); - - - // ---------- SPLIT URL - - $aUriSplit= explode('/', preg_replace('/\?.*$/', '', $_SERVER["REQUEST_URI"])); - - array_shift($aUriSplit); - array_shift($aUriSplit); - _wd('<pre>$aUriSplit: '.print_r($aUriSplit, 1).'</pre>'); - /* - - /api/v1/projects/ci/build/... - $aUriSplit: Array - ( - [0] => v1 - [1] => projects - [2] => ci - [3] => build - ) - */ - $sApiVersion = isset($aUriSplit[0]) ? $aUriSplit[0] : false; - $sApiItem = isset($aUriSplit[1]) ? $aUriSplit[1] : false; - - - if(!$sApiVersion){ - _quit('ERROR: no param for api version was found.'); - } - if(!$sApiItem){ - _quit('ERROR: no param for item was found.'); - } - - - switch ($sApiVersion){ - case 'v1': - switch($sApiItem){ - case 'projects': - - $oProject=new project(); - $aList=$oProject->getProjects(); - _wd('<pre>'.print_r($aList,1).'</pre>'); - _done($aList); - break;; - - case 'project': - // path /api/v1/project - - $sPrjId = isset($aUriSplit[2]) ? $aUriSplit[2] : false; - $sPrjAction = isset($aUriSplit[3]) ? $aUriSplit[3] : false; - $sBranch = implode('/', array_slice($aUriSplit, 4)); - - // $sParam4 = isset($aUriSplit[4]) ? $aUriSplit[4] : false; - // $sParam5 = isset($aUriSplit[5]) ? $aUriSplit[5] : false; - $sMethod = $_SERVER['REQUEST_METHOD']; - _wd('$sPrjId = '.$sPrjId); - _wd('$sPrjAction = '.$sPrjAction); - _wd('$sBranch = ' . $sBranch); - - $oCLog = new logger(); - // try to init the given project - try{ - ob_start(); - $oProject=new project($sPrjId); - - // $oProject->setProjectById($sPrjId); - ob_end_clean(); - - } catch (Exception $exc) { - _quit('ERROR: project with id ['.$sPrjId.'] does not exist.', 404); - } - - // get secret - $aPrjCfg=$oProject->getConfig(); - $sProjectSecret=isset($aPrjCfg['api']['secret']) ? $aPrjCfg['api']['secret'] : false; - if(!$sProjectSecret){ - _quit('Access denied. API access is disabled.'); - } - - // check authorization - _checkAuth($sProjectSecret); - - // echo "OK: request was authorized successfully.\n"; - - $oProject->oUser->setUser('api'); - - switch($sPrjAction){ - case "build": - if ($sBranch){ - $aResult=$oProject->setBranchname($sBranch); - } - $sBranchname=$oProject->getBranchname(); - $aRepodata = $oProject->getRemoteBranches(true); // ignore cache = true - if(!isset($aRepodata[$sBranchname])){ - _quit('ERROR: branch not found: '.$sBranchname, 404); - } - - - // echo "branch is set to ".$oProject->getBranchname()."\n"; - if ($sMethod==='GET'){ - $sNext=$oProject->getNextPhase(); - _done(array( - 'branch'=>$sBranchname, - 'phase'=>$sNext, - 'repo'=>$aRepodata[$sBranchname] - )); - } - if ($sMethod==='POST'){ - echo "starting build() ..."; - flush(); - echo $oProject->build(); - } - break;; - case "phases": - _done($oProject->getAllPhaseInfos()); - break;; - default: - _quit('ERROR: Wrong action ['.$sApiItem.'].'); - } - - break;; - - default: - _quit('ERROR: item ['.$sApiItem.'] is invalid.'); - } - break; - default: - _quit('ERROR: Wrong (unsupported) api version.'); - } \ No newline at end of file +_wd('request uri is ' . $_SERVER["REQUEST_URI"]); +_wd('<pre>GET: ' . print_r($_GET, 1) . '</pre>'); + + +// ---------- SPLIT URL + +$aUriSplit = explode('/', preg_replace('/\?.*$/', '', $_SERVER["REQUEST_URI"])); + +array_shift($aUriSplit); +array_shift($aUriSplit); +_wd('<pre>$aUriSplit: ' . print_r($aUriSplit, 1) . '</pre>'); +/* + +/api/v1/projects/ci/build/... +$aUriSplit: Array + ( + [0] => v1 + [1] => projects + [2] => ci + [3] => build + ) + */ +$sApiVersion = isset($aUriSplit[0]) ? $aUriSplit[0] : false; +$sApiItem = isset($aUriSplit[1]) ? $aUriSplit[1] : false; + + +if (!$sApiVersion) { + _quit('ERROR: no param for api version was found.'); +} +if (!$sApiItem) { + _quit('ERROR: no param for item was found.'); +} + + +switch ($sApiVersion) { + case 'v1': + switch ($sApiItem) { + case 'projects': + + $oProject = new project(); + $aList = $oProject->getProjects(); + _wd('<pre>' . print_r($aList, 1) . '</pre>'); + _done($aList); + break; + ; + + case 'project': + // path /api/v1/project + + $sPrjId = isset($aUriSplit[2]) ? $aUriSplit[2] : false; + $sPrjAction = isset($aUriSplit[3]) ? $aUriSplit[3] : false; + $sBranch = implode('/', array_slice($aUriSplit, 4)); + + // $sParam4 = isset($aUriSplit[4]) ? $aUriSplit[4] : false; + // $sParam5 = isset($aUriSplit[5]) ? $aUriSplit[5] : false; + $sMethod = $_SERVER['REQUEST_METHOD']; + _wd('$sPrjId = ' . $sPrjId); + _wd('$sPrjAction = ' . $sPrjAction); + _wd('$sBranch = ' . $sBranch); + + $oCLog = new logger(); + // try to init the given project + try { + ob_start(); + $oProject = new project($sPrjId); + + // $oProject->setProjectById($sPrjId); + ob_end_clean(); + + } catch (Exception $exc) { + _quit('ERROR: project with id [' . $sPrjId . '] does not exist.', 404); + } + + // get secret + $aPrjCfg = $oProject->getConfig(); + $sProjectSecret = isset($aPrjCfg['api']['secret']) ? $aPrjCfg['api']['secret'] : false; + if (!$sProjectSecret) { + _quit('Access denied. API access is disabled.'); + } + + // check authorization + _checkAuth($sProjectSecret); + + // echo "OK: request was authorized successfully.\n"; + + $oProject->oUser->setUser('api'); + + switch ($sPrjAction) { + case "build": + if ($sBranch) { + $aResult = $oProject->setBranchname($sBranch); + } + $sBranchname = $oProject->getBranchname(); + $aRepodata = $oProject->getRemoteBranches(true); // ignore cache = true + if (!isset($aRepodata[$sBranchname])) { + _quit('ERROR: branch not found: ' . $sBranchname, 404); + } + + + // echo "branch is set to ".$oProject->getBranchname()."\n"; + if ($sMethod === 'GET') { + $sNext = $oProject->getNextPhase(); + _done([ + 'branch' => $sBranchname, + 'phase' => $sNext, + 'repo' => $aRepodata[$sBranchname] + ]); + } + if ($sMethod === 'POST') { + echo "starting build() ..."; + flush(); + echo $oProject->build(); + } + break; + ; + case "phases": + _done($oProject->getAllPhaseInfos()); + break; + ; + default: + _quit('ERROR: Wrong action [' . $sApiItem . '].'); + } + + break; + ; + + default: + _quit('ERROR: item [' . $sApiItem . '] is invalid.'); + } + break; + default: + _quit('ERROR: Wrong (unsupported) api version.'); +} \ No newline at end of file