Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions pkg/line/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,16 +331,28 @@ func (c *Client) waitForLoginLF1(verifier string) (*LoginResult, error) {
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read LF1 polling response: %w", err)
}
if resp.StatusCode != http.StatusOK {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪️ Nit · New LF1 polling failure handling has no tests

Three new rejection paths (non-200, missing code, nonzero code) plus the reason whitelist have no tests. The whitelist is security-relevant, and its output must stay parseable by parseLoginErrorDetails so wrapLineLoginError keeps classifying errors correctly.

Found by Indent Review Agent

return nil, loginPollingFailure(resp.StatusCode, body)
}

var wrapper struct {
Code int `json:"code"`
Code *int `json:"code"`
Message string `json:"message"`
Data LoginPollingResult `json:"data"`
}
if err := json.Unmarshal(body, &wrapper); err != nil {
return nil, fmt.Errorf("failed to parse LF1 polling response: %w", err)
}
if wrapper.Code == nil {
return nil, errors.New("LF1 polling returned an invalid response without a success code")
}
if *wrapper.Code != 0 {
return nil, loginPollingFailure(resp.StatusCode, body)
}

meta := wrapper.Data.Result.Metadata

Expand Down
45 changes: 45 additions & 0 deletions pkg/line/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
)
Expand Down Expand Up @@ -256,6 +257,50 @@ type talkExceptionData struct {
Reason string `json:"reason"`
}

func loginPollingFailure(httpStatus int, body []byte) error {
var response struct {
Code *int `json:"code"`
Message string `json:"message"`
Data talkExceptionData `json:"data"`
}
decodeErr := json.Unmarshal(body, &response)
details := "LINE response without a valid code"
if decodeErr == nil && response.Code != nil {
details = fmt.Sprintf("LINE response code %d", *response.Code)
}
if decodeErr == nil && response.Code != nil && *response.Code == 10051 && strings.EqualFold(response.Message, "RESPONSE_ERROR") && strings.EqualFold(response.Data.Name, "TalkException") {
response.Message = "RESPONSE_ERROR"
response.Data.Name = "TalkException"
response.Data.Message = safeLoginPollingReason(response.Data.Message)
response.Data.Reason = safeLoginPollingReason(response.Data.Reason)
if sanitized, err := json.Marshal(response); err == nil {
details = string(sanitized)
}
}
if httpStatus != http.StatusOK {
return fmt.Errorf("LF1 polling failed: API error %d: %s", httpStatus, details)
}
return fmt.Errorf("LF1 polling failed: %s", details)
}

func safeLoginPollingReason(reason string) string {
// Arbitrary provider text can echo secrets, so retain only fixed rejection messages.
switch strings.ToLower(strings.TrimSpace(reason)) {
case "authentication failed":
return "authentication failed"
case "failed to issue v3 token":
return "Failed to issue V3 token"
case "blocked user":
return "blocked user"
case "account id or password is invalid":
return "Account ID or password is invalid"
case "too many login attempts":
return "Too many login attempts"
default:
return ""
}
}

// IsE2EEGroupMemberMismatch identifies a rejected registration that needs a
// fresh server member/key snapshot, not a plaintext fallback.
func IsE2EEGroupMemberMismatch(err error) bool {
Expand Down
Loading