From ce99d467a27442b10e7facd393a526f036dfe61f Mon Sep 17 00:00:00 2001 From: "Hahn Axel (hahn)" <axel.hahn@iml.unibe.ch> Date: Tue, 6 Apr 2021 13:41:09 +0200 Subject: [PATCH] Add features for "1.0" server: - allow single usage of a hash - storing/ cleanup hashes - prevent access to upper directories using "../" client: - added parameter "-l" to list items --- hooks/templates/inc_config.php.erb | 28 ++++++++++-- public_html/inc_config.php.dist | 23 ++++++++-- public_html/inc_functions.php | 67 ++++++++++++++++++++++++---- public_html/packages/index.php | 29 +++++++++--- readme.md | 14 +++--- shellscripts/getfile.sh | 71 +++++++++++++++++++++--------- tmp/.htkeep | 1 + 7 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 tmp/.htkeep diff --git a/hooks/templates/inc_config.php.erb b/hooks/templates/inc_config.php.erb index a7198aa..f9c9708 100644 --- a/hooks/templates/inc_config.php.erb +++ b/hooks/templates/inc_config.php.erb @@ -1,13 +1,33 @@ <?php +$approot=dirname(__DIR__); return array( // define a secret aka pi key - 'apikey'=>'@replace["apikey"]', + 'apikey'=>'<%= @replace["apikey"] %>', - // local directory of synched ci packages - 'packagedir'=>dirname(__DIR__).'/packages', + // define a secret aka api key + 'apikey'=>'our-package-server-secret', - // allow directory listing when accessing a path of a package + // packages to deliver where files from ci server are synched + 'packagedir'=>$approot.'/packages', + + // max age of request ... client and server need to be in sync + 'maxage'=>60, + + // force that a hash can be used only once + // a side effect is that fast repeat or simultanius requests + // will be denied. + 'onetimesecret'=>true, + + // filesize of lock file with stored hashed before starting garbage collection + // 10.000 byte are reached after 114 req + 'maxlockfilesize'=>50000, + + // tmp dir to store used hashes + 'tmpdir'=>$approot.'/tmp', + + // allow directory listing when accessing a path of a package + // true is required to fetch all packages 'showdircontent'=>true, ); diff --git a/public_html/inc_config.php.dist b/public_html/inc_config.php.dist index 080cff8..58ac35d 100644 --- a/public_html/inc_config.php.dist +++ b/public_html/inc_config.php.dist @@ -1,13 +1,30 @@ <?php +$approot=dirname(__DIR__); return array( - // define a secret aka pi key + // define a secret aka api key 'apikey'=>'our-package-server-secret', + + // packages to deliver where files from ci server are synched + 'packagedir'=>$approot.'/packages', + + // max age of request ... client and server need to be in sync + 'maxage'=>60, + + // force that a hash can be used only once + // a side effect is that fast repeat or simultanius requests + // will be denied. + 'onetimesecret'=>true, + + // filesize of lock file with stored hashed before starting garbage collection + // 10.000 byte are reached after 114 req + 'maxlockfilesize'=>10000, - // local directory of synched ci packages - 'packagedir'=>dirname(__DIR__).'/packages', + // tmp dir to store used hashes + 'tmpdir'=>$approot.'/tmp', // allow directory listing when accessing a path of a package + // true is required to fetch all packages 'showdircontent'=>true, ); \ No newline at end of file diff --git a/public_html/inc_functions.php b/public_html/inc_functions.php index 44a2704..1756e2e 100644 --- a/public_html/inc_functions.php +++ b/public_html/inc_functions.php @@ -3,13 +3,13 @@ /** * 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 type $sMySecret - * @return boolean + * On a failed check the request will be terminated; on suceess it returns + * the hash + * @param string $sMySecret server api key (from config file) + * @param int $iMaxAge max allowed age in [sec] + * @return string */ -function _checkAuth($sMySecret){ - global $iMaxAge; +function _checkAuth($sMySecret, $iMaxAge=60){ $aReqHeaders=apache_request_headers(); _wd('request headers: <pre>'.print_r($aReqHeaders, 1).'</pre>'); if(!isset($aReqHeaders['Authorization'])){ @@ -37,11 +37,63 @@ function _checkAuth($sMySecret){ $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); + _quit('Access denied. Hash is out of date: '.$iAge. ' sec is older '.$iMaxAge.' sec. Maybe client or server time 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 $sMyHash; +} + +/** + * check if a given secret was used already. + * It is for hardening - allow one time usage of a hash + * @param string $lockfile filename of lockfile with stored hashes + * @param string $sMyHash hash to verify + * @return boolean + */ +function _checkIfHashWasUsedAlready($lockfile, $sMyHash){ + $bFound=false; + $handle = @fopen($lockfile, "r"); + if ($handle){ + while (!feof($handle)) + { + $buffer = fgets($handle); + if(strstr($buffer, $sMyHash)){ + $bFound = true; + } + } + fclose($handle); + } + return $bFound; +} + +/** + * helper function for enabled one time secret: start cleanup of storage with + * used keys if a given filesize was reached. It keeps entries younger a given + * max age (same max age like in _checkAuth() + * @param string $lockfile filename of lockfile with stored hashes + * @param integer $iMaxLockfilesize size in byte when to start garbage collection + * @param integer $iMaxAge max allowed age in [sec] + * @return boolean + */ +function _cleanupLockdata($lockfile, $iMaxLockfilesize, $iMaxAge){ + if (filesize($lockfile)<$iMaxLockfilesize){ + return false; + } + $sLockdata=''; + $handle = @fopen($lockfile, "r"); + if ($handle){ + while (!feof($handle)) { + $buffer = fgets($handle); + $iTimestamp=(int)preg_replace('/\-.*$/', '', $buffer); + if ($iTimestamp && date('U') - $iTimestamp < $iMaxAge){ + $sLockdata.=$buffer; + } + } + fclose($handle); + file_put_contents($lockfile, $sLockdata); + } return true; } @@ -69,7 +121,6 @@ function _quit($s, $iStatus=400){ 404=>'HTTP/1.0 404 Not found', ); header($aStatus[$iStatus]); - # _done(array('status'=>$iStatus, 'info'=>$aStatus[$iStatus], 'message'=>$s)); _sendHtml($aStatus[$iStatus], $s); die(); } diff --git a/public_html/packages/index.php b/public_html/packages/index.php index 7f35bd4..7141902 100644 --- a/public_html/packages/index.php +++ b/public_html/packages/index.php @@ -15,11 +15,12 @@ ini_set('display_startup_errors', 1); error_reporting(E_ALL); - require('../inc_functions.php'); + require_once('../inc_functions.php'); $aConfig=require_once("../inc_config.php"); - $iMaxAge=60; - + $lockfile=$aConfig['tmpdir'].'/used_hashes.txt'; + $iMaxAge=$aConfig['maxage']; + // ---------------------------------------------------------------------- // MAIN @@ -32,20 +33,36 @@ _wd('request uri is '.$_SERVER["REQUEST_URI"]); _wd('<pre>GET: '.print_r($_GET, 1).'</pre>'); - - _checkAuth($aConfig['apikey']); - + // verify hashed secret + $sMyHash=_checkAuth($aConfig['apikey'], $iMaxAge); // if I am here then authentication was successful. + // limit to one time usage of a hash + if($aConfig['onetimesecret']){ + if(_checkIfHashWasUsedAlready($lockfile, $sMyHash)) { + _quit('Access denied. The hashed was used already.'); + } + _cleanupLockdata($lockfile, $aConfig['maxlockfilesize'], $iMaxAge); + + // first item must be unix ts followed by "-" char ... see + // _cleanupLockdata() to detect outdated data lines + file_put_contents($lockfile, date('U').'-'.date('Y-m-d__H:i:s').'-'.$sMyHash."\n", FILE_APPEND); + } + // ---------- SPLIT URL $sRelfile=preg_replace('#^/packages#', '', $_SERVER["REQUEST_URI"]); _wd('$sRelfile: '.$sRelfile); + // prevent going up a directory + if (strstr($sRelfile, '..')){ + _quit('Bad request. Invalid access to [..].', 400); + } $sMyFile=$aConfig['packagedir'].$sRelfile; _wd('full path of file: '.$sMyFile); + // handle a requested directory if (is_dir($sMyFile)){ if(!$aConfig['showdircontent']){ _quit('Filelisting is denied by config.', 403); diff --git a/readme.md b/readme.md index 47fd255..1c97abb 100644 --- a/readme.md +++ b/readme.md @@ -121,6 +121,7 @@ OPTIONS: -d enable debug infos -e PHASE phase; overrides env variable IMLCI_PHASE -f FILE filename to get (without path); overrides env variable IMLCI_FILE + -l ITEM list -o OUTFILE optional output file -p PROJECT ci project id; overrides env variable IMLCI_PROJECT -s SECRET override secret in IMLCI_PKG_SECRET @@ -135,6 +136,9 @@ VALUES: PROJECT project id of the ci server SECRET secret to access project data on package server. Your given secret must match the secret on package server to get access to any url. + ITEM type what to list; one of phases|projects|files + To list projects a phase must be set. + To list files a phase and a project must be set. DEFAULTS: @@ -156,11 +160,11 @@ EXAMPLES: there is a special file ALL; it fetches all filenames by executing a directory listing and then downloads all remote files with their original name - getfile.sh -f '' - empty file = directory listing of all your project files - - getfile.sh -p '' - empty project = directory listing of all projects with current phase + getfile.sh -e preview -l projects + list existing projects in phase preview + + getfile.sh -l files + list existing files of current project Remark: The directory listing can be turned off on the package server and results in a 403 status. diff --git a/shellscripts/getfile.sh b/shellscripts/getfile.sh index f7ecb33..33508ce 100755 --- a/shellscripts/getfile.sh +++ b/shellscripts/getfile.sh @@ -22,7 +22,12 @@ bDebug=0 function showhelp(){ self=$( basename $0 ) -echo "SYNTAX: +echo " +CIPGK GETTER + +Get packages from a software sattelite of IML ci server. + +SYNTAX: $self [OPTIONS] @@ -31,6 +36,7 @@ OPTIONS: -d enable debug infos -e PHASE phase; overrides env variable IMLCI_PHASE -f FILE filename to get (without path); overrides env variable IMLCI_FILE + -l ITEM list -o OUTFILE optional output file -p PROJECT ci project id; overrides env variable IMLCI_PROJECT -s SECRET override secret in IMLCI_PKG_SECRET @@ -45,7 +51,9 @@ VALUES: PROJECT project id of the ci server SECRET secret to access project data on package server. Your given secret must match the secret on package server to get access to any url. - + ITEM type what to list; one of phases|projects|files + To list projects a phase must be set. + To list files a phase and a project must be set. DEFAULTS: You don't need to set all values by command line. Use a config to set defaults @@ -66,11 +74,11 @@ EXAMPLES: there is a special file ALL; it fetches all filenames by executing a directory listing and then downloads all remote files with their original name - $self -f '' - empty file = directory listing of all your project files - - $self -p '' - empty project = directory listing of all projects with current phase + $self -e preview -l projects + list existing projects in phase preview + + $self -l files + list existing files of current project Remark: The directory listing can be turned off on the package server and results in a 403 status. @@ -118,12 +126,16 @@ ${apiTS} -X $apiMethod \ -o "${outfile}.downloading" \ --fail \ + -s \ ${IMLCI_URL}${apiRequest} + if [ $? -eq 0 ]; then - echo Download OK. + # echo OK. + + # no outfile (= request to a directory) if [ -z "$outfile" ]; then - echo - echo ----- RESPONSE BODY: + # echo + # echo ----- RESPONSE BODY: cat "${outfile}.downloading" rm -f "${outfile}.downloading" else @@ -148,21 +160,37 @@ ${apiTS} # MAIN # ---------------------------------------------------------------------- -echo -echo ===== CIPGK GETTER :: `date` ===== -echo - if [ $# -lt 1 ]; then showhelp exit 1 fi -while getopts "de:f:o:p:s:u:" option; do +while getopts "de:f:l:o:p:s:u:" option; do case ${option} in d) bDebug=1 ;; e) export IMLCI_PHASE=$OPTARG ;; f) export IMLCI_FILE=$OPTARG ;; + l) case $OPTARG in + phases) + IMLCI_PHASE='' + IMLCI_PROJECT='' + IMLCI_FILE='' + ;; + projects) + IMLCI_PROJECT='' + IMLCI_FILE='' + ;; + files) + IMLCI_FILE='' + ;; + *) + echo ERROR: invalid value for option [-l] + echo + showhelp + exit 2 + esac + ;; o) export IMLCI_OUTFILE=$OPTARG ;; p) export IMLCI_PROJECT=$OPTARG ;; s) export IMLCI_PKG_SECRET=$OPTARG ;; @@ -200,21 +228,22 @@ if [ $bDebug = 1 ]; then fi if [ "$IMLCI_FILE" = "ALL" ]; then - echo ALL files were requested ... - echo Get filelist ... - tmpfilelist=/tmp/myfilelist_gdgsdgadg + # echo ALL files were requested ... + printf "%-30s" "get list of all files... " + tmpfilelist=$( mktemp ) $0 -u "${IMLCI_URL}" \ -p "${IMLCI_PROJECT}" \ -e "${IMLCI_PHASE}" \ -s "${IMLCI_PKG_SECRET}" \ + -l files \ -o "${tmpfilelist}" - cat "${tmpfilelist}" + # cat "${tmpfilelist}" cat "${tmpfilelist}" | grep "^file:" | while read fileline do - echo $line + # echo $line myfile=$( echo $fileline | cut -f 2- -d ':' ) - echo GET $myfile... + printf "%-30s" "GET $myfile... " $0 -u "${IMLCI_URL}" \ -p "${IMLCI_PROJECT}" \ -e "${IMLCI_PHASE}" \ diff --git a/tmp/.htkeep b/tmp/.htkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tmp/.htkeep @@ -0,0 +1 @@ + -- GitLab