mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 00:55:53 +08:00
internal/util imported internal/proxyplugin (SharedTransport, FallbackTransport, NewHTTPClient, and WarnIfProxied via proxyPluginStatus), so a foundational util package depended up into a feature package, pulling binding/core/vfs into the transitive cone of every util importer. Move internal/proxyplugin -> internal/transport and make it the single owner of outbound transport: fold the two SharedTransport functions into one Shared() (proxy-plugin override -> LARK_CLI_NO_PROXY -> http.DefaultTransport), and move Fallback/NewHTTPClient/WarnIfProxied/DetectProxyEnv/noProxyTransport out of the now-deleted internal/util/proxy.go into the new package. The proxy-plugin probe is demoted to a private pluginTransport(); the duplicate redactProxyURL collapses to one. internal/util keeps no proxy code and is a leaf again. Re-point all consumers (registry, doctor, config, auth, cmdutil, update) to internal/transport. Behavior-preserving: package move + symbol rename + dedup. Two new tests lock the fail-closed contract (plugin overrides NO_PROXY; malformed config never falls through to direct egress).
239 lines
6.4 KiB
Go
239 lines
6.4 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/errclass"
|
|
"github.com/larksuite/cli/internal/transport"
|
|
)
|
|
|
|
// SecurityPolicyTransport is an http.RoundTripper that intercepts all responses
|
|
// and checks for security policy errors.
|
|
type SecurityPolicyTransport struct {
|
|
Base http.RoundTripper
|
|
}
|
|
|
|
// base returns the underlying RoundTripper or http.DefaultTransport if nil.
|
|
func (t *SecurityPolicyTransport) base() http.RoundTripper {
|
|
if t.Base != nil {
|
|
return t.Base
|
|
}
|
|
return transport.Fallback()
|
|
}
|
|
|
|
// RoundTrip implements http.RoundTripper.
|
|
func (t *SecurityPolicyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
resp, err := t.base().RoundTrip(req)
|
|
if err != nil {
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
return nil, err
|
|
}
|
|
if resp == nil || resp.Body == nil {
|
|
return resp, nil
|
|
}
|
|
|
|
// Only process JSON responses to avoid memory spikes on large files
|
|
contentType := strings.ToLower(resp.Header.Get("Content-Type"))
|
|
if !strings.Contains(contentType, "application/json") {
|
|
return resp, nil
|
|
}
|
|
|
|
// Read up to 64KB of the body to check for security policy errors
|
|
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
|
if err != nil {
|
|
resp.Body.Close()
|
|
return nil, fmt.Errorf("failed to read response body in security transport: %w", err)
|
|
}
|
|
|
|
// Restore the body so it can be read by the caller, preserving streaming capability
|
|
resp.Body = struct {
|
|
io.Reader
|
|
io.Closer
|
|
}{
|
|
io.MultiReader(bytes.NewReader(bodyBytes), resp.Body),
|
|
resp.Body,
|
|
}
|
|
|
|
// Try to parse it as JSON
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &result); err != nil {
|
|
return resp, nil
|
|
}
|
|
|
|
// 1. Try to handle as MCP (JSON-RPC) format first
|
|
if err := t.tryHandleMCPResponse(result); err != nil {
|
|
resp.Body.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// 2. Try to handle as OpenAPI error format
|
|
if err := t.tryHandleOAPIResponse(result); err != nil {
|
|
resp.Body.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// tryHandleMCPResponse attempts to parse a JSON-RPC (MCP) formatted error
|
|
// response coming back from a remote server (this transport is installed on
|
|
// lark-cli's outbound HTTP client; the bodies it inspects are produced by the
|
|
// remote, not by lark-cli itself).
|
|
//
|
|
// Observed production shape from the MCP gateway — Lark code in the outer
|
|
// `error.code` slot, hint under `data.cli_hint`:
|
|
//
|
|
// {"jsonrpc": "2.0", "id": 1,
|
|
// "error": {"code": 21000, "message": "...",
|
|
// "data": {"challenge_url": "...", "cli_hint": "..."}}}
|
|
//
|
|
// The parser also accepts a JSON-RPC-canonical shape (outer `error.code`
|
|
// carrying the JSON-RPC status like -32603, Lark code under `error.data.code`,
|
|
// hint under `data.hint`) so a future server-side migration to that layout
|
|
// would not silently drop policy detection. The Lark code is looked up in the
|
|
// central code registry; the hint key is read from `data.hint` first and
|
|
// falls back to `data.cli_hint`.
|
|
func (t *SecurityPolicyTransport) tryHandleMCPResponse(result map[string]interface{}) error {
|
|
errMap, ok := result["error"].(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
dataMap, _ := errMap["data"].(map[string]interface{})
|
|
|
|
// Try data.code first (shape B); fall back to outer error.code (shape A).
|
|
code := 0
|
|
if dataMap != nil {
|
|
code = getInt(dataMap, "code", 0)
|
|
}
|
|
if code == 0 {
|
|
code = getInt(errMap, "code", 0)
|
|
}
|
|
meta, ok := errclass.LookupCodeMeta(code)
|
|
if !ok || meta.Category != errs.CategoryPolicy {
|
|
return nil
|
|
}
|
|
|
|
if dataMap == nil {
|
|
return nil
|
|
}
|
|
|
|
// Clean up backticks and spaces from challenge_url
|
|
challengeUrl := strings.Trim(getStr(dataMap, "challenge_url"), " `")
|
|
// Read `hint` first; fall back to `cli_hint` so either spelling surfaces.
|
|
cliHint := getStr(dataMap, "hint")
|
|
if cliHint == "" {
|
|
cliHint = getStr(dataMap, "cli_hint")
|
|
}
|
|
msg := getStr(errMap, "message")
|
|
|
|
if challengeUrl != "" || cliHint != "" {
|
|
// Security validation for challengeUrl
|
|
if challengeUrl != "" && !isValidChallengeURL(challengeUrl) {
|
|
challengeUrl = ""
|
|
}
|
|
|
|
if challengeUrl != "" || cliHint != "" {
|
|
return &errs.SecurityPolicyError{
|
|
Problem: errs.Problem{
|
|
Category: errs.CategoryPolicy,
|
|
Subtype: meta.Subtype,
|
|
Code: code,
|
|
Message: msg,
|
|
Hint: cliHint,
|
|
},
|
|
ChallengeURL: challengeUrl,
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// tryHandleOAPIResponse attempts to parse a standard Lark OpenAPI formatted error response.
|
|
func (t *SecurityPolicyTransport) tryHandleOAPIResponse(result map[string]interface{}) error {
|
|
// 1. Extract code
|
|
code := getInt(result, "code", 0)
|
|
|
|
// If code is 0, check if it's already in our error format {"error": {"code": 21000, ...}, "ok": false}
|
|
if code == 0 {
|
|
if errMap, ok := result["error"].(map[string]interface{}); ok {
|
|
code = getInt(errMap, "code", 0)
|
|
}
|
|
}
|
|
|
|
// 2. Check if it's a security policy error (consult central code registry)
|
|
meta, ok := errclass.LookupCodeMeta(code)
|
|
if !ok || meta.Category != errs.CategoryPolicy {
|
|
return nil
|
|
}
|
|
|
|
// 3. Extract details
|
|
var challengeUrl, cliHint, msg string
|
|
if dataMap, ok := result["data"].(map[string]interface{}); ok {
|
|
// Standard OAPI format
|
|
challengeUrl = getStr(dataMap, "challenge_url")
|
|
cliHint = getStr(dataMap, "cli_hint")
|
|
msg = getStr(result, "msg")
|
|
} else if errMap, ok := result["error"].(map[string]interface{}); ok {
|
|
// Already formatted error format (e.g. from internal API or CLI output)
|
|
challengeUrl = getStr(errMap, "challenge_url")
|
|
cliHint = getStr(errMap, "hint")
|
|
msg = getStr(errMap, "message")
|
|
}
|
|
|
|
// 4. Print and exit if we have enough info
|
|
if msg != "" || challengeUrl != "" || cliHint != "" {
|
|
// Security validation for challengeUrl
|
|
if challengeUrl != "" && !isValidChallengeURL(challengeUrl) {
|
|
challengeUrl = ""
|
|
}
|
|
|
|
if msg != "" || challengeUrl != "" || cliHint != "" {
|
|
return &errs.SecurityPolicyError{
|
|
Problem: errs.Problem{
|
|
Category: errs.CategoryPolicy,
|
|
Subtype: meta.Subtype,
|
|
Code: code,
|
|
Message: msg,
|
|
Hint: cliHint,
|
|
},
|
|
ChallengeURL: challengeUrl,
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isValidChallengeURL checks if the given URL is a valid challenge URL.
|
|
func isValidChallengeURL(rawURL string) bool {
|
|
if rawURL == "" {
|
|
return false
|
|
}
|
|
|
|
u, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// 1. Must be https
|
|
if u.Scheme != "https" {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|