mirror of
https://github.com/Alexey71/opera-proxy.git
synced 2026-05-13 22:20:59 +00:00
Add new API settings
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type bypassPattern struct {
|
||||
raw string
|
||||
host string
|
||||
}
|
||||
|
||||
type BypassDialer struct {
|
||||
direct ContextDialer
|
||||
proxied ContextDialer
|
||||
patterns []bypassPattern
|
||||
}
|
||||
|
||||
func NewBypassDialer(patterns []string, direct, proxied ContextDialer) (*BypassDialer, error) {
|
||||
compiled := make([]bypassPattern, 0, len(patterns))
|
||||
for _, raw := range patterns {
|
||||
hostPattern, err := normalizeBypassPattern(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid proxy bypass pattern %q: %w", raw, err)
|
||||
}
|
||||
if _, err := path.Match(hostPattern, ""); err != nil {
|
||||
return nil, fmt.Errorf("invalid proxy bypass pattern %q: %w", raw, err)
|
||||
}
|
||||
compiled = append(compiled, bypassPattern{
|
||||
raw: raw,
|
||||
host: hostPattern,
|
||||
})
|
||||
}
|
||||
return &BypassDialer{
|
||||
direct: direct,
|
||||
proxied: proxied,
|
||||
patterns: compiled,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *BypassDialer) Dial(network, address string) (net.Conn, error) {
|
||||
return d.DialContext(context.Background(), network, address)
|
||||
}
|
||||
|
||||
func (d *BypassDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if d.shouldBypass(address) {
|
||||
return d.direct.DialContext(ctx, network, address)
|
||||
}
|
||||
return d.proxied.DialContext(ctx, network, address)
|
||||
}
|
||||
|
||||
func (d *BypassDialer) shouldBypass(address string) bool {
|
||||
host := normalizeDialTarget(address)
|
||||
if host == "" {
|
||||
return false
|
||||
}
|
||||
for _, pattern := range d.patterns {
|
||||
matched, err := path.Match(pattern.host, host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeBypassPattern(raw string) (string, error) {
|
||||
pattern := strings.TrimSpace(raw)
|
||||
if pattern == "" {
|
||||
return "", fmt.Errorf("empty pattern")
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(pattern, "://"):
|
||||
parsed, err := url.Parse(pattern)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to parse URL: %w", err)
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return "", fmt.Errorf("URL does not contain a host")
|
||||
}
|
||||
pattern = parsed.Hostname()
|
||||
case strings.HasPrefix(pattern, "//"):
|
||||
parsed, err := url.Parse("https:" + pattern)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to parse URL: %w", err)
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return "", fmt.Errorf("URL does not contain a host")
|
||||
}
|
||||
pattern = parsed.Hostname()
|
||||
case strings.ContainsAny(pattern, "/?"):
|
||||
parsed, err := url.Parse("https://" + strings.TrimLeft(pattern, "/"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to parse URL-like pattern: %w", err)
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return "", fmt.Errorf("pattern does not contain a host")
|
||||
}
|
||||
pattern = parsed.Hostname()
|
||||
default:
|
||||
if host, _, err := net.SplitHostPort(pattern); err == nil {
|
||||
pattern = host
|
||||
}
|
||||
}
|
||||
|
||||
pattern = strings.Trim(pattern, "[]")
|
||||
pattern = strings.ToLower(pattern)
|
||||
if pattern == "" {
|
||||
return "", fmt.Errorf("empty host pattern")
|
||||
}
|
||||
return pattern, nil
|
||||
}
|
||||
|
||||
func normalizeDialTarget(address string) string {
|
||||
host := strings.TrimSpace(address)
|
||||
if host == "" {
|
||||
return ""
|
||||
}
|
||||
if parsed, err := url.Parse(host); err == nil && parsed.Host != "" {
|
||||
host = parsed.Hostname()
|
||||
} else if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = h
|
||||
}
|
||||
host = strings.Trim(host, "[]")
|
||||
return strings.ToLower(host)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type recordingDialer struct {
|
||||
name string
|
||||
addresses []string
|
||||
}
|
||||
|
||||
func (d *recordingDialer) Dial(network, address string) (net.Conn, error) {
|
||||
return d.DialContext(context.Background(), network, address)
|
||||
}
|
||||
|
||||
func (d *recordingDialer) DialContext(_ context.Context, _ string, address string) (net.Conn, error) {
|
||||
d.addresses = append(d.addresses, address)
|
||||
return nil, errors.New(d.name)
|
||||
}
|
||||
|
||||
func TestBypassDialerRoutesByHostPattern(t *testing.T) {
|
||||
direct := &recordingDialer{name: "direct"}
|
||||
proxied := &recordingDialer{name: "proxied"}
|
||||
|
||||
d, err := NewBypassDialer([]string{
|
||||
"api2.sec-tunnel.com",
|
||||
"*.example.com",
|
||||
"https://already.url.test/some/path",
|
||||
}, direct, proxied)
|
||||
if err != nil {
|
||||
t.Fatalf("NewBypassDialer() error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
address string
|
||||
wantDirect bool
|
||||
description string
|
||||
}{
|
||||
{address: "api2.sec-tunnel.com:443", wantDirect: true, description: "exact host"},
|
||||
{address: "cdn.example.com:8443", wantDirect: true, description: "wildcard host"},
|
||||
{address: "ALREADY.URL.TEST:443", wantDirect: true, description: "case insensitive"},
|
||||
{address: "other.test:443", wantDirect: false, description: "unmatched host"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, err := d.DialContext(context.Background(), "tcp", tt.address)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: expected dial error", tt.description)
|
||||
}
|
||||
if tt.wantDirect && err.Error() != "direct" {
|
||||
t.Fatalf("%s: expected direct dialer, got %v", tt.description, err)
|
||||
}
|
||||
if !tt.wantDirect && err.Error() != "proxied" {
|
||||
t.Fatalf("%s: expected proxied dialer, got %v", tt.description, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBypassDialerRejectsInvalidPattern(t *testing.T) {
|
||||
_, err := NewBypassDialer([]string{" "}, &recordingDialer{}, &recordingDialer{})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid pattern error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user