This commit is contained in:
MengMengCode
2026-08-19 21:47:26 +08:00
parent 60501d4831
commit 73a72680ad
5 changed files with 51 additions and 7 deletions
+32 -2
View File
@@ -31,12 +31,42 @@ func TestValidateATCommandBlocksTrafficMessagingAndDialActions(t *testing.T) {
"AT+CSQ;+CMSS=7",
"AT+CSQ;D12345;",
} {
if err := validateATCommand(command); err == nil {
if err := validateATCommand(command, false); err == nil {
t.Errorf("validateATCommand(%q) permitted a guarded mutation", command)
}
}
}
func TestValidateATCommandForceBypassesGuard(t *testing.T) {
t.Parallel()
for _, command := range []string{
"AT+CGATT=1",
"AT+CFUN=1",
"AT+CGACT=1,1",
"AT+CUSD=1,\"*100#\"",
"ATD12345;",
} {
if err := validateATCommand(command, true); err != nil {
t.Errorf("validateATCommand(%q, true): %v", command, err)
}
}
}
func TestValidateATCommandForceKeepsSyntaxChecks(t *testing.T) {
t.Parallel()
for _, command := range []string{
"A",
"",
"AT\r",
"AT\n",
string(make([]byte, 513)),
} {
if err := validateATCommand(command, true); err == nil {
t.Errorf("validateATCommand(%q, true) skipped syntax check", command)
}
}
}
func TestValidateATCommandAllowsReadOnlyStatusQueries(t *testing.T) {
t.Parallel()
for _, command := range []string{
@@ -48,7 +78,7 @@ func TestValidateATCommandAllowsReadOnlyStatusQueries(t *testing.T) {
"AT+CIMI",
"AT+CCID",
} {
if err := validateATCommand(command); err != nil {
if err := validateATCommand(command, false); err != nil {
t.Errorf("validateATCommand(%q): %v", command, err)
}
}
+6 -2
View File
@@ -1045,13 +1045,14 @@ func (s *Server) handleAT(w http.ResponseWriter, r *http.Request, id string) boo
var request struct {
Command string `json:"cmd"`
TimeoutMs int `json:"timeout_ms"`
Force bool `json:"force"`
}
if err := s.decodeJSON(w, r, &request); err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
return true
}
command := strings.TrimSpace(request.Command)
if err := validateATCommand(command); err != nil {
if err := validateATCommand(command, request.Force); err != nil {
writeError(w, http.StatusBadRequest, "unsafe_at_command", err.Error())
return true
}
@@ -1100,7 +1101,7 @@ func (s *Server) handleAT(w http.ResponseWriter, r *http.Request, id string) boo
return true
}
func validateATCommand(command string) error {
func validateATCommand(command string, force bool) error {
upper := strings.ToUpper(command)
if len(command) < 2 || len(command) > 512 || !strings.HasPrefix(upper, "AT") {
return errors.New("AT command must start with AT and contain at most 512 characters")
@@ -1108,6 +1109,9 @@ func validateATCommand(command string) error {
if strings.ContainsAny(command, "\r\n\x00") {
return errors.New("AT command must contain exactly one line")
}
if force {
return nil
}
canonical := strings.NewReplacer(" ", "", "\t", "").Replace(upper)
for _, blocked := range []string{
`+QCFG="USBNET"`,
+1 -1
View File
@@ -1981,7 +1981,7 @@ func (bot *telegramBot) handleATCommand(ctx context.Context, config telegramRunt
func (bot *telegramBot) executeATCommand(ctx context.Context, deviceID, command string) (string, error) {
command = strings.TrimSpace(command)
if err := validateATCommand(command); err != nil {
if err := validateATCommand(command, false); err != nil {
return "", err
}
_, _, physicalID, err := bot.device(deviceID)
+10 -2
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { WindowConsoleRegular, WarningRegular } from "@fluentui/react-icons";
import { api } from "../../api";
import { Button, Input, Select } from "../ui";
import { Button, Input, Select, Switch } from "../ui";
import { AT_COMMAND_GROUPS } from "./atCommands";
import { AtLogEntry, AtTypingBubble, type AtLogItem } from "./AtLogEntry";
import { useI18n } from "../../lib/i18n";
@@ -19,6 +19,7 @@ export function DeviceAtTab({ deviceId, backendMode, atPort, running }: DeviceAt
const [template, setTemplate] = useState("");
const [timeoutMs, setTimeoutMs] = useState<number>(10000);
const [sending, setSending] = useState(false);
const [force, setForce] = useState(false);
const [log, setLog] = useState<AtLogItem[]>([]);
const hasAtPort = String(atPort || "").trim().length > 0;
@@ -40,7 +41,7 @@ export function DeviceAtTab({ deviceId, backendMode, atPort, running }: DeviceAt
try {
const res = await api<{ ok?: boolean; response?: string; result?: string }>(`/devices/${deviceId}/actions/at`, {
method: "POST",
body: { cmd: command, timeoutMs: timeoutMs || 10000 },
body: { cmd: command, timeoutMs: timeoutMs || 10000, force },
});
setLog((prev) => [
...prev,
@@ -120,6 +121,13 @@ export function DeviceAtTab({ deviceId, backendMode, atPort, running }: DeviceAt
</div>
</div>
</div>
<div className="mt-3 flex items-center justify-end gap-3">
<div className="flex items-center gap-2 text-sm text-orange-600 dark:text-orange-400">
<WarningRegular className="text-base" />
<span>{t("强制模式允许发送默认被拦截的 AT 指令(如切网、拨号、短信、USSD),误操作可能导致断网或费用扣除。")}</span>
</div>
<Switch checked={force} onChange={setForce} ariaLabel={t("强制发送 AT 指令")} />
</div>
</>
) : (
<div className="mt-4 flex flex-col items-center justify-center rounded-xl border border-orange-100 bg-orange-50 p-8 dark:border-orange-900/50 dark:bg-orange-900/20">
+2
View File
@@ -685,6 +685,8 @@ export const EN_DICT: Record<string, string> = {
"AT 终端暂不可用": "AT terminal unavailable",
"AT=串口 / QMI=纯 QMI": "AT=serial / QMI=pure QMI",
"AT=传统串口 / QMI=纯 QMI": "AT=legacy serial / QMI=pure QMI",
"强制发送 AT 指令": "Force-send AT command",
"强制模式允许发送默认被拦截的 AT 指令(如切网、拨号、短信、USSD),误操作可能导致断网或费用扣除。": "Force mode allows sending AT commands that are normally blocked (e.g. mode switching, dialing, SMS, USSD). Mistakes may disconnect the network or incur charges.",
"E911地址": "E911 Address",
"E911地址设置页面打开失败": "Failed to open the E911 address setup page",
"IMEI 绑定": "IMEI Binding",