Compare commits

...

25 Commits

Author SHA1 Message Date
xboard
13756956a6 fix: reset traffic stats when copying server nodes 2026-04-11 20:24:43 +08:00
Valentin Lobstein
121511523f Fix: CVE-2026-39912 - Magic link token leak in loginWithMailLink (#873)
The loginWithMailLink endpoint returns the magic login link in the
HTTP response body, allowing unauthenticated account takeover.

The fix returns true instead of the link. The email delivery is
the authentication factor.

Bug inherited from V2Board commit bdb10bed (2022-06-27).
2026-04-10 02:44:20 +08:00
xboard
1fe6531924 fix(update): avoid duplicate safe.directory entries for repo and admin submodule 2026-04-09 20:31:19 +08:00
xboard
38ea7d0067 docs: add donation section 2026-04-09 00:21:28 +08:00
xboard
58ef46f754 fix: stop sending VLESS decryption when encryption is disabled 2026-04-08 11:05:55 +08:00
yootus
ec49ba3fd1 Loon和Surfboard适配anytls (#854)
* Loon适配anytls

* Surfboard适配anytls

Surfboard适配anytls
2026-04-02 15:47:41 +08:00
NFamou
b7c8b31a91 Merge pull request #856 from NFamou/master
支持Surfboard下发SS2022
2026-04-02 15:46:55 +08:00
xboard
f3fd40008b updata admin asset 2026-04-02 05:51:16 +08:00
Xboard
94fc5f6942 Merge pull request #841 from cedar2025/revert-755-feat/server-id-stat-user
Revert "feat: Track user traffic per node (server_id)"
2026-03-30 18:18:35 +08:00
Xboard
c5a8c836c0 Revert "feat: Track user traffic per node (server_id)" 2026-03-30 18:17:27 +08:00
xboard
048530a893 Remove duplicate doc files 2026-03-30 18:04:41 +08:00
xboard
7ed5fc8fd3 fix: remove 2026_03_28_050000_lowercase_existing_emails.php 2026-03-30 17:59:39 +08:00
xboard
5f1afe4bdc feat: add Vless Encryption support 2026-03-30 17:03:37 +08:00
Xboard
0cd20d12dd Merge pull request #755 from socksprox/feat/server-id-stat-user
feat: Track user traffic per node (server_id)
2026-03-30 13:55:11 +08:00
Xboard
b4a94d1605 Merge pull request #689 from socksprox/fix-user-generation-multiple-prefix
Fix user generation with email_prefix to support multiple users
2026-03-30 13:32:46 +08:00
Xboard
7879a9ef85 Merge pull request #786 from lithromantic/master
Add sha256salt hashing option in password verification
2026-03-30 13:05:39 +08:00
lithromantic
6cac241144 Merge branch 'cedar2025:master' into master 2026-03-29 00:00:34 +01:00
lithromantic
f6abc362fd Add sha256salt hashing option in password verification 2026-01-18 00:04:00 +01:00
socksprox
c327fecb49 do not return strings, but int 2025-11-29 17:05:07 +01:00
socksprox
0446f88e9e again: update api combining times 2025-11-29 17:05:07 +01:00
socksprox
a01151130e Revert "Combine data with node_id in api output, so its all still "one day", and fits vanilla xboard behaviour"
This reverts commit de39230cbe111bbf793f11bcf5046ef717c67f87.

The api change caused issues
2025-11-29 17:05:07 +01:00
socksprox
9ca8da045c Combine data with node_id in api output, so its all still "one day", and fits vanilla xboard behaviour 2025-11-29 14:07:10 +01:00
socksprox
1ebf86b510 fix: do not merge traffic from different nodes 2025-11-29 13:47:21 +01:00
socksprox
9e35d16fa6 User traffic can now be viewed by node 2025-11-29 13:47:15 +01:00
socksprox
051813d39d Make that user batch generation works again 2025-09-15 15:43:43 +02:00
15 changed files with 206 additions and 46 deletions

View File

@@ -73,6 +73,12 @@ docker compose up -d
This project is for learning and communication purposes only. Users are responsible for any consequences of using this project.
## ❤️ Support The Project
If this project has helped you, donations are appreciated. They help support ongoing maintenance and would make me very happy.
TRC20: `TLypStEWsVrj6Wz9mCxbXffqgt5yz3Y4XB`
## 🌟 Maintenance Notice
This project is currently under light maintenance. We will:

View File

@@ -211,9 +211,14 @@ class ManageController extends Controller
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
$server->show = 0;
$server->code = null;
Server::create($server->toArray());
$copiedServer = $server->replicate();
$copiedServer->show = 0;
$copiedServer->code = null;
$copiedServer->u = 0;
$copiedServer->d = 0;
$copiedServer->save();
return $this->success(true);
}
}

View File

@@ -363,6 +363,12 @@ class UserController extends Controller
public function generate(UserGenerate $request)
{
if ($request->input('email_prefix')) {
// If generate_count is specified with email_prefix, generate multiple users with incremented emails
if ($request->input('generate_count')) {
return $this->multiGenerateWithPrefix($request);
}
// Single user generation with email_prefix
$email = $request->input('email_prefix') . '@' . $request->input('email_suffix');
if (User::byEmail($email)->exists()) {
@@ -459,6 +465,87 @@ class UserController extends Controller
]);
}
private function multiGenerateWithPrefix(Request $request)
{
$userService = app(UserService::class);
$usersData = [];
$emailPrefix = $request->input('email_prefix');
$emailSuffix = $request->input('email_suffix');
$generateCount = $request->input('generate_count');
// Check if any of the emails with prefix already exist
for ($i = 1; $i <= $generateCount; $i++) {
$email = $emailPrefix . '_' . $i . '@' . $emailSuffix;
if (User::where('email', $email)->exists()) {
return $this->fail([400201, '邮箱 ' . $email . ' 已存在于系统中']);
}
}
// Generate user data for batch creation
for ($i = 1; $i <= $generateCount; $i++) {
$email = $emailPrefix . '_' . $i . '@' . $emailSuffix;
$usersData[] = [
'email' => $email,
'password' => $request->input('password') ?? $email,
'plan_id' => $request->input('plan_id'),
'expired_at' => $request->input('expired_at'),
];
}
try {
DB::beginTransaction();
$users = [];
foreach ($usersData as $userData) {
$user = $userService->createUser($userData);
$user->save();
$users[] = $user;
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
return $this->fail([500, '生成失败']);
}
// 判断是否导出 CSV
if ($request->input('download_csv')) {
$headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="users.csv"',
];
$callback = function () use ($users, $request) {
$handle = fopen('php://output', 'w');
fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']);
foreach ($users as $user) {
$user = $user->refresh();
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$createDate = date('Y-m-d H:i:s', $user['created_at']);
$password = $request->input('password') ?? $user['email'];
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
fputcsv($handle, [$user['email'], $password, $expireDate, $user['uuid'], $createDate, $subscribeUrl]);
}
fclose($handle);
};
return response()->streamDownload($callback, 'users.csv', $headers);
}
// 默认返回 JSON
$data = collect($users)->map(function ($user) use ($request) {
return [
'email' => $user['email'],
'password' => $request->input('password') ?? $user['email'],
'expired_at' => $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']),
'uuid' => $user['uuid'],
'created_at' => date('Y-m-d H:i:s', $user['created_at']),
'subscribe_url' => Helper::getSubscribeUrl($user['token']),
];
});
return response()->json([
'code' => 0,
'message' => '批量生成成功',
'data' => $data,
]);
}
public function sendMail(UserSendMail $request)
{
ini_set('memory_limit', '-1');

View File

@@ -71,6 +71,10 @@ class ServerSave extends FormRequest
'network' => 'required|string',
'network_settings' => 'nullable|array',
'flow' => 'nullable|string',
'encryption' => 'nullable|array',
'encryption.enabled' => 'nullable|boolean',
'encryption.encryption' => 'nullable|string',
'encryption.decryption' => 'nullable|string',
'tls_settings.server_name' => 'nullable|string',
'tls_settings.allow_insecure' => 'nullable|boolean',
'reality_settings.allow_insecure' => 'nullable|boolean',

View File

@@ -203,6 +203,15 @@ class Server extends Model
'tls' => ['type' => 'integer', 'default' => 0],
'tls_settings' => ['type' => 'array', 'default' => null],
'flow' => ['type' => 'string', 'default' => null],
'encryption' => [
'type' => 'object',
'default' => null,
'fields' => [
'enabled' => ['type' => 'boolean', 'default' => false],
'encryption' => ['type' => 'string', 'default' => null], // 客户端公钥
'decryption' => ['type' => 'string', 'default' => null], // 服务端私钥
]
],
'network' => ['type' => 'string', 'default' => null],
'network_settings' => ['type' => 'array', 'default' => null],
...self::REALITY_CONFIGURATION,

View File

@@ -332,6 +332,10 @@ class ClashMeta extends AbstractProtocol
'cipher' => 'auto',
'udp' => true,
'flow' => data_get($protocol_settings, 'flow'),
'encryption' => match (data_get($protocol_settings, 'encryption.enabled')) {
true => data_get($protocol_settings, 'encryption.encryption', 'none'),
default => 'none'
},
'tls' => false
];

View File

@@ -151,7 +151,10 @@ class General extends AbstractProtocol
$config = [
'mode' => 'multi', //grpc传输模式
'security' => '', //传输层安全 tls/reality
'encryption' => 'none', //加密方式
'encryption' => match (data_get($protocol_settings, 'encryption.enabled')) {
true => data_get($protocol_settings, 'encryption.encryption', 'none'),
default => 'none'
},
'type' => data_get($server, 'protocol_settings.network'), //传输协议
'flow' => data_get($protocol_settings, 'flow'),
];

View File

@@ -15,6 +15,7 @@ class Loon extends AbstractProtocol
Server::TYPE_TROJAN,
Server::TYPE_HYSTERIA,
Server::TYPE_VLESS,
Server::TYPE_ANYTLS,
];
protected $protocolRequirements = [
@@ -47,6 +48,9 @@ class Loon extends AbstractProtocol
if ($item['type'] === Server::TYPE_VLESS) {
$uri .= self::buildVless($item['password'], $item);
}
if ($item['type'] === Server::TYPE_ANYTLS) {
$uri .= self::buildAnyTLS($item['password'], $item);
}
}
return response($uri)
->header('content-type', 'text/plain')
@@ -325,4 +329,29 @@ class Loon extends AbstractProtocol
$uri .= "\r\n";
return $uri;
}
public static function buildAnyTLS($password, $server)
{
$protocol_settings = data_get($server, 'protocol_settings', []);
$config = [
"{$server['name']}=anytls",
"{$server['host']}",
"{$server['port']}",
"{$password}",
"udp=true"
];
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
$config[] = "sni={$serverName}";
}
// ✅ 跳过证书校验
if (data_get($protocol_settings, 'tls.allow_insecure')) {
$config[] = 'skip-cert-verify=true';
}
$config = array_filter($config);
return implode(',', $config) . "\r\n";
}
}

View File

@@ -14,6 +14,7 @@ class Surfboard extends AbstractProtocol
Server::TYPE_SHADOWSOCKS,
Server::TYPE_VMESS,
Server::TYPE_TROJAN,
Server::TYPE_ANYTLS,
];
const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.surfboard.conf';
const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.surfboard.conf';
@@ -36,7 +37,10 @@ class Surfboard extends AbstractProtocol
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305'
'chacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305'
])
) {
// [Proxy]
@@ -56,6 +60,10 @@ class Surfboard extends AbstractProtocol
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === Server::TYPE_ANYTLS) {
$proxies .= self::buildAnyTLS($item['password'], $item);
$proxyGroup .= $item['name'] . ', ';
}
}
$config = subscribe_template('surfboard');
@@ -190,4 +198,32 @@ class Surfboard extends AbstractProtocol
$uri .= "\r\n";
return $uri;
}
public static function buildAnyTLS($password, $server)
{
$protocol_settings = data_get($server, 'protocol_settings', []);
$config = [
"{$server['name']}=anytls",
"{$server['host']}",
"{$server['port']}",
"password={$password}",
"tfo=true",
"udp-relay=true"
];
// SNI
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
$config[] = "sni={$serverName}";
}
// 跳过证书校验
if (data_get($protocol_settings, 'tls.allow_insecure')) {
$config[] = "skip-cert-verify=true";
}
$config = array_filter($config);
return implode(',', $config) . "\r\n";
}
}

View File

@@ -46,7 +46,7 @@ class MailLinkService
$this->sendMailLinkEmail($user, $link);
return [true, $link];
return [true, true];
}
/**

View File

@@ -183,6 +183,10 @@ class ServerService
...$baseConfig,
'tls' => (int) $protocolSettings['tls'],
'flow' => $protocolSettings['flow'],
'decryption' => match (data_get($protocolSettings, 'encryption.enabled')) {
true => data_get($protocolSettings, 'encryption.decryption'),
default => null,
},
'tls_settings' => match ((int) $protocolSettings['tls']) {
2 => $protocolSettings['reality_settings'],
default => $protocolSettings['tls_settings'],

View File

@@ -86,6 +86,7 @@ class Helper
case 'md5': return md5($password) === $hash;
case 'sha256': return hash('sha256', $password) === $hash;
case 'md5salt': return md5($password . $salt) === $hash;
case 'sha256salt': return hash('sha256', $password . $salt) === $hash;
default: return password_verify($password, $hash);
}
}

View File

@@ -1,38 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// 统计需要转换的记录数
$count = DB::table('v2_user')
->whereNotNull('email')
->whereRaw('email != LOWER(email)')
->count();
if ($count > 0) {
Log::info("Converting {$count} email(s) to lowercase");
DB::table('v2_user')
->whereNotNull('email')
->whereRaw('email != LOWER(email)')
->update(['email' => DB::raw('LOWER(email)')]);
Log::info("Email lowercase conversion completed");
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// 无法恢复原始大小写
}
};

View File

@@ -10,7 +10,17 @@ if ! command -v git &> /dev/null; then
exit 1
fi
git config --global --add safe.directory $(pwd)
repo_root="$(pwd)"
add_safe_directory() {
local dir="$1"
git config --global --get-all safe.directory | grep -Fx "$dir" > /dev/null || git config --global --add safe.directory "$dir"
}
add_safe_directory "$repo_root"
add_safe_directory "$repo_root/public/assets/admin"
git fetch --all && git reset --hard origin/master && git pull origin master
rm -rf composer.lock composer.phar
wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar