Files
Memoh/internal/channel/common/proxy.go
T
BBQ f376a2abe3 fix(channel): add wechatoa webhook delivery and proxy config (#356)
Unify webhook handling across channel adapters and add the WeChat Official Account channel so inbound routing and replies work without platform-specific handlers. Add adapter-scoped proxy support and stable config field ordering so restricted network environments can deliver WeChat and Telegram messages reliably.
2026-04-10 21:26:11 +08:00

103 lines
2.6 KiB
Go

// Package common provides shared utilities for channel adapters.
package common
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/memohai/memoh/internal/channel"
)
// HTTPProxyConfig holds adapter-scoped HTTP proxy settings.
// Explicit adapter config takes precedence over environment variables.
type HTTPProxyConfig struct {
URL string
}
// ParseHTTPProxyConfig reads proxy settings from a channel config map.
// It accepts both camelCase and snake_case keys.
func ParseHTTPProxyConfig(raw map[string]any) (HTTPProxyConfig, error) {
cfg := HTTPProxyConfig{
URL: strings.TrimSpace(channel.ReadString(raw, "httpProxyUrl", "http_proxy_url")),
}
if cfg.URL == "" {
return cfg, nil
}
parsed, err := parseProxyURL(cfg.URL)
if err != nil {
return HTTPProxyConfig{}, err
}
cfg.URL = parsed.String()
return cfg, nil
}
// NormalizeHTTPProxyConfig writes canonical proxy fields into the provided map.
func NormalizeHTTPProxyConfig(out map[string]any, cfg HTTPProxyConfig) {
if out == nil {
return
}
if cfg.URL != "" {
out["httpProxyUrl"] = cfg.URL
}
}
// CacheKey returns a stable cache key segment for proxy-aware client caches.
func (c HTTPProxyConfig) CacheKey() string {
return c.URL
}
// BuildProxyURL returns the proxy URL with optional explicit credentials applied.
func (c HTTPProxyConfig) BuildProxyURL() (*url.URL, error) {
if strings.TrimSpace(c.URL) == "" {
return nil, nil
}
return parseProxyURL(c.URL)
}
// NewHTTPClient constructs an HTTP client using explicit adapter proxy settings
// when present, otherwise falling back to ProxyFromEnvironment.
func NewHTTPClient(timeout time.Duration, proxyCfg HTTPProxyConfig) (*http.Client, error) {
if timeout <= 0 {
timeout = 30 * time.Second
}
transport := http.DefaultTransport.(*http.Transport).Clone()
if strings.TrimSpace(proxyCfg.URL) != "" {
proxyURL, err := proxyCfg.BuildProxyURL()
if err != nil {
return nil, err
}
transport.Proxy = http.ProxyURL(proxyURL)
} else {
transport.Proxy = http.ProxyFromEnvironment
}
return &http.Client{
Transport: transport,
Timeout: timeout,
}, nil
}
func parseProxyURL(raw string) (*url.URL, error) {
value := strings.TrimSpace(raw)
if value == "" {
return nil, nil
}
if !strings.Contains(value, "://") {
value = "http://" + value
}
parsed, err := url.Parse(value)
if err != nil {
return nil, fmt.Errorf("invalid http proxy url: %w", err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, fmt.Errorf("unsupported http proxy scheme: %s", parsed.Scheme)
}
if strings.TrimSpace(parsed.Host) == "" {
return nil, errors.New("http proxy host is required")
}
return parsed, nil
}