parse.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. // Package proxyurl 提供代理 URL 的统一验证(fail-fast,无效代理不回退直连)
  2. //
  3. // 所有需要解析代理 URL 的地方必须通过此包的 Parse 函数。
  4. // 直接使用 url.Parse 处理代理 URL 是被禁止的。
  5. // 这确保了 fail-fast 行为:无效代理配置在创建时立即失败,
  6. // 而不是在运行时静默回退到直连(产生 IP 关联风险)。
  7. package proxyurl
  8. import (
  9. "fmt"
  10. "net"
  11. "net/url"
  12. "regexp"
  13. "strings"
  14. )
  15. // allowedSchemes 代理协议白名单
  16. var allowedSchemes = map[string]bool{
  17. "http": true,
  18. "https": true,
  19. "socks5": true,
  20. "socks5h": true,
  21. }
  22. // Parse 解析并验证代理 URL。
  23. //
  24. // 语义:
  25. // - 空字符串 → ("", nil, nil),表示直连
  26. // - 非空且有效 → (trimmed, *url.URL, nil)
  27. // - 非空但无效 → ("", nil, error),fail-fast 不回退
  28. //
  29. // 验证规则:
  30. // - TrimSpace 后为空视为直连
  31. // - url.Parse 失败返回 error(不含原始 URL,防凭据泄露)
  32. // - Host 为空返回 error(用 Redacted() 脱敏)
  33. // - Scheme 必须为 http/https/socks5/socks5h
  34. // - socks5:// 自动升级为 socks5h://(确保 DNS 由代理端解析,防止 DNS 泄漏)
  35. func Parse(raw string) (trimmed string, parsed *url.URL, err error) {
  36. trimmed = strings.TrimSpace(raw)
  37. if trimmed == "" {
  38. return "", nil, nil
  39. }
  40. parsed, err = url.Parse(trimmed)
  41. if err != nil {
  42. // 不使用 %w 包装,避免 url.Parse 的底层错误消息泄漏原始 URL(可能含凭据)
  43. return "", nil, fmt.Errorf("invalid proxy URL: %v", err)
  44. }
  45. if parsed.Host == "" || parsed.Hostname() == "" {
  46. return "", nil, fmt.Errorf("proxy URL missing host: %s", parsed.Redacted())
  47. }
  48. scheme := strings.ToLower(parsed.Scheme)
  49. if !allowedSchemes[scheme] {
  50. return "", nil, fmt.Errorf("unsupported proxy scheme %q (allowed: http, https, socks5, socks5h)", scheme)
  51. }
  52. // 自动升级 socks5 → socks5h,确保 DNS 由代理端解析,防止 DNS 泄漏。
  53. // Go 的 golang.org/x/net/proxy 对 socks5:// 默认在客户端本地解析 DNS,
  54. // 仅 socks5h:// 才将域名发送给代理端做远程 DNS 解析。
  55. if scheme == "socks5" {
  56. parsed.Scheme = "socks5h"
  57. trimmed = parsed.String()
  58. }
  59. return trimmed, parsed, nil
  60. }
  61. // ParseFlexible 支持多格式代理输入(兼容前端 proxyParser 的所有格式):
  62. // - http://user:pass@host:port
  63. // - https://host:port
  64. // - socks5://host:port
  65. // - host:port:user:pass (geo.miyaip.app:8001:qsddxeswee:otemiuwmowemouu)
  66. // - user:pass@host:port
  67. // - user:pass:host:port
  68. // - host:port (无认证)
  69. // - [ipv6]:port:user:pass 等带括号 IPv6
  70. //
  71. // defaultScheme 为解析无显式 scheme 时的默认协议(http/https/socks5/socks5h),
  72. // 若为空或非法则默认 http。
  73. func ParseFlexible(raw string, defaultScheme string) (trimmed string, parsed *url.URL, err error) {
  74. trimmed = strings.TrimSpace(raw)
  75. if trimmed == "" {
  76. return "", nil, nil
  77. }
  78. // 显式 scheme 直接走严格校验
  79. if regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9+.-]*://`).MatchString(trimmed) {
  80. return Parse(trimmed)
  81. }
  82. ds := strings.ToLower(strings.TrimSpace(defaultScheme))
  83. if !allowedSchemes[ds] {
  84. ds = "http"
  85. }
  86. // 依次尝试无 scheme 的多种常见格式
  87. if p := tryFlexibleParse(trimmed, ds); p != "" {
  88. return Parse(p)
  89. }
  90. return "", nil, fmt.Errorf("invalid proxy format %q (allowed: http, https, socks5, socks5h + host:port:user:pass etc.)", redactedRaw(raw))
  91. }
  92. func tryFlexibleParse(raw string, defaultScheme string) string {
  93. // 1) user:pass@host:port (last @)
  94. if idx := strings.LastIndex(raw, "@"); idx > 0 {
  95. creds := raw[:idx]
  96. hostPort := raw[idx+1:]
  97. if h, p := parseHostPortFlexible(hostPort); h != "" && p != "" {
  98. if u, pw := splitCreds(creds); u != "" || pw != "" {
  99. return buildProxyURL(defaultScheme, h, p, u, pw)
  100. }
  101. }
  102. }
  103. // 2) host:port:user:pass (port 为第2字段,password 可含冒号)
  104. if m := regexp.MustCompile(`^(\[[0-9a-fA-F:.]+\]|[^:@\s]+):(\d+):([^:@]*):(.*)$`).FindStringSubmatch(raw); m != nil {
  105. host := normalizeHostFlexible(m[1])
  106. port := m[2]
  107. u := strings.TrimSpace(m[3])
  108. pw := strings.TrimSpace(m[4])
  109. if host != "" && validPort(port) && (u != "" || pw != "") && !strings.Contains(u, "@") && !strings.Contains(pw, "@") {
  110. return buildProxyURL(defaultScheme, host, port, u, pw)
  111. }
  112. }
  113. // 3) user:pass:host:port (port 为最后字段)
  114. if m := regexp.MustCompile(`^([^:@]*):([^:@]*):(\[[0-9a-fA-F:.]+\]|[^:@\s]+):(\d+)$`).FindStringSubmatch(raw); m != nil {
  115. u := strings.TrimSpace(m[1])
  116. pw := strings.TrimSpace(m[2])
  117. host := normalizeHostFlexible(m[3])
  118. port := m[4]
  119. if host != "" && validPort(port) && (u != "" || pw != "") {
  120. return buildProxyURL(defaultScheme, host, port, u, pw)
  121. }
  122. }
  123. // 4) host:port 无认证
  124. if h, p := parseHostPortFlexible(raw); h != "" && p != "" {
  125. return buildProxyURL(defaultScheme, h, p, "", "")
  126. }
  127. return ""
  128. }
  129. func parseHostPortFlexible(s string) (host, port string) {
  130. s = strings.TrimSpace(s)
  131. if s == "" {
  132. return "", ""
  133. }
  134. if strings.HasPrefix(s, "[") {
  135. ci := strings.Index(s, "]")
  136. if ci < 0 || ci+1 >= len(s) || s[ci+1] != ':' {
  137. return "", ""
  138. }
  139. host = normalizeHostFlexible(s[:ci+1])
  140. port = strings.TrimSpace(s[ci+2:])
  141. if !validPort(port) {
  142. return "", ""
  143. }
  144. return host, port
  145. }
  146. li := strings.LastIndex(s, ":")
  147. if li <= 0 {
  148. return "", ""
  149. }
  150. host = normalizeHostFlexible(s[:li])
  151. port = strings.TrimSpace(s[li+1:])
  152. if host == "" || !validPort(port) {
  153. return "", ""
  154. }
  155. return host, port
  156. }
  157. func normalizeHostFlexible(v string) string {
  158. h := strings.TrimSpace(v)
  159. if h == "" || strings.Contains(h, " ") || strings.Contains(h, "@") {
  160. return ""
  161. }
  162. if strings.HasPrefix(h, "[") && strings.HasSuffix(h, "]") {
  163. inner := h[1 : len(h)-1]
  164. if inner == "" || strings.Contains(inner, " ") {
  165. return ""
  166. }
  167. // 允许 IPv6 字符
  168. if !regexp.MustCompile(`^[0-9a-fA-F:.]+$`).MatchString(inner) {
  169. return ""
  170. }
  171. return inner
  172. }
  173. if strings.Contains(h, ":") {
  174. return ""
  175. }
  176. return h
  177. }
  178. func validPort(p string) bool {
  179. if p == "" {
  180. return false
  181. }
  182. for _, c := range p {
  183. if c < '0' || c > '9' {
  184. return false
  185. }
  186. }
  187. // 1-65535
  188. n := 0
  189. for _, c := range p {
  190. n = n*10 + int(c-'0')
  191. if n > 65535 {
  192. return false
  193. }
  194. }
  195. return n >= 1 && n <= 65535
  196. }
  197. func splitCreds(s string) (user, pass string) {
  198. idx := strings.Index(s, ":")
  199. if idx < 0 {
  200. u := strings.TrimSpace(s)
  201. if u == "" || strings.Contains(u, "@") {
  202. return "", ""
  203. }
  204. return u, ""
  205. }
  206. u := strings.TrimSpace(s[:idx])
  207. pw := strings.TrimSpace(s[idx+1:])
  208. if !strings.Contains(u, "@") && !strings.Contains(pw, "@") && (u != "" || pw != "") {
  209. return u, pw
  210. }
  211. return "", ""
  212. }
  213. func buildProxyURL(scheme, host, port, user, pass string) string {
  214. u := &url.URL{Scheme: scheme, Host: net.JoinHostPort(host, port)}
  215. switch {
  216. case user != "" && pass != "":
  217. u.User = url.UserPassword(user, pass)
  218. case user != "":
  219. u.User = url.User(user)
  220. case pass != "":
  221. u.User = url.UserPassword("", pass)
  222. }
  223. return u.String()
  224. }
  225. func redactedRaw(raw string) string {
  226. // 脱敏:只保留长度和前后字符
  227. r := strings.TrimSpace(raw)
  228. if len(r) <= 8 {
  229. return "***"
  230. }
  231. return r[:3] + "***" + r[len(r)-3:]
  232. }