From 74486ebf082ad787ae4246b2950a369d52f1d198 Mon Sep 17 00:00:00 2001 From: m1saka Date: Mon, 20 Jul 2026 11:20:39 +0800 Subject: [PATCH] fix: harden custom QR login output --- README.md | 48 ++++++++++++++++++++++++++++++++++++------------ client.go | 11 ++++++++++- client_test.go | 20 ++++++++++++++++++++ 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f4ebc33..3bdfc80 100644 --- a/README.md +++ b/README.md @@ -41,24 +41,48 @@ func main() { `auth.json` 包含 `serviceToken`、`passToken`、`ssecurity` 等敏感认证数据。库写入该文件时使用 `0600` 权限;请保持此权限,并且不要将该文件提交到版本库。 -Web/GUI 应用可通过 `WithQRWriter` 接收登录输出,再将其展示给用户: +Web/GUI 应用可通过 `WithQRWriter` 接收登录输出,并在阻塞的 `Login` 等待期间实时展示给用户: ```go -var loginOutput bytes.Buffer -client, err := mijia.NewClient("", mijia.WithQRWriter(&loginOutput)) -if err != nil { - return err -} +import ( + "bufio" + "context" + "io" -if _, err := client.Login(ctx); err != nil { - return err -} + mijia "git.misaka.ren/m1saka/mijia-go-api" +) -// loginOutput 包含二维码 URL 和终端二维码文本,可交给 Web/GUI 展示层。 -renderLoginOutput(loginOutput.String()) +func streamLogin(ctx context.Context, renderLoginLine func(string)) error { + reader, writer := io.Pipe() + defer reader.Close() + + client, err := mijia.NewClient("", mijia.WithQRWriter(writer)) + if err != nil { + writer.Close() + return err + } + + loginDone := make(chan error, 1) + go func() { + _, err := client.Login(ctx) + writer.CloseWithError(err) + loginDone <- err + }() + + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + renderLoginLine(scanner.Text()) + } + + loginErr := <-loginDone + if loginErr != nil { + return loginErr + } + return scanner.Err() +} ``` -Writer 接收的内容包含登录二维码 URL,属于敏感登录信息;调用方不得将其写入日志、监控事件或其他持久化记录。 +`Login` 会阻塞等待扫码,因此必须同步消费 Writer 输出。Writer 可能由另一个 goroutine 写入,不能无同步地并发读写 `bytes.Buffer`。Writer 接收的内容包含登录二维码 URL,属于敏感登录信息;调用方不得将其写入日志、监控事件或其他持久化记录。 ## 底层 API diff --git a/client.go b/client.go index d7b9465..2b9ded5 100644 --- a/client.go +++ b/client.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "path/filepath" + "reflect" "strconv" "strings" "sync" @@ -39,7 +40,15 @@ func WithHTTPClient(httpClient *http.Client) Option { // WithQRWriter configures where QR login output is written. func WithQRWriter(writer io.Writer) Option { return func(client *Client) error { - if writer == nil { + isNil := writer == nil + if !isNil { + value := reflect.ValueOf(writer) + switch value.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + isNil = value.IsNil() + } + } + if isNil { return fmt.Errorf("QR writer must not be nil") } client.qrWriter = writer diff --git a/client_test.go b/client_test.go index 1c8c6e0..e326d3e 100644 --- a/client_test.go +++ b/client_test.go @@ -37,6 +37,26 @@ func TestWithQRWriterRejectsNil(t *testing.T) { } } +func TestWithQRWriterRejectsTypedNil(t *testing.T) { + var output *bytes.Buffer + _, err := NewClient(t.TempDir(), WithQRWriter(output)) + if err == nil || err.Error() != "QR writer must not be nil" { + t.Fatalf("error = %v, want QR writer must not be nil", err) + } +} + +func TestWithQRWriterAcceptsStructWriter(t *testing.T) { + if _, err := NewClient(t.TempDir(), WithQRWriter(structWriter{})); err != nil { + t.Fatal(err) + } +} + +type structWriter struct{} + +func (structWriter) Write(data []byte) (int, error) { + return len(data), nil +} + func TestDefaultQRWriterIsStdout(t *testing.T) { client, err := NewClient(t.TempDir()) if err != nil {