diff --git a/backend/commands/VpnController.php b/backend/commands/VpnController.php index 08f658813..dcb60313b 100644 --- a/backend/commands/VpnController.php +++ b/backend/commands/VpnController.php @@ -11,7 +11,9 @@ use yii\helpers\Console; use yii\console\Controller; use app\modules\frontend\models\Player; +use app\modules\activity\models\PlayerLast; use app\components\OpenVPN; +use yii\console\ExitCode; /** * Manages VPN specific operations. * @@ -65,7 +67,15 @@ public function actionKill($player) throw new ConsoleException(Yii::t('app', 'Player not found with id or username of [{values}]', ['values' => $player])); } printf("Killing %d with last local IP [%s]\n",$pM->id,$pM->last->vpn_local_address_octet); - OpenVPN::kill($pM->id,intval($pM->last->vpn_local_address)); + try { + OpenVPN::kill($pM->id,intval($pM->last->vpn_local_address)); + } + catch(\Exception $e) + { + echo "Error: ",$e->getMessage(),"\n"; + return ExitCode::UNSPECIFIED_ERROR; + } + } /** @@ -74,14 +84,25 @@ public function actionKill($player) public function actionKillall() { - foreach(Player::find()->where(['status'=>10])->all() as $pM) + foreach(PlayerLast::find()->all() as $pM) { - printf("Logging out %d\n",$pM->id); - OpenVPN::logout($pM->id); + try { + if($pM->vpn_local_address!==null) + { + printf("Killing %s => %d\n",$pM->player->username,$pM->id); + OpenVPN::kill($pM->id,intval($pM->vpn_local_address)); + } + } + catch(\Exception $e) + { + echo "Error: ",$e->getMessage(),"\n"; + } } + Yii::$app->db->createCommand("UPDATE player_last SET vpn_local_address=NULL, vpn_remote_address=NULL")->execute(); + } /** - * Logoutall stall connections from OpenVPN. + * Logout all stall connections from all player_last entries */ public function actionLogoutall() { @@ -89,43 +110,98 @@ public function actionLogoutall() } /** - * Load configuration from filesystem + * Load OpenVPN configuration from filesystem and store to the database. + * Parses the configuration file for management interface and ranges. + * @param string $filename The filename to read and store to the database */ public function actionLoad($filepath) { $file=basename($filepath); + $conf=file_get_contents($filepath); try{ - $contents=file_get_contents($filepath); - Yii::$app->db->createCommand("UPDATE openvpn SET conf=:config WHERE name=:filename",[':config'=>$contents,':filename'=>$file])->execute(); + if(preg_match('/server (.*) (.*)/',$conf,$matches) && count($matches)>1) + { + $ovpnModel=\app\modules\settings\models\Openvpn::find()->where(['name'=>$file,'net'=>ip2long($matches[1])]); + } + if(($ovpn=$ovpnModel->one())===null) + { + $ovpn=new \app\modules\settings\models\Openvpn; + } + $ovpn->conf=file_get_contents($filepath); + $ovpn->name=$file; + $ovpn->server=gethostbyaddr(gethostbyname(gethostname())); + if(preg_match('/status (.*)/',$conf,$matches) && count($matches)>1) + { + $ovpn->status_log=trim($matches[1]); + } + if(preg_match('/management (.*) (.*) (.*)/',$conf,$matches) && count($matches)>1) + { + $ovpn->management_ip_octet=$matches[1]; + $ovpn->management_port=$matches[2]; + if(str_starts_with($matches[3], '/')) + { + if(file_exists($matches[3])) + $ovpn->management_passwd=file_get_contents($matches[3]); + else + echo "WARNING: The provided config uses a file as a management password\n\t but the file [",$matches[3], "] does not exist!\n"; + } + } + if(preg_match('/server (.*) (.*)/',$conf,$matches) && count($matches)>1) + { + $ovpn->net_octet=$matches[1]; + $ovpn->mask_octet=$matches[2]; + } + if($ovpn->save()) + { + echo $ovpn->isNewRecord ? "Record created successfully!\n" : "Record updated successfully!\n"; + } + else + { + echo "Failed to save record: ",$ovpn->getErrorSummary(true),"\n"; + } } catch (\Exception $e) { - printf("Error: ",$e->getMessage()); + printf("Error: %s",$e->getMessage()); } - } /** - * Save configuration to filesystem + * Save OpenVPN configuration to filesystem. + * Uses the provided file basename and current system hostname to find the actual entry. + * @param string $filepath The full path to store the config contents. */ public function actionSave($filepath) { try{ $file=basename($filepath); - $contents=Yii::$app->db->createCommand("SELECT conf FROM openvpn WHERE name=:filename",[':filename'=>$file])->queryScalar(); - file_put_contents($filepath,$contents); + $ovpnModel=\app\modules\settings\models\Openvpn::find()->where(['server'=>gethostname(),'name'=>$file]); + if(($ovpn=$ovpnModel->one())===null) + { + echo "No record found for the given file and server!\n"; + return ExitCode::CANTCREAT; + } + if(file_put_contents($filepath,$ovpn->conf)) + { + echo "File saved at ",$filepath,"\n"; + } + else + { + echo "Failed to save ",$filepath,"\n"; + return ExitCode::UNSPECIFIED_ERROR; + } } catch (\Exception $e) { - printf("Error: ",$e->getMessage()); + printf("Error: %s",$e->getMessage()); } } /** * Display openvpn status enriched with database details */ - public function actionStatus($provider_id=null) + public function actionStatus() { - $q=\app\modules\settings\models\Openvpn::find()->select('status_log')->andFilterWhere(['LIKE','provider_id',$provider_id]); + $q=\app\modules\settings\models\Openvpn::find()->select('status_log')->andFilterWhere(['server'=>gethostname()]); $status['routing_table']=[]; $status['client_list']=[]; foreach($q->all() as $entry) @@ -139,11 +215,11 @@ public function actionStatus($provider_id=null) } catch (\Exception $e) { - + printf("Error: %s",$e->getMessage()); } unset($entry); } - $this->stdout( sprintf("%-5s %-10s %-10s %-18s %-10s %-10s\n", 'ID', 'Username','Local IP','Remote IP', 'Received', 'Send'), Console::BOLD); + $this->stdout(sprintf("%-5s %-10s %-10s %-18s %-10s %-10s\n", 'ID', 'Username','Local IP','Remote IP', 'Received', 'Send'), Console::BOLD); foreach($status['client_list'] as $entry) { $p=\app\modules\frontend\models\Player::findOne($entry->player_id); diff --git a/backend/components/OpenVPN.php b/backend/components/OpenVPN.php index 59772c794..90ade6c2e 100644 --- a/backend/components/OpenVPN.php +++ b/backend/components/OpenVPN.php @@ -49,9 +49,9 @@ static public function kill(int $player_id,int $player_ip) { echo "connected to {$creds->management_ip_octet}\n"; fwrite($fp, "$creds->management_passwd\n"); - echo "send {$creds->management_ip_octet}\n"; + echo "sending to ",$creds->management_ip_octet,"\n"; usleep(250000); - fwrite($fp, "kill ${player_id}\n"); + fwrite($fp, "kill $player_id\n"); usleep(250000); fwrite($fp, "exit\n"); usleep(250000); @@ -77,6 +77,10 @@ static public function determineServerByAddr(int $player_ip) static public function parseStatus(string $location) { + if(!file_exists($location)) + { + throw new yii\base\UserException("Status file does not exist"); + } $statusLines=explode("\n",file_get_contents($location)); if(count($statusLines)==0) return new stdClass; diff --git a/backend/components/validators/VerifymailValidator.php b/backend/components/validators/VerifymailValidator.php new file mode 100644 index 000000000..bccd0c047 --- /dev/null +++ b/backend/components/validators/VerifymailValidator.php @@ -0,0 +1,112 @@ +sys->signup_ValidatemailValidator===false) + return; + + $data = http_build_query(['key'=>\Yii::$app->sys->verifymail_key]); + + $domain=$value; + + if(str_contains($value,'@')) + { + $parts=explode("@",$value); + if(count($parts)<2) + { + return ['Verifymail.io: Failed to parse email!', [ + 'email' => $value, + ]]; + } + $domain=end($parts); + } + + $req=$this->url.$domain.'?'.$data; + $ch = curl_init($req); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 40); + + try + { + $result = curl_exec($ch); + curl_close($ch); + $retData=json_decode($result); + if($retData != null && $retData->disposable===true) + { + return [$this->message, [ + 'email' => $value, + ]]; + } + } + catch(\Exception $e) + { + if(curl_errno($ch)===0) + return [$this->message, ['email' => $value]]; + } + } + + public function validateAttribute($model, $attribute) + { + if(\Yii::$app->sys->signup_ValidatemailValidator===false) + return; + $value = $model->$attribute; + + $data = http_build_query(['key'=>\Yii::$app->sys->verifymail_key]); + + if(str_contains($value,'@')) + { + $parts=explode("@",$value); + if(count($parts)<2) + { + return ['Verifymail.io: Failed to parse email!', [ + 'email' => $value, + ]]; + } + $domain=end($parts); + } + + $req=$this->url.$domain.'?'.$data; + $ch = curl_init($req); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 40); + + try + { + $result = curl_exec($ch); + curl_close($ch); + $retData=json_decode($result); + if($retData && $retData->message) + { + \Yii::error("Verifymail.io return message: ".$retData->message); + } + if($retData != null && $retData->disposable===true) + { + return [$this->message, ['email' => $value]]; + } + } + catch(\Exception $e) + { + if(curl_errno($ch)===0) + return [$this->message, ['email' => $value]]; + } + } +} diff --git a/backend/migrations/m240320_235210_add_server_column_to_openvpn_table.php b/backend/migrations/m240320_235210_add_server_column_to_openvpn_table.php new file mode 100644 index 000000000..39b8ee733 --- /dev/null +++ b/backend/migrations/m240320_235210_add_server_column_to_openvpn_table.php @@ -0,0 +1,25 @@ +addColumn('{{%openvpn}}', 'server', $this->string()->after('id')); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropColumn('{{%openvpn}}', 'server'); + } +} diff --git a/backend/migrations/m240320_235211_alter_openvpn_table_unique_keys.php b/backend/migrations/m240320_235211_alter_openvpn_table_unique_keys.php new file mode 100644 index 000000000..4027d158d --- /dev/null +++ b/backend/migrations/m240320_235211_alter_openvpn_table_unique_keys.php @@ -0,0 +1,31 @@ +dropIndex('name', 'openvpn'); + $this->dropIndex('net', 'openvpn'); + $this->createIndex('server_name_net', 'openvpn', ['server','name','net'], true ); + + + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropIndex('server_name_net', 'openvpn'); + $this->createIndex('name', 'openvpn', ['name'], true ); + $this->createIndex('net', 'openvpn', ['name'], true ); + } +} diff --git a/backend/modules/activity/models/PlayerLast.php b/backend/modules/activity/models/PlayerLast.php index 2a1976dc1..84c4ffe63 100644 --- a/backend/modules/activity/models/PlayerLast.php +++ b/backend/modules/activity/models/PlayerLast.php @@ -56,6 +56,10 @@ public function attributeLabels() 'signup_ip' => 'Signup IP', ]; } + public function resetVPN() { + return $this->updateAttributes(['vpn_remote_address' => null,'vpn_local_address'=>null]); + } + public function afterFind() { parent::afterFind(); $this->signin_ipoctet=long2ip($this->signin_ip); diff --git a/backend/modules/frontend/controllers/PlayerController.php b/backend/modules/frontend/controllers/PlayerController.php index 44650a81f..d751b803f 100644 --- a/backend/modules/frontend/controllers/PlayerController.php +++ b/backend/modules/frontend/controllers/PlayerController.php @@ -36,6 +36,7 @@ public function behaviors() 'actions' => [ 'set-deleted' => ['POST'], 'clear-verification-token' => ['POST'], + 'clear-vpn' => ['POST'], 'delete' => ['POST'], 'ban' => ['POST'], 'ban-filtered' => ['POST'], @@ -407,6 +408,37 @@ public function actionDeleteFiltered() } return $this->redirect(['index']); } + + /** + * Clear VPN related data for a given user + * @param integer $id The player ID + * @throws NotFoundHttpException if the player id cannot be found + */ + public function actionClearVpn($id) + { + $player=$this->findModel($id); + $ip=Yii::$app->cache->Memcache->get("ovpn:".$player->id); + if($ip!==false) + { + Yii::$app->cache->Memcache->delete("ovpn:".$player->id); + $memid=Yii::$app->cache->Memcache->get("ovpn:".long2ip($ip)); + if($memid!==false && intval($memid)===intval($id)) + { + Yii::$app->cache->Memcache->delete("ovpn:".long2ip($ip)); + } + } + if($player->last->resetVPN()) + { + \Yii::$app->session->addFlash('success', \Yii::t('app', "Player VPN details cleared")); + } + else + { + \Yii::$app->session->addFlash('error', \Yii::t('app', "Player VPN details failed to clear")); + } + + return $this->redirect(['index']); + } + /** * Finds the Player model based on its primary key value. * If the model is not found, a 404 HTTP exception will be thrown. diff --git a/backend/modules/frontend/models/PlayerAR.php b/backend/modules/frontend/models/PlayerAR.php index dd794f6b0..680d5befd 100644 --- a/backend/modules/frontend/models/PlayerAR.php +++ b/backend/modules/frontend/models/PlayerAR.php @@ -131,8 +131,9 @@ public function rules() $this->addError($attribute, '{attribute} must be empty when player active.'); },'on'=>'validator'], //['email', 'email','checkDNS'=>true,'on'=>'validator','message'=>'This domain does not resolve.'], + //['email', '\app\components\validators\VerifymailValidator', 'when' => function($model) { return (bool)Yii::$app->sys->signup_ValidatemailValidator;}], //['email', '\app\components\validators\StopForumSpamValidator', 'max'=>Yii::$app->sys->signup_StopForumSpamValidator,'when' => function($model) { return Yii::$app->sys->signup_StopForumSpamValidator!==false;},'on'=>'validator'], - ['email', '\app\components\validators\MXServersValidator', 'mxonly'=>false, 'when' => function($model) { return Yii::$app->sys->signup_MXServersValidator!==false;},'on'=>'validator'], + //['email', '\app\components\validators\MXServersValidator', 'mxonly'=>false, 'when' => function($model) { return Yii::$app->sys->signup_MXServersValidator!==false;},'on'=>'validator'], ]; } diff --git a/backend/modules/frontend/views/player/index.php b/backend/modules/frontend/views/player/index.php index 0a771305f..f7069ad55 100644 --- a/backend/modules/frontend/views/player/index.php +++ b/backend/modules/frontend/views/player/index.php @@ -59,6 +59,8 @@ ], [ 'attribute' => 'avatar', + 'label'=>false, + 'headerOptions' => ['style' => 'width:3.5em'], 'format' => ['image', ['width' => '40px', 'class' => 'img-thumbnail']], 'value' => function ($data) { return '//' . Yii::$app->sys->offense_domain . '/images/avatars/' . $data->profile->avatar; @@ -88,7 +90,11 @@ return Html::encode($model->affiliation); }, ], - 'email:email', + [ + 'attribute'=>'email', + 'format'=>'email', + 'contentOptions' => ['class' => 'small'], + ], [ 'attribute' => 'vpn_local_address', 'label' => 'VPN IP', @@ -101,22 +107,26 @@ [ 'attribute' => 'academic', 'value' => 'academicShort', + 'contentOptions' => ['class' => 'small'], 'filter' => [0 => Yii::$app->sys->academic_0short, 1 => Yii::$app->sys->academic_1short, 2 => Yii::$app->sys->academic_2short], ], [ 'attribute' => 'status', 'format' => 'playerStatus', 'filter' => [10 => 'Enabled', 9 => 'Inactive', 8 => "Change", 0 => "Deleted",], + 'contentOptions' => ['class' => 'small'], + ], [ 'attribute' => 'type', - 'filter' => ["offense"=>"offense","defense"=>"defense"] + 'filter' => ["offense"=>"offense","defense"=>"defense"], + 'contentOptions' => ['class' => 'small'], ], [ 'attribute'=>'created', - 'contentOptions' => ['class' => 'text-small'] + 'contentOptions' => ['class' => 'small'] ], //'ts', [ @@ -124,11 +134,11 @@ 'visibleButtons'=>[ 'generate-ssl'=>function($model){ if ($model->status==10 || $model->active==1) return true; return false;}, 'mail'=>function($model){ if ($model->status==10 || $model->active==1) return false; return true;}, - 'kill-vpn'=>function($model){ if ($model->last->vpn_local_address!==null) return true; return false;}, + 'clear-vpn'=>function($model){ if ($model->last->vpn_local_address!==null) return true; return false;}, 'delete'=>function($model){ if (\Yii::$app->user->identity->isAdmin) return true; return false;}, 'reset-activkey'=>function($model){ if ($model->active && trim($model->activkey)!=="") return true; return false;}, ], - 'template' => '{player-view-full} {kill-vpn} {view} {generate-ssl} {set-deleted} ' . '{update} {delete} {ban} {mail} {reset-activkey}', + 'template' => '{player-view-full} {clear-vpn} {view} {generate-ssl} {set-deleted} ' . '{update} {delete} {ban} {mail} {reset-activkey}', 'header' => Html::a( '', ['ban-filtered'], @@ -167,12 +177,12 @@ ], ]); }, - 'kill-vpn' => function($url, $model) { - return Html::a('', ['kill-vpn', 'id' => $model->id], [ + 'clear-vpn' => function($url, $model) { + return Html::a('', ['clear-vpn', 'id' => $model->id], [ 'class' => '', - 'title'=>'Kill VPN Session', + 'title'=>'Clear VPN Session', 'data' => [ - 'confirm' => 'Are you absolutely sure you want to kill the vpn session for ['.Html::encode($model->username).'] ?', + 'confirm' => 'Are you absolutely sure you want to clear the vpn session for ['.Html::encode($model->username).'] ?', 'method' => 'post', ], ]); diff --git a/backend/modules/settings/models/ConfigureForm.php b/backend/modules/settings/models/ConfigureForm.php index 31d8b3d3f..636faf9bc 100644 --- a/backend/modules/settings/models/ConfigureForm.php +++ b/backend/modules/settings/models/ConfigureForm.php @@ -55,8 +55,8 @@ class ConfigureForm extends Model public $leaderboard_visible_after_event_end; public $leaderboard_show_zero; public $time_zone; - public $target_days_new; - public $target_days_updated; + public $target_days_new=2; + public $target_days_updated=1; public $discord_invite_url; public $discord_news_webhook; public $pf_state_limits; @@ -219,8 +219,6 @@ public function rules() 'mail_fromName', 'approved_avatar', 'team_manage_members', - 'target_days_new', - 'target_days_updated', ], 'required'], ['challenge_home','default','value'=>'@web/uploads'], ['challenge_root','default','value'=>'/uploads/'], @@ -233,6 +231,8 @@ public function rules() [['online_timeout', 'spins_per_day','members_per_team','target_days_new','target_days_updated'], 'integer'], [['online_timeout'], 'default', 'value'=>900], [['spins_per_day'], 'default', 'value'=> 2], + ['target_days_new','default','value'=>1], + ['target_days_updated','default','value'=>2], [['event_start','event_end','registrations_start','registrations_end'], 'datetime', 'format' => 'php:Y-m-d H:i:s'], [[ 'dashboard_is_home', diff --git a/backend/modules/settings/models/Openvpn.php b/backend/modules/settings/models/Openvpn.php index 60254971f..7c827ef45 100644 --- a/backend/modules/settings/models/Openvpn.php +++ b/backend/modules/settings/models/Openvpn.php @@ -9,6 +9,7 @@ * * @property int $id * @property string|null $provider_id + * @property string|null $server * @property string|null $name * @property int|null $net * @property int|null $mask @@ -41,10 +42,9 @@ public function rules() [['net', 'mask', 'management_ip', 'management_port'], 'integer'], [['conf'], 'string'], [['created_at', 'updated_at'], 'safe'], - [['provider_id', 'name', 'management_passwd','status_log'], 'string', 'max' => 255], + [['provider_id', 'name', 'management_passwd','status_log','server'], 'string', 'max' => 255], [['net_octet','mask_octet','management_ip_octet'], 'ip'], - [['name'], 'unique'], - [['net'], 'unique'], + [['server','name','net'], 'unique','targetAttribute'=>['server','name','net']], ]; } @@ -56,6 +56,7 @@ public function attributeLabels() return [ 'id' => Yii::t('app', 'ID'), 'provider_id' => Yii::t('app', 'Provider ID'), + 'server' => Yii::t('app', 'Server'), 'name' => Yii::t('app', 'Name'), 'net' => Yii::t('app', 'Net'), 'mask' => Yii::t('app', 'Mask'), @@ -87,10 +88,12 @@ public function beforeSave($insert) { return false; } - - $this->net=ip2long($this->net_octet); - $this->mask=ip2long($this->mask_octet); - $this->management_ip=ip2long($this->management_ip_octet); + if(empty($this->net)) + $this->net=ip2long($this->net_octet); + if(empty($this->mask)) + $this->mask=ip2long($this->mask_octet); + if(empty($this->management_ip)) + $this->management_ip=ip2long($this->management_ip_octet); return true; } diff --git a/backend/modules/settings/views/openvpn/_form.php b/backend/modules/settings/views/openvpn/_form.php index 72cf59613..6cd4c4f9f 100644 --- a/backend/modules/settings/views/openvpn/_form.php +++ b/backend/modules/settings/views/openvpn/_form.php @@ -14,7 +14,9 @@ field($model, 'provider_id')->textInput(['maxlength' => true,'placeholder'=>'vpn01-eu01.example.com'])->hint("An string to help you distinguish the server instance that this entry refers to (eg. vpn01-eu01.example.com)") ?> - field($model, 'name')->textInput(['maxlength' => true,'placeholder'=>'openvpn_tun0.conf'])->hint("A unique name to distinguish this instance from others on the same server, this can be the configuration file name (eg. server_tun0.conf)") ?> + field($model, 'server')->textInput(['maxlength' => true,'placeholder'=>'vpn.example.com'])->hint("The server name for this entry)") ?> + + field($model, 'name')->textInput(['maxlength' => true,'placeholder'=>'openvpn_tun0.conf'])->hint("The configuration file name for this instance of openvpn (eg. server_tun0.conf)") ?> field($model, 'net_octet')->textInput(['maxlength' => true,'placeholder'=>'10.10.0.0'])->hint("Network address that this OpenVPN instance serves. This must reflect your server block ie server 10.10.0.0 255.255.0.0") ?> diff --git a/backend/modules/settings/views/openvpn/index.php b/backend/modules/settings/views/openvpn/index.php index dc051f7de..3fc3c49d0 100644 --- a/backend/modules/settings/views/openvpn/index.php +++ b/backend/modules/settings/views/openvpn/index.php @@ -33,6 +33,7 @@ 'columns' => [ 'id', 'provider_id', + 'server', 'name', 'net_octet', 'mask_octet', diff --git a/backend/modules/settings/views/openvpn/view.php b/backend/modules/settings/views/openvpn/view.php index fc6462c05..3333c21ea 100644 --- a/backend/modules/settings/views/openvpn/view.php +++ b/backend/modules/settings/views/openvpn/view.php @@ -31,6 +31,7 @@ 'attributes' => [ 'id', 'provider_id', + 'server', 'name', 'net_octet', 'mask_octet', diff --git a/contrib/Dockerfile-backend b/contrib/Dockerfile-backend index f09cdeddd..24be11053 100644 --- a/contrib/Dockerfile-backend +++ b/contrib/Dockerfile-backend @@ -33,11 +33,6 @@ RUN set -ex \ && chmod a+rwx /var/www/echoCTF.RED/${RED_APP}/web/uploads \ && cd ${RED_APP} \ && composer validate \ -# [ -z "${GITHUB_OAUTH_TOKEN}" ] || git config --global url."https://".insteadOf "git://" ; \ -# [ -z "${GITHUB_OAUTH_TOKEN}" ] || composer config -g github-oauth.github.com "${GITHUB_OAUTH_TOKEN}"; \ -# [ -z "${GITHUB_OAUTH_TOKEN}" ] || composer config --global github-protocols https; \ -# [ -z "${GITHUB_OAUTH_TOKEN}" ] || composer install --no-dev --prefer-dist --no-progress --no-suggest ; \ -# [ -z "${GITHUB_OAUTH_TOKEN}" ] || composer clearcache; \ && cd .. \ && mv /var/www/html /var/www/html.old \ && ln -s /var/www/echoCTF.RED/${RED_APP}/web /var/www/html \ diff --git a/contrib/Dockerfile-vpn b/contrib/Dockerfile-vpn index 251c89cb4..9e0a79ffc 100644 --- a/contrib/Dockerfile-vpn +++ b/contrib/Dockerfile-vpn @@ -28,10 +28,6 @@ RUN set -ex \ touch /sbin/pfctl && chmod +x /sbin/pfctl; \ ln -s /var/www/echoCTF.RED/${RED_APP}/yii /usr/local/sbin/backend; \ git config --global url."https://".insteadOf "git://" ; \ -# [ -z "${GITHUB_OAUTH_TOKEN}" ] || composer config -g github-oauth.github.com "${GITHUB_OAUTH_TOKEN}"; \ -# [ -z "${GITHUB_OAUTH_TOKEN}" ] || composer config --global github-protocols https; \ -# [ -z "${GITHUB_OAUTH_TOKEN}" ] || composer install --no-dev --prefer-dist --no-progress --no-suggest; \ -# [ -z "${GITHUB_OAUTH_TOKEN}" ] || composer clearcache; \ chmod +x /entrypoint.sh; useradd _openvpn; \ touch /var/log/openvpn/openvpn.log; \ rm -rf /root/.composer /usr/src/* /var/lib/apt/lists/* diff --git a/contrib/entrypoint-vpn.sh b/contrib/entrypoint-vpn.sh index fb7e8fb54..7a3ef6b44 100644 --- a/contrib/entrypoint-vpn.sh +++ b/contrib/entrypoint-vpn.sh @@ -24,6 +24,7 @@ if [ ! -f /etc/openvpn/.configured ]; then /var/www/echoCTF.RED/backend/yii ssl/create-ca /var/www/echoCTF.RED/backend/yii ssl/get-ca 1 /var/www/echoCTF.RED/backend/yii ssl/create-cert "VPN Server" + /var/www/echoCTF.RED/backend/yii vpn/load /etc/openvpn/openvpn_tun0.conf mv echoCTF-OVPN-CA.crt /etc/openvpn/private/echoCTF-OVPN-CA.crt mv echoCTF-OVPN-CA.key /etc/openvpn/private/echoCTF-OVPN-CA.key mv VPN\ Server.crt /etc/openvpn/private/VPN\ Server.crt diff --git a/docs/Sysconfig-Keys.md b/docs/Sysconfig-Keys.md index 837229fd0..27a7ce04c 100644 --- a/docs/Sysconfig-Keys.md +++ b/docs/Sysconfig-Keys.md @@ -22,6 +22,8 @@ * `players_require_approval` If player activation requires moderator approval first * `disable_registration` Whether online registrations are allowed * `team_visible_instances` Whether or not player instances are visible to the rest of the team by default otherwise the per-instance field `team_allowed` takes priority +* `guest_visible_leaderboards` Whether or not the leaderboards will be visible to guest users (this still respects the event start/end restrictions) +* `hide_timezone` Whether or not the Timezone information should be visible * `profile_discord`: Whether the field will be visible under the player profile page. This is different than `profile_settings_fields` * `profile_echoctf`: Whether the field will be visible under the player profile page. This is different than `profile_settings_fields` @@ -117,6 +119,8 @@ backend/yii sysconfig/set academic_1short "anothersite" * `signup_TotalRegistrationsValidator` Number of total registrations allowed per single IP overall on the platform. `0` Disables the check completely * `signup_HourRegistrationValidator` Number of total registrations per IP allowed. `0` Disables the check completely * `signup_StopForumSpamValidator` Percentage of confidence required before we mark an email offensive from StopForumSpam (eg `80`). `0` Disables the check completely +* `signup_VerifymailValidator` Enable or disable verifymail.io validator +* `verifymail_key` The API key for verifymail.io * `signup_MXServersValidator` Enable/Disable validating `MX` and `IN A` DNS records for given domains. `0` Disables the check completely * `failed_login_ip` A number of failed logins are allowed per IP. `0` Disables the check completely * `failed_login_ip_timeout` timeout of failed login ip counter expires diff --git a/docs/console-commands/Vpn.md b/docs/console-commands/Vpn.md index 68d8362c3..61732aa2a 100644 --- a/docs/console-commands/Vpn.md +++ b/docs/console-commands/Vpn.md @@ -2,25 +2,57 @@ General purpose VPN related commands. ## is-online -Check if a given player username or id is currently online +Check if a given player `username` or `id` is currently online -## kill -Connect to an OpenVPN management port and issue a kill command. +Usage: `./backend/yii vpn/is-online ` + +## kill +Kill the OpenVPN connection of a given player `username` or `id`. + +Usage: `./backend/yii vpn/kill ` ## killall -Kill all connections and set players offline. +Kill all (not just online ones) player connections from OpenVPN and set players offline. + +Usage: `./backend/yii vpn/killall` + + +This command connects to the OpenVPN management port and issues a `kill` command for each active player which currently has an IP assigned to them. Once the OpenVPN operations are complete a cleanup is performed to reset the online status of all players. ## load Load an OpenVPN instance configuration file into the database +Usage: `./backend/yii vpn/load ` + +The command extracts the following details from the provided configuration file: +* Gets the hostname from the running host +* Player allocated Network and Netmask +* OpenVPN Management IP, port and password to authenticate +* OpenVPN status file location + ## logout -Logout a given user ignoring OpenVPN sessions +Logout a given player `username` or `id`, from the database, ignoring OpenVPN sessions. + +Usage: `./backend/yii vpn/load ` + ## logoutall -Logout all users ignoring OpenVPN sessions +Logout all players from the database. It **does NOT** issue an OpenVPN kill command for connected players. + +**NOTE**: _Only issue this command when you are certain there are no users connected to OpenVPN._ ## save Save an OpenVPN instance configuration file with data from the database +Usage: `./backend/yii vpn/save ` + +The command looks for a record with the following criteria: +* uses the current system `hostname` as a `server` +* uses the filename (basename) from the openvpn configuration file provided as cli argument + ## status A small wrapper for OpenVPN status files. The details are merged with data from the database. + +Usage: `./backend/yii vpn/status` + +The command uses the status file defined in the Backend Settings=>OpenVPN entries matching the current server. diff --git a/frontend/commands/ValidatorController.php b/frontend/commands/ValidatorController.php index fce5de705..0c0db6e8d 100644 --- a/frontend/commands/ValidatorController.php +++ b/frontend/commands/ValidatorController.php @@ -12,7 +12,6 @@ use app\models\Stream; class ValidatorController extends Controller { - public function actionIndex() { $this->stdout("The Validator provides the following actions: \n", Console::BOLD); diff --git a/frontend/components/validators/VerifymailValidator.php b/frontend/components/validators/VerifymailValidator.php new file mode 100644 index 000000000..bccd0c047 --- /dev/null +++ b/frontend/components/validators/VerifymailValidator.php @@ -0,0 +1,112 @@ +sys->signup_ValidatemailValidator===false) + return; + + $data = http_build_query(['key'=>\Yii::$app->sys->verifymail_key]); + + $domain=$value; + + if(str_contains($value,'@')) + { + $parts=explode("@",$value); + if(count($parts)<2) + { + return ['Verifymail.io: Failed to parse email!', [ + 'email' => $value, + ]]; + } + $domain=end($parts); + } + + $req=$this->url.$domain.'?'.$data; + $ch = curl_init($req); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 40); + + try + { + $result = curl_exec($ch); + curl_close($ch); + $retData=json_decode($result); + if($retData != null && $retData->disposable===true) + { + return [$this->message, [ + 'email' => $value, + ]]; + } + } + catch(\Exception $e) + { + if(curl_errno($ch)===0) + return [$this->message, ['email' => $value]]; + } + } + + public function validateAttribute($model, $attribute) + { + if(\Yii::$app->sys->signup_ValidatemailValidator===false) + return; + $value = $model->$attribute; + + $data = http_build_query(['key'=>\Yii::$app->sys->verifymail_key]); + + if(str_contains($value,'@')) + { + $parts=explode("@",$value); + if(count($parts)<2) + { + return ['Verifymail.io: Failed to parse email!', [ + 'email' => $value, + ]]; + } + $domain=end($parts); + } + + $req=$this->url.$domain.'?'.$data; + $ch = curl_init($req); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 40); + + try + { + $result = curl_exec($ch); + curl_close($ch); + $retData=json_decode($result); + if($retData && $retData->message) + { + \Yii::error("Verifymail.io return message: ".$retData->message); + } + if($retData != null && $retData->disposable===true) + { + return [$this->message, ['email' => $value]]; + } + } + catch(\Exception $e) + { + if(curl_errno($ch)===0) + return [$this->message, ['email' => $value]]; + } + } +} diff --git a/frontend/models/forms/SignupForm.php b/frontend/models/forms/SignupForm.php index 13fd2b838..1d62c233c 100644 --- a/frontend/models/forms/SignupForm.php +++ b/frontend/models/forms/SignupForm.php @@ -54,6 +54,7 @@ public function rules() }], ['username', '\app\components\validators\HourRegistrationValidator', 'client_ip'=>\Yii::$app->request->userIp, 'max'=>Yii::$app->sys->signup_HourRegistrationValidator,'when' => function($model) { return Yii::$app->sys->signup_HourRegistrationValidator!==false;}], ['username', '\app\components\validators\TotalRegistrationsValidator', 'client_ip'=>\Yii::$app->request->userIp, 'max'=>Yii::$app->sys->signup_TotalRegistrationsValidator,'when' => function($model) { return Yii::$app->sys->signup_TotalRegistrationsValidator!==false;}], + ['email', '\app\components\validators\VerifymailValidator', 'when' => function($model) { return (bool)Yii::$app->sys->signup_ValidatemailValidator;}], ['email', '\app\components\validators\StopForumSpamValidator', 'max'=>Yii::$app->sys->signup_StopForumSpamValidator,'when' => function($model) { return Yii::$app->sys->signup_StopForumSpamValidator!==false;}], ['email', '\app\components\validators\MXServersValidator', 'mxonly'=>true, 'when' => function($model) { return Yii::$app->sys->signup_MXServersValidator!==false;}], //['email', '\app\components\validators\WhoisValidator', ], diff --git a/frontend/modules/game/controllers/LeaderboardsController.php b/frontend/modules/game/controllers/LeaderboardsController.php index f66051f89..8f75111f4 100644 --- a/frontend/modules/game/controllers/LeaderboardsController.php +++ b/frontend/modules/game/controllers/LeaderboardsController.php @@ -56,6 +56,14 @@ public function behaviors() 'allow' => true, 'roles' => ['@'] ], + [ + 'allow' => true, + 'roles' => ['?'], + 'matchCallback' => function () { + return \Yii::$app->sys->guest_visible_leaderboards; + }, + ], + ], ] ]); diff --git a/frontend/themes/material/layouts/left.php b/frontend/themes/material/layouts/left.php index e0fc55911..9b9c736f8 100644 --- a/frontend/themes/material/layouts/left.php +++ b/frontend/themes/material/layouts/left.php @@ -20,7 +20,7 @@ Avatar of <?=Html::encode(Yii::$app->user->identity->username)?>
" style="font-size: 0.75em"> user->identity->username)?> (user->identity->profile->score->points)?> pts) -
+
sys->hide_timezone):?>