mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
30ba1f416b
27 changed files with 340 additions and 73 deletions
|
|
@ -1 +1 @@
|
|||
3.2.6
|
||||
3.2.7
|
||||
|
|
@ -181,7 +181,7 @@ func runSeeders(isUsersEmpty bool) error {
|
|||
}
|
||||
|
||||
if empty && isUsersEmpty {
|
||||
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix"}
|
||||
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "ApiTokensHash"}
|
||||
for _, name := range seeders {
|
||||
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
|
||||
return err
|
||||
|
|
@ -232,6 +232,12 @@ func runSeeders(isUsersEmpty bool) error {
|
|||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(seedersHistory, "ApiTokensHash") {
|
||||
if err := hashExistingApiTokens(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(seedersHistory, "ClientsTable") {
|
||||
if err := seedClientsFromInboundJSON(); err != nil {
|
||||
return err
|
||||
|
|
@ -646,6 +652,28 @@ func seedApiTokens() error {
|
|||
return db.Create(&model.HistoryOfSeeders{SeederName: "ApiTokensTable"}).Error
|
||||
}
|
||||
|
||||
// hashExistingApiTokens replaces any plaintext token stored before tokens were
|
||||
// hashed at rest with its SHA-256 digest. Callers keep their plaintext copy
|
||||
// (used on remote nodes), so existing tokens keep authenticating; the panel
|
||||
// just can no longer reveal them. Idempotent — already-hashed rows are skipped.
|
||||
func hashExistingApiTokens() error {
|
||||
var rows []*model.ApiToken
|
||||
if err := db.Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range rows {
|
||||
if crypto.IsSHA256Hex(r.Token) {
|
||||
continue
|
||||
}
|
||||
hashed := crypto.HashTokenSHA256(r.Token)
|
||||
if err := db.Model(model.ApiToken{}).Where("id = ?", r.Id).Update("token", hashed).Error; err != nil {
|
||||
log.Printf("Error hashing api token %d: %v", r.Id, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return db.Create(&model.HistoryOfSeeders{SeederName: "ApiTokensHash"}).Error
|
||||
}
|
||||
|
||||
// isTableEmpty returns true if the named table contains zero rows.
|
||||
func isTableEmpty(tableName string) (bool, error) {
|
||||
var count int64
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ type HistoryOfSeeders struct {
|
|||
type ApiToken struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"uniqueIndex;not null"`
|
||||
Token string `json:"token" gorm:"not null"`
|
||||
Token string `json:"token" gorm:"not null"` // SHA-256 hash; the plaintext is shown only once at creation
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
},
|
||||
{
|
||||
"name": "API Tokens",
|
||||
"description": "Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored plaintext so the SPA can show them on demand. Send one as <code>Authorization: Bearer <token></code> on any /panel/api/* request."
|
||||
"description": "Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as <code>Authorization: Bearer <token></code> on any /panel/api/* request."
|
||||
},
|
||||
{
|
||||
"name": "Xray Settings",
|
||||
|
|
@ -5105,7 +5105,7 @@
|
|||
"tags": [
|
||||
"API Tokens"
|
||||
],
|
||||
"summary": "List every API token, enabled or not.",
|
||||
"summary": "List every API token, enabled or not. The token value is never returned — only metadata.",
|
||||
"operationId": "get_panel_setting_apiTokens",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
|
@ -5130,7 +5130,6 @@
|
|||
{
|
||||
"id": 1,
|
||||
"name": "default",
|
||||
"token": "abcdef-12345-...",
|
||||
"enabled": true,
|
||||
"createdAt": 1736000000
|
||||
}
|
||||
|
|
@ -5147,7 +5146,7 @@
|
|||
"tags": [
|
||||
"API Tokens"
|
||||
],
|
||||
"summary": "Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated.",
|
||||
"summary": "Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated and returned only in this response — it is stored hashed and cannot be retrieved later.",
|
||||
"operationId": "post_panel_setting_apiTokens_create",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ export default function AppSidebar() {
|
|||
<div className="ant-sidebar">
|
||||
<Layout.Sider
|
||||
theme={currentTheme}
|
||||
width={220}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
breakpoint="md"
|
||||
|
|
|
|||
|
|
@ -951,18 +951,18 @@ export const sections: readonly Section[] = [
|
|||
id: 'api-tokens',
|
||||
title: 'API Tokens',
|
||||
description:
|
||||
'Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored plaintext so the SPA can show them on demand. Send one as <code>Authorization: Bearer <token></code> on any /panel/api/* request.',
|
||||
'Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as <code>Authorization: Bearer <token></code> on any /panel/api/* request.',
|
||||
endpoints: [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/setting/apiTokens',
|
||||
summary: 'List every API token, enabled or not.',
|
||||
response: '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "name": "default",\n "token": "abcdef-12345-...",\n "enabled": true,\n "createdAt": 1736000000\n }\n ]\n}',
|
||||
summary: 'List every API token, enabled or not. The token value is never returned — only metadata.',
|
||||
response: '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "name": "default",\n "enabled": true,\n "createdAt": 1736000000\n }\n ]\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/setting/apiTokens/create',
|
||||
summary: 'Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated.',
|
||||
summary: 'Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated and returned only in this response — it is stored hashed and cannot be retrieved later.',
|
||||
params: [
|
||||
{ name: 'name', in: 'body', type: 'string', desc: 'Human-readable label, e.g. "central-panel-a".' },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@
|
|||
word-break: break-all;
|
||||
}
|
||||
|
||||
.api-token-created-notice {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.security-actions {
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ interface ApiMsg<T = unknown> {
|
|||
interface ApiTokenRow {
|
||||
id: number;
|
||||
name: string;
|
||||
token: string;
|
||||
enabled: boolean;
|
||||
createdAt: number;
|
||||
}
|
||||
|
|
@ -77,10 +76,10 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
|||
|
||||
const [apiTokens, setApiTokens] = useState<ApiTokenRow[]>([]);
|
||||
const [apiTokensLoading, setApiTokensLoading] = useState(false);
|
||||
const [visibleTokenIds, setVisibleTokenIds] = useState<Set<number>>(() => new Set());
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createName, setCreateName] = useState('');
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [createdToken, setCreatedToken] = useState<{ name: string; token: string } | null>(null);
|
||||
|
||||
const openTfa = useCallback((opts: Omit<TfaState, 'open'>) => {
|
||||
setTfa({ ...opts, open: true });
|
||||
|
|
@ -137,14 +136,6 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
|||
loadApiTokens();
|
||||
}, [loadApiTokens]);
|
||||
|
||||
function toggleTokenVisibility(id: number) {
|
||||
setVisibleTokenIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id); else next.add(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
async function copyToken(token: string) {
|
||||
if (!token) return;
|
||||
const ok = await ClipboardManager.copyText(token);
|
||||
|
|
@ -165,17 +156,12 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
|||
}
|
||||
setCreating(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/setting/apiTokens/create', { name }) as ApiMsg<{ id?: number }>;
|
||||
const msg = await HttpUtil.post('/panel/setting/apiTokens/create', { name }) as ApiMsg<{ token?: string }>;
|
||||
if (msg?.success) {
|
||||
setCreateOpen(false);
|
||||
await loadApiTokens();
|
||||
if (msg.obj?.id != null) {
|
||||
const id = msg.obj.id;
|
||||
setVisibleTokenIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(id);
|
||||
return next;
|
||||
});
|
||||
if (msg.obj?.token) {
|
||||
setCreatedToken({ name, token: msg.obj.token });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -206,11 +192,6 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
|||
}
|
||||
}
|
||||
|
||||
function maskToken(token: string): string {
|
||||
if (!token) return '';
|
||||
return '•'.repeat(Math.min(token.length, 24));
|
||||
}
|
||||
|
||||
function formatTokenDate(ts: number): string {
|
||||
if (!ts) return '';
|
||||
return new Date(ts * 1000).toLocaleString();
|
||||
|
|
@ -326,17 +307,6 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="api-token-value-wrap">
|
||||
<code className="api-token-value">
|
||||
{visibleTokenIds.has(row.id) ? row.token : maskToken(row.token)}
|
||||
</code>
|
||||
<Button size="small" onClick={() => toggleTokenVisibility(row.id)}>
|
||||
{visibleTokenIds.has(row.id)
|
||||
? (t('pages.settings.security.hide') || 'Hide')
|
||||
: (t('pages.settings.security.show') || 'Show')}
|
||||
</Button>
|
||||
<Button size="small" onClick={() => copyToken(row.token)}>{t('copy')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Spin>
|
||||
|
|
@ -367,6 +337,26 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
|||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={!!createdToken}
|
||||
title={t('pages.settings.security.apiTokenCreatedTitle') || 'Token created'}
|
||||
okText={t('done')}
|
||||
onOk={() => setCreatedToken(null)}
|
||||
onCancel={() => setCreatedToken(null)}
|
||||
cancelButtonProps={{ style: { display: 'none' } }}
|
||||
>
|
||||
<p className="api-token-created-notice">
|
||||
{t('pages.settings.security.apiTokenCreatedNotice')
|
||||
|| 'Copy this token now. For security it is not stored in readable form and will not be shown again.'}
|
||||
</p>
|
||||
<div className="api-token-value-wrap">
|
||||
<code className="api-token-value">{createdToken?.token}</code>
|
||||
<Button size="small" type="primary" onClick={() => createdToken && copyToken(createdToken.token)}>
|
||||
{t('copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<TwoFactorModal
|
||||
open={tfa.open}
|
||||
title={tfa.title}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Button, Input, Modal, Select, Space, Switch, Tabs } from 'antd';
|
||||
import { Alert, Button, Input, InputNumber, Modal, Select, Space, Switch, Tabs } from 'antd';
|
||||
import {
|
||||
BarChartOutlined,
|
||||
ClockCircleOutlined,
|
||||
FileTextOutlined,
|
||||
ReloadOutlined,
|
||||
SettingOutlined,
|
||||
|
|
@ -54,6 +55,20 @@ export default function BasicsTab({
|
|||
[setTemplateSettings],
|
||||
);
|
||||
|
||||
const setLevel0 = useCallback(
|
||||
(field: string, value: number | null) => mutate((tt) => {
|
||||
if (!tt.policy) tt.policy = {};
|
||||
if (!tt.policy.levels) tt.policy.levels = {};
|
||||
if (!tt.policy.levels['0']) tt.policy.levels['0'] = {};
|
||||
if (value === null || value === undefined) {
|
||||
delete tt.policy.levels['0'][field];
|
||||
} else {
|
||||
tt.policy.levels['0'][field] = value;
|
||||
}
|
||||
}),
|
||||
[mutate],
|
||||
);
|
||||
|
||||
function confirmResetDefault() {
|
||||
modal.confirm({
|
||||
title: t('pages.settings.resetDefaultConfig'),
|
||||
|
|
@ -72,6 +87,7 @@ export default function BasicsTab({
|
|||
const routingStrategy = templateSettings?.routing?.domainStrategy ?? 'AsIs';
|
||||
const log = (templateSettings?.log || {}) as Record<string, unknown>;
|
||||
const policy = (templateSettings?.policy?.system || {}) as Record<string, boolean>;
|
||||
const level0 = (templateSettings?.policy?.levels?.['0'] || {}) as Record<string, unknown>;
|
||||
|
||||
const items = [
|
||||
{
|
||||
|
|
@ -168,6 +184,50 @@ export default function BasicsTab({
|
|||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'connection',
|
||||
label: catTabLabel(<ClockCircleOutlined />, t('pages.xray.connectionLimits'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="mb-12 hint-alert"
|
||||
title={t('pages.xray.connectionLimitsDesc')}
|
||||
/>
|
||||
<SettingListItem
|
||||
title={t('pages.xray.connIdle')}
|
||||
description={t('pages.xray.connIdleDesc')}
|
||||
paddings="small"
|
||||
control={
|
||||
<InputNumber
|
||||
value={typeof level0.connIdle === 'number' ? level0.connIdle : undefined}
|
||||
min={0}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="300"
|
||||
addonAfter={t('pages.xray.seconds')}
|
||||
onChange={(v) => setLevel0('connIdle', v as number | null)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingListItem
|
||||
title={t('pages.xray.bufferSize')}
|
||||
description={t('pages.xray.bufferSizeDesc')}
|
||||
paddings="small"
|
||||
control={
|
||||
<InputNumber
|
||||
value={typeof level0.bufferSize === 'number' ? level0.bufferSize : undefined}
|
||||
min={0}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('pages.xray.bufferSizePlaceholder')}
|
||||
addonAfter="KB"
|
||||
onChange={(v) => setLevel0('bufferSize', v as number | null)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: catTabLabel(<FileTextOutlined />, t('pages.xray.logConfigs'), isMobile),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const XraySettingsValueSchema = z.object({
|
|||
log: z.record(z.string(), z.unknown()).optional(),
|
||||
policy: z.object({
|
||||
system: z.record(z.string(), z.boolean()).optional(),
|
||||
levels: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
|
||||
}).loose().optional(),
|
||||
observatory: z.unknown().optional(),
|
||||
burstObservatory: z.unknown().optional(),
|
||||
|
|
|
|||
13
go.mod
13
go.mod
|
|
@ -21,7 +21,7 @@ require (
|
|||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/valyala/fasthttp v1.71.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/xtls/xray-core v1.260327.0
|
||||
github.com/xtls/xray-core v1.260327.1-0.20260601021109-94ffd50060f1
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/sys v0.45.0
|
||||
|
|
@ -33,10 +33,19 @@ require (
|
|||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/pion/dtls/v3 v3.1.2 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/stun/v3 v3.1.2 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
|
|
|
|||
20
go.sum
20
go.sum
|
|
@ -6,8 +6,8 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
|
|||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
|
||||
|
|
@ -148,6 +148,14 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
|
|||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
||||
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY=
|
||||
github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
|
@ -202,12 +210,14 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW
|
|||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
|
||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
|
||||
github.com/xtls/xray-core v1.260327.0 h1:g4TzxMwyPrxslZh6uD+FiG3lXKTrnNO+b4ky2OhogHE=
|
||||
github.com/xtls/xray-core v1.260327.0/go.mod h1:OXMlhBloFry8mw0KwWLWLd3RQyXJzEYsCGlgsX36h60=
|
||||
github.com/xtls/xray-core v1.260327.1-0.20260601021109-94ffd50060f1 h1:RAxvdTekSZCn1OO5P9d0ioDrdiiqdOsdqllxLvC+IGQ=
|
||||
github.com/xtls/xray-core v1.260327.1-0.20260601021109-94ffd50060f1/go.mod h1:klRI+zA2uG6qrelDRoUaEur3gasszRE9W8e2zTgqXNU=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
|
|
@ -263,6 +273,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
|||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 h1:cqHQ3AycTHvM2R7ikgyX57D+XvtcSnGylsLkOVhta/w=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
|
|
@ -20,3 +23,25 @@ func IsHashed(s string) bool {
|
|||
_, err := bcrypt.Cost([]byte(s))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// HashTokenSHA256 returns the hex-encoded SHA-256 digest of token. API tokens
|
||||
// are high-entropy random strings, so a fast unsalted digest is sufficient to
|
||||
// keep them irrecoverable at rest while allowing constant-time verification.
|
||||
func HashTokenSHA256(token string) string {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// IsSHA256Hex reports whether s looks like a hex-encoded SHA-256 digest
|
||||
// (64 lowercase hex characters), used to skip already-hashed token rows.
|
||||
func IsSHA256Hex(s string) bool {
|
||||
if len(s) != 64 {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/mhsanaei/3x-ui/v3/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/random"
|
||||
)
|
||||
|
||||
|
|
@ -18,16 +19,18 @@ const apiTokenLength = 48
|
|||
type ApiTokenView struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
// toView builds the metadata view returned by List. It never carries the
|
||||
// token value: only a SHA-256 hash is stored, and the plaintext is shown
|
||||
// exactly once at creation time.
|
||||
func toView(t *model.ApiToken) *ApiTokenView {
|
||||
return &ApiTokenView{
|
||||
Id: t.Id,
|
||||
Name: t.Name,
|
||||
Token: t.Token,
|
||||
Enabled: t.Enabled,
|
||||
CreatedAt: t.CreatedAt,
|
||||
}
|
||||
|
|
@ -62,15 +65,18 @@ func (s *ApiTokenService) Create(name string) (*ApiTokenView, error) {
|
|||
if count > 0 {
|
||||
return nil, common.NewError("a token with that name already exists")
|
||||
}
|
||||
plaintext := random.Seq(apiTokenLength)
|
||||
row := &model.ApiToken{
|
||||
Name: name,
|
||||
Token: random.Seq(apiTokenLength),
|
||||
Token: crypto.HashTokenSHA256(plaintext),
|
||||
Enabled: true,
|
||||
}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toView(row), nil
|
||||
view := toView(row)
|
||||
view.Token = plaintext
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (s *ApiTokenService) Delete(id int) error {
|
||||
|
|
@ -97,8 +103,9 @@ func (s *ApiTokenService) SetEnabled(id int, enabled bool) error {
|
|||
}
|
||||
|
||||
// Match returns true when the presented bearer token matches any enabled
|
||||
// row in api_tokens. Uses constant-time compare per row so a remote
|
||||
// attacker can't time-attack tokens byte-by-byte.
|
||||
// row in api_tokens. Tokens are stored as SHA-256 hashes, so the presented
|
||||
// value is hashed before a constant-time compare per row keeps a remote
|
||||
// attacker from timing the comparison byte-by-byte.
|
||||
func (s *ApiTokenService) Match(presented string) bool {
|
||||
if presented == "" {
|
||||
return false
|
||||
|
|
@ -108,10 +115,10 @@ func (s *ApiTokenService) Match(presented string) bool {
|
|||
if err := db.Model(model.ApiToken{}).Where("enabled = ?", true).Find(&rows).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
presentedBytes := []byte(presented)
|
||||
presentedHash := []byte(crypto.HashTokenSHA256(presented))
|
||||
matched := false
|
||||
for _, r := range rows {
|
||||
if subtle.ConstantTimeCompare([]byte(r.Token), presentedBytes) == 1 {
|
||||
if subtle.ConstantTimeCompare([]byte(r.Token), presentedHash) == 1 {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "مثل central-panel-a",
|
||||
"apiTokenNameRequired": "الاسم مطلوب",
|
||||
"apiTokenEmpty": "لا توجد رموز بعد — أنشئ واحدًا لمصادقة الروبوتات أو اللوحات البعيدة.",
|
||||
"apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا."
|
||||
"apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا.",
|
||||
"apiTokenCreatedTitle": "تم إنشاء الرمز",
|
||||
"apiTokenCreatedNotice": "انسخ هذا الرمز الآن. لأسباب أمنية لا يتم تخزينه بصيغة قابلة للقراءة ولن يتم عرضه مرة أخرى."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "تم تغيير المعلمات.",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من المخرجات.",
|
||||
"statsOutboundDownlink": "إحصائيات تنزيل المخرجات",
|
||||
"statsOutboundDownlinkDesc": "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من المخرجات.",
|
||||
"connectionLimits": "حدود الاتصال",
|
||||
"connectionLimitsDesc": "سياسات على مستوى الاتصال لمستوى المستخدم 0. اترك الحقل فارغًا لاستخدام القيمة الافتراضية لـ Xray.",
|
||||
"connIdle": "مهلة الخمول",
|
||||
"connIdleDesc": "يغلق الاتصال بعد بقائه خاملًا لهذا العدد من الثواني. خفضه يحرر الذاكرة وواصفات الملفات بشكل أسرع على الخوادم المزدحمة (الافتراضي في Xray: 300).",
|
||||
"bufferSize": "حجم المخزن المؤقت",
|
||||
"bufferSizeDesc": "حجم المخزن المؤقت الداخلي لكل اتصال بالكيلوبايت. اضبطه على 0 لتقليل استهلاك الذاكرة على الخوادم منخفضة الذاكرة (الافتراضي في Xray يعتمد على المنصة).",
|
||||
"bufferSizePlaceholder": "تلقائي",
|
||||
"seconds": "ثانية",
|
||||
"rules": {
|
||||
"first": "أول",
|
||||
"last": "آخر",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "e.g. central-panel-a",
|
||||
"apiTokenNameRequired": "Name is required",
|
||||
"apiTokenEmpty": "No tokens yet — create one to authenticate bots or remote panels.",
|
||||
"apiTokenDeleteWarning": "Any caller using this token will stop authenticating immediately."
|
||||
"apiTokenDeleteWarning": "Any caller using this token will stop authenticating immediately.",
|
||||
"apiTokenCreatedTitle": "Token created",
|
||||
"apiTokenCreatedNotice": "Copy this token now. For security it is not stored in readable form and will not be shown again."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "The parameters have been changed.",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "Enables the statistics collection for upstream traffic of all outbound proxies.",
|
||||
"statsOutboundDownlink": "Outbound Download Statistics",
|
||||
"statsOutboundDownlinkDesc": "Enables the statistics collection for downstream traffic of all outbound proxies.",
|
||||
"connectionLimits": "Connection Limits",
|
||||
"connectionLimitsDesc": "Connection-level policies for user level 0. Leave a field empty to use Xray's default.",
|
||||
"connIdle": "Idle Timeout",
|
||||
"connIdleDesc": "Closes a connection after it stays idle for this many seconds. Lowering it frees memory and file descriptors faster on busy servers (Xray default: 300).",
|
||||
"bufferSize": "Buffer Size",
|
||||
"bufferSizeDesc": "Per-connection internal buffer size in KB. Set to 0 to minimize memory usage on low-RAM servers (Xray default depends on the platform).",
|
||||
"bufferSizePlaceholder": "auto",
|
||||
"seconds": "seconds",
|
||||
"rules": {
|
||||
"first": "First",
|
||||
"last": "Last",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "por ejemplo central-panel-a",
|
||||
"apiTokenNameRequired": "El nombre es obligatorio",
|
||||
"apiTokenEmpty": "Aún no hay tokens — crea uno para autenticar bots o paneles remotos.",
|
||||
"apiTokenDeleteWarning": "Cualquier cliente que use este token dejará de autenticarse inmediatamente."
|
||||
"apiTokenDeleteWarning": "Cualquier cliente que use este token dejará de autenticarse inmediatamente.",
|
||||
"apiTokenCreatedTitle": "Token creado",
|
||||
"apiTokenCreatedNotice": "Copia este token ahora. Por seguridad, no se almacena de forma legible y no se volverá a mostrar."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Los parámetros han sido modificados.",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de salida.",
|
||||
"statsOutboundDownlink": "Estadísticas de Bajada de Salida",
|
||||
"statsOutboundDownlinkDesc": "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de salida.",
|
||||
"connectionLimits": "Límites de conexión",
|
||||
"connectionLimitsDesc": "Políticas a nivel de conexión para el nivel de usuario 0. Deja un campo vacío para usar el valor predeterminado de Xray.",
|
||||
"connIdle": "Tiempo de inactividad",
|
||||
"connIdleDesc": "Cierra una conexión después de que permanezca inactiva durante esta cantidad de segundos. Reducirlo libera memoria y descriptores de archivo más rápido en servidores con mucha carga (predeterminado de Xray: 300).",
|
||||
"bufferSize": "Tamaño del búfer",
|
||||
"bufferSizeDesc": "Tamaño del búfer interno por conexión en KB. Ponlo en 0 para minimizar el uso de memoria en servidores con poca RAM (el valor predeterminado de Xray depende de la plataforma).",
|
||||
"bufferSizePlaceholder": "automático",
|
||||
"seconds": "segundos",
|
||||
"rules": {
|
||||
"first": "Primero",
|
||||
"last": "Último",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "مثلاً central-panel-a",
|
||||
"apiTokenNameRequired": "نام الزامی است",
|
||||
"apiTokenEmpty": "هنوز توکنی وجود ندارد — برای احراز هویت رباتها یا پنلهای راه دور یکی بسازید.",
|
||||
"apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده میکند بلافاصله احراز هویتش قطع میشود."
|
||||
"apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده میکند بلافاصله احراز هویتش قطع میشود.",
|
||||
"apiTokenCreatedTitle": "توکن ساخته شد",
|
||||
"apiTokenCreatedNotice": "اکنون این توکن را کپی کنید. بهدلیل امنیتی بهصورت قابلخواندن ذخیره نمیشود و دوباره نمایش داده نخواهد شد."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "پارامترها تغییر کردهاند.",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "جمعآوری آمار برای ترافیک بالارو (آپلود) تمام پروکسیهای خروجی را فعال میکند.",
|
||||
"statsOutboundDownlink": "آمار دانلود خروجی",
|
||||
"statsOutboundDownlinkDesc": "جمعآوری آمار برای ترافیک پایینرو (دانلود) تمام پروکسیهای خروجی را فعال میکند.",
|
||||
"connectionLimits": "محدودیت اتصال",
|
||||
"connectionLimitsDesc": "سیاستهای سطح اتصال برای کاربرانِ سطح ۰. هر فیلد را خالی بگذارید تا مقدار پیشفرض Xray استفاده شود.",
|
||||
"connIdle": "مهلت بیکاری",
|
||||
"connIdleDesc": "اتصال را پس از این تعداد ثانیه بیکار ماندن میبندد. کمکردن آن، روی سرورهای شلوغ حافظه و file descriptor را زودتر آزاد میکند (پیشفرض Xray: ۳۰۰).",
|
||||
"bufferSize": "اندازهٔ بافر",
|
||||
"bufferSizeDesc": "اندازهٔ بافر داخلی هر اتصال بر حسب کیلوبایت. برای کمکردن مصرف حافظه روی سرورهای کمرم روی ۰ بگذارید (پیشفرض Xray به پلتفرم بستگی دارد).",
|
||||
"bufferSizePlaceholder": "خودکار",
|
||||
"seconds": "ثانیه",
|
||||
"rules": {
|
||||
"first": "اولین",
|
||||
"last": "آخرین",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "misalnya central-panel-a",
|
||||
"apiTokenNameRequired": "Nama wajib diisi",
|
||||
"apiTokenEmpty": "Belum ada token — buat satu untuk mengautentikasi bot atau panel jarak jauh.",
|
||||
"apiTokenDeleteWarning": "Setiap pemanggil yang menggunakan token ini akan berhenti terautentikasi segera."
|
||||
"apiTokenDeleteWarning": "Setiap pemanggil yang menggunakan token ini akan berhenti terautentikasi segera.",
|
||||
"apiTokenCreatedTitle": "Token dibuat",
|
||||
"apiTokenCreatedNotice": "Salin token ini sekarang. Demi keamanan, token tidak disimpan dalam bentuk yang dapat dibaca dan tidak akan ditampilkan lagi."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Parameter telah diubah.",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy keluar.",
|
||||
"statsOutboundDownlink": "Statistik Unduh Keluar",
|
||||
"statsOutboundDownlinkDesc": "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy keluar.",
|
||||
"connectionLimits": "Batas Koneksi",
|
||||
"connectionLimitsDesc": "Kebijakan tingkat koneksi untuk level pengguna 0. Biarkan kolom kosong untuk menggunakan nilai bawaan Xray.",
|
||||
"connIdle": "Batas Waktu Idle",
|
||||
"connIdleDesc": "Menutup koneksi setelah idle selama sekian detik. Menurunkannya membebaskan memori dan file descriptor lebih cepat pada server yang sibuk (bawaan Xray: 300).",
|
||||
"bufferSize": "Ukuran Buffer",
|
||||
"bufferSizeDesc": "Ukuran buffer internal per koneksi dalam KB. Setel ke 0 untuk meminimalkan penggunaan memori pada server ber-RAM rendah (nilai bawaan Xray bergantung pada platform).",
|
||||
"bufferSizePlaceholder": "otomatis",
|
||||
"seconds": "detik",
|
||||
"rules": {
|
||||
"first": "Pertama",
|
||||
"last": "Terakhir",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "例: central-panel-a",
|
||||
"apiTokenNameRequired": "名前は必須です",
|
||||
"apiTokenEmpty": "トークンがまだありません — ボットやリモートパネルを認証するために作成してください。",
|
||||
"apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。"
|
||||
"apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。",
|
||||
"apiTokenCreatedTitle": "トークンを作成しました",
|
||||
"apiTokenCreatedNotice": "このトークンを今すぐコピーしてください。セキュリティ上、読み取り可能な形式では保存されず、再表示されません。"
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "パラメーターが変更されました。",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。",
|
||||
"statsOutboundDownlink": "アウトバウンドダウンロード統計",
|
||||
"statsOutboundDownlinkDesc": "すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。",
|
||||
"connectionLimits": "接続制限",
|
||||
"connectionLimitsDesc": "ユーザーレベル0の接続レベルのポリシーです。フィールドを空のままにすると Xray のデフォルト値が使用されます。",
|
||||
"connIdle": "アイドルタイムアウト",
|
||||
"connIdleDesc": "接続がこの秒数アイドル状態のままになると接続を閉じます。値を下げると、混雑したサーバーでメモリとファイルディスクリプタをより早く解放できます(Xray のデフォルト: 300)。",
|
||||
"bufferSize": "バッファサイズ",
|
||||
"bufferSizeDesc": "接続ごとの内部バッファサイズ(KB単位)。低メモリのサーバーでメモリ使用量を最小限にするには 0 に設定します(Xray のデフォルトはプラットフォームに依存します)。",
|
||||
"bufferSizePlaceholder": "自動",
|
||||
"seconds": "秒",
|
||||
"rules": {
|
||||
"first": "最初",
|
||||
"last": "最後",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "ex.: central-panel-a",
|
||||
"apiTokenNameRequired": "O nome é obrigatório",
|
||||
"apiTokenEmpty": "Nenhum token ainda — crie um para autenticar bots ou painéis remotos.",
|
||||
"apiTokenDeleteWarning": "Qualquer cliente usando este token deixará de se autenticar imediatamente."
|
||||
"apiTokenDeleteWarning": "Qualquer cliente usando este token deixará de se autenticar imediatamente.",
|
||||
"apiTokenCreatedTitle": "Token criado",
|
||||
"apiTokenCreatedNotice": "Copie este token agora. Por segurança, ele não é armazenado de forma legível e não será exibido novamente."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Os parâmetros foram alterados.",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de saída.",
|
||||
"statsOutboundDownlink": "Estatísticas de Download de Saída",
|
||||
"statsOutboundDownlinkDesc": "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de saída.",
|
||||
"connectionLimits": "Limites de conexão",
|
||||
"connectionLimitsDesc": "Políticas em nível de conexão para o nível de usuário 0. Deixe um campo vazio para usar o padrão do Xray.",
|
||||
"connIdle": "Tempo limite de inatividade",
|
||||
"connIdleDesc": "Fecha uma conexão depois que ela fica inativa por esta quantidade de segundos. Reduzi-lo libera memória e descritores de arquivo mais rápido em servidores ocupados (padrão do Xray: 300).",
|
||||
"bufferSize": "Tamanho do buffer",
|
||||
"bufferSizeDesc": "Tamanho do buffer interno por conexão em KB. Defina como 0 para minimizar o uso de memória em servidores com pouca RAM (o padrão do Xray depende da plataforma).",
|
||||
"bufferSizePlaceholder": "automático",
|
||||
"seconds": "segundos",
|
||||
"rules": {
|
||||
"first": "Primeiro",
|
||||
"last": "Último",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "например, central-panel-a",
|
||||
"apiTokenNameRequired": "Имя обязательно",
|
||||
"apiTokenEmpty": "Токенов пока нет — создайте один для аутентификации ботов или удалённых панелей.",
|
||||
"apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию."
|
||||
"apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию.",
|
||||
"apiTokenCreatedTitle": "Токен создан",
|
||||
"apiTokenCreatedNotice": "Скопируйте этот токен сейчас. В целях безопасности он не хранится в читаемом виде и больше не будет показан."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Настройки изменены",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "Включает сбор статистики для исходящего трафика всех исходящих прокси.",
|
||||
"statsOutboundDownlink": "Статистика исходящего даунлинка",
|
||||
"statsOutboundDownlinkDesc": "Включает сбор статистики для входящего трафика всех исходящих прокси.",
|
||||
"connectionLimits": "Ограничения соединения",
|
||||
"connectionLimitsDesc": "Политики уровня соединения для пользователей уровня 0. Оставьте поле пустым, чтобы использовать значение Xray по умолчанию.",
|
||||
"connIdle": "Тайм-аут простоя",
|
||||
"connIdleDesc": "Закрывает соединение после простоя в течение указанного числа секунд. Уменьшение значения быстрее освобождает память и файловые дескрипторы на нагруженных серверах (по умолчанию в Xray: 300).",
|
||||
"bufferSize": "Размер буфера",
|
||||
"bufferSizeDesc": "Размер внутреннего буфера на соединение в КБ. Установите 0, чтобы минимизировать использование памяти на серверах с малым объёмом ОЗУ (значение Xray по умолчанию зависит от платформы).",
|
||||
"bufferSizePlaceholder": "авто",
|
||||
"seconds": "секунд",
|
||||
"rules": {
|
||||
"first": "Первый",
|
||||
"last": "Последний",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "örn. central-panel-a",
|
||||
"apiTokenNameRequired": "Ad zorunludur",
|
||||
"apiTokenEmpty": "Henüz token yok — bot veya uzak panelleri doğrulamak için bir tane oluşturun.",
|
||||
"apiTokenDeleteWarning": "Bu tokenı kullanan tüm istemciler anında kimlik doğrulamasını kaybeder."
|
||||
"apiTokenDeleteWarning": "Bu tokenı kullanan tüm istemciler anında kimlik doğrulamasını kaybeder.",
|
||||
"apiTokenCreatedTitle": "Belirteç oluşturuldu",
|
||||
"apiTokenCreatedNotice": "Bu belirteci şimdi kopyalayın. Güvenlik nedeniyle okunabilir biçimde saklanmaz ve tekrar gösterilmez."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Parametreler değiştirildi.",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "Tüm giden proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir.",
|
||||
"statsOutboundDownlink": "Giden İndirme İstatistikleri",
|
||||
"statsOutboundDownlinkDesc": "Tüm giden proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir.",
|
||||
"connectionLimits": "Bağlantı Sınırları",
|
||||
"connectionLimitsDesc": "Kullanıcı seviyesi 0 için bağlantı düzeyi politikaları. Xray'in varsayılanını kullanmak için alanı boş bırakın.",
|
||||
"connIdle": "Boşta Kalma Zaman Aşımı",
|
||||
"connIdleDesc": "Bağlantı bu kadar saniye boşta kaldıktan sonra kapatılır. Değerin düşürülmesi, yoğun sunucularda belleği ve dosya tanımlayıcılarını daha hızlı serbest bırakır (Xray varsayılanı: 300).",
|
||||
"bufferSize": "Arabellek Boyutu",
|
||||
"bufferSizeDesc": "Bağlantı başına dahili arabellek boyutu (KB). Düşük RAM'li sunucularda bellek kullanımını en aza indirmek için 0 olarak ayarlayın (Xray varsayılanı platforma bağlıdır).",
|
||||
"bufferSizePlaceholder": "otomatik",
|
||||
"seconds": "saniye",
|
||||
"rules": {
|
||||
"first": "İlk",
|
||||
"last": "Son",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "наприклад, central-panel-a",
|
||||
"apiTokenNameRequired": "Назва обов'язкова",
|
||||
"apiTokenEmpty": "Поки немає токенів — створіть один для автентифікації ботів або віддалених панелей.",
|
||||
"apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію."
|
||||
"apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію.",
|
||||
"apiTokenCreatedTitle": "Токен створено",
|
||||
"apiTokenCreatedNotice": "Скопіюйте цей токен зараз. З міркувань безпеки він не зберігається у читабельному вигляді й більше не відображатиметься."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Параметри було змінено.",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "Увімкнення збору статистики для вхідного трафіку всіх вихідних проксі.",
|
||||
"statsOutboundDownlink": "Статистика вихідного даунлінку",
|
||||
"statsOutboundDownlinkDesc": "Увімкнення збору статистики для вихідного трафіку всіх вихідних проксі.",
|
||||
"connectionLimits": "Обмеження з'єднання",
|
||||
"connectionLimitsDesc": "Політики рівня з'єднання для користувачів рівня 0. Залиште поле порожнім, щоб використовувати значення Xray за замовчуванням.",
|
||||
"connIdle": "Тайм-аут простою",
|
||||
"connIdleDesc": "Закриває з'єднання після простою протягом вказаної кількості секунд. Зменшення значення швидше звільняє пам'ять і файлові дескриптори на завантажених серверах (за замовчуванням у Xray: 300).",
|
||||
"bufferSize": "Розмір буфера",
|
||||
"bufferSizeDesc": "Розмір внутрішнього буфера на з'єднання в КБ. Встановіть 0, щоб мінімізувати використання пам'яті на серверах з малим обсягом ОЗП (значення Xray за замовчуванням залежить від платформи).",
|
||||
"bufferSizePlaceholder": "авто",
|
||||
"seconds": "секунд",
|
||||
"rules": {
|
||||
"first": "Перший",
|
||||
"last": "Останній",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "ví dụ: central-panel-a",
|
||||
"apiTokenNameRequired": "Tên là bắt buộc",
|
||||
"apiTokenEmpty": "Chưa có token nào — tạo một token để xác thực bot hoặc panel từ xa.",
|
||||
"apiTokenDeleteWarning": "Mọi client đang dùng token này sẽ ngừng xác thực ngay lập tức."
|
||||
"apiTokenDeleteWarning": "Mọi client đang dùng token này sẽ ngừng xác thực ngay lập tức.",
|
||||
"apiTokenCreatedTitle": "Đã tạo token",
|
||||
"apiTokenCreatedNotice": "Hãy sao chép token này ngay bây giờ. Vì lý do bảo mật, token không được lưu ở dạng đọc được và sẽ không hiển thị lại."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Các tham số đã được thay đổi.",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu ra.",
|
||||
"statsOutboundDownlink": "Thống kê tải xuống đầu ra",
|
||||
"statsOutboundDownlinkDesc": "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu ra.",
|
||||
"connectionLimits": "Giới hạn kết nối",
|
||||
"connectionLimitsDesc": "Chính sách cấp kết nối cho người dùng cấp 0. Để trống một trường để sử dụng giá trị mặc định của Xray.",
|
||||
"connIdle": "Thời gian chờ nhàn rỗi",
|
||||
"connIdleDesc": "Đóng kết nối sau khi nó ở trạng thái nhàn rỗi trong số giây này. Giảm giá trị này giúp giải phóng bộ nhớ và file descriptor nhanh hơn trên các máy chủ bận (mặc định của Xray: 300).",
|
||||
"bufferSize": "Kích thước bộ đệm",
|
||||
"bufferSizeDesc": "Kích thước bộ đệm nội bộ trên mỗi kết nối tính bằng KB. Đặt thành 0 để giảm thiểu mức sử dụng bộ nhớ trên các máy chủ ít RAM (giá trị mặc định của Xray tùy thuộc vào nền tảng).",
|
||||
"bufferSizePlaceholder": "tự động",
|
||||
"seconds": "giây",
|
||||
"rules": {
|
||||
"first": "Đầu tiên",
|
||||
"last": "Cuối cùng",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "例如 central-panel-a",
|
||||
"apiTokenNameRequired": "名称必填",
|
||||
"apiTokenEmpty": "暂无令牌 — 创建一个用于认证机器人或远程面板。",
|
||||
"apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。"
|
||||
"apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。",
|
||||
"apiTokenCreatedTitle": "令牌已创建",
|
||||
"apiTokenCreatedNotice": "请立即复制此令牌。出于安全考虑,它不会以可读形式存储,也不会再次显示。"
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "参数已更改。",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "启用所有出站代理的上行流量统计收集。",
|
||||
"statsOutboundDownlink": "出站下载统计",
|
||||
"statsOutboundDownlinkDesc": "启用所有出站代理的下行流量统计收集。",
|
||||
"connectionLimits": "连接限制",
|
||||
"connectionLimitsDesc": "用户等级 0 的连接级策略。留空则使用 Xray 的默认值。",
|
||||
"connIdle": "空闲超时",
|
||||
"connIdleDesc": "连接空闲达到该秒数后将被关闭。在繁忙的服务器上调低此值可更快释放内存和文件描述符(Xray 默认值:300)。",
|
||||
"bufferSize": "缓冲区大小",
|
||||
"bufferSizeDesc": "每个连接的内部缓冲区大小(KB)。在低内存服务器上设为 0 可最大限度减少内存占用(Xray 默认值取决于平台)。",
|
||||
"bufferSizePlaceholder": "自动",
|
||||
"seconds": "秒",
|
||||
"rules": {
|
||||
"first": "置顶",
|
||||
"last": "置底",
|
||||
|
|
|
|||
|
|
@ -1123,7 +1123,9 @@
|
|||
"apiTokenNamePlaceholder": "例如 central-panel-a",
|
||||
"apiTokenNameRequired": "名稱必填",
|
||||
"apiTokenEmpty": "尚無令牌 — 建立一個以認證機器人或遠端面板。",
|
||||
"apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。"
|
||||
"apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。",
|
||||
"apiTokenCreatedTitle": "權杖已建立",
|
||||
"apiTokenCreatedNotice": "請立即複製此權杖。基於安全考量,它不會以可讀形式儲存,也不會再次顯示。"
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "參數已更改。",
|
||||
|
|
@ -1205,6 +1207,14 @@
|
|||
"statsOutboundUplinkDesc": "啟用所有出站代理的上行流量統計收集。",
|
||||
"statsOutboundDownlink": "出站下載統計",
|
||||
"statsOutboundDownlinkDesc": "啟用所有出站代理的下行流量統計收集。",
|
||||
"connectionLimits": "連線限制",
|
||||
"connectionLimitsDesc": "使用者等級 0 的連線層級原則。留空則使用 Xray 的預設值。",
|
||||
"connIdle": "閒置逾時",
|
||||
"connIdleDesc": "連線閒置達到該秒數後將被關閉。在繁忙的伺服器上調低此值可更快釋放記憶體和檔案描述符(Xray 預設值:300)。",
|
||||
"bufferSize": "緩衝區大小",
|
||||
"bufferSizeDesc": "每個連線的內部緩衝區大小(KB)。在低記憶體伺服器上設為 0 可最大限度減少記憶體佔用(Xray 預設值取決於平台)。",
|
||||
"bufferSizePlaceholder": "自動",
|
||||
"seconds": "秒",
|
||||
"rules": {
|
||||
"first": "置頂",
|
||||
"last": "置底",
|
||||
|
|
|
|||
Loading…
Reference in a new issue