Compare commits

..

No commits in common. "master" and "v2.1.0" have entirely different histories.

9 changed files with 154 additions and 335 deletions

View File

@ -2,58 +2,41 @@
_A round-robin load balancer for HTTP proxies._ _A round-robin load balancer for HTTP proxies._
This is a simple proxy load balancer that will route requests to a cluster of proxy backends in a round-robin fashion. This is a simple proxy load balancer that will route requests to a cluster of proxy backends in a round-robin fashion. This makes it easy to connect your clients to a large number of proxy servers without worrying about implementing anything special clientside.
This makes it easy to connect your clients to a large number of proxy servers without worrying about implementing
anything special client-side.
This proxy server will transparently forward HTTPS requests without terminating them, meaning a self-signed certificate This proxy server will transparently forward HTTPS requests without terminating them, meaning a self-signed certificate is not required. Downstream HTTPS proxy servers are not supported.
is not required. Downstream HTTPS proxy servers are not supported.
Memory usage sits at around 25M under load. Memory usage sits at around 25M under load.
## Install ## Install
1. Download the latest release from [/releases](https://git.evulid.cc/cyberes/proxy-loadbalancer/releases) or 1. Download the latest release from [/releases](https://git.evulid.cc/cyberes/proxy-loadbalancer/releases) or run `./build.sh` to build the program locally.
run `./build.sh` to build the program locally. 2. `cp config.example.yml config.yml`
2. `cp config.example.yml config.yml` 3. Edit the config.
3. Edit the config. 4. Start the loadbalancer with `./proxy-loadbalancer --config [path to your config.yml]`
4. Start the loadbalancer with `./proxy-loadbalancer --config [path to your config.yml]`
The load balancer has experimental support for using [curl-impersonate](https://github.com/lwthiker/curl-impersonate) to masquerade as the Chrome browser when
performing proxy checks.
1. Download `*.x86_64-linux-gnu.tar.gz ` from https://github.com/lwthiker/curl-impersonate/releases
2. Set `proxy_check_impersonate_chrome: true`
3. Enter the path to the `curl_chrome116` binary in `proxy_check_impersonate_chrome_binary`
## Use ## Use
You can run your own "public IP delivery server" `canihazip` <https://git.evulid.cc/cyberes/canihazip> or use the You can run your own "public IP delivery server" `canihazip` <https://git.evulid.cc/cyberes/canihazip> or use the default `api.ipify.org`
default `api.ipify.org`
An example systemd service `loadbalancer.service` is provided. An example systemd service `loadbalancer.service` is provided.
The server displays health, stats, info at `/json`. The server displays health, stats, info at `/json`.
Use `--log-third-party-test-failures` along with `--debug` when you want extra info on the third-party proxy tests. This
can get very noisy if you have lots of third-party proxies so it's hidden behind an extra flag.
``` ```
=== Proxy Load Balancer === === Proxy Load Balancer ===
Usage of /tmp/go-build1714785557/b001/exe/proxy-loadbalancer: Usage of ./proxy-loadbalancer:
--config [string] --config [string]
Path to the config file Path to the config file
-d, --debug -d, --debug
Enable debug mode Enable debug mode
-l, --log-third-party-test-failures --v Print version and exit
Log third-party test debug info -h, --help Print this help message
-v Print version and exit
``` ```
## Special Headers ## Special Headers
The load balancer accepts special headers to control its behavior: The load balancer accepts special headers to control its behavior:
- `Thirdparty-Bypass`: don't use any third-party endpoints for this request. - `Thirdparty-Bypass`: don't use any third-party endpoints for this request.
- `Thirdparty-Include-Broken`: use all online endpoints for this request, including third-party ones that failed the - `Thirdparty-Include-Broken`: use all online endpoints for this request, including third-party ones that failed the special test.
special test.

View File

@ -1,25 +1,17 @@
# Port to run on. # Port to run on.
http_port: 9000 http_port: 9000
# How many proxies will be checked at once? # How many proxies will be checked at once?
proxy_checkers: 50 proxy_checkers: 50
# The interval between proxy checks in seconds. # The interval between proxy checks in seconds.
proxy_check_interval: 60 proxy_check_interval: 60
# URL to get a proxy's IP. # URL to get a proxy's IP.
ip_checker_url: https://api.ipify.org ip_checker_url: https://api.ipify.org
# Connection timeout for the proxies in seconds. # Connection timeout for the proxies in seconds.
proxy_connect_timeout: 60 proxy_connect_timeout: 60
# How many times to retry a proxy connection.
# On each retry a new proxy will be chosen.
proxy_connect_retries: 3
# Use `curl-impersonate` to pretend to be Chrome when testing proxies.
proxy_check_impersonate_chrome: false
proxy_check_impersonate_chrome_binary: ./curl_chrome116
# Your proxies. # Your proxies.
proxy_pool_ours: proxy_pool_ours:
@ -42,7 +34,7 @@ thirdparty_bypass_domains:
# Shuffle the proxy lists whenever the background thread refreshes them. # Shuffle the proxy lists whenever the background thread refreshes them.
# If false, round-robin on default order. # If false, round-robin on default order.
shuffle_proxies: false shuffle_proxies: false
# Don't allow requests to these domains through the proxy. # Don't allow requests to these domains through the proxy.
blocked_domains: blocked_domains:
@ -51,4 +43,4 @@ blocked_domains:
# Resolve specific domains through specific proxies. # Resolve specific domains through specific proxies.
# Proxies here are not validated. # Proxies here are not validated.
resolve_through: resolve_through:
github.com: http://1.2.3.4:3128 github.com: http://1.2.3.4:3128

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
proxy.py @ git+https://github.com/abhinavsingh/proxy.py.git@develop
redis
requests
coloredlogs
psutil
flask
async_timeout

View File

@ -2,10 +2,7 @@ package config
import ( import (
"errors" "errors"
"fmt"
"github.com/spf13/viper" "github.com/spf13/viper"
"os"
"os/exec"
"time" "time"
) )
@ -13,21 +10,18 @@ import (
var cfg *Config var cfg *Config
type Config struct { type Config struct {
HTTPPort string HTTPPort string
IpCheckerURL string IpCheckerURL string
MaxProxyCheckers int MaxProxyCheckers int
ProxyConnectTimeout time.Duration ProxyConnectTimeout time.Duration
ProxyPoolOurs []string ProxyPoolOurs []string
ProxyPoolThirdparty []string ProxyPoolThirdparty []string
ThirdpartyTestUrls []string ThirdpartyTestUrls []string
ThirdpartyBypassDomains []string ThirdpartyBypassDomains []string
ShuffleProxies bool ShuffleProxies bool
BlockedDomains []string BlockedDomains []string
ResolveThrough map[string]string ResolveThrough map[string]string
ProxyCheckInterval int ProxyCheckInterval int
ProxyCheckImpersonateChrome bool
ProxyCheckImpersonateChromeBinary string
ProxyConnectRetries int
} }
func SetConfig(configFile string) (*Config, error) { func SetConfig(configFile string) (*Config, error) {
@ -48,9 +42,6 @@ func SetConfig(configFile string) (*Config, error) {
viper.SetDefault("blocked_domains", make([]string, 0)) viper.SetDefault("blocked_domains", make([]string, 0))
viper.SetDefault("resolve_through", make(map[string]string)) viper.SetDefault("resolve_through", make(map[string]string))
viper.SetDefault("proxy_check_interval", 60) viper.SetDefault("proxy_check_interval", 60)
viper.SetDefault("proxy_check_impersonate_chrome", false)
viper.SetDefault("proxy_check_impersonate_chrome_binary", nil)
viper.SetDefault("proxy_connect_retries", 3)
err := viper.ReadInConfig() err := viper.ReadInConfig()
if err != nil { if err != nil {
@ -58,20 +49,17 @@ func SetConfig(configFile string) (*Config, error) {
} }
config := &Config{ config := &Config{
HTTPPort: viper.GetString("http_port"), HTTPPort: viper.GetString("http_port"),
IpCheckerURL: viper.GetString("ip_checker_url"), IpCheckerURL: viper.GetString("ip_checker_url"),
MaxProxyCheckers: viper.GetInt("proxy_checkers"), MaxProxyCheckers: viper.GetInt("proxy_checkers"),
ProxyPoolOurs: viper.GetStringSlice("proxy_pool_ours"), ProxyPoolOurs: viper.GetStringSlice("proxy_pool_ours"),
ProxyPoolThirdparty: viper.GetStringSlice("proxy_pool_thirdparty"), ProxyPoolThirdparty: viper.GetStringSlice("proxy_pool_thirdparty"),
ThirdpartyTestUrls: viper.GetStringSlice("thirdparty_test_urls"), ThirdpartyTestUrls: viper.GetStringSlice("thirdparty_test_urls"),
ThirdpartyBypassDomains: viper.GetStringSlice("thirdparty_bypass_domains"), ThirdpartyBypassDomains: viper.GetStringSlice("thirdparty_bypass_domains"),
ShuffleProxies: viper.GetBool("shuffle_proxies"), ShuffleProxies: viper.GetBool("shuffle_proxies"),
BlockedDomains: viper.GetStringSlice("blocked_domains"), BlockedDomains: viper.GetStringSlice("blocked_domains"),
ResolveThrough: viper.GetStringMapString("resolve_through"), ResolveThrough: viper.GetStringMapString("resolve_through"),
ProxyCheckInterval: viper.GetInt("proxy_check_interval"), ProxyCheckInterval: viper.GetInt("proxy_check_interval"),
ProxyCheckImpersonateChrome: viper.GetBool("proxy_check_impersonate_chrome"),
ProxyCheckImpersonateChromeBinary: viper.GetString("proxy_check_impersonate_chrome_binary"),
ProxyConnectRetries: viper.GetInt("proxy_connect_retries"),
} }
if len(config.ProxyPoolOurs) == 0 && len(config.ProxyPoolThirdparty) == 0 { if len(config.ProxyPoolOurs) == 0 && len(config.ProxyPoolThirdparty) == 0 {
@ -98,21 +86,6 @@ func SetConfig(configFile string) (*Config, error) {
return nil, proxyPoolThirdpartyErr return nil, proxyPoolThirdpartyErr
} }
if config.ProxyCheckImpersonateChrome {
if _, err := os.Stat(config.ProxyCheckImpersonateChromeBinary); os.IsNotExist(err) {
return nil, errors.New(fmt.Sprintf(`curl-impersonate-chrome binary does not exist: "%s"`, config.ProxyCheckImpersonateChromeBinary))
}
cmd := exec.Command(config.ProxyCheckImpersonateChromeBinary, "--help")
err = cmd.Run()
if err != nil {
return nil, errors.New(fmt.Sprintf(`curl-impersonate-chrome binary failed to run: %s`, err))
}
}
if config.ProxyConnectRetries <= 0 {
return nil, errors.New("proxy_connect_retries must be greater than 0")
}
cfg = config cfg = config
return config, nil return config, nil
} }

View File

@ -1,27 +0,0 @@
package config
import "flag"
var CliArgs *CliConfig
type CliConfig struct {
ConfigFile string
Debug bool
Help bool
Version bool
LogThirdPartyTest bool
}
func ParseArgs() {
if CliArgs != nil {
panic("already defined")
}
CliArgs = &CliConfig{}
flag.StringVar(&CliArgs.ConfigFile, "config", "", "Path to the config file")
flag.BoolVar(&CliArgs.Debug, "d", false, "Enable debug mode")
flag.BoolVar(&CliArgs.Debug, "debug", false, "Enable debug mode")
flag.BoolVar(&CliArgs.Debug, "l", false, "Log third-party test debug info")
flag.BoolVar(&CliArgs.Debug, "log-third-party-test-failures", false, "Log third-party test debug info")
flag.BoolVar(&CliArgs.Version, "v", false, "Print version and exit")
flag.Parse()
}

View File

@ -13,17 +13,26 @@ import (
"runtime/debug" "runtime/debug"
) )
type cliConfig struct {
configFile string
initialCrawl bool
debug bool
disableElasticSync bool
help bool
version bool
}
var Version = "development" var Version = "development"
var VersionDate = "not set" var VersionDate = "not set"
func main() { func main() {
fmt.Println("=== Proxy Load Balancer ===") fmt.Println("=== Proxy Load Balancer ===")
config.ParseArgs() cliArgs := parseArgs()
if config.CliArgs.Help { if cliArgs.help {
flag.Usage() flag.Usage()
os.Exit(0) os.Exit(0)
} }
if config.CliArgs.Version { if cliArgs.version {
buildInfo, ok := debug.ReadBuildInfo() buildInfo, ok := debug.ReadBuildInfo()
if ok { if ok {
@ -42,7 +51,7 @@ func main() {
os.Exit(0) os.Exit(0)
} }
if config.CliArgs.Debug { if cliArgs.debug {
logging.InitLogger(logrus.DebugLevel) logging.InitLogger(logrus.DebugLevel)
} else { } else {
logging.InitLogger(logrus.InfoLevel) logging.InitLogger(logrus.InfoLevel)
@ -50,7 +59,7 @@ func main() {
log := logging.GetLogger() log := logging.GetLogger()
log.Debugln("Initializing...") log.Debugln("Initializing...")
if config.CliArgs.ConfigFile == "" { if cliArgs.configFile == "" {
exePath, err := os.Executable() exePath, err := os.Executable()
if err != nil { if err != nil {
panic(err) panic(err)
@ -61,24 +70,20 @@ func main() {
if _, err := os.Stat(filepath.Join(exeDir, "config.yaml")); err == nil { if _, err := os.Stat(filepath.Join(exeDir, "config.yaml")); err == nil {
log.Fatalln("Both config.yml and config.yaml exist in the executable directory. Please specify one with the --config flag.") log.Fatalln("Both config.yml and config.yaml exist in the executable directory. Please specify one with the --config flag.")
} }
config.CliArgs.ConfigFile = filepath.Join(exeDir, "config.yml") cliArgs.configFile = filepath.Join(exeDir, "config.yml")
} else if _, err := os.Stat(filepath.Join(exeDir, "config.yaml")); err == nil { } else if _, err := os.Stat(filepath.Join(exeDir, "config.yaml")); err == nil {
config.CliArgs.ConfigFile = filepath.Join(exeDir, "config.yaml") cliArgs.configFile = filepath.Join(exeDir, "config.yaml")
} else { } else {
log.Fatalln("No config file found in the executable directory. Please provide one with the --config flag.") log.Fatalln("No config file found in the executable directory. Please provide one with the --config flag.")
} }
} }
configData, err := config.SetConfig(config.CliArgs.ConfigFile) configData, err := config.SetConfig(cliArgs.configFile)
if err != nil { if err != nil {
log.Fatalf(`Failed to load config: %s`, err) log.Fatalf(`Failed to load config: %s`, err)
} }
log.Debugf(`Proxy check interval: %d sec`, config.GetConfig().ProxyCheckInterval) log.Debugf(`Proxy check interval: %d sec`, config.GetConfig().ProxyCheckInterval)
if config.GetConfig().ProxyCheckImpersonateChrome {
log.Debugf(`Using curl-impersonate binary: %s`, config.GetConfig().ProxyCheckImpersonateChromeBinary)
}
proxyCluster := proxy.NewForwardProxyCluster() proxyCluster := proxy.NewForwardProxyCluster()
go func() { go func() {
log.Fatal(http.ListenAndServe(":"+configData.HTTPPort, proxyCluster)) log.Fatal(http.ListenAndServe(":"+configData.HTTPPort, proxyCluster))
@ -91,3 +96,13 @@ func main() {
select {} select {}
} }
func parseArgs() cliConfig {
var cliArgs cliConfig
flag.StringVar(&cliArgs.configFile, "config", "", "Path to the config file")
flag.BoolVar(&cliArgs.debug, "d", false, "Enable debug mode")
flag.BoolVar(&cliArgs.debug, "debug", false, "Enable debug mode")
flag.BoolVar(&cliArgs.version, "v", false, "Print version and exit")
flag.Parse()
return cliArgs
}

View File

@ -1,17 +1,11 @@
package proxy package proxy
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"main/config" "main/config"
"net/http" "net/http"
"net/url" "net/url"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
) )
func sendRequestThroughProxy(pxy string, targetURL string) (string, error) { func sendRequestThroughProxy(pxy string, targetURL string) (string, error) {
@ -23,128 +17,43 @@ func sendRequestThroughProxy(pxy string, targetURL string) (string, error) {
if proxyUser != "" && proxyPass != "" { if proxyUser != "" && proxyPass != "" {
parsedProxyUrl.User = url.UserPassword(proxyUser, proxyPass) parsedProxyUrl.User = url.UserPassword(proxyUser, proxyPass)
} }
transport := &http.Transport{
if !config.GetConfig().ProxyCheckImpersonateChrome { Proxy: http.ProxyURL(parsedProxyUrl),
transport := &http.Transport{
Proxy: http.ProxyURL(parsedProxyUrl),
}
client := &http.Client{
Transport: transport,
Timeout: config.GetConfig().ProxyConnectTimeout,
}
req, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
req.Header.Set("Priority", "u=0, i")
req.Header.Set("Sec-Ch-Ua", `Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"`)
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
req.Header.Set("Sec-Ch-Ua-Platform", `"Windows"`)
req.Header.Set("Sec-Fetch-Dest", "document")
req.Header.Set("Sec-Fetch-Mode", "navigate")
req.Header.Set("Sec-Fetch-Site", "none")
req.Header.Set("Sec-Fetch-User", "?1")
req.Header.Set("Upgrade-Insecure-Requests", "1")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(bodyBytes), nil
}
return "", fmt.Errorf("bad response code %d", resp.StatusCode)
} else {
tmpfile, err := os.CreateTemp("", "response")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
cmd := exec.Command(
config.GetConfig().ProxyCheckImpersonateChromeBinary,
"--proxy",
parsedProxyUrl.String(),
"-o",
tmpfile.Name(),
"-w",
"%{http_code}",
"--ciphers",
"TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA,AES128-GCM-SHA256,AES256-GCM-SHA384,AES128-SHA,AES256-SHA",
"-H",
`sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"`,
"-H",
`sec-ch-ua-mobile: ?0`,
"-H",
`sec-ch-ua-platform: "Windows"`,
"-H",
`Upgrade-Insecure-Requests: 1`,
"-H",
`User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36`,
"-H",
`Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7`,
"-H",
`Sec-Fetch-Site: none`,
"-H",
`Sec-Fetch-Mode: navigate`,
"-H",
`Sec-Fetch-User: ?1`,
"-H",
`Sec-Fetch-Dest: document`,
"-H",
`Accept-Encoding: gzip, deflate, br`,
"-H",
`Accept-Language: en-US,en;q=0.9`,
"--http2",
"--http2-no-server-push",
"--compressed",
"--tlsv1.2",
"--alps",
"--tls-permute-extensions",
"--cert-compression",
"brotli",
targetURL,
)
output, err := cmd.Output()
if err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return "", fmt.Errorf("command exited with code %d: %s", exitError.ExitCode(), exitError.Stderr)
}
return "", err
}
outputStr := strings.TrimSpace(string(output))
match, _ := regexp.MatchString(`^\d{3}$`, outputStr)
if !match {
return "", fmt.Errorf("unexpected output from curl command: %s", outputStr)
}
statusCode, err := strconv.Atoi(outputStr)
if err != nil {
return "", err
}
if statusCode != http.StatusOK {
return "", fmt.Errorf("bad response code %d", statusCode)
}
body, err := os.ReadFile(tmpfile.Name())
if err != nil {
return "", err
}
return string(body), nil
} }
client := &http.Client{
Transport: transport,
Timeout: config.GetConfig().ProxyConnectTimeout,
}
req, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
req.Header.Set("Priority", "u=0, i")
req.Header.Set("Sec-Ch-Ua", `"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"`)
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
req.Header.Set("Sec-Ch-Ua-Platform", `"Windows"`)
req.Header.Set("Sec-Fetch-Dest", "document")
req.Header.Set("Sec-Fetch-Mode", "navigate")
req.Header.Set("Sec-Fetch-Site", "cross-site")
req.Header.Set("Sec-Fetch-User", "?1")
req.Header.Set("Upgrade-Insecure-Requests", "1")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(bodyBytes), nil
}
return "", fmt.Errorf("bad response code %d", resp.StatusCode)
} }

View File

@ -19,13 +19,8 @@ var (
HeaderThirdpartyBypass = "Thirdparty-Bypass" HeaderThirdpartyBypass = "Thirdparty-Bypass"
) )
func logProxyRequest(remoteAddr string, proxyHost string, targetHost string, returnCode *int, proxyConnectMode string, requestStartTime time.Time, errorMsg *string) { func logProxyRequest(remoteAddr string, proxyHost string, targetHost string, returnCode *int, proxyConnectMode string, requestStartTime time.Time) {
msg := fmt.Sprintf(`%s -> %s -> %s -> %d -- %s -- %d ms`, remoteAddr, proxyHost, targetHost, *returnCode, proxyConnectMode, time.Since(requestStartTime).Milliseconds()) log.Infof(`%s -> %s -> %s -> %d -- %s -- %d ms`, remoteAddr, proxyHost, targetHost, *returnCode, proxyConnectMode, time.Since(requestStartTime).Milliseconds())
if *errorMsg == "" {
log.Infof(msg)
} else {
log.Errorf(`%s -- %s`, msg, *errorMsg)
}
} }
func (p *ForwardProxyCluster) validateRequestAndGetProxy(w http.ResponseWriter, req *http.Request) (string, string, string, string, *url.URL, error) { func (p *ForwardProxyCluster) validateRequestAndGetProxy(w http.ResponseWriter, req *http.Request) (string, string, string, string, *url.URL, error) {
@ -51,11 +46,11 @@ func (p *ForwardProxyCluster) validateRequestAndGetProxy(w http.ResponseWriter,
return "", "", "", "", nil, errors.New(errStr) return "", "", "", "", nil, errors.New(errStr)
} }
headerIncludeBrokenThirdparty := req.Header.Get(HeaderThirdpartyIncludeBroken) != "" headerIncludeBrokenThirdparty := req.Header.Get(HeaderThirdpartyIncludeBroken)
req.Header.Del(HeaderThirdpartyIncludeBroken) req.Header.Del(HeaderThirdpartyIncludeBroken)
headerBypassThirdparty := req.Header.Get(HeaderThirdpartyBypass) != "" headerBypassThirdparty := req.Header.Get(HeaderThirdpartyBypass)
req.Header.Del(HeaderThirdpartyBypass) req.Header.Del(HeaderThirdpartyBypass)
if headerBypassThirdparty && headerIncludeBrokenThirdparty { if headerBypassThirdparty != "" && headerIncludeBrokenThirdparty != "" {
errStr := "duplicate options headers detected, rejecting request" errStr := "duplicate options headers detected, rejecting request"
http.Error(w, errStr, http.StatusBadRequest) http.Error(w, errStr, http.StatusBadRequest)
return "", "", "", "", nil, errors.New(errStr) return "", "", "", "", nil, errors.New(errStr)
@ -69,9 +64,9 @@ func (p *ForwardProxyCluster) validateRequestAndGetProxy(w http.ResponseWriter,
if slices.Contains(config.GetConfig().ThirdpartyBypassDomains, urlHostname) { if slices.Contains(config.GetConfig().ThirdpartyBypassDomains, urlHostname) {
selectedProxy = p.getProxyFromOurs() selectedProxy = p.getProxyFromOurs()
} else { } else {
if headerIncludeBrokenThirdparty { if headerIncludeBrokenThirdparty != "" {
selectedProxy = p.getProxyFromAllWithBroken() selectedProxy = p.getProxyFromAllWithBroken()
} else if headerBypassThirdparty { } else if headerBypassThirdparty != "" {
selectedProxy = p.getProxyFromOurs() selectedProxy = p.getProxyFromOurs()
} else { } else {
selectedProxy = p.getProxyFromAll() selectedProxy = p.getProxyFromAll()
@ -90,6 +85,7 @@ func (p *ForwardProxyCluster) validateRequestAndGetProxy(w http.ResponseWriter,
} }
return selectedProxy, proxyUser, proxyPass, proxyHost, parsedProxyUrl, nil return selectedProxy, proxyUser, proxyPass, proxyHost, parsedProxyUrl, nil
} }
func (p *ForwardProxyCluster) proxyHttpConnect(w http.ResponseWriter, req *http.Request) { func (p *ForwardProxyCluster) proxyHttpConnect(w http.ResponseWriter, req *http.Request) {
@ -97,20 +93,17 @@ func (p *ForwardProxyCluster) proxyHttpConnect(w http.ResponseWriter, req *http.
remoteAddr, _, _ := net.SplitHostPort(req.RemoteAddr) remoteAddr, _, _ := net.SplitHostPort(req.RemoteAddr)
_, proxyUser, proxyPass, proxyHost, parsedProxyUrl, err := p.validateRequestAndGetProxy(w, req) _, proxyUser, proxyPass, proxyHost, parsedProxyUrl, err := p.validateRequestAndGetProxy(w, req)
if err != nil { if err != nil {
// Error has already been handled, just log and return.
if proxyHost == "" { if proxyHost == "" {
proxyHost = "none" proxyHost = "none"
} }
log.Debugf(`%s -> %s -> %s -- HTTP -- Rejecting request: %s`, remoteAddr, proxyHost, req.Host, err) log.Debugf(`%s -> %s -> %s -- HTTP -- Rejecting request: %s`, remoteAddr, proxyHost, req.Host, err)
return return
} }
var returnCode *int var returnCode *int
returnCode = new(int) returnCode = new(int)
*returnCode = -1 *returnCode = -1
var errorMsg *string defer logProxyRequest(remoteAddr, proxyHost, req.Host, returnCode, "HTTP", requestStartTime)
errorMsg = new(string)
*errorMsg = ""
defer logProxyRequest(remoteAddr, proxyHost, req.Host, returnCode, "HTTP", requestStartTime, errorMsg)
parsedProxyUrl.Scheme = "http" parsedProxyUrl.Scheme = "http"
if proxyUser != "" && proxyPass != "" { if proxyUser != "" && proxyPass != "" {
@ -125,7 +118,7 @@ func (p *ForwardProxyCluster) proxyHttpConnect(w http.ResponseWriter, req *http.
proxyReq, err := http.NewRequest(req.Method, req.URL.String(), req.Body) proxyReq, err := http.NewRequest(req.Method, req.URL.String(), req.Body)
if err != nil { if err != nil {
*errorMsg = fmt.Sprintf(`Failed to make %s request to "%s": %s`, req.Method, req.URL.String(), err) log.Errorf(`Failed to make %s request to "%s": %s`, req.Method, req.URL.String(), err)
http.Error(w, "failed to make request to downstream", http.StatusInternalServerError) http.Error(w, "failed to make request to downstream", http.StatusInternalServerError)
return return
} }
@ -133,25 +126,18 @@ func (p *ForwardProxyCluster) proxyHttpConnect(w http.ResponseWriter, req *http.
copyHeader(proxyReq.Header, req.Header) copyHeader(proxyReq.Header, req.Header)
proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr) proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
for i := 0; i < config.GetConfig().ProxyConnectRetries; i++ { // Retry mechanic resp, err := client.Do(proxyReq)
resp, err := client.Do(proxyReq) if err != nil {
if err != nil { log.Errorf(`Failed to execute %s request to "%s": %s`, req.Method, req.URL.String(), err)
*errorMsg = fmt.Sprintf(`Failed to execute %s request to "%s" - attempt %d/%d - %s`, req.Method, req.URL.String(), i+1, config.GetConfig().ProxyConnectRetries, err) http.Error(w, "failed to execute request to downstream", http.StatusServiceUnavailable)
if i < config.GetConfig().ProxyConnectRetries-1 { return
continue
} else {
http.Error(w, "failed to execute request to downstream", http.StatusServiceUnavailable)
return
}
} else {
defer resp.Body.Close()
*returnCode = resp.StatusCode
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
break
}
} }
defer resp.Body.Close()
*returnCode = resp.StatusCode
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
} }
func (p *ForwardProxyCluster) proxyHttpsConnect(w http.ResponseWriter, req *http.Request) { func (p *ForwardProxyCluster) proxyHttpsConnect(w http.ResponseWriter, req *http.Request) {
@ -160,6 +146,7 @@ func (p *ForwardProxyCluster) proxyHttpsConnect(w http.ResponseWriter, req *http
targetHost, _, _ := net.SplitHostPort(req.Host) targetHost, _, _ := net.SplitHostPort(req.Host)
_, proxyUser, proxyPass, proxyHost, _, err := p.validateRequestAndGetProxy(w, req) _, proxyUser, proxyPass, proxyHost, _, err := p.validateRequestAndGetProxy(w, req)
if err != nil { if err != nil {
// Error has already been handled, just log and return.
if proxyHost == "" { if proxyHost == "" {
proxyHost = "none" proxyHost = "none"
} }
@ -169,26 +156,14 @@ func (p *ForwardProxyCluster) proxyHttpsConnect(w http.ResponseWriter, req *http
var returnCode *int var returnCode *int
returnCode = new(int) returnCode = new(int)
*returnCode = -1 *returnCode = -1
var errorMsg *string defer logProxyRequest(remoteAddr, proxyHost, targetHost, returnCode, "CONNECT", requestStartTime)
errorMsg = new(string)
*errorMsg = ""
defer logProxyRequest(remoteAddr, proxyHost, targetHost, returnCode, "CONNECT", requestStartTime, errorMsg)
var proxyConn net.Conn // Start a connection to the downstream proxy server.
for i := 0; i < config.GetConfig().ProxyConnectRetries; i++ { proxyConn, err := net.DialTimeout("tcp", proxyHost, config.GetConfig().ProxyConnectTimeout)
// Start a connection to the downstream proxy server. if err != nil {
proxyConn, err = net.DialTimeout("tcp", proxyHost, config.GetConfig().ProxyConnectTimeout) log.Errorf(`Failed to dial proxy %s - %s`, proxyHost, err)
if err != nil { http.Error(w, "failed to make request to downstream", http.StatusServiceUnavailable)
*errorMsg = fmt.Sprintf(`Failed to dial proxy %s - attempt %d/%d - %s`, proxyHost, i+1, config.GetConfig().ProxyConnectRetries, err) return
if i < config.GetConfig().ProxyConnectRetries-1 {
continue
} else {
http.Error(w, "failed to make request to downstream", http.StatusServiceUnavailable)
return
}
} else {
break
}
} }
// Proxy authentication // Proxy authentication
@ -203,7 +178,7 @@ func (p *ForwardProxyCluster) proxyHttpsConnect(w http.ResponseWriter, req *http
} }
resp, err := http.ReadResponse(bufio.NewReader(proxyConn), req) resp, err := http.ReadResponse(bufio.NewReader(proxyConn), req)
if resp == nil { if resp == nil {
*errorMsg = fmt.Sprintf(`Failed to CONNECT to %s using proxy %s: %s`, req.Host, proxyHost, err) log.Errorf(`Failed to CONNECT to %s using proxy %s: %s`, req.Host, proxyHost, err)
http.Error(w, "failed to execute request to downstream", http.StatusServiceUnavailable) http.Error(w, "failed to execute request to downstream", http.StatusServiceUnavailable)
return return
} else if err != nil { } else if err != nil {
@ -214,14 +189,14 @@ func (p *ForwardProxyCluster) proxyHttpsConnect(w http.ResponseWriter, req *http
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
hj, ok := w.(http.Hijacker) hj, ok := w.(http.Hijacker)
if !ok { if !ok {
*errorMsg = fmt.Sprintf(`Failed to forward connection to %s using proxy %s`, req.Host, proxyHost) log.Errorf(`Failed to forward connection to %s using proxy %s`, req.Host, proxyHost)
http.Error(w, "failed to forward connection to downstream", http.StatusServiceUnavailable) http.Error(w, "failed to forward connection to downstream", http.StatusServiceUnavailable)
return return
} }
clientConn, _, err := hj.Hijack() clientConn, _, err := hj.Hijack()
if err != nil { if err != nil {
*errorMsg = fmt.Sprintf(`Failed to execute connection forwarding to %s using proxy %s`, req.Host, proxyHost) log.Errorf(`Failed to execute connection forwarding to %s using proxy %s`, req.Host, proxyHost)
http.Error(w, "failed to execute connection forwarding to downstream", http.StatusServiceUnavailable) http.Error(w, "failed to execute connection forwarding to downstream", http.StatusServiceUnavailable)
return return
} }

View File

@ -10,8 +10,6 @@ import (
"time" "time"
) )
// TODO: fix 503 errors returned during proxy checking process
func (p *ForwardProxyCluster) ValidateProxiesThread() { func (p *ForwardProxyCluster) ValidateProxiesThread() {
log.Infoln("Doing initial backend check, please wait...") log.Infoln("Doing initial backend check, please wait...")
started := false started := false
@ -19,7 +17,6 @@ func (p *ForwardProxyCluster) ValidateProxiesThread() {
ctx := context.TODO() ctx := context.TODO()
for { for {
startTime := time.Now()
p.refreshInProgress = true p.refreshInProgress = true
allProxies := removeDuplicates(append(config.GetConfig().ProxyPoolOurs, config.GetConfig().ProxyPoolThirdparty...)) allProxies := removeDuplicates(append(config.GetConfig().ProxyPoolOurs, config.GetConfig().ProxyPoolThirdparty...))
newOurOnlineProxies := make([]string, 0) newOurOnlineProxies := make([]string, 0)
@ -60,13 +57,10 @@ func (p *ForwardProxyCluster) ValidateProxiesThread() {
// Test the proxy. // Test the proxy.
ipAddr, testErr := sendRequestThroughProxy(pxy, config.GetConfig().IpCheckerURL) ipAddr, testErr := sendRequestThroughProxy(pxy, config.GetConfig().IpCheckerURL)
if testErr != nil { if testErr != nil {
log.Warnf("Validate - %s failed: %s", proxyHost, testErr)
if isThirdparty(pxy) { if isThirdparty(pxy) {
if config.CliArgs.LogThirdPartyTest {
log.Debugf("Validate - %s failed: %s", proxyHost, testErr)
}
newThirdpartyOfflineProxies = append(newThirdpartyOfflineProxies, pxy) newThirdpartyOfflineProxies = append(newThirdpartyOfflineProxies, pxy)
} else { } else {
log.Debugf("Validate - %s failed: %s", proxyHost, testErr)
newOurOfflineProxies = append(newOurOfflineProxies, pxy) newOurOfflineProxies = append(newOurOfflineProxies, pxy)
} }
return return
@ -90,9 +84,7 @@ func (p *ForwardProxyCluster) ValidateProxiesThread() {
if bv3hiErr != nil { if bv3hiErr != nil {
okToAdd = false okToAdd = false
newThirdpartyBrokenProxies = append(newThirdpartyBrokenProxies, pxy) newThirdpartyBrokenProxies = append(newThirdpartyBrokenProxies, pxy)
if config.CliArgs.LogThirdPartyTest { log.Debugf("Validate - %s failed third-party test: %s", proxyHost, bv3hiErr)
log.Debugf(`%s failed third-party test for URL "%s" -- %s`, proxyHost, d, bv3hiErr)
}
break break
} }
} }
@ -129,8 +121,8 @@ func (p *ForwardProxyCluster) ValidateProxiesThread() {
} }
p.mu.RLock() p.mu.RLock()
log.Infof("Our Endpoints Online: %d, Third-Party Endpoints Online: %d, Third-Party Broken Endpoints: %d, Total Valid: %d, Elapsed: %s", log.Infof("Our Endpoints Online: %d, Third-Party Endpoints Online: %d, Third-Party Broken Endpoints: %d, Total Valid: %d",
len(p.ourOnlineProxies), len(p.thirdpartyOnlineProxies), len(p.thirdpartyBrokenProxies), len(p.ourOnlineProxies)+len(p.thirdpartyOnlineProxies), time.Since(startTime), len(p.ourOnlineProxies), len(p.thirdpartyOnlineProxies), len(p.thirdpartyBrokenProxies), len(p.ourOnlineProxies)+len(p.thirdpartyOnlineProxies),
) )
p.mu.RUnlock() p.mu.RUnlock()