<?php /* ====================================================================== * * A P I F O R C I S E R V E R * * GET /api/v1/projects/ * GET /api/v1/project/[ID]/build/[name-of-branch] * POST /api/v1/project/[ID]/build/[name-of-branch] * GET /api/v1/project/[ID]/phases * * ---------------------------------------------------------------------- * 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); /** * 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>"; } return true; } /** * Abort execution of API request with 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', 401 => 'HTTP/1.0 401 Unauthorized', 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.', 401); } 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.', 401); } $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; } // ---------------------------------------------------------------------- // 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 . '] cannot be initialized.', 400); // never reached } // get secret $aPrjCfg = $oProject->getConfig(); if(!count($aPrjCfg)){ _quit('ERROR: project with id [' . $sPrjId . '] does not exist.', 404); } $sProjectSecret = $aPrjCfg['api']['secret'] ?? false; if (!$sProjectSecret) { _quit('Access denied. API access is disabled.', 403); } // 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.'); }