Skip to content

Commit

Permalink
feat: custom container lifetime
Browse files Browse the repository at this point in the history
  • Loading branch information
GZTimeWalker committed Apr 1, 2024
1 parent 0810efd commit 906598d
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 33 deletions.
23 changes: 22 additions & 1 deletion src/GZCTF/ClientApp/src/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,27 @@ export interface ContainerPolicy {
* @format int32
*/
maxExerciseContainerCountPerUser?: number;
/**
* 容器的默认生命周期,以分钟计
* @format int32
* @min 1
* @max 7200
*/
defaultLifetime?: number;
/**
* 容器每次续期的时长,以分钟计
* @format int32
* @min 1
* @max 7200
*/
extensionDuration?: number;
/**
* 容器停止前的可续期时间段,以分钟计
* @format int32
* @min 1
* @max 360
*/
renewalWindow?: number;
}

/** 列表响应 */
Expand Down Expand Up @@ -1226,7 +1247,7 @@ export interface ScoreboardModel {
bloodBonus: number;
/** 前十名的时间线 */
timeLines?: Record<string, TopTimeLine[]>;
/** 队伍信息 */
/** 队伍信息列表 */
items?: ScoreboardItem[];
/** 题目信息 */
challenges?: Record<string, ChallengeInfo[]>;
Expand Down
18 changes: 15 additions & 3 deletions src/GZCTF/ClientApp/src/locales/en_US/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,24 @@
"label": "Enable CAPTCHA"
}
},
"game": {
"container": {
"auto_destroy": {
"description": "Whether to auto-destroy the old instance when the creates new instance but meets the limit",
"description": "Whether to auto-destroy the old instance when meets the limit",
"label": "Auto-destroy Old Instances"
},
"title": "Game Policy"
"default_lifetime": {
"description": "The default lifetime of the container",
"label": "Default Lifetime (min)"
},
"extension_duration": {
"description": "The duration of each renewal of the container",
"label": "Renewal Duration (min)"
},
"renewal_window": {
"description": "The time window before the container expires to renew",
"label": "Renewal Window (min)"
},
"title": "Container Policy"
},
"platform": {
"footer": {
Expand Down
20 changes: 16 additions & 4 deletions src/GZCTF/ClientApp/src/locales/ja_JP/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,24 @@
"label": "キャプチャ認証を使用"
}
},
"game": {
"container": {
"auto_destroy": {
"description": "ユーザーがチャレンジインスタンスを開いて上限に達したときに、古いインスタンスを自動的に破棄するか",
"label": "古いインスタンスを自動的に破棄する"
"description": "ユーザーインスタンスが上限を超える場合は古いインスタンスを自動的に破棄する",
"label": "古いインスタンスの自動破棄"
},
"title": "ゲームポリシー"
"default_lifetime": {
"description": "コンテナの既定の有効期限",
"label": "既定の有効期限(分)"
},
"extension_duration": {
"description": "コンテナが毎回延長される期間",
"label": "延長される期間(分)"
},
"renewal_window": {
"description": "コンテナの有効期限が切れるまでの延長が許可される期間",
"label": "許可される期間(分)"
},
"title": "コンテナーポリシー"
},
"platform": {
"footer": {
Expand Down
18 changes: 15 additions & 3 deletions src/GZCTF/ClientApp/src/locales/zh_CN/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,24 @@
"label": "启用验证码"
}
},
"game": {
"container": {
"auto_destroy": {
"description": "是否在用户开启题目实例但达到上限时自动销毁旧实例",
"description": "是否在用户实例达到上限时自动销毁旧实例",
"label": "自动销毁旧实例"
},
"title": "比赛策略"
"default_lifetime": {
"description": "容器默认生命周期",
"label": "默认有效期(分钟)"
},
"extension_duration": {
"description": "容器每次续期的时长",
"label": "续期时长(分钟)"
},
"renewal_window": {
"description": "容器到期前多少分钟内可以续期",
"label": "续期窗口(分钟)"
},
"title": "容器策略"
},
"platform": {
"footer": {
Expand Down
56 changes: 51 additions & 5 deletions src/GZCTF/ClientApp/src/pages/admin/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Button, Divider, Grid, SimpleGrid, Stack, Switch, TextInput, Title } from '@mantine/core'
import {
Button,
Divider,
Grid,
NumberInput,
SimpleGrid,
Stack,
Switch,
TextInput,
Title,
} from '@mantine/core'
import { mdiCheck, mdiContentSaveOutline } from '@mdi/js'
import { Icon } from '@mdi/react'
import { FC, useEffect, useState } from 'react'
Expand Down Expand Up @@ -180,15 +190,51 @@ const Configs: FC = () => {
</Stack>

<Stack>
<Title order={2}>{t('admin.content.settings.game.title')}</Title>
<Title order={2}>{t('admin.content.settings.container.title')}</Title>
<Divider />
<SimpleGrid cols={2}>
<SimpleGrid cols={4} style={{ alignItems: 'center' }}>
<NumberInput
label={t('admin.content.settings.container.default_lifetime.label')}
description={t('admin.content.settings.container.default_lifetime.description')}
placeholder="120"
min={1}
max={7200}
value={containerPolicy?.defaultLifetime ?? 120}
onChange={(e) => {
const num = e ? Math.min(Math.max(e, 1), 7200) : 120
setContainerPolicy({ ...(containerPolicy ?? {}), defaultLifetime: num })
}}
/>
<NumberInput
label={t('admin.content.settings.container.extension_duration.label')}
description={t('admin.content.settings.container.extension_duration.description')}
placeholder="120"
min={1}
max={7200}
value={containerPolicy?.extensionDuration ?? 120}
onChange={(e) => {
const num = e ? Math.min(Math.max(e, 1), 7200) : 120
setContainerPolicy({ ...(containerPolicy ?? {}), extensionDuration: num })
}}
/>
<NumberInput
label={t('admin.content.settings.container.renewal_window.label')}
description={t('admin.content.settings.container.renewal_window.description')}
placeholder="10"
min={1}
max={360}
value={containerPolicy?.renewalWindow ?? 10}
onChange={(e) => {
const num = e ? Math.min(Math.max(e, 1), 360) : 10
setContainerPolicy({ ...(containerPolicy ?? {}), renewalWindow: num })
}}
/>
<Switch
checked={containerPolicy?.autoDestroyOnLimitReached ?? true}
disabled={disabled}
label={SwitchLabel(
t('admin.content.settings.game.auto_destroy.label'),
t('admin.content.settings.game.auto_destroy.description')
t('admin.content.settings.container.auto_destroy.label'),
t('admin.content.settings.container.auto_destroy.description')
)}
onChange={(e) =>
setContainerPolicy({
Expand Down
11 changes: 7 additions & 4 deletions src/GZCTF/Controllers/GameController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
using System.Security.Claims;
using System.Threading.Channels;
using GZCTF.Middlewares;
using GZCTF.Models.Internal;
using GZCTF.Models.Request.Admin;
using GZCTF.Models.Request.Game;
using GZCTF.Repositories.Interface;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;

namespace GZCTF.Controllers;

Expand All @@ -32,13 +34,14 @@ public class GameController(
ITeamRepository teamRepository,
IGameEventRepository eventRepository,
IGameNoticeRepository noticeRepository,
IGameInstanceRepository gameInstanceRepository,
ICheatInfoRepository cheatInfoRepository,
IGameChallengeRepository challengeRepository,
IContainerRepository containerRepository,
IGameEventRepository gameEventRepository,
ISubmissionRepository submissionRepository,
IGameChallengeRepository challengeRepository,
IGameInstanceRepository gameInstanceRepository,
IParticipationRepository participationRepository,
IOptionsSnapshot<ContainerPolicy> containerPolicy,
IStringLocalizer<Program> localizer) : ControllerBase
{
/// <summary>
Expand Down Expand Up @@ -982,11 +985,11 @@ public async Task<IActionResult> SubmitWriteup([FromRoute] int id, IFormFile fil
if (instance.Container is null)
return BadRequest(new RequestResponse(localizer[nameof(Resources.Program.Game_ContainerNotCreated)]));

if (instance.Container.ExpectStopAt - DateTimeOffset.UtcNow > TimeSpan.FromMinutes(10))
if (instance.Container.ExpectStopAt - DateTimeOffset.UtcNow > TimeSpan.FromMinutes(containerPolicy.Value.RenewalWindow))
return BadRequest(
new RequestResponse(localizer[nameof(Resources.Program.Game_ContainerExtensionNotAvailable)]));

await containerRepository.ExtendLifetime(instance.Container, TimeSpan.FromHours(2), token);
await containerRepository.ExtendLifetime(instance.Container, TimeSpan.FromMinutes(containerPolicy.Value.ExtensionDuration), token);

return Ok(ContainerInfoModel.FromContainer(instance.Container));
}
Expand Down
3 changes: 3 additions & 0 deletions src/GZCTF/Models/Data/Container.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public class Container
/// <summary>
/// 容器期望终止时间
/// </summary>
/// <remarks>
/// 此处设置 2 小时避免创建后立即被销毁,实际销毁时间由容器管理器决定
/// </remarks>
[Required]
public DateTimeOffset ExpectStopAt { get; set; } = DateTimeOffset.UtcNow + TimeSpan.FromHours(2);

Expand Down
24 changes: 23 additions & 1 deletion src/GZCTF/Models/Internal/Configs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Reflection;
using System.Text.Json.Serialization;
using GZCTF.Extensions;
Expand Down Expand Up @@ -51,6 +52,27 @@ public class ContainerPolicy
/// 用户容器数量限制,用于限制练习题目的容器数量
/// </summary>
public int MaxExerciseContainerCountPerUser { get; set; } = 1;

/// <summary>
/// 容器的默认生命周期,以分钟计
/// </summary>
[Range(1, 7200, ErrorMessageResourceName = nameof(Resources.Program.Model_OutOfRange),
ErrorMessageResourceType = typeof(Resources.Program))]
public int DefaultLifetime { get; set; } = 120;

/// <summary>
/// 容器每次续期的时长,以分钟计
/// </summary>
[Range(1, 7200, ErrorMessageResourceName = nameof(Resources.Program.Model_OutOfRange),
ErrorMessageResourceType = typeof(Resources.Program))]
public int ExtensionDuration { get; set; } = 120;

/// <summary>
/// 容器停止前的可续期时间段,以分钟计
/// </summary>
[Range(1, 360, ErrorMessageResourceName = nameof(Resources.Program.Model_OutOfRange),
ErrorMessageResourceType = typeof(Resources.Program))]
public int RenewalWindow { get; set; } = 10;
}

/// <summary>
Expand Down
25 changes: 14 additions & 11 deletions src/GZCTF/Repositories/GameInstanceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ public class GameInstanceRepository(
return instance;
}

GameChallenge? challenge = instance.Challenge;
GameChallenge challenge = instance.Challenge;

if (challenge is null || !challenge.IsEnabled)
if (!challenge.IsEnabled)
{
await transaction.CommitAsync(token);
return null;
Expand Down Expand Up @@ -180,6 +180,9 @@ public class GameInstanceRepository(
return new TaskResult<Container>(TaskStatus.Failed);
}

// update the ExpectStopAt with config
container.ExpectStopAt = container.StartedAt.AddMinutes(containerPolicy.Value.DefaultLifetime);

gameInstance.Container = container;
gameInstance.LastContainerOperation = DateTimeOffset.UtcNow;

Expand Down Expand Up @@ -219,20 +222,20 @@ public async Task<CheatCheckInfo> CheckCheat(Submission submission, Cancellation

foreach (GameInstance instance in instances)
{
if (instance.FlagContext?.Flag == submission.Answer)
{
Submission updateSub = await Context.Submissions.Where(s => s.Id == submission.Id).SingleAsync(token);
if (instance.FlagContext?.Flag != submission.Answer)
continue;

CheatInfo cheatInfo = await cheatInfoRepository.CreateCheatInfo(updateSub, instance, token);
Submission updateSub = await Context.Submissions.Where(s => s.Id == submission.Id).SingleAsync(token);

checkInfo = CheatCheckInfo.FromCheatInfo(cheatInfo);
CheatInfo cheatInfo = await cheatInfoRepository.CreateCheatInfo(updateSub, instance, token);

updateSub.Status = AnswerResult.CheatDetected;
checkInfo = CheatCheckInfo.FromCheatInfo(cheatInfo);

await SaveAsync(token);
updateSub.Status = AnswerResult.CheatDetected;

return checkInfo;
}
await SaveAsync(token);

return checkInfo;
}

return checkInfo;
Expand Down
2 changes: 1 addition & 1 deletion src/GZCTF/Services/Container/Manager/DockerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,4 @@ public async Task DestroyContainerAsync(Models.Data.Container container, Cancell
NetworkMode = _meta.Config.ChallengeNetwork
}
};
}
}

0 comments on commit 906598d

Please sign in to comment.