104 lines
2.9 KiB
Go
104 lines
2.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ImportResult 是批量导入的汇总结果。
|
|
type ImportResult struct {
|
|
Created []ProxyView `json:"created"`
|
|
Failed []ImportFail `json:"failed"`
|
|
}
|
|
|
|
// ImportFail 记录解析或入库失败的行;Line 已脱敏,凭据段以 *** 代替。
|
|
type ImportFail struct {
|
|
LineNo int `json:"lineNo"`
|
|
Line string `json:"line"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// Import 逐行解析并创建代理;空行与 # 注释跳过,失败行不阻断后续。
|
|
func (s *ProxyService) Import(ctx context.Context, text string) (ImportResult, error) {
|
|
res := ImportResult{Created: []ProxyView{}, Failed: []ImportFail{}}
|
|
for i, raw := range strings.Split(text, "\n") {
|
|
line := strings.TrimSpace(raw)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
in, err := parseProxyLine(line)
|
|
if err == nil {
|
|
var view ProxyView
|
|
if view, err = s.Create(ctx, in); err == nil {
|
|
res.Created = append(res.Created, view)
|
|
continue
|
|
}
|
|
}
|
|
res.Failed = append(res.Failed, ImportFail{
|
|
LineNo: i + 1, Line: maskProxyLine(line), Reason: err.Error(),
|
|
})
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// parseProxyLine 解析一行代理,支持两种格式:
|
|
//
|
|
// scheme://[user:pass@]host:port(scheme 为 socks5 / http / https)
|
|
// host:port[:user:pass](缺省按 socks5)
|
|
func parseProxyLine(line string) (ProxyInput, error) {
|
|
if strings.Contains(line, "://") {
|
|
return parseProxyURI(line)
|
|
}
|
|
parts := strings.Split(line, ":")
|
|
if len(parts) != 2 && len(parts) != 4 {
|
|
return ProxyInput{}, fmt.Errorf("格式应为 host:port 或 host:port:user:pass: %w", ErrProxyInvalid)
|
|
}
|
|
port, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return ProxyInput{}, fmt.Errorf("端口非数字: %w", ErrProxyInvalid)
|
|
}
|
|
in := ProxyInput{Type: "socks5", Host: parts[0], Port: port}
|
|
if len(parts) == 4 {
|
|
in.Username = parts[2]
|
|
in.Password = &parts[3]
|
|
}
|
|
return in, nil
|
|
}
|
|
|
|
// parseProxyURI 解析 scheme://[user:pass@]host:port。
|
|
func parseProxyURI(line string) (ProxyInput, error) {
|
|
u, err := url.Parse(line)
|
|
if err != nil {
|
|
return ProxyInput{}, fmt.Errorf("URI 解析失败: %w", ErrProxyInvalid)
|
|
}
|
|
port, err := strconv.Atoi(u.Port())
|
|
if err != nil {
|
|
return ProxyInput{}, fmt.Errorf("缺少或非法端口: %w", ErrProxyInvalid)
|
|
}
|
|
in := ProxyInput{Type: u.Scheme, Host: u.Hostname(), Port: port}
|
|
if u.User != nil {
|
|
in.Username = u.User.Username()
|
|
if pw, ok := u.User.Password(); ok {
|
|
in.Password = &pw
|
|
}
|
|
}
|
|
return in, nil
|
|
}
|
|
|
|
// maskProxyLine 把行内可能的凭据段替换为 ***,避免密码回显到响应。
|
|
func maskProxyLine(line string) string {
|
|
if i := strings.Index(line, "://"); i >= 0 {
|
|
if j := strings.LastIndex(line, "@"); j > i {
|
|
return line[:i+3] + "***@" + line[j+1:]
|
|
}
|
|
return line
|
|
}
|
|
if parts := strings.Split(line, ":"); len(parts) > 2 {
|
|
return strings.Join(parts[:2], ":") + ":***"
|
|
}
|
|
return line
|
|
}
|