NewLife/Stardust

新增远程命令执行能力并完善安全审计

本次提交实现了通过 bash/cmd 命令远程执行功能,支持字符串及 JSON 参数格式,执行过程全程记录详细日志和事件,便于安全审计。优化了 WebHelper 的 URL 端口处理逻辑。新增《远程命令执行安全方案》文档,涵盖危险命令识别、异常行为检测、告警通知与 RBAC 权限控制等,全面提升平台安全性和可追溯性。
智能大石头 authored at 2026-01-27 21:29:10
8286b55
Tree
1 Parent(s) 9d3090a
Summary: 3 changed files with 580 additions and 2 deletions.
Added +450 -0
Modified +122 -0
Modified +8 -2
Added +450 -0
diff --git "a/Doc/\350\277\234\347\250\213\345\221\275\344\273\244\346\211\247\350\241\214\345\256\211\345\205\250\346\226\271\346\241\210.md" "b/Doc/\350\277\234\347\250\213\345\221\275\344\273\244\346\211\247\350\241\214\345\256\211\345\205\250\346\226\271\346\241\210.md"
new file mode 100644
index 0000000..d473b0d
--- /dev/null
+++ "b/Doc/\350\277\234\347\250\213\345\221\275\344\273\244\346\211\247\350\241\214\345\256\211\345\205\250\346\226\271\346\241\210.md"
@@ -0,0 +1,450 @@
+# Զ������ִ�а�ȫ����
+
+## ����
+
+StarAgent ֧��ͨ���dz�ƽ̨Զ��ִ�� bash/cmd ������������������Ⱥ�������� Agent �ǿ�Դ�ģ��ͻ��˷�����Ч�����**��ȫ�����ص���ƽ̨��**��
+
+## ����ԭ��
+
+1. **�ͻ��˿ɱ��ƹ�**�������߿����޸� Agent Դ��ȥ������
+2. **ƽ̨�����α߽�**�����з���������ƽ̨��ʵʩ
+3. **��������ڷ���**���޷���ֹʱ��ȷ����׷��
+
+---
+
+## ��ʵʩ������A - ��ǿ�����־
+
+### Agent ��Ľ�
+
+#### 1. ʹ�� WriteEvent �ϱ���־
+```csharp
+// ִ��ǰ����¼�����ʱ��ʱ���
+WriteEvent("warn", "RunBash", $"ִ�����{cmd}����ʱ��{timeout}ms��ʱ�䣺{now:yyyy-MM-dd HH:mm:ss}");
+
+// ִ�гɹ�����¼��ʱ���������
+WriteEvent("info", "RunBash", $"ִ�гɹ������{cmd}����ʱ��{sw.ElapsedMilliseconds}ms��������ȣ�{rs?.Length ?? 0}");
+
+// ִ��ʧ�ܣ���¼������Ϣ
+WriteEvent("error", "RunBash", $"ִ��ʧ�ܣ����{cmd}����ʱ��{sw.ElapsedMilliseconds}ms������{ex.Message}");
+```
+
+#### 2. �ؼ�����
+- ? **����ɾ��**��`WriteEvent` ֱ���ϱ����dz�ƽ̨���ݿ�
+- ? **������¼**�����ʱ�䡢��ʱ�������������Ϣ
+- ? **���ܼ���**��ʹ�� `Stopwatch` ��¼��ȷ��ʱ
+- ? **����ض�**��������־���󣨳���1000�ַ��ضϣ�
+
+#### 3. �����־�ֶ�
+| �ֶ� | ˵�� | ʾ�� |
+|------|------|------|
+| ���� | warn/info/error | warn��ִ��ǰ����info���ɹ�����error��ʧ�ܣ� |
+| ���� | RunBash/RunCmd | RunBash |
+| ���� | ������������ | `systemctl restart nginx` |
+| ��ʱ | ��ʱʱ�䣨���룩 | 30000 |
+| ��ʱ | ʵ��ִ��ʱ�䣨���룩 | 1523 |
+| ������� | ���ؽ������ | 256 |
+| ʱ��� | ִ��ʱ�� | 2025-01-01 14:30:00 |
+| ������Ϣ | ʧ��ԭ�� | Command execution timeout |
+
+---
+
+## ��ʵʩ������B - ƽ̨�����
+
+### 1. ���ݿ�����
+
+#### NodeCommand���ڵ������¼����
+```sql
+CREATE TABLE NodeCommand (
+    Id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    NodeId INT NOT NULL COMMENT '�ڵ�ID',
+    NodeName VARCHAR(100) COMMENT '�ڵ�����',
+    Command TEXT NOT NULL COMMENT 'ִ�е�����',
+    Type VARCHAR(20) COMMENT 'bash/cmd',
+    
+    -- ��������Ϣ
+    OperatorId INT COMMENT '������ID',
+    OperatorName VARCHAR(50) COMMENT '����������',
+    OperatorRole VARCHAR(50) COMMENT '�����˽�ɫ',
+    ClientIP VARCHAR(50) COMMENT '������IP',
+    ClientLocation VARCHAR(100) COMMENT 'IP�����',
+    
+    -- ִ�н��
+    Success BOOLEAN COMMENT '�Ƿ�ɹ�',
+    Duration INT COMMENT '��ʱ(ms)',
+    OutputLength INT COMMENT '�������',
+    ErrorMessage TEXT COMMENT '������Ϣ',
+    
+    -- ʱ���
+    ExecuteTime DATETIME COMMENT 'ִ��ʱ��',
+    CreateTime DATETIME NOT NULL,
+    
+    INDEX idx_node (NodeId, ExecuteTime),
+    INDEX idx_operator (OperatorId, ExecuteTime),
+    INDEX idx_time (ExecuteTime)
+) COMMENT='�ڵ�����ִ�м�¼������ɾ����';
+```
+
+#### NodeCommandAlert�����в����澯����
+```sql
+CREATE TABLE NodeCommandAlert (
+    Id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    CommandId BIGINT COMMENT '����NodeCommand.Id',
+    AlertType VARCHAR(50) COMMENT '�澯���ͣ�dangerous_command/batch_operation/abnormal_time/abnormal_ip',
+    AlertLevel VARCHAR(20) COMMENT '�澯����info/warn/error/critical',
+    AlertMessage TEXT COMMENT '�澯����',
+    
+    -- �澯����
+    Status VARCHAR(20) DEFAULT 'pending' COMMENT '״̬��pending/confirmed/ignored',
+    Handler INT COMMENT '������ID',
+    HandleTime DATETIME COMMENT '����ʱ��',
+    HandleRemark TEXT COMMENT '������ע',
+    
+    -- ֪ͨ״̬
+    NotifyDingTalk BOOLEAN DEFAULT FALSE COMMENT '�Ƿ��Ѷ���֪ͨ',
+    NotifyWechat BOOLEAN DEFAULT FALSE COMMENT '�Ƿ���΢��֪ͨ',
+    NotifySMS BOOLEAN DEFAULT FALSE COMMENT '�Ƿ��Ѷ���֪ͨ',
+    
+    CreateTime DATETIME NOT NULL,
+    
+    INDEX idx_status (Status, CreateTime),
+    INDEX idx_command (CommandId)
+) COMMENT='�ڵ�����澯��¼';
+```
+
+### 2. �������������
+
+#### Σ�չؼ����б�
+```csharp
+public static class DangerousPatterns
+{
+    // ϵͳ�ƻ�
+    public static readonly String[] SystemDestruction = 
+    [
+        "rm -rf /",           // ɾ����Ŀ¼
+        "mkfs",               // ��ʽ������
+        "dd if=/dev/zero",    // д�����ֽ�
+        ":(){:|:&};:",        // Forkը��
+    ];
+    
+    // Ȩ���޸�
+    public static readonly String[] PermissionChange = 
+    [
+        "chmod 777",          // Σ��Ȩ��
+        "chown root",         // �ı�������
+        "passwd",             // �޸�����
+        "useradd",            // �����û�
+        "userdel",            // ɾ���û�
+    ];
+    
+    // Զ��ִ��
+    public static readonly String[] RemoteExecution = 
+    [
+        "curl.*|.*bash",      // ���ز�ִ��
+        "wget.*|.*sh",        // ���ز�ִ��
+        "nc -l",              // ����shell
+        "/dev/tcp",           // �������
+    ];
+    
+    // ���簲ȫ
+    public static readonly String[] NetworkSecurity = 
+    [
+        "iptables -F",        // ��շ���ǽ
+        "systemctl stop firewalld", // ֹͣ����ǽ
+        "ufw disable",        // ���÷���ǽ
+    ];
+    
+    // ϵͳ����
+    public static readonly String[] SystemControl = 
+    [
+        "shutdown",           // �ػ�
+        "reboot",             // ������ע�⣺����node/rebootָ�
+        "init 0",             // �ػ�
+        "init 6",             // ����
+    ];
+}
+```
+
+#### ����߼�
+```csharp
+public class CommandSecurityChecker
+{
+    /// <summary>��������Ƿ����Σ�ղ���</summary>
+    public static (Boolean isDangerous, String category, String pattern) CheckDangerous(String command)
+    {
+        foreach (var pattern in DangerousPatterns.SystemDestruction)
+        {
+            if (Regex.IsMatch(command, pattern, RegexOptions.IgnoreCase))
+                return (true, "ϵͳ�ƻ�", pattern);
+        }
+        
+        foreach (var pattern in DangerousPatterns.PermissionChange)
+        {
+            if (Regex.IsMatch(command, pattern, RegexOptions.IgnoreCase))
+                return (true, "Ȩ���޸�", pattern);
+        }
+        
+        foreach (var pattern in DangerousPatterns.RemoteExecution)
+        {
+            if (Regex.IsMatch(command, pattern, RegexOptions.IgnoreCase))
+                return (true, "Զ��ִ��", pattern);
+        }
+        
+        foreach (var pattern in DangerousPatterns.NetworkSecurity)
+        {
+            if (Regex.IsMatch(command, pattern, RegexOptions.IgnoreCase))
+                return (true, "���簲ȫ", pattern);
+        }
+        
+        foreach (var pattern in DangerousPatterns.SystemControl)
+        {
+            if (Regex.IsMatch(command, pattern, RegexOptions.IgnoreCase))
+                return (true, "ϵͳ����", pattern);
+        }
+        
+        return (false, null, null);
+    }
+}
+```
+
+### 3. �쳣��Ϊ���
+
+#### �����������
+```csharp
+// ��⣺5������ͬһ�û�ִ�������100��
+var count = NodeCommand.FindCount(
+    _.OperatorId == userId && 
+    _.ExecuteTime >= DateTime.Now.AddMinutes(-5)
+);
+
+if (count > 100)
+{
+    // �����澯
+    CreateAlert(commandId, "batch_operation", "error", 
+        $"�û� {operatorName} ��5������ִ���� {count} �����������������");
+    
+    // ��ʱ�����˺�
+    FreezeUser(userId, "���������澯����ʱ����30����");
+    
+    // ����֪ͨ
+    SendAlert("����", $"�û� {operatorName} �˺����Ʊ���������ʱ����");
+}
+```
+
+#### �쳣ʱ����
+```csharp
+// ��⣺�賿2-5��ִ������ && ���û���δ�ڴ�ʱ�β���
+var hour = DateTime.Now.Hour;
+if (hour >= 2 && hour < 5)
+{
+    var historyCount = NodeCommand.FindCount(
+        _.OperatorId == userId && 
+        _.ExecuteTime.Hour >= 2 && 
+        _.ExecuteTime.Hour < 5 &&
+        _.ExecuteTime < DateTime.Now.AddDays(-7) // ��7��ǰ����ʷ
+    );
+    
+    if (historyCount == 0)
+    {
+        CreateAlert(commandId, "abnormal_time", "warn",
+            $"�û� {operatorName} �״����賿 {hour} ��ִ�����{command}");
+        
+        SendAlert("����", $"�û� {operatorName} ���쳣ʱ��β���");
+    }
+}
+```
+
+#### �쳣IP���
+```csharp
+// ��⣺����IP����û���ʷIP����λ�����>1000km
+var lastLogin = UserLoginLog.FindLast(_.UserId == userId);
+if (lastLogin != null && lastLogin.ClientIP != currentIP)
+{
+    var lastLocation = GetIPLocation(lastLogin.ClientIP);
+    var currentLocation = GetIPLocation(currentIP);
+    var distance = CalculateDistance(lastLocation, currentLocation);
+    
+    if (distance > 1000)
+    {
+        CreateAlert(commandId, "abnormal_ip", "error",
+            $"�û� {operatorName} ���쳣�ص��¼��{currentLocation}�������ϴε�¼ {distance}km��");
+        
+        // Ҫ�������֤
+        RequireTwoFactorAuth(userId);
+        
+        SendAlert("����", $"�û� {operatorName} �쳣�ص��¼��������ȷ��");
+    }
+}
+```
+
+### 4. �澯֪ͨʵ��
+
+#### ����������֪ͨ
+```csharp
+public async Task SendDingTalkAlert(String title, String message, String level)
+{
+    var webhook = "https://oapi.dingtalk.com/robot/send?access_token=xxx";
+    
+    var color = level switch
+    {
+        "info" => "#00FF00",
+        "warn" => "#FFA500",
+        "error" => "#FF0000",
+        "critical" => "#8B0000",
+        _ => "#808080"
+    };
+    
+    var content = new
+    {
+        msgtype = "markdown",
+        markdown = new
+        {
+            title = title,
+            text = $"## {title}\n\n" +
+                   $"**����**��{level}\n\n" +
+                   $"**����**��{message}\n\n" +
+                   $"**ʱ��**��{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n\n" +
+                   $"[�鿴����](https://stardust.newlifex.com/deployment/nodecommand)"
+        }
+    };
+    
+    await HttpClient.PostJsonAsync(webhook, content);
+}
+```
+
+#### ��ҵ΢��֪ͨ
+```csharp
+public async Task SendWechatAlert(String title, String message, String level)
+{
+    var webhook = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx";
+    
+    var content = new
+    {
+        msgtype = "markdown",
+        markdown = new
+        {
+            content = $"## {title}\n" +
+                     $">����<font color=\"warning\">{level}</font>\n" +
+                     $">���ݣ�{message}\n" +
+                     $">ʱ�䣺{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n" +
+                     $"[�鿴����](https://stardust.newlifex.com/deployment/nodecommand)"
+        }
+    };
+    
+    await HttpClient.PostJsonAsync(webhook, content);
+}
+```
+
+### 5. Ȩ�޿��ƣ�RBAC��
+
+#### ��ɫ����
+```csharp
+public enum NodeCommandPermission
+{
+    None = 0,          // ��Ȩ��
+    View = 1,          // �鿴�ڵ�
+    ReadOnly = 2,      // ִ��ֻ�����ps/df/top�ȣ�
+    Manage = 3,        // ִ�й��������������ȣ�
+    Dangerous = 4,     // ִ��Σ�����ɾ���ļ�/�޸�����ȣ�
+    Admin = 5,         // ��������Ա
+}
+```
+
+#### Ȩ�޼��
+```csharp
+public Boolean CheckPermission(Int32 userId, String command)
+{
+    var user = User.FindById(userId);
+    var permission = GetCommandPermission(command);
+    
+    // ����û���ɫ�Ƿ�����Ҫ��
+    if (user.NodeCommandPermission < permission)
+    {
+        XTrace.WriteLine($"Ȩ�޲��㣺�û� {user.Name} Ȩ��Ϊ {user.NodeCommandPermission}��������Ҫ {permission}");
+        return false;
+    }
+    
+    return true;
+}
+
+private NodeCommandPermission GetCommandPermission(String command)
+{
+    // ֻ������
+    if (Regex.IsMatch(command, "^(ps|top|df|free|ls|cat|tail|head|uptime|hostname)"))
+        return NodeCommandPermission.ReadOnly;
+    
+    // ��������
+    if (Regex.IsMatch(command, "^(systemctl restart|systemctl stop|systemctl start)"))
+        return NodeCommandPermission.Manage;
+    
+    // ������
+    if (Regex.IsMatch(command, "^(rm|passwd|userdel|useradd|chmod|chown)"))
+        return NodeCommandPermission.Dangerous;
+    
+    // Ĭ����Ҫ����ԱȨ��
+    return NodeCommandPermission.Admin;
+}
+```
+
+### 6. ʵʩ����
+
+#### ��һ�׶Σ�������ƣ�����ɣ�
+- [x] Agent ��ʹ�� WriteEvent ��¼��־
+- [x] ��¼����ִ��ǰ���������Ϣ
+- [x] ��¼��ʱ���������
+
+#### �ڶ��׶Σ�ƽ̨���¼����ʵʩ��
+- [ ] ���� NodeCommand ��
+- [ ] ���� NodeCommandAlert ��
+- [ ] ʵ�������¼�ӿ�
+- [ ] ������̨չʾ������ʷ
+
+#### �����׶Σ����澯����ʵʩ��
+- [ ] ʵ��Σ��������
+- [ ] ʵ���쳣��Ϊ���
+- [ ] ���ɶ���/��ҵ΢�Ÿ澯
+- [ ] ʵ�ָ澯��������
+
+#### ���Ľ׶Σ�Ȩ�޿��ƣ���ʵʩ��
+- [ ] ʵ�� RBAC Ȩ����ϵ
+- [ ] Ȩ�޼��������
+- [ ] �������̣���ѡ��
+
+---
+
+## ��¼����ȫ���ʵ��
+
+### 1. ��СȨ��ԭ��
+- ��ͨ�û���ֻ�ܲ鿴�ڵ�״̬
+- ��ά��Ա����ִ��ֻ���͹�������
+- �߼���ά����ִ��Σ�������������
+- ��������Ա���������ƣ�������ƣ�
+
+### 2. ˫������֤
+����Σ�ղ�����Ҫ��
+1. ������֤
+2. ����/������֤��
+3. ��̬���ƣ���ѡ��
+
+### 3. ��������
+- ������������
+- ������ɾ�������־
+- ���ڹ鵵��ʷ����
+- �ؼ�����ʵʱ�澯
+
+### 4. �������
+- ÿ�����ɲ�������
+- �쳣��Ϊ�˹�����
+- Ȩ�޶��ڸ���
+- ��ȫ���Գ����Ż�
+
+---
+
+## �ܽ�
+
+Զ������ִ�й��ܵİ�ȫ������**������ƽ̨��**��
+
+1. **Agent ��**����ϸ�������־������ɣ�
+2. **ƽ̨��**��Ȩ�޿��� + �쳣��� + ʵʱ�澯����ʵʩ��
+3. **��Ӫ��**��������� + Ӧ����Ӧ
+
+**��ס**����ʹ�����߹���ƽ̨�������������־Ҳ�ܰ���׷�����в������������һ�����ߡ�
Modified +122 -0
diff --git a/StarAgent/MyStarClient.cs b/StarAgent/MyStarClient.cs
index 77a292b..e7a7ad4 100644
--- a/StarAgent/MyStarClient.cs
+++ b/StarAgent/MyStarClient.cs
@@ -64,6 +64,8 @@ internal class MyStarClient(StarAgentSetting set) : StarClient(set)
         this.RegisterCommand("node/reboot", Reboot);
         this.RegisterCommand("node/setchannel", SetChannel);
         this.RegisterCommand("node/synctime", SyncTime);
+        this.RegisterCommand("bash", RunBash);
+        this.RegisterCommand("cmd", RunCmd);
 
         base.Open();
     }
@@ -370,5 +372,125 @@ internal class MyStarClient(StarAgentSetting set) : StarClient(set)
         public Int16 Second;
         public Int16 Milliseconds;
     }
+
+    /// <summary>执行Bash命令</summary>
+    /// <param name="argument">命令参数。可以是纯字符串命令,或JSON格式{"cmd":"命令","timeout":超时毫秒}</param>
+    /// <returns></returns>
+    public String? RunBash(String? argument)
+    {
+        if (argument.IsNullOrEmpty()) return "参数为空";
+
+        var cmd = argument;
+        var timeout = 30_000;
+
+        // 尝试解析JSON格式参数
+        if (argument.StartsWith("{"))
+        {
+            try
+            {
+                var dic = JsonParser.Decode(argument);
+                if (dic != null)
+                {
+                    cmd = dic["cmd"] + "";
+                    if (dic.ContainsKey("timeout")) timeout = dic["timeout"].ToInt();
+                }
+            }
+            catch
+            {
+                // 解析失败则当作普通命令处理
+            }
+        }
+
+        if (cmd.IsNullOrEmpty()) return "命令为空";
+
+        // 审计日志:记录命令执行前的信息
+        var now = DateTime.Now;
+        WriteLog("执行Bash命令:{0},超时:{1}ms", cmd, timeout);
+        WriteEvent("warn", "RunBash", $"执行命令:{cmd},超时:{timeout}ms,时间:{now:yyyy-MM-dd HH:mm:ss}");
+
+        var sw = Stopwatch.StartNew();
+        try
+        {
+            var rs = "bash".Execute($"-c \"{cmd.Replace("\"", "\\\"")}\"", timeout);
+            sw.Stop();
+
+            // 审计日志:记录成功执行的详细信息(输出过长时截断)
+            var resultPreview = rs?.Length > 1000 ? rs.Substring(0, 1000) + "..." : rs;
+            WriteLog("执行成功,耗时:{0}ms,输出长度:{1}", sw.ElapsedMilliseconds, rs?.Length ?? 0);
+            WriteEvent("info", "RunBash", $"执行成功,命令:{cmd},耗时:{sw.ElapsedMilliseconds}ms,输出长度:{rs?.Length ?? 0}");
+
+            return rs;
+        }
+        catch (Exception ex)
+        {
+            sw.Stop();
+
+            // 审计日志:记录失败信息
+            WriteLog("执行失败:{0},耗时:{1}ms", ex.Message, sw.ElapsedMilliseconds);
+            WriteEvent("error", "RunBash", $"执行失败,命令:{cmd},耗时:{sw.ElapsedMilliseconds}ms,错误:{ex.Message}");
+
+            return $"执行失败:{ex.Message}";
+        }
+    }
+
+    /// <summary>执行CMD命令</summary>
+    /// <param name="argument">命令参数。可以是纯字符串命令,或JSON格式{"cmd":"命令","timeout":超时毫秒}</param>
+    /// <returns></returns>
+    public String? RunCmd(String? argument)
+    {
+        if (argument.IsNullOrEmpty()) return "参数为空";
+
+        var cmd = argument;
+        var timeout = 30_000;
+
+        // 尝试解析JSON格式参数
+        if (argument.StartsWith("{"))
+        {
+            try
+            {
+                var dic = JsonParser.Decode(argument);
+                if (dic != null)
+                {
+                    cmd = dic["cmd"] + "";
+                    if (dic.ContainsKey("timeout")) timeout = dic["timeout"].ToInt();
+                }
+            }
+            catch
+            {
+                // 解析失败则当作普通命令处理
+            }
+        }
+
+        if (cmd.IsNullOrEmpty()) return "命令为空";
+
+        // 审计日志:记录命令执行前的信息
+        var now = DateTime.Now;
+        WriteLog("执行CMD命令:{0},超时:{1}ms", cmd, timeout);
+        WriteEvent("warn", "RunCmd", $"执行命令:{cmd},超时:{timeout}ms,时间:{now:yyyy-MM-dd HH:mm:ss}");
+
+        var sw = Stopwatch.StartNew();
+        try
+        {
+            var rs = "cmd".Execute($"/c {cmd}", timeout);
+            sw.Stop();
+
+            // 审计日志:记录成功执行的详细信息(输出过长时截断)
+            var resultPreview = rs?.Length > 1000 ? rs.Substring(0, 1000) + "..." : rs;
+            WriteLog("执行成功,耗时:{0}ms,输出长度:{1}", sw.ElapsedMilliseconds, rs?.Length ?? 0);
+            WriteEvent("info", "RunCmd", $"执行成功,命令:{cmd},耗时:{sw.ElapsedMilliseconds}ms,输出长度:{rs?.Length ?? 0}");
+
+            return rs;
+        }
+        catch (Exception ex)
+        {
+            sw.Stop();
+
+            // 审计日志:记录失败信息
+            WriteLog("执行失败:{0},耗时:{1}ms", ex.Message, sw.ElapsedMilliseconds);
+            WriteEvent("error", "RunCmd", $"执行失败,命令:{cmd},耗时:{sw.ElapsedMilliseconds}ms,错误:{ex.Message}");
+
+            return $"执行失败:{ex.Message}";
+        }
+    }
     #endregion
 }
Modified +8 -2
diff --git a/Stardust.Extensions/WebHelper.cs b/Stardust.Extensions/WebHelper.cs
index aabc620..c285120 100644
--- a/Stardust.Extensions/WebHelper.cs
+++ b/Stardust.Extensions/WebHelper.cs
@@ -25,11 +25,17 @@ static class WebHelper
         catch (Exception ex)
         {
             DefaultSpan.Current?.AppendTag($"GetRawUrl:{url} 失败:{ex.Message}");
-            uri = new UriInfo("")
+            var port = request.Scheme switch
+            {
+                "https" => 443,
+                "http" => 80,
+                _ => 0
+            };
+            uri = new UriInfo
             {
                 Scheme = request.Scheme,
                 Host = request.Host.Host,
-                Port = request.Host.Port ?? (request.Scheme == "https" ? 443 : 80),
+                Port = request.Host.Port ?? port,
                 AbsolutePath = request.PathBase + request.Path,
                 Query = request.QueryString.ToUriComponent()
             };