mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +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).
84 lines
3.1 KiB
Go
84 lines
3.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package transport
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Shared returns the base http.RoundTripper for all CLI HTTP clients.
|
|
//
|
|
// Precedence (highest first):
|
|
// 1. proxy-plugin mode — force traffic through a fixed loopback proxy;
|
|
// FAIL-CLOSED when the plugin config exists but is invalid.
|
|
// 2. LARK_CLI_NO_PROXY — direct egress, proxy disabled.
|
|
// 3. http.DefaultTransport — the stdlib process-wide singleton (honors
|
|
// HTTP(S)_PROXY), so every client shares one connection pool / TLS cache.
|
|
//
|
|
// The returned RoundTripper MUST NOT be mutated. Callers that need a customized
|
|
// transport should assert to *http.Transport and Clone() it. A shared base is
|
|
// required so persistConn read/write goroutines are reused; cloning per call
|
|
// leaks them until IdleConnTimeout (~90s) fires.
|
|
func Shared() http.RoundTripper {
|
|
// Proxy-plugin mode overrides everything, INCLUDING LARK_CLI_NO_PROXY. When
|
|
// the plugin config exists but is invalid, pluginTransport returns a
|
|
// fail-closed transport with ok=true and we return it here — we MUST NOT
|
|
// fall through to the NO_PROXY / DefaultTransport direct-egress paths below.
|
|
if t, ok := pluginTransport(); ok {
|
|
return t
|
|
}
|
|
if os.Getenv(EnvNoProxy) != "" {
|
|
return noProxyTransport()
|
|
}
|
|
return http.DefaultTransport
|
|
}
|
|
|
|
// Fallback returns a shared *http.Transport. It is a thin wrapper over Shared
|
|
// retained so modules already on the leak-free singleton path (internal/auth,
|
|
// internal/cmdutil transport decorators) do not have to migrate. New code
|
|
// should prefer Shared and treat the base as an http.RoundTripper.
|
|
//
|
|
// Fail-closed invariant: pluginTransport always expresses its blocked transport
|
|
// as a concrete *http.Transport (see failClosedTransport), so the assertion
|
|
// below preserves the block. The noProxyTransport() fallback is therefore only
|
|
// reached when no proxy plugin is configured and some external code replaced
|
|
// http.DefaultTransport with a non-*http.Transport — a case with no fail-closed
|
|
// intent, where a proxy-disabled transport is acceptable.
|
|
func Fallback() *http.Transport {
|
|
if t, ok := Shared().(*http.Transport); ok {
|
|
return t
|
|
}
|
|
return noProxyTransport()
|
|
}
|
|
|
|
// NewHTTPClient returns an *http.Client whose Transport is the shared,
|
|
// proxy-plugin-aware base (see Shared). Prefer this over a bare &http.Client{}
|
|
// for outbound requests: a bare client falls back to http.DefaultTransport and
|
|
// therefore silently bypasses proxy plugin mode (fixed proxy + trusted CA, or
|
|
// fail-closed), creating an audit blind spot.
|
|
//
|
|
// A zero timeout means no client-level timeout (callers relying on context
|
|
// deadlines pass 0).
|
|
func NewHTTPClient(timeout time.Duration) *http.Client {
|
|
return &http.Client{
|
|
Transport: Shared(),
|
|
Timeout: timeout,
|
|
}
|
|
}
|
|
|
|
// noProxyTransport is a proxy-disabled clone of http.DefaultTransport, lazily
|
|
// built the first time LARK_CLI_NO_PROXY is observed set.
|
|
var noProxyTransport = sync.OnceValue(func() *http.Transport {
|
|
def, ok := http.DefaultTransport.(*http.Transport)
|
|
if !ok {
|
|
return &http.Transport{}
|
|
}
|
|
t := def.Clone()
|
|
t.Proxy = nil
|
|
return t
|
|
})
|