This commit is contained in:
parent
11c0b14b12
commit
b183aef0c7
@ -39,16 +39,19 @@ func run(args []string) int {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
conn, err := adminreset.DialControl(ctx, adminreset.DefaultControlPath)
|
conn, err := adminreset.DialControl(ctx, adminreset.DefaultControlPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "超级管理员重置通道不可用")
|
return finishReset("超级管理员重置通道不可用", 1)
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
if err := adminreset.SendVerifiedReset(ctx, conn, adminreset.ReadVisibleInput, adminreset.ReadProtectedPassword); err != nil {
|
if err := adminreset.SendVerifiedResetWithInput(ctx, conn, adminreset.ReadVisibleInputContext, adminreset.ReadProtectedPassword); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "超级管理员重置失败:", safeError(err))
|
return finishReset("超级管理员重置失败:"+safeError(err), 1)
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
fmt.Println("超级管理员密码重置成功")
|
return finishReset("超级管理员密码重置成功,请使用新密码登录", 0)
|
||||||
return 0
|
}
|
||||||
|
|
||||||
|
func finishReset(message string, code int) int {
|
||||||
|
fmt.Fprintln(os.Stderr, message)
|
||||||
|
adminreset.ShowResult(message)
|
||||||
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDoctor() int {
|
func runDoctor() int {
|
||||||
@ -91,6 +94,9 @@ func safeError(err error) string {
|
|||||||
if errors.Is(err, adminreset.ErrVerificationFailed) {
|
if errors.Is(err, adminreset.ErrVerificationFailed) {
|
||||||
return "安全验证失败、已过期或请求过于频繁,请稍后重新启动重置"
|
return "安全验证失败、已过期或请求过于频繁,请稍后重新启动重置"
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, adminreset.ErrEmailDeliveryFailed) {
|
||||||
|
return "邮件验证码发送失败或请求过于频繁,请稍后重试或选择其他验证方式"
|
||||||
|
}
|
||||||
if errors.Is(err, adminreset.ErrExpiredSession) {
|
if errors.Is(err, adminreset.ErrExpiredSession) {
|
||||||
return "重置会话已过期,请重新启动重置"
|
return "重置会话已过期,请重新启动重置"
|
||||||
}
|
}
|
||||||
|
|||||||
36
backend/cmd/smail-admin/result_linux_test.go
Normal file
36
backend/cmd/smail-admin/result_linux_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.pcux.cn/xuans/Smail/services/adminreset"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 由隔离的 systemd 临时服务人工检查终端显示,不连接生产重置通道。
|
||||||
|
func TestSystemdResultPrompt(t *testing.T) {
|
||||||
|
if os.Getenv("SMAIL_TEST_RESULT_PROMPT") != "1" || os.Getenv("INVOCATION_ID") == "" {
|
||||||
|
t.Skip("需要隔离的 systemd 交互测试服务")
|
||||||
|
}
|
||||||
|
if code := finishReset("显示测试:重置结果提示正常", 0); code != 0 {
|
||||||
|
t.Fatalf("退出状态:%d", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSystemdCancelledInputResult(t *testing.T) {
|
||||||
|
if os.Getenv("SMAIL_TEST_RESULT_PROMPT") != "1" || os.Getenv("INVOCATION_ID") == "" {
|
||||||
|
t.Skip("需要隔离的 systemd 交互测试服务")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := adminreset.ReadVisibleInputContext(ctx, "取消测试:请等待自动结束"); err == nil {
|
||||||
|
t.Fatal("取消输入后未返回错误")
|
||||||
|
}
|
||||||
|
if code := finishReset("显示测试:发送失败后输入已取消", 1); code != 1 {
|
||||||
|
t.Fatalf("退出状态:%d", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
ErrVerificationUnavailable = errors.New("no verification method available")
|
ErrVerificationUnavailable = errors.New("no verification method available")
|
||||||
ErrVerificationFailed = errors.New("verification failed")
|
ErrVerificationFailed = errors.New("verification failed")
|
||||||
|
ErrEmailDeliveryFailed = errors.New("verification email delivery failed")
|
||||||
)
|
)
|
||||||
|
|
||||||
func responseError(code string) error {
|
func responseError(code string) error {
|
||||||
@ -22,6 +23,8 @@ func responseError(code string) error {
|
|||||||
return ErrVerificationUnavailable
|
return ErrVerificationUnavailable
|
||||||
case "VERIFICATION_FAILED":
|
case "VERIFICATION_FAILED":
|
||||||
return ErrVerificationFailed
|
return ErrVerificationFailed
|
||||||
|
case "EMAIL_DELIVERY_FAILED":
|
||||||
|
return ErrEmailDeliveryFailed
|
||||||
case "REPLAY_OR_EXPIRED":
|
case "REPLAY_OR_EXPIRED":
|
||||||
return ErrExpiredSession
|
return ErrExpiredSession
|
||||||
default:
|
default:
|
||||||
@ -30,6 +33,16 @@ func responseError(code string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SendVerifiedReset(ctx context.Context, conn net.Conn, input func(string) (string, error), readPassword func() (string, error)) error {
|
func SendVerifiedReset(ctx context.Context, conn net.Conn, input func(string) (string, error), readPassword func() (string, error)) error {
|
||||||
|
if input == nil {
|
||||||
|
return ErrInvalidRequest
|
||||||
|
}
|
||||||
|
return SendVerifiedResetWithInput(ctx, conn, func(_ context.Context, prompt string) (string, error) {
|
||||||
|
return input(prompt)
|
||||||
|
}, readPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendVerifiedResetWithInput 允许邮件发送与验证码输入并行,发送失败会取消输入。
|
||||||
|
func SendVerifiedResetWithInput(ctx context.Context, conn net.Conn, input func(context.Context, string) (string, error), readPassword func() (string, error)) error {
|
||||||
if conn == nil || input == nil || readPassword == nil {
|
if conn == nil || input == nil || readPassword == nil {
|
||||||
return ErrInvalidRequest
|
return ErrInvalidRequest
|
||||||
}
|
}
|
||||||
@ -57,7 +70,7 @@ func SendVerifiedReset(ctx context.Context, conn net.Conn, input func(string) (s
|
|||||||
fmt.Fprintf(&prompt, "%d. %s\n", i+1, method.Label)
|
fmt.Fprintf(&prompt, "%d. %s\n", i+1, method.Label)
|
||||||
}
|
}
|
||||||
prompt.WriteString("\n选项:")
|
prompt.WriteString("\n选项:")
|
||||||
choice, err := input(prompt.String())
|
choice, err := input(ctx, prompt.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -79,10 +92,17 @@ func SendVerifiedReset(ctx context.Context, conn net.Conn, input func(string) (s
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := exchange(Request{Action: "select", Method: challenge.Methods[index-1].ID}); err != nil {
|
method := challenge.Methods[index-1].ID
|
||||||
return err
|
request := Request{Version: ProtocolVersion, Nonce: challenge.Nonce, Action: "select", Method: method}
|
||||||
|
var code string
|
||||||
|
if strings.HasPrefix(method, "email:") {
|
||||||
|
code, err = selectEmailAndReadCode(ctx, conn, request, input)
|
||||||
|
} else {
|
||||||
|
if err = exchange(request); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
code, err = input(ctx, "请输入所选方式的验证码或安全码")
|
||||||
}
|
}
|
||||||
code, err := input("请输入所选方式的验证码或安全码")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -108,3 +128,36 @@ func SendVerifiedReset(ctx context.Context, conn net.Conn, input func(string) (s
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectEmailAndReadCode(ctx context.Context, conn net.Conn, request Request, input func(context.Context, string) (string, error)) (string, error) {
|
||||||
|
if err := json.NewEncoder(conn).Encode(request); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
inputContext, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
result := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
response, err := DecodeResponse(conn)
|
||||||
|
if err == nil && !response.OK {
|
||||||
|
err = responseError(response.Code)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
result <- err
|
||||||
|
}()
|
||||||
|
code, inputErr := input(inputContext, "正在发送邮件验证码,收到后输入\n请输入邮件验证码")
|
||||||
|
if inputErr != nil {
|
||||||
|
// 取消输入时关闭本次连接,同时解除等待投递结果的读取。
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
deliveryErr := <-result
|
||||||
|
if deliveryErr != nil || inputErr != nil {
|
||||||
|
clearPassword(&code)
|
||||||
|
if deliveryErr != nil {
|
||||||
|
return "", deliveryErr
|
||||||
|
}
|
||||||
|
return "", inputErr
|
||||||
|
}
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
|||||||
111
backend/services/adminreset/client_test.go
Normal file
111
backend/services/adminreset/client_test.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package adminreset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type delayedEmailFlow struct {
|
||||||
|
testResetFlow
|
||||||
|
start func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *delayedEmailFlow) Methods() []Method {
|
||||||
|
return []Method{{ID: "email:1", Label: "发送验证码到 root@example.com"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *delayedEmailFlow) Start(method string) error {
|
||||||
|
if method != "email:1" {
|
||||||
|
return ErrInvalidRequest
|
||||||
|
}
|
||||||
|
if err := f.start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.selected = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmailPromptDoesNotWaitForDelivery(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
inputShown := make(chan struct{})
|
||||||
|
reset := false
|
||||||
|
flow := &delayedEmailFlow{
|
||||||
|
testResetFlow: testResetFlow{reset: func(string) error { reset = true; return nil }},
|
||||||
|
start: func() error {
|
||||||
|
select {
|
||||||
|
case <-inputShown:
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
server, client := net.Pipe()
|
||||||
|
defer client.Close()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
defer server.Close()
|
||||||
|
handleResetConnection(server, server.SetDeadline, func() (ResetSession, error) { return flow, nil }, NewSessionGuard(time.Minute))
|
||||||
|
}()
|
||||||
|
steps := 0
|
||||||
|
err := SendVerifiedResetWithInput(ctx, client, func(_ context.Context, prompt string) (string, error) {
|
||||||
|
steps++
|
||||||
|
if steps == 1 {
|
||||||
|
return "1", nil
|
||||||
|
}
|
||||||
|
if !strings.Contains(prompt, "正在发送") || !strings.Contains(prompt, "请输入邮件验证码") {
|
||||||
|
t.Errorf("缺少邮件发送状态或输入提示:%q", prompt)
|
||||||
|
}
|
||||||
|
close(inputShown)
|
||||||
|
return "fixture-code", nil
|
||||||
|
}, func() (string, error) { return "NewPass123", nil })
|
||||||
|
<-done
|
||||||
|
if err != nil || !reset {
|
||||||
|
t.Fatalf("邮件输入与发送未正确衔接:reset=%v err=%v", reset, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmailDeliveryFailureCancelsInputAndNeverAsksPassword(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
inputShown := make(chan struct{})
|
||||||
|
flow := &delayedEmailFlow{start: func() error {
|
||||||
|
select {
|
||||||
|
case <-inputShown:
|
||||||
|
return errors.New("mail unavailable")
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
server, client := net.Pipe()
|
||||||
|
defer client.Close()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
defer server.Close()
|
||||||
|
handleResetConnection(server, server.SetDeadline, func() (ResetSession, error) { return flow, nil }, NewSessionGuard(time.Minute))
|
||||||
|
}()
|
||||||
|
steps := 0
|
||||||
|
err := SendVerifiedResetWithInput(ctx, client, func(inputContext context.Context, _ string) (string, error) {
|
||||||
|
steps++
|
||||||
|
if steps == 1 {
|
||||||
|
return "1", nil
|
||||||
|
}
|
||||||
|
close(inputShown)
|
||||||
|
<-inputContext.Done()
|
||||||
|
return "", inputContext.Err()
|
||||||
|
}, func() (string, error) {
|
||||||
|
t.Error("邮件发送失败后不应进入密码输入")
|
||||||
|
return "NewPass123", nil
|
||||||
|
})
|
||||||
|
<-done
|
||||||
|
if !errors.Is(err, ErrEmailDeliveryFailed) || ctx.Err() != nil {
|
||||||
|
t.Fatalf("邮件发送失败未及时取消输入:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,10 +3,13 @@
|
|||||||
package adminreset
|
package adminreset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.pcux.cn/xuans/Smail/utils/password"
|
"git.pcux.cn/xuans/Smail/utils/password"
|
||||||
)
|
)
|
||||||
@ -42,14 +45,31 @@ func askPassword(prompt string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReadVisibleInput(prompt string) (string, error) {
|
func ReadVisibleInput(prompt string) (string, error) {
|
||||||
return readSystemdInput(prompt, "yes")
|
return ReadVisibleInputContext(context.Background(), prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadVisibleInputContext(ctx context.Context, prompt string) (string, error) {
|
||||||
|
return readSystemdInputContext(ctx, prompt, "yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowResult 使用当前 systemctl 的密码代理显示结果,避免提示只进入系统日志。
|
||||||
|
func ShowResult(message string) {
|
||||||
|
_, _ = readSystemdInputContext(context.Background(), message+"\n按回车结束", "yes", "--timeout=30")
|
||||||
}
|
}
|
||||||
|
|
||||||
func readSystemdInput(prompt, echo string) (string, error) {
|
func readSystemdInput(prompt, echo string) (string, error) {
|
||||||
|
return readSystemdInputContext(context.Background(), prompt, echo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSystemdInputContext(ctx context.Context, prompt, echo string, options ...string) (string, error) {
|
||||||
prompt = strings.TrimRight(prompt, ":: ") + ":"
|
prompt = strings.TrimRight(prompt, ":: ") + ":"
|
||||||
// systemd 代理把提示保存在单行配置中,用终端换行控制显示列表,避免配置被拆行。
|
// systemd 代理把提示保存在单行配置中,用终端换行控制显示列表,避免配置被拆行。
|
||||||
prompt = strings.ReplaceAll(strings.ReplaceAll(prompt, "\r\n", "\n"), "\n", "\x1bE")
|
prompt = strings.ReplaceAll(strings.ReplaceAll(prompt, "\r\n", "\n"), "\n", "\x1bE")
|
||||||
command := exec.Command("systemd-ask-password", "--no-tty", "--echo="+echo, prompt)
|
args := append([]string{"--no-tty", "--echo=" + echo}, options...)
|
||||||
|
command := exec.CommandContext(ctx, "systemd-ask-password", append(args, prompt)...)
|
||||||
|
// 先允许代理清理请求文件,避免取消后继续弹出已经失效的输入提示。
|
||||||
|
command.Cancel = func() error { return command.Process.Signal(syscall.SIGTERM) }
|
||||||
|
command.WaitDelay = time.Second
|
||||||
command.Stderr = io.Discard
|
command.Stderr = io.Discard
|
||||||
output, err := command.Output()
|
output, err := command.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -2,5 +2,11 @@
|
|||||||
|
|
||||||
package adminreset
|
package adminreset
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
func ReadProtectedPassword() (string, error) { return "", ErrUnsupported }
|
func ReadProtectedPassword() (string, error) { return "", ErrUnsupported }
|
||||||
func ReadVisibleInput(string) (string, error) { return "", ErrUnsupported }
|
func ReadVisibleInput(string) (string, error) { return "", ErrUnsupported }
|
||||||
|
func ReadVisibleInputContext(context.Context, string) (string, error) {
|
||||||
|
return "", ErrUnsupported
|
||||||
|
}
|
||||||
|
func ShowResult(string) {}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ package adminreset
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@ -67,3 +68,27 @@ func ReadVisibleInput(prompt string) (string, error) {
|
|||||||
}
|
}
|
||||||
return strings.TrimSuffix(strings.TrimSuffix(value, "\n"), "\r"), nil
|
return strings.TrimSuffix(strings.TrimSuffix(value, "\n"), "\r"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReadVisibleInputContext(ctx context.Context, prompt string) (string, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
type result struct {
|
||||||
|
value string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
done := make(chan result, 1)
|
||||||
|
go func() {
|
||||||
|
value, err := ReadVisibleInput(prompt)
|
||||||
|
done <- result{value: value, err: err}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case value := <-done:
|
||||||
|
return value.value, value.err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return "", ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows 控制台直接显示标准输出,无需额外的密码代理确认。
|
||||||
|
func ShowResult(string) {}
|
||||||
|
|||||||
@ -276,6 +276,10 @@ func handleResetConnection(conn io.ReadWriter, setDeadline func(time.Time) error
|
|||||||
}
|
}
|
||||||
clearPassword(&request.Code)
|
clearPassword(&request.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if action == "select" && strings.HasPrefix(request.Method, "email:") {
|
||||||
|
_ = EncodeResponse(conn, Response{Code: "EMAIL_DELIVERY_FAILED", Message: "邮件验证码发送失败或请求过于频繁,请稍后重试或选择其他验证方式"})
|
||||||
|
return
|
||||||
|
}
|
||||||
_ = EncodeResponse(conn, Response{Code: "VERIFICATION_FAILED", Message: "安全验证失败、已过期或请求过于频繁,请稍后重试"})
|
_ = EncodeResponse(conn, Response{Code: "VERIFICATION_FAILED", Message: "安全验证失败、已过期或请求过于频繁,请稍后重试"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,7 +70,8 @@ func deliverMX(hosts []string, attempt func(host, port string) error) error {
|
|||||||
func deliverMXContext(ctx stdcontext.Context, hosts []string, attempt func(host, port string) error) error {
|
func deliverMXContext(ctx stdcontext.Context, hosts []string, attempt func(host, port string) error) error {
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
for _, port := range []string{"25", "587", "465"} {
|
// 标准投递端口不可达时优先尝试隐式 TLS,避免先等待不提供欢迎响应的提交端口。
|
||||||
|
for _, port := range []string{"25", "465", "587"} {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,3 +93,17 @@ func TestRetryable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeliverMXUsesTLSFallbackBeforeSubmissionPort(t *testing.T) {
|
||||||
|
var calls []string
|
||||||
|
err := deliverMX([]string{"mx.example.com"}, func(host, port string) error {
|
||||||
|
calls = append(calls, port)
|
||||||
|
if port == "465" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &net.OpError{Op: "dial", Err: io.EOF}
|
||||||
|
})
|
||||||
|
if err != nil || !reflect.DeepEqual(calls, []string{"25", "465"}) {
|
||||||
|
t.Fatalf("加密路由可用时不应等待提交端口:calls=%v err=%v", calls, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user