mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 17:45:15 +08:00
165 lines
5.2 KiB
Go
165 lines
5.2 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package util
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/larksuite/cli/internal/secplugin"
|
|
)
|
|
|
|
// Proxy environment constants control shared transport proxy behavior.
|
|
const (
|
|
// EnvNoProxy disables automatic proxy support when set to any non-empty value.
|
|
EnvNoProxy = "LARK_CLI_NO_PROXY"
|
|
)
|
|
|
|
// proxyEnvKeys lists environment variables that Go's ProxyFromEnvironment reads.
|
|
var proxyEnvKeys = []string{
|
|
"HTTPS_PROXY", "https_proxy",
|
|
"HTTP_PROXY", "http_proxy",
|
|
"ALL_PROXY", "all_proxy",
|
|
}
|
|
|
|
// DetectProxyEnv returns the first proxy-related environment variable that is set,
|
|
// or empty strings if none are configured.
|
|
func DetectProxyEnv() (key, value string) {
|
|
for _, k := range proxyEnvKeys {
|
|
if v := os.Getenv(k); v != "" {
|
|
return k, v
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// proxyWarningOnce ensures proxy environment warnings are emitted at most once.
|
|
var proxyWarningOnce sync.Once
|
|
|
|
// redactProxyURL masks userinfo (username:password) in a proxy URL.
|
|
// Handles both scheme-prefixed ("http://user:pass@host") and bare ("user:pass@host") formats.
|
|
func redactProxyURL(raw string) string {
|
|
// Try standard url.Parse first (works when scheme is present)
|
|
u, err := url.Parse(raw)
|
|
if err == nil && u.User != nil {
|
|
return u.Scheme + "://***@" + u.Host + u.RequestURI()
|
|
}
|
|
|
|
// Fallback: handle bare URLs without scheme (e.g. "user:pass@proxy:8080")
|
|
if at := strings.LastIndex(raw, "@"); at > 0 {
|
|
return "***@" + raw[at+1:]
|
|
}
|
|
|
|
return raw
|
|
}
|
|
|
|
// WarnIfProxied prints a one-time warning to w when a proxy environment variable
|
|
// is detected and proxy is not disabled via LARK_CLI_NO_PROXY. Proxy credentials
|
|
// are redacted. Safe to call multiple times; only the first call prints.
|
|
func WarnIfProxied(w io.Writer) {
|
|
proxyWarningOnce.Do(func() {
|
|
if os.Getenv(EnvNoProxy) != "" {
|
|
return
|
|
}
|
|
key, val := DetectProxyEnv()
|
|
if key == "" {
|
|
return
|
|
}
|
|
fmt.Fprintf(w, "[lark-cli] [WARN] proxy detected: %s=%s — requests (including credentials) will transit through this proxy. Set %s=1 to disable proxy.\n",
|
|
key, redactProxyURL(val), EnvNoProxy)
|
|
})
|
|
}
|
|
|
|
// 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
|
|
})
|
|
|
|
// secProxyTransport is a fixed-proxy clone of http.DefaultTransport (with optional
|
|
// custom root CA), lazily built on first use when sec plugin mode is enabled.
|
|
var secProxyTransport = sync.OnceValue(func() *http.Transport {
|
|
def, ok := http.DefaultTransport.(*http.Transport)
|
|
if !ok {
|
|
return &http.Transport{}
|
|
}
|
|
|
|
cfg, err := secplugin.Load()
|
|
if err != nil || cfg == nil || !cfg.Enabled() {
|
|
return def
|
|
}
|
|
t, err := cfg.ApplyToTransport(def)
|
|
if err != nil {
|
|
// Fail closed: do not silently fall back to direct egress when the
|
|
// operator explicitly enabled SEC plugin mode.
|
|
blocked := def.Clone()
|
|
blocked.Proxy = func(*http.Request) (*url.URL, error) {
|
|
return nil, fmt.Errorf("sec plugin enabled but config is invalid: %v", err)
|
|
}
|
|
return blocked
|
|
}
|
|
return t
|
|
})
|
|
|
|
// SharedTransport returns the base http.RoundTripper for CLI HTTP clients.
|
|
//
|
|
// By default it returns http.DefaultTransport — the stdlib-provided
|
|
// process-wide singleton — so every HTTP client in the process shares one
|
|
// TCP connection pool, TLS session cache, and HTTP/2 state. When
|
|
// LARK_CLI_NO_PROXY is set it returns a separate proxy-disabled singleton
|
|
// clone; LARK_CLI_NO_PROXY is checked on every call, but the clone is built
|
|
// at most once.
|
|
//
|
|
// The returned RoundTripper MUST NOT be mutated. Callers that need a
|
|
// customized transport should assert to *http.Transport and Clone() it.
|
|
// Using a shared base is required so persistConn readLoop/writeLoop
|
|
// goroutines are reused; cloning per call leaks them until IdleConnTimeout
|
|
// (~90s) fires.
|
|
func SharedTransport() http.RoundTripper {
|
|
// SEC plugin mode overrides all other proxy behavior (env proxies and
|
|
// LARK_CLI_NO_PROXY), per operator intent.
|
|
if cfg, err := secplugin.Load(); err != nil {
|
|
// Fail closed: if the config file exists but is malformed/unreadable,
|
|
// do not silently fall back to direct egress.
|
|
def, ok := http.DefaultTransport.(*http.Transport)
|
|
if !ok {
|
|
return http.DefaultTransport
|
|
}
|
|
blocked := def.Clone()
|
|
blocked.Proxy = func(*http.Request) (*url.URL, error) {
|
|
return nil, fmt.Errorf("sec plugin config is invalid: %v", err)
|
|
}
|
|
return blocked
|
|
} else if cfg != nil && cfg.Enabled() {
|
|
return secProxyTransport()
|
|
}
|
|
if os.Getenv(EnvNoProxy) != "" {
|
|
return noProxyTransport()
|
|
}
|
|
return http.DefaultTransport
|
|
}
|
|
|
|
// FallbackTransport returns a shared *http.Transport singleton. It is a
|
|
// thin wrapper over SharedTransport retained so modules that were already
|
|
// on the leak-free singleton path (internal/auth, internal/cmdutil
|
|
// transport decorators) do not have to migrate. New code should prefer
|
|
// SharedTransport and treat the base as an http.RoundTripper.
|
|
func FallbackTransport() *http.Transport {
|
|
if t, ok := SharedTransport().(*http.Transport); ok {
|
|
return t
|
|
}
|
|
return noProxyTransport()
|
|
}
|