Merge pull request #5 from HexBen123/feat/http-socks5-upstream
feat: 支持 HTTP 和 SOCKS5 上游节点 URI
This commit is contained in:
@@ -349,6 +349,18 @@ func buildNodeOutbound(tag, rawURI string, skipCertVerify bool) (option.Outbound
|
||||
return option.Outbound{}, fmt.Errorf("parse uri: %w", err)
|
||||
}
|
||||
switch strings.ToLower(parsed.Scheme) {
|
||||
case "http":
|
||||
opts, err := buildHTTPOptions(parsed, skipCertVerify)
|
||||
if err != nil {
|
||||
return option.Outbound{}, err
|
||||
}
|
||||
return option.Outbound{Type: C.TypeHTTP, Tag: tag, Options: &opts}, nil
|
||||
case "socks5":
|
||||
opts, err := buildSOCKSOptions(parsed)
|
||||
if err != nil {
|
||||
return option.Outbound{}, err
|
||||
}
|
||||
return option.Outbound{Type: C.TypeSOCKS, Tag: tag, Options: &opts}, nil
|
||||
case "vless":
|
||||
opts, err := buildVLESSOptions(parsed, skipCertVerify)
|
||||
if err != nil {
|
||||
@@ -390,6 +402,64 @@ func buildNodeOutbound(tag, rawURI string, skipCertVerify bool) (option.Outbound
|
||||
}
|
||||
}
|
||||
|
||||
func buildHTTPOptions(u *url.URL, skipCertVerify bool) (option.HTTPOutboundOptions, error) {
|
||||
query := u.Query()
|
||||
defaultPort := 80
|
||||
if strings.EqualFold(query.Get("security"), "tls") {
|
||||
defaultPort = 443
|
||||
}
|
||||
server, port, err := hostPort(u, defaultPort)
|
||||
if err != nil {
|
||||
return option.HTTPOutboundOptions{}, err
|
||||
}
|
||||
|
||||
opts := option.HTTPOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{Server: server, ServerPort: uint16(port)},
|
||||
}
|
||||
if u.User != nil {
|
||||
opts.Username = u.User.Username()
|
||||
if password, ok := u.User.Password(); ok {
|
||||
opts.Password = password
|
||||
}
|
||||
}
|
||||
if path := u.EscapedPath(); path != "" {
|
||||
opts.Path = path
|
||||
}
|
||||
if host := strings.TrimSpace(query.Get("host")); host != "" {
|
||||
opts.Headers = badoption.HTTPHeader{"Host": {host}}
|
||||
}
|
||||
if tlsOptions, err := buildTLSOptions(query, skipCertVerify); err != nil {
|
||||
return option.HTTPOutboundOptions{}, err
|
||||
} else if tlsOptions != nil {
|
||||
opts.OutboundTLSOptionsContainer = option.OutboundTLSOptionsContainer{TLS: tlsOptions}
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func buildSOCKSOptions(u *url.URL) (option.SOCKSOutboundOptions, error) {
|
||||
server, port, err := hostPort(u, 1080)
|
||||
if err != nil {
|
||||
return option.SOCKSOutboundOptions{}, err
|
||||
}
|
||||
|
||||
opts := option.SOCKSOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{Server: server, ServerPort: uint16(port)},
|
||||
Version: "5",
|
||||
}
|
||||
if u.User != nil {
|
||||
opts.Username = u.User.Username()
|
||||
if password, ok := u.User.Password(); ok {
|
||||
opts.Password = password
|
||||
}
|
||||
}
|
||||
if network := strings.ToLower(strings.TrimSpace(u.Query().Get("network"))); network != "" {
|
||||
opts.Network = option.NetworkList(network)
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func buildAnyTLSOptions(u *url.URL, skipCertVerify bool) (option.AnyTLSOutboundOptions, error) {
|
||||
password := ""
|
||||
if u.User != nil {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func TestBuildNodeOutboundSupportsSOCKS5(t *testing.T) {
|
||||
outbound, err := buildNodeOutbound("socks-node", "socks5://demo:secret@99.144.123.135:30350", false)
|
||||
if err != nil {
|
||||
t.Fatalf("buildNodeOutbound returned error: %v", err)
|
||||
}
|
||||
if outbound.Type != C.TypeSOCKS {
|
||||
t.Fatalf("outbound type = %q, want %q", outbound.Type, C.TypeSOCKS)
|
||||
}
|
||||
|
||||
opts, ok := outbound.Options.(*option.SOCKSOutboundOptions)
|
||||
if !ok {
|
||||
t.Fatalf("outbound options type = %T, want *option.SOCKSOutboundOptions", outbound.Options)
|
||||
}
|
||||
if opts.Server != "99.144.123.135" {
|
||||
t.Fatalf("server = %q, want %q", opts.Server, "99.144.123.135")
|
||||
}
|
||||
if opts.ServerPort != 30350 {
|
||||
t.Fatalf("server port = %d, want %d", opts.ServerPort, 30350)
|
||||
}
|
||||
if opts.Username != "demo" {
|
||||
t.Fatalf("username = %q, want %q", opts.Username, "demo")
|
||||
}
|
||||
if opts.Password != "secret" {
|
||||
t.Fatalf("password = %q, want %q", opts.Password, "secret")
|
||||
}
|
||||
if opts.Version != "5" {
|
||||
t.Fatalf("version = %q, want %q", opts.Version, "5")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNodeOutboundSupportsHTTP(t *testing.T) {
|
||||
outbound, err := buildNodeOutbound("http-node", "http://alice:wonderland@example.com:8080/proxy", false)
|
||||
if err != nil {
|
||||
t.Fatalf("buildNodeOutbound returned error: %v", err)
|
||||
}
|
||||
if outbound.Type != C.TypeHTTP {
|
||||
t.Fatalf("outbound type = %q, want %q", outbound.Type, C.TypeHTTP)
|
||||
}
|
||||
|
||||
opts, ok := outbound.Options.(*option.HTTPOutboundOptions)
|
||||
if !ok {
|
||||
t.Fatalf("outbound options type = %T, want *option.HTTPOutboundOptions", outbound.Options)
|
||||
}
|
||||
if opts.Server != "example.com" {
|
||||
t.Fatalf("server = %q, want %q", opts.Server, "example.com")
|
||||
}
|
||||
if opts.ServerPort != 8080 {
|
||||
t.Fatalf("server port = %d, want %d", opts.ServerPort, 8080)
|
||||
}
|
||||
if opts.Username != "alice" {
|
||||
t.Fatalf("username = %q, want %q", opts.Username, "alice")
|
||||
}
|
||||
if opts.Password != "wonderland" {
|
||||
t.Fatalf("password = %q, want %q", opts.Password, "wonderland")
|
||||
}
|
||||
if opts.Path != "/proxy" {
|
||||
t.Fatalf("path = %q, want %q", opts.Path, "/proxy")
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,20 @@ const (
|
||||
InboundProtocolMixed = "mixed"
|
||||
)
|
||||
|
||||
var supportedProxyURISchemes = []string{
|
||||
"vmess://",
|
||||
"vless://",
|
||||
"trojan://",
|
||||
"ss://",
|
||||
"ssr://",
|
||||
"hysteria://",
|
||||
"hysteria2://",
|
||||
"hy2://",
|
||||
"anytls://",
|
||||
"http://",
|
||||
"socks5://",
|
||||
}
|
||||
|
||||
// NormalizeInboundProtocol normalizes inbound protocol aliases and validates the value.
|
||||
func NormalizeInboundProtocol(value string) (string, error) {
|
||||
protocol := strings.ToLower(strings.TrimSpace(value))
|
||||
@@ -683,9 +697,9 @@ func isBase64(s string) bool {
|
||||
|
||||
// IsProxyURI checks if a string is a valid proxy URI
|
||||
func IsProxyURI(s string) bool {
|
||||
schemes := []string{"vmess://", "vless://", "trojan://", "ss://", "ssr://", "hysteria://", "hysteria2://", "hy2://", "anytls://"}
|
||||
for _, scheme := range schemes {
|
||||
if strings.HasPrefix(strings.ToLower(s), scheme) {
|
||||
lower := strings.ToLower(strings.TrimSpace(s))
|
||||
for _, scheme := range supportedProxyURISchemes {
|
||||
if strings.HasPrefix(lower, scheme) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsProxyURIRecognizesHTTPAndSOCKS5(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
uri string
|
||||
want bool
|
||||
}{
|
||||
{name: "http", uri: "http://alice:secret@example.com:8080", want: true},
|
||||
{name: "socks5", uri: "socks5://alice:secret@example.com:1080", want: true},
|
||||
{name: "vmess", uri: "vmess://example", want: true},
|
||||
{name: "invalid", uri: "ftp://example.com", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := IsProxyURI(tt.uri); got != tt.want {
|
||||
t.Fatalf("%s: IsProxyURI(%q) = %v, want %v", tt.name, tt.uri, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,14 +495,16 @@ func extractHostFromURI(uri string) string {
|
||||
// Handle different URI schemes
|
||||
lowerURI := strings.ToLower(uri)
|
||||
|
||||
// vmess:// vless:// trojan:// ss:// ssr:// hysteria:// hysteria2:// hy2:// anytls://
|
||||
// Standard URL formats, including HTTP and SOCKS upstream proxies.
|
||||
if strings.HasPrefix(lowerURI, "vmess://") ||
|
||||
strings.HasPrefix(lowerURI, "vless://") ||
|
||||
strings.HasPrefix(lowerURI, "trojan://") ||
|
||||
strings.HasPrefix(lowerURI, "hysteria://") ||
|
||||
strings.HasPrefix(lowerURI, "hysteria2://") ||
|
||||
strings.HasPrefix(lowerURI, "hy2://") ||
|
||||
strings.HasPrefix(lowerURI, "anytls://") {
|
||||
strings.HasPrefix(lowerURI, "anytls://") ||
|
||||
strings.HasPrefix(lowerURI, "http://") ||
|
||||
strings.HasPrefix(lowerURI, "socks5://") {
|
||||
// Standard URL format: scheme://user@host:port?params#fragment
|
||||
parsed, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package geoip
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExtractHostFromURISupportsHTTPAndSOCKS5(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
uri string
|
||||
want string
|
||||
}{
|
||||
{name: "http", uri: "http://alice:secret@example.com:8080", want: "example.com"},
|
||||
{name: "socks5", uri: "socks5://alice:secret@99.144.123.135:30350", want: "99.144.123.135"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := extractHostFromURI(tt.uri); got != tt.want {
|
||||
t.Fatalf("%s: extractHostFromURI(%q) = %q, want %q", tt.name, tt.uri, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"easy_proxies/internal/config"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
@@ -175,26 +176,13 @@ func loadNodesFromLegacyFile(path string) ([]string, error) {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if isProxyURI(line) {
|
||||
if config.IsProxyURI(line) {
|
||||
uris = append(uris, line)
|
||||
}
|
||||
}
|
||||
return uris, nil
|
||||
}
|
||||
|
||||
// isProxyURI checks if a string looks like a proxy URI.
|
||||
func isProxyURI(s string) bool {
|
||||
schemes := []string{"vmess://", "vless://", "trojan://", "ss://", "ssr://",
|
||||
"hysteria://", "hysteria2://", "hy2://", "anytls://", "http://", "socks5://"}
|
||||
lower := strings.ToLower(s)
|
||||
for _, scheme := range schemes {
|
||||
if strings.HasPrefix(lower, scheme) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// extractNameFromURI extracts a name from the URI fragment (#name).
|
||||
func extractNameFromURI(uri string) string {
|
||||
parsed, err := url.Parse(uri)
|
||||
|
||||
@@ -651,24 +651,13 @@ func parseNodesFromContent(content string) ([]config.NodeConfig, error) {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if isProxyURI(line) {
|
||||
if config.IsProxyURI(line) {
|
||||
nodes = append(nodes, config.NodeConfig{URI: line})
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func isProxyURI(s string) bool {
|
||||
schemes := []string{"vmess://", "vless://", "trojan://", "ss://", "ssr://", "hysteria://", "hysteria2://", "hy2://", "anytls://"}
|
||||
lower := strings.ToLower(s)
|
||||
for _, scheme := range schemes {
|
||||
if strings.HasPrefix(lower, scheme) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type defaultLogger struct{}
|
||||
|
||||
func (defaultLogger) Infof(format string, args ...any) {
|
||||
|
||||
Reference in New Issue
Block a user