7 Commits

Author SHA1 Message Date
Alexey71 2f4651508c Revert changes fakeSNI 2026-05-14 11:14:52 +03:00
Alexey71 e7d9ae4706 Update go 2026-05-14 11:14:32 +03:00
Alexey71 7546402687 Update readme/x509roots 2026-05-04 10:26:58 +03:00
xteamlyer 63b9203be4 Some change 2026-04-30 19:04:28 +03:00
Alexey71 b99a41e040 Update readme 2026-04-26 20:02:03 +03:00
Photo 91ea01b43b Add new API settings 2026-04-26 15:35:01 +03:00
Alexey71 726a68d22b Small update 2026-04-26 14:58:32 +03:00
19 changed files with 2618 additions and 278 deletions
+3 -3
View File
@@ -14,12 +14,12 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
-
name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v6
with:
go-version: 'stable'
-
@@ -35,7 +35,7 @@ jobs:
VERSION=${{steps.tag.outputs.tag}}
-
name: Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v3
with:
files: bin/*
fail_on_unmatched_files: true
+10
View File
@@ -0,0 +1,10 @@
31.59.20.176:6754:oxcestpe:2yoo1hy06sho
198.23.239.134:6540:oxcestpe:2yoo1hy06sho
45.38.107.97:6014:oxcestpe:2yoo1hy06sho
107.172.163.27:6543:oxcestpe:2yoo1hy06sho
198.105.121.200:6462:oxcestpe:2yoo1hy06sho
216.10.27.159:6837:oxcestpe:2yoo1hy06sho
142.111.67.146:5611:oxcestpe:2yoo1hy06sho
191.96.254.138:6185:oxcestpe:2yoo1hy06sho
31.58.9.4:6077:oxcestpe:2yoo1hy06sho
23.26.71.145:5628:oxcestpe:2yoo1hy06sho
+87 -30
View File
@@ -60,38 +60,95 @@ eu2.sec-tunnel.com,77.111.247.51,443
eu3.sec-tunnel.com,77.111.244.22,443
```
You can also skip the SurfEasy discover request and take endpoints from an existing CSV, while still using the normal server selection logic including `-server-selection fastest`:
```
$ ./opera-proxy -discover-csv proxies.csv -server-selection fastest
```
If direct SurfEasy API access is unstable, you can point discovery at a text file with fallback proxies. The app will read `proxies.txt`, test API proxies in parallel, and stop on the first one that successfully completes init/discover. File entries may be plain `host:port`, URL-style `http://user:pass@host:port`, or `host:port:user:pass` like `10proxies.txt`:
```
$ ./opera-proxy -api-proxy-file proxies.txt -country EU
```
By default it tests up to 5 candidates at once. You can change that with `-api-proxy-parallel`:
```
$ ./opera-proxy -api-proxy-file proxies.txt -api-proxy-parallel 5 -country EU
```
You can also download the proxy list from a URL. If the download fails, the app can fall back to a local file:
```
$ ./opera-proxy -api-proxy-list-url https://example.com/proxies.txt -country EU
```
```
$ ./opera-proxy -api-proxy-list-url https://example.com/proxies.txt -api-proxy-file proxies.txt -country EU
```
You can free download proxy servers (default `https://advanced.name/freeproxy`) into a file named `proxies.txt`, use `-fetch-freeproxy-out`. The file name and path can be anything (`D:\myproxy.txt`, `xxxxx.txt`). By default, the `proxies.txt` file is created alongside the `opera-proxy` binary.
```
$ ./opera-proxy -fetch-freeproxy-out proxies.txt
```
You can also run two commands sequentially. The first command will download the proxies and save them to proxies.txt. The second command will launch opera-proxy using the proxies you downloaded.
```
$ ./opera-proxy -fetch-freeproxy-out proxies.txt
$ ./opera-proxy -api-proxy-file proxies.txt -country EU
```
If you want selected destinations to go directly instead of through the Opera proxy, use `-proxy-bypass`. It accepts a comma-separated list of host or URL patterns and supports `*` in hostnames:
```
$ ./opera-proxy -country EU -proxy-bypass "api2.sec-tunnel.com,*.example.com,https://download.test.local/list.txt"
```
If `-proxy-bypass` is not passed, the app also tries to read `proxy-bypass.txt` from the current working directory. The file supports one pattern per line, comments via `#`, and comma-separated values on the same line.
If SurfEasy discover returns API error `801`, the app also automatically tries `proxies.csv` from the current working directory, even when `-discover-csv` was not passed.
## List of arguments
| Argument | Type | Description |
| -------- | ---- | ----------- |
| api-address | String | override IP address of api2.sec-tunnel.com |
| api-client-type | String | client type reported to SurfEasy API (default "se0316") |
| api-client-version | String | client version reported to SurfEasy API (default "Stable 114.0.5282.21") |
| api-login | String | SurfEasy API login (default "se0316") |
| api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") |
| api-proxy | String | additional proxy server used to access SurfEasy API |
| api-user-agent | String | user agent reported to SurfEasy API (default "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0") |
| bind-address | String | proxy listen address (default "127.0.0.1:18080") |
| bootstrap-dns | String | Comma-separated list of DNS/DoH/DoT resolvers for initial discovery of SurfEasy API address. Supported schemes are: `dns://`, `https://`, `tls://`, `tcp://`. Examples: `https://1.1.1.1/dns-query`, `tls://9.9.9.9:853` (default `https://1.1.1.3/dns-query,https://8.8.8.8/dns-query,https://dns.google/dns-query,https://security.cloudflare-dns.com/dns-query,https://fidelity.vm-0.com/q,https://wikimedia-dns.org/dns-query,https://dns.adguard-dns.com/dns-query,https://dns.quad9.net/dns-query,https://doh.cleanbrowsing.org/doh/adult-filter/`) |
| cafile | String | use custom CA certificate bundle file |
| config | String | read configuration from file with space-separated keys and values |
| country | String | desired proxy location (default "EU") |
| dp-export | - | export configuration for dumbproxy |
| fake-SNI | String | domain name to use as SNI in communications with servers |
| init-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
| init-retry-interval | Duration | delay between initialization retries (default 5s) |
| list-countries | - | list available countries and exit |
| list-proxies | - | output proxy list and exit |
| override-proxy-address | string | use fixed proxy address instead of server address returned by SurfEasy API |
| proxy | String | sets base proxy to use for all dial-outs. Format: `<http\|https\|socks5\|socks5h>://[login:password@]host[:port]` Examples: `http://user:password@192.168.1.1:3128`, `socks5://10.0.0.1:1080` |
| refresh | Duration | login refresh interval (default 4h0m0s) |
| refresh-retry | Duration | login refresh retry interval (default 5s) |
| server-selection | Enum | server selection policy (first/random/fastest) (default fastest) |
| server-selection-dl-limit | Number | restrict amount of downloaded data per connection by fastest server selection |
| server-selection-test-url | String | URL used for download benchmark by fastest server selection policy (default `https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js`) |
| server-selection-timeout | Duration | timeout given for server selection function to produce result (default 30s) |
| timeout | Duration | timeout for network operations (default 10s) |
| verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
| version | - | show program version and exit |
| socks-mode | - | listen for SOCKS requests instead of HTTP |
| -api-address | String | override IP address of api2.sec-tunnel.com |
| -api-client-type | String | client type reported to SurfEasy API (default "se0316") |
| -api-client-version | String | client version reported to SurfEasy API (default "Stable 114.0.5282.21") |
| -api-login | String | SurfEasy API login (default "se0316") |
| -api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") |
| -api-proxy | String | additional proxy server used to access SurfEasy API |
| -api-proxy-file | String | path to text file with candidate proxy servers for SurfEasy API access, one per line; proxies are tried in order until init/discover succeeds |
| -api-proxy-list-url | String | URL of a text file with candidate proxy servers for SurfEasy API access; falls back to `-api-proxy-file` if download fails |
| -api-proxy-parallel | Number | number of API proxy candidates tested in parallel when `-api-proxy-file` is used (default 5) |
| -api-user-agent | String | user agent reported to SurfEasy API (default "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0") |
| -bind-address | String | proxy listen address (default "127.0.0.1:18080") |
| -bootstrap-dns | String | Comma-separated list of DNS/DoH/DoT resolvers for initial discovery of SurfEasy API address. Supported schemes are: `dns://`, `https://`, `tls://`, `tcp://`. Examples: `https://1.1.1.1/dns-query`, `tls://9.9.9.9:853` (default `https://1.1.1.3/dns-query,https://8.8.8.8/dns-query,https://dns.google/dns-query,https://security.cloudflare-dns.com/dns-query,https://fidelity.vm-0.com/q,https://wikimedia-dns.org/dns-query,https://dns.adguard-dns.com/dns-query,https://dns.quad9.net/dns-query,https://doh.cleanbrowsing.org/doh/adult-filter/`) |
| -cafile | String | use custom CA certificate bundle file |
| -config | String | read configuration from file with space-separated keys and values |
| -country | String | desired proxy location (default "EU") |
| -discover-csv | String | read proxy endpoints from CSV instead of SurfEasy discover API |
| -dp-export | - | export configuration for dumbproxy |
| -fetch-freeproxy-out | - | download proxy list from `https://advanced.name/freeproxy` and save it as a text file with one ip:port per line. Examples: `-fetch-freeproxy-out proxies.txt` or `-fetch-freeproxy-out D:\myproxy.txt` |
| -fake-SNI | String | domain name to use as SNI in communications with servers |
| -init-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
| -init-retry-interval | Duration | delay between initialization retries (default 5s) |
| -list-countries | - | list available countries and exit |
| -list-proxies | - | output proxy list and exit |
| -override-proxy-address | string | use fixed proxy address instead of server address returned by SurfEasy API |
| -proxy | String | sets base proxy to use for all dial-outs. Format: `<http\|https\|socks5\|socks5h>://[login:password@]host[:port]` Examples: `http://user:password@192.168.1.1:3128`, `socks5://10.0.0.1:1080` |
| -proxy-bypass | String | comma-separated list of destination host or URL patterns that should bypass proxying and connect directly; matching is case-insensitive and supports `*` in hostnames; if omitted, `proxy-bypass.txt` from the current working directory is loaded automatically when present |
| -proxy-blacklist | String | path to file with blacklisted proxy addresses, one `host[:port]` per line |
| -refresh | Duration | login refresh interval (default 4h0m0s) |
| -refresh-retry | Duration | login refresh retry interval (default 5s) |
| -server-selection | Enum | server selection policy (first/random/fastest) (default fastest) |
| -server-selection-dl-limit | Number | restrict amount of downloaded data per connection by fastest server selection |
| -server-selection-test-url | String | URL used for download benchmark by fastest server selection policy (default `https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js`) |
| -server-selection-timeout | Duration | timeout given for server selection function to produce result (default 30s) |
| -socks-mode | - | listen for SOCKS requests instead of HTTP |
| -timeout | Duration | timeout for network operations (default 10s) |
| -verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
| -version | - | show program version and exit |
+133
View File
@@ -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)
}
+67
View File
@@ -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")
}
}
+14 -3
View File
@@ -115,9 +115,14 @@ func (d *ProxyDialer) DialContext(ctx context.Context, network, address string)
return nil, err
}
if uTLSServerName != "" {
// Custom cert verification logic:
// DO NOT send SNI extension of TLS ClientHello
// DO peer certificate verification against specified servername
// Custom TLS verification strategy:
// - Do NOT send SNI in ClientHello (use fakeSNI, may be empty string).
// - Verify the peer certificate against the real server name using
// the explicit caPool (Mozilla NSS bundle via bundle.Roots()).
//
// No cross-signed intermediate injection needed: bundle.Roots() already
// contains USERTrust ECC CA as a trusted root, so Go's chain builder
// resolves Opera's certificate chain without any manual patching.
conn = tls.Client(conn, &tls.Config{
ServerName: fakeSNI,
InsecureSkipVerify: true,
@@ -186,6 +191,12 @@ func (d *ProxyDialer) Address() (string, error) {
return d.address()
}
// readResponse reads an HTTP/1.1 response from the raw conn after a CONNECT
// request. It reads byte-by-byte until the \r\n\r\n header terminator is found,
// then hands the accumulated bytes to http.ReadResponse.
//
// Note: byte-by-byte reading is intentional — we must not over-read past the
// end of headers into the tunneled TLS stream.
func readResponse(r io.Reader, req *http.Request) (*http.Response, error) {
endOfResponse := []byte("\r\n\r\n")
buf := &bytes.Buffer{}
+130
View File
@@ -0,0 +1,130 @@
package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"time"
)
var (
freeProxyIPPattern = regexp.MustCompile(`data-ip="([^"]+)"`)
freeProxyPortPattern = regexp.MustCompile(`data-port="([^"]+)"`)
)
func fetchFreeProxyToFile(sourceURL, outPath string, timeout time.Duration) (int, error) {
proxies, err := fetchFreeProxyList(sourceURL, timeout)
if err != nil {
return 0, err
}
if len(proxies) == 0 {
return 0, fmt.Errorf("no proxies found at %s", sourceURL)
}
if err := os.WriteFile(outPath, []byte(strings.Join(proxies, "\n")+"\n"), 0o644); err != nil {
return 0, err
}
return len(proxies), nil
}
func fetchFreeProxyList(sourceURL string, timeout time.Duration) ([]string, error) {
client := &http.Client{Timeout: timeout}
seen := make(map[string]struct{})
proxies := make([]string, 0, 256)
for page := 1; ; page++ {
pageURL, err := freeProxyPageURL(sourceURL, page)
if err != nil {
return nil, err
}
pageProxies, err := fetchFreeProxyPage(client, pageURL)
if err != nil {
return nil, fmt.Errorf("page %d: %w", page, err)
}
if len(pageProxies) == 0 {
break
}
for _, proxy := range pageProxies {
if _, ok := seen[proxy]; ok {
continue
}
seen[proxy] = struct{}{}
proxies = append(proxies, proxy)
}
}
return proxies, nil
}
func freeProxyPageURL(rawURL string, page int) (string, error) {
parsed, err := url.Parse(rawURL)
if err != nil {
return "", fmt.Errorf("parse url: %w", err)
}
query := parsed.Query()
if page <= 1 {
query.Del("page")
} else {
query.Set("page", fmt.Sprintf("%d", page))
}
parsed.RawQuery = query.Encode()
return parsed.String(), nil
}
func fetchFreeProxyPage(client *http.Client, pageURL string) ([]string, error) {
req, err := http.NewRequest(http.MethodGet, pageURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "opera-proxy freeproxy fetcher/1.0")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
ipMatches := freeProxyIPPattern.FindAllSubmatch(body, -1)
portMatches := freeProxyPortPattern.FindAllSubmatch(body, -1)
rowCount := min(len(ipMatches), len(portMatches))
proxies := make([]string, 0, rowCount)
for i := 0; i < rowCount; i++ {
ip, err := freeProxyDecodeBase64(string(ipMatches[i][1]))
if err != nil {
return nil, fmt.Errorf("decode ip on row %d: %w", i+1, err)
}
port, err := freeProxyDecodeBase64(string(portMatches[i][1]))
if err != nil {
return nil, fmt.Errorf("decode port on row %d: %w", i+1, err)
}
proxies = append(proxies, ip+":"+port)
}
return proxies, nil
}
func freeProxyDecodeBase64(value string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return "", err
}
return string(decoded), nil
}
+3 -3
View File
@@ -2,13 +2,13 @@ module github.com/Alexey71/opera-proxy
go 1.25.0
toolchain go1.25.9
toolchain go1.26.3
require (
github.com/Alexey71/go-http-digest-auth-client v1.1.3
github.com/Alexey71/go-multierror v1.1.3
github.com/ncruces/go-dns v1.3.3
github.com/things-go/go-socks5 v0.1.1
golang.org/x/crypto/x509roots/fallback v0.0.0-20260413170323-a8e9237a216b
golang.org/x/net v0.53.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2
golang.org/x/net v0.54.0
)
+4 -4
View File
@@ -12,9 +12,9 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/things-go/go-socks5 v0.1.1 h1:48hy9cHEXPKeG91G/g4n8zW4uynzPUQy/FkcrJ7r5AY=
github.com/things-go/go-socks5 v0.1.1/go.mod h1:1YBHVYG7Oli5ae+Pwkp630cPAwY1pjUPmohO1n0Emg0=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260413170323-a8e9237a216b h1:ZG2SxTKsx1w3pUpOMD9dliRYnhWC5R5jmL6UDPCbYj4=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260413170323-a8e9237a216b/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2 h1:7Y5FZkvYs5XMyG0VS/pONmKIgD9+9eqcm1DGar541SA=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+93
View File
@@ -0,0 +1,93 @@
package handler
import (
"context"
"errors"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/Snawoot/opera-proxy/dialer"
clog "github.com/Snawoot/opera-proxy/log"
)
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 testProxyLogger() *clog.CondLogger {
return clog.NewCondLogger(log.New(io.Discard, "", 0), clog.CRITICAL)
}
func TestProxyHandlerBypassesHTTPRequestsByTargetHost(t *testing.T) {
direct := &recordingDialer{name: "direct"}
proxied := &recordingDialer{name: "proxied"}
bypassDialer, err := dialer.NewBypassDialer([]string{"*.example.com"}, direct, proxied)
if err != nil {
t.Fatalf("NewBypassDialer() error = %v", err)
}
h := NewProxyHandler(bypassDialer, testProxyLogger(), "")
req := httptest.NewRequest(http.MethodGet, "http://check.example.com/path", nil)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusInternalServerError {
t.Fatalf("ServeHTTP() status = %d, want %d", rr.Code, http.StatusInternalServerError)
}
if len(direct.addresses) != 1 || direct.addresses[0] != "check.example.com:80" {
t.Fatalf("direct dialer addresses = %#v, want []string{\"check.example.com:80\"}", direct.addresses)
}
if len(proxied.addresses) != 0 {
t.Fatalf("proxied dialer should not be used, got %#v", proxied.addresses)
}
}
func TestProxyHandlerBypassesConnectRequestsByTargetHost(t *testing.T) {
direct := &recordingDialer{name: "direct"}
proxied := &recordingDialer{name: "proxied"}
bypassDialer, err := dialer.NewBypassDialer([]string{"*.example.com"}, direct, proxied)
if err != nil {
t.Fatalf("NewBypassDialer() error = %v", err)
}
h := NewProxyHandler(bypassDialer, testProxyLogger(), "")
req := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{Host: "secure.example.com:443"},
Host: "secure.example.com:443",
RequestURI: "secure.example.com:443",
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
}
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusBadGateway {
t.Fatalf("ServeHTTP() status = %d, want %d", rr.Code, http.StatusBadGateway)
}
if len(direct.addresses) != 1 || direct.addresses[0] != "secure.example.com:443" {
t.Fatalf("direct dialer addresses = %#v, want []string{\"secure.example.com:443\"}", direct.addresses)
}
if len(proxied.addresses) != 0 {
t.Fatalf("proxied dialer should not be used, got %#v", proxied.addresses)
}
}
+31 -7
View File
@@ -19,8 +19,24 @@ import (
const (
COPY_BUF = 128 * 1024
BAD_REQ_MSG = "Bad Request\n"
// Reduced idle pool: the proxy handler makes upstream connections per request,
// not persistent keep-alive sessions. 10 total / 2 per host is plenty and
// avoids leaking hundreds of idle goroutines/sockets under bursty traffic.
TRANSPORT_MAX_IDLE_CONNS = 10
TRANSPORT_MAX_IDLE_CONNS_PER_HOST = 2
TRANSPORT_IDLE_CONN_TIMEOUT = 60 * time.Second
)
// copyBufPool reuses 128 KiB buffers for bidirectional data relay,
// avoiding per-connection heap allocations.
var copyBufPool = sync.Pool{
New: func() any {
b := make([]byte, COPY_BUF)
return &b
},
}
type ProxyHandler struct {
logger *clog.CondLogger
dialer dialer.ContextDialer
@@ -29,8 +45,9 @@ type ProxyHandler struct {
func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger) *ProxyHandler {
httptransport := &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
MaxIdleConns: TRANSPORT_MAX_IDLE_CONNS,
MaxIdleConnsPerHost: TRANSPORT_MAX_IDLE_CONNS_PER_HOST,
IdleConnTimeout: TRANSPORT_IDLE_CONN_TIMEOUT,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: dialer.DialContext,
@@ -119,7 +136,10 @@ func proxy(ctx context.Context, left, right net.Conn) {
wg := sync.WaitGroup{}
cpy := func(dst, src net.Conn) {
defer wg.Done()
io.Copy(dst, src)
// Grab a pooled buffer for this copy direction.
bufp := copyBufPool.Get().(*[]byte)
defer copyBufPool.Put(bufp)
io.CopyBuffer(dst, src, *bufp)
dst.Close()
}
wg.Add(2)
@@ -145,7 +165,9 @@ func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer
wg := sync.WaitGroup{}
ltr := func(dst net.Conn, src io.Reader) {
defer wg.Done()
io.Copy(dst, src)
bufp := copyBufPool.Get().(*[]byte)
defer copyBufPool.Put(bufp)
io.CopyBuffer(dst, src, *bufp)
dst.Close()
}
rtl := func(dst io.Writer, src io.Reader) {
@@ -226,12 +248,14 @@ func flush(flusher interface{}) bool {
}
func copyBody(wr io.Writer, body io.Reader) {
buf := make([]byte, COPY_BUF)
// Use pooled buffer to avoid per-call allocation.
bufp := copyBufPool.Get().(*[]byte)
defer copyBufPool.Put(bufp)
for {
bread, read_err := body.Read(buf)
bread, read_err := body.Read(*bufp)
var write_err error
if bread > 0 {
_, write_err = wr.Write(buf[:bread])
_, write_err = wr.Write((*bufp)[:bread])
flush(wr)
}
if read_err != nil || write_err != nil {
+38 -1
View File
@@ -2,11 +2,15 @@ package handler
import (
"context"
"fmt"
"io"
"log"
"net"
"strings"
"github.com/Alexey71/opera-proxy/dialer"
"github.com/things-go/go-socks5"
"github.com/things-go/go-socks5/statute"
)
func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger) (*socks5.Server, error) {
@@ -17,12 +21,45 @@ func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger) (*socks5.Se
EnableConnect: true,
},
),
socks5.WithDial(dialer.DialContext),
socks5.WithResolver(DummySocksResolver{}),
socks5.WithConnectHandle(func(ctx context.Context, writer io.Writer, request *socks5.Request) error {
return handleSocksConnect(ctx, writer, request, dialer)
}),
}
return socks5.NewServer(opts...), nil
}
func handleSocksConnect(ctx context.Context, writer io.Writer, request *socks5.Request, upstream dialer.ContextDialer) error {
target, err := upstream.DialContext(ctx, "tcp", request.DestAddr.String())
if err != nil {
reply := statute.RepHostUnreachable
msg := err.Error()
if strings.Contains(msg, "refused") {
reply = statute.RepConnectionRefused
} else if strings.Contains(msg, "network is unreachable") {
reply = statute.RepNetworkUnreachable
}
if sendErr := socks5.SendReply(writer, reply, nil); sendErr != nil {
return fmt.Errorf("failed to send reply, %v", sendErr)
}
return fmt.Errorf("connect to %v failed, %v", request.RawDestAddr, err)
}
if err := socks5.SendReply(writer, statute.RepSuccess, target.LocalAddr()); err != nil {
target.Close()
return fmt.Errorf("failed to send reply, %v", err)
}
clientConn, ok := writer.(net.Conn)
if !ok {
target.Close()
return fmt.Errorf("writer is %T, expected net.Conn", writer)
}
proxy(ctx, clientConn, target)
return nil
}
type DummySocksResolver struct{}
func (_ DummySocksResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
+1444 -174
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
package main
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func TestLoadProxyBypassList(t *testing.T) {
dir := t.TempDir()
filename := filepath.Join(dir, "proxy-bypass.txt")
content := "" +
"# comment only\n" +
"api2.sec-tunnel.com\n" +
"*.example.com, https://download.test.local/list.txt\n" +
"api2.sec-tunnel.com # duplicate\n" +
"\n"
if err := os.WriteFile(filename, []byte(content), 0o644); err != nil {
t.Fatalf("os.WriteFile() error = %v", err)
}
got, err := loadProxyBypassList(filename)
if err != nil {
t.Fatalf("loadProxyBypassList() error = %v", err)
}
want := []string{
"api2.sec-tunnel.com",
"*.example.com",
"https://download.test.local/list.txt",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("loadProxyBypassList() = %#v, want %#v", got, want)
}
}
+21
View File
@@ -0,0 +1,21 @@
country_code,country_name,host,ip_address,port,speed_ms,speed_status
EU,,eu14.sec-tunnel.com,77.111.247.43,443,566,ok
EU,,eu15.sec-tunnel.com,77.111.247.44,443,577,ok
EU,,eu17.sec-tunnel.com,77.111.247.55,443,587,ok
EU,,eu13.sec-tunnel.com,77.111.247.233,443,600,ok
EU,,eu16.sec-tunnel.com,77.111.247.46,443,606,ok
EU,,eu19.sec-tunnel.com,77.111.247.57,443,607,ok
EU,,eu12.sec-tunnel.com,77.111.247.232,443,622,ok
EU,,eu18.sec-tunnel.com,77.111.247.56,443,651,ok
AM,,am1.sec-tunnel.com,77.111.246.14,443,1269,ok
AM,,am2.sec-tunnel.com,77.111.246.60,443,1269,ok
AM,,am0.sec-tunnel.com,77.111.246.126,443,1427,ok
EU,,eu11.sec-tunnel.com,77.111.244.37,443,1429,ok
EU,,eu10.sec-tunnel.com,77.111.244.210,443,1535,ok
AS,,as8.sec-tunnel.com,77.111.245.16,443,2200,ok
AS,,as5.sec-tunnel.com,77.111.245.10,443,2263,ok
AS,,as7.sec-tunnel.com,77.111.245.15,443,2359,ok
AS,,as6.sec-tunnel.com,77.111.245.12,443,2430,ok
AS,,as9.sec-tunnel.com,77.111.245.17,443,3300,ok
AM,,am3.sec-tunnel.com,77.111.246.62,443,11270,ok
AM,,am4.sec-tunnel.com,77.111.246.64,443,11654,ok
1 country_code country_name host ip_address port speed_ms speed_status
2 EU eu14.sec-tunnel.com 77.111.247.43 443 566 ok
3 EU eu15.sec-tunnel.com 77.111.247.44 443 577 ok
4 EU eu17.sec-tunnel.com 77.111.247.55 443 587 ok
5 EU eu13.sec-tunnel.com 77.111.247.233 443 600 ok
6 EU eu16.sec-tunnel.com 77.111.247.46 443 606 ok
7 EU eu19.sec-tunnel.com 77.111.247.57 443 607 ok
8 EU eu12.sec-tunnel.com 77.111.247.232 443 622 ok
9 EU eu18.sec-tunnel.com 77.111.247.56 443 651 ok
10 AM am1.sec-tunnel.com 77.111.246.14 443 1269 ok
11 AM am2.sec-tunnel.com 77.111.246.60 443 1269 ok
12 AM am0.sec-tunnel.com 77.111.246.126 443 1427 ok
13 EU eu11.sec-tunnel.com 77.111.244.37 443 1429 ok
14 EU eu10.sec-tunnel.com 77.111.244.210 443 1535 ok
15 AS as8.sec-tunnel.com 77.111.245.16 443 2200 ok
16 AS as5.sec-tunnel.com 77.111.245.10 443 2263 ok
17 AS as7.sec-tunnel.com 77.111.245.15 443 2359 ok
18 AS as6.sec-tunnel.com 77.111.245.12 443 2430 ok
19 AS as9.sec-tunnel.com 77.111.245.17 443 3300 ok
20 AM am3.sec-tunnel.com 77.111.246.62 443 11270 ok
21 AM am4.sec-tunnel.com 77.111.246.64 443 11654 ok
+436
View File
@@ -0,0 +1,436 @@
106.51.185.233:8080
213.169.33.10:8001
103.227.187.1:6080
131.161.68.41:35944
61.49.87.3:80
203.28.67.74:8080
152.42.201.82:1080
82.115.60.66:80
24.152.58.108:999
190.60.47.62:999
176.88.168.107:8080
38.51.207.184:999
103.136.171.145:8080
118.69.186.75:1452
200.35.34.134:999
204.157.251.178:999
185.76.240.94:10001
176.88.166.207:8080
51.159.28.39:80
112.203.192.108:8082
2.27.32.81:3128
79.137.204.101:1080
46.107.230.122:1080
8.209.239.31:30000
179.49.113.225:999
193.43.159.163:8080
186.248.87.172:5678
138.68.235.51:80
118.70.151.55:1080
79.101.106.74:33608
58.33.109.114:2021
103.78.83.22:8083
185.76.240.97:10001
192.252.211.197:14921
168.138.202.218:3128
77.91.77.220:3128
103.132.52.229:8080
185.76.240.99:10001
138.84.65.117:8081
187.111.144.102:8080
181.94.197.37:8080
185.242.107.91:3129
5.102.109.41:999
185.76.240.91:10001
31.97.100.157:8080
114.111.151.41:80
5.196.101.18:3128
102.68.135.147:8080
194.87.215.166:9443
14.232.166.150:5678
185.114.73.2:1080
8.212.177.126:8080
103.153.246.54:8181
103.46.186.161:8080
201.234.50.194:3128
210.223.44.230:3128
176.61.151.123:80
213.131.85.28:1976
45.92.108.112:80
135.148.120.6:80
108.161.135.118:80
138.124.99.216:8888
103.103.146.149:7080
79.137.17.104:80
185.85.111.18:80
112.198.22.122:80
80.78.73.142:65530
94.23.9.170:80
104.225.220.233:80
133.18.234.13:80
34.135.166.24:80
195.231.69.203:80
193.105.62.11:58973
186.26.95.249:61445
174.104.115.21:80
35.209.198.222:80
167.99.124.118:80
201.222.50.218:80
174.138.119.88:80
185.76.241.223:10001
159.223.225.118:8888
163.172.167.48:80
65.108.103.19:80
46.17.47.48:80
13.80.134.180:80
154.43.62.22:80
190.119.132.61:80
46.21.186.252:1080
147.231.163.133:80
124.108.6.20:8085
5.161.103.41:88
143.198.135.176:80
103.205.64.153:80
190.110.226.122:80
37.187.74.125:80
185.76.241.158:10001
103.151.20.131:80
66.111.113.34:80
183.110.216.159:8090
31.220.78.244:80
95.214.9.93:3128
23.247.136.254:80
185.76.240.64:10001
194.102.38.53:80
62.149.165.171:80
185.76.240.60:10001
103.65.237.92:5678
138.91.159.185:80
219.93.101.62:80
185.76.240.61:10001
154.65.39.7:80
79.174.12.190:80
197.221.240.240:80
51.79.135.131:8080
152.53.65.215:8090
200.174.198.32:8888
167.99.236.14:80
27.34.242.98:80
141.147.9.254:80
13.230.49.39:8080
149.248.215.39:8081
113.160.132.26:8080
8.219.97.248:80
209.135.168.41:80
203.23.238.21:80
41.220.16.213:80
163.172.53.142:80
34.44.49.215:80
152.230.215.123:80
211.38.188.120:9080
35.225.22.61:80
45.146.163.31:80
213.177.2.55:80
41.220.16.209:80
196.1.93.16:80
197.221.249.196:80
195.26.224.135:80
219.249.37.107:8382
168.222.254.88:3128
156.38.112.11:80
212.231.191.23:80
170.64.170.204:8080
45.167.125.21:999
102.223.9.53:80
69.70.244.34:80
97.74.87.226:80
190.119.132.62:80
219.65.73.81:80
210.177.178.148:80
87.255.196.143:80
41.220.16.218:80
219.93.101.63:80
219.93.101.60:80
85.214.204.79:80
39.109.113.97:4090
196.223.129.21:80
34.81.160.132:80
38.60.196.214:80
194.71.227.149:80
197.221.240.178:80
78.28.152.113:80
41.220.16.208:80
154.65.39.8:80
197.221.234.252:80
197.221.249.198:80
41.220.16.215:80
193.181.35.106:8118
46.249.100.124:80
194.150.110.134:80
147.91.22.150:80
159.65.221.25:80
139.162.200.213:80
150.136.163.51:80
91.132.92.231:80
93.127.163.52:80
91.132.92.150:80
34.140.137.151:80
198.111.166.184:80
192.73.244.36:80
66.135.16.53:80
89.58.55.33:80
151.236.24.38:80
207.180.254.198:8080
212.47.232.28:80
84.39.112.144:3128
202.133.88.173:80
80.74.54.148:3128
109.236.88.82:80
178.156.224.42:3128
195.91.129.101:1337
103.90.66.19:1080
187.19.127.246:8011
138.36.199.14:4153
41.223.234.116:37259
110.93.206.62:5678
194.61.24.198:1080
103.37.82.134:39873
103.136.107.70:1080
14.241.241.185:4145
110.232.86.221:1080
83.218.186.22:5678
103.174.122.197:8199
110.235.248.142:1080
69.48.201.94:80
31.43.194.184:1080
119.148.47.166:22122
76.81.6.107:31008
103.88.169.106:33149
195.138.65.34:5678
109.166.207.162:3629
91.247.92.63:5678
81.228.44.187:65530
181.115.75.102:5678
113.53.29.228:13629
103.111.136.82:8199
168.253.92.93:10808
138.68.247.51:1080
36.37.244.41:5678
103.156.14.234:1080
160.22.22.85:5678
45.236.152.21:4145
177.136.124.47:56113
111.67.103.162:1080
128.199.37.92:1080
202.70.80.153:5678
192.248.95.98:54126
34.122.187.196:80
186.47.213.158:5678
46.146.216.44:1080
87.117.11.57:1080
20.64.237.168:1080
202.51.103.154:5678
198.0.198.132:54321
163.47.37.190:1080
125.24.156.113:7080
139.255.63.170:14888
46.30.46.133:3128
103.39.245.106:1080
202.84.76.190:5678
193.233.254.82:1080
206.189.154.204:8888
31.43.63.70:4145
199.229.254.129:4145
187.157.30.202:4153
114.130.153.122:58080
192.252.214.20:15864
144.124.227.145:8080
203.189.135.73:1080
213.169.33.8:8001
202.5.53.145:9355
103.172.120.102:8097
198.8.94.174:39078
202.40.188.2:1088
192.111.135.17:18302
192.252.209.158:4145
124.83.114.43:8081
41.21.182.179:5678
103.175.127.230:60606
103.102.12.134:1111
103.19.58.151:8080
45.79.203.254:48388
143.137.116.72:1080
103.120.221.129:8083
202.154.241.199:808
103.99.110.222:5678
103.151.74.5:2025
5.134.48.59:8080
202.154.19.63:8083
103.17.246.60:1080
103.156.248.78:8080
202.74.243.182:228
97.105.12.186:4153
103.163.244.106:1080
102.212.44.151:12354
163.53.204.178:9813
157.15.40.251:7777
79.132.136.58:3128
50.238.47.86:32100
192.111.135.18:18301
103.132.52.32:8080
47.237.169.183:1080
202.181.16.45:52929
192.252.209.155:14455
94.247.241.70:51006
65.21.201.149:8080
147.45.167.84:3128
193.233.254.61:1080
5.255.123.43:1080
192.111.139.165:4145
20.64.246.197:1080
114.130.175.146:1088
194.67.99.223:1080
91.108.243.203:3128
91.150.189.122:60647
192.252.210.233:4145
157.66.16.52:8080
91.99.182.39:4000
31.43.33.56:4153
8.220.143.77:1080
45.148.30.212:3128
89.43.133.146:8080
168.110.52.228:3128
103.56.115.156:7890
181.236.221.138:4145
103.167.156.202:1080
103.183.19.34:3128
41.139.147.86:5678
113.192.31.17:8080
203.189.141.138:1080
103.97.140.157:1080
37.60.237.110:30001
65.108.203.36:18080
192.252.216.81:4145
192.111.129.150:4145
103.153.63.223:1080
103.189.63.149:53053
198.8.94.170:4145
38.76.200.182:58367
162.240.19.30:80
185.76.240.178:10001
41.169.151.154:4153
103.180.123.27:8080
5.161.50.82:8118
121.135.144.141:8096
75.84.71.14:80
193.23.194.147:3128
185.76.241.162:10001
196.1.97.198:80
185.76.240.208:10001
185.76.240.45:10001
46.47.197.210:3128
23.88.88.102:80
45.12.151.226:2829
196.1.93.10:80
185.76.240.203:10001
150.107.140.238:3128
41.184.92.220:80
8.209.255.13:3128
143.42.66.91:80
46.29.162.166:80
41.184.92.219:80
23.88.88.105:443
116.203.139.209:5678
176.236.37.132:1080
8.211.152.121:10000
20.57.134.61:1080
202.62.34.102:1080
195.133.250.173:3128
110.235.247.105:1080
103.112.234.65:5678
212.23.69.121:1080
109.120.151.2:1080
203.189.154.80:1080
213.149.154.213:5678
117.102.115.154:4153
202.4.117.136:12321
103.239.52.100:1080
210.16.86.105:5678
168.232.213.9:4153
20.120.246.129:1080
31.13.239.4:5678
31.10.83.158:8080
27.123.254.158:6969
23.133.196.12:9000
202.166.196.105:48293
102.36.127.53:1080
154.79.248.156:5678
95.179.255.75:1080
119.148.51.30:22122
191.7.208.86:8080
122.252.179.66:5678
171.249.163.170:1452
49.151.177.202:8082
107.219.228.250:7777
113.160.188.21:1080
103.18.47.79:4145
223.25.110.37:8199
190.60.40.242:999
81.12.157.98:5678
115.127.112.178:1080
103.10.99.110:5678
124.105.55.176:30906
130.193.123.34:5678
160.19.41.60:80
31.42.6.125:5678
190.102.233.100:999
158.160.215.167:8123
103.206.68.241:1080
27.147.153.34:10888
123.200.8.6:1081
113.161.6.170:1080
185.14.149.33:4145
103.239.201.50:58765
165.0.100.149:5678
118.174.233.113:4153
110.74.195.152:1080
164.138.205.232:8080
80.78.74.8:65530
185.95.186.165:48293
202.138.240.249:8080
45.81.146.7:8080
163.61.254.104:1111
103.39.51.184:8088
107.172.102.234:40621
197.164.101.13:1981
181.48.243.194:4153
82.209.251.53:45678
1.179.172.45:31225
209.14.108.98:999
120.28.193.126:8082
38.58.170.10:999
103.66.62.177:8080
205.177.85.130:39593
103.30.211.34:80
91.211.115.127:1080
202.58.77.194:8031
37.26.86.206:4145
62.4.37.104:60606
91.147.235.162:4153
185.76.240.33:10001
43.153.28.68:3128
185.76.240.57:10001
185.76.240.12:10001
185.76.241.132:10001
185.76.241.232:10001
185.76.240.19:10001
167.71.196.28:8080
185.76.240.177:10001
185.76.240.18:10001
185.76.240.47:10001
172.236.140.124:3128
185.76.240.55:10001
185.76.240.201:10001
91.121.63.51:1080
62.113.119.14:8080
213.154.2.210:3128
+19
View File
@@ -0,0 +1,19 @@
# комментарий
*.ru
#avito
avito.ru
*.avito.ru
avito.st
cdn.avito.ru
img.avito.ru
static.avito.ru
#ozon
ozon.ru
ozon.com
*.ozon.ru
ozonusercontent.com
ozon-images.ru
ozon-st.com
cdn.ozon.ru
+17
View File
@@ -12,6 +12,22 @@ const (
SE_STATUS_OK int64 = 0
)
type APIError struct {
Code int64
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("API responded with error message: code=%d, msg=%q", e.Code, e.Message)
}
func newAPIError(status SEStatusPair) error {
return &APIError{
Code: status.Code,
Message: status.Message,
}
}
type SEStatusPair struct {
Code int64
Message string
@@ -85,6 +101,7 @@ type SEGeoListResponse struct {
type SEIPEntry struct {
Geo SEGeoEntry `json:"geo"`
Host string `json:"host,omitempty"`
IP string `json:"ip"`
Ports []uint16 `json:"ports"`
}
+32 -53
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
@@ -16,10 +15,10 @@ import (
)
const (
ANON_EMAIL_LOCALPART_BYTES = 32
ANON_PASSWORD_BYTES = 20
DEVICE_ID_BYTES = 20
READ_LIMIT int64 = 128 * 1024
ANON_EMAIL_LOCALPART_BYTES = 32
ANON_PASSWORD_BYTES = 20
DEVICE_ID_BYTES = 20
READ_LIMIT int64 = 128 * 1024
)
type SEEndpoints struct {
@@ -73,9 +72,10 @@ type SEClient struct {
type StrKV map[string]string
// Instantiates SurfEasy client with default settings and given API keys.
// Optional `transport` parameter allows to override HTTP transport used
// for HTTP calls
// NewSEClient instantiates a SurfEasy client.
// apiUsername/apiSecret are the application-level Digest Auth credentials
// embedded in every Opera client — they are NOT per-user and must not be randomised.
// transport may be nil (uses http.DefaultTransport).
func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*SEClient, error) {
if transport == nil {
transport = http.DefaultTransport
@@ -83,7 +83,7 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S
rng := rand.New(RandomSource)
device_id, err := randomCapitalHexString(rng, DEVICE_ID_BYTES)
deviceID, err := randomCapitalHexString(rng, DEVICE_ID_BYTES)
if err != nil {
return nil, err
}
@@ -93,17 +93,15 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S
return nil, err
}
res := &SEClient{
return &SEClient{
httpClient: &http.Client{
Jar: jar,
Transport: dac.NewDigestTransport(apiUsername, apiSecret, transport),
},
Settings: DefaultSESettings,
rng: rng,
DeviceID: device_id,
}
return res, nil
DeviceID: deviceID,
}, nil
}
func (c *SEClient) ResetCookies() error {
@@ -125,6 +123,8 @@ func (c *SEClient) AnonRegister(ctx context.Context) error {
return err
}
// Each run generates a fresh random subscriber identity — this is the
// actual anonymisation layer. The API-level credentials above are fixed.
c.SubscriberEmail = fmt.Sprintf("%s@%s.best.vpn", localPart, c.Settings.ClientType)
c.SubscriberPassword = capitalHexSHA1(c.SubscriberEmail)
@@ -138,13 +138,12 @@ func (c *SEClient) Register(ctx context.Context) error {
}
func (c *SEClient) register(ctx context.Context) error {
err := c.resetCookies()
if err != nil {
if err := c.resetCookies(); err != nil {
return err
}
var regRes SERegisterSubscriberResponse
err = c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{
err := c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{
"email": c.SubscriberEmail,
"password": c.SubscriberPassword,
}, &regRes)
@@ -153,8 +152,7 @@ func (c *SEClient) register(ctx context.Context) error {
}
if regRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
regRes.Status.Code, regRes.Status.Message)
return newAPIError(regRes.Status)
}
return nil
}
@@ -174,8 +172,7 @@ func (c *SEClient) RegisterDevice(ctx context.Context) error {
}
if regRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
regRes.Status.Code, regRes.Status.Message)
return newAPIError(regRes.Status)
}
c.AssignedDeviceID = regRes.Data.DeviceID
@@ -197,8 +194,7 @@ func (c *SEClient) GeoList(ctx context.Context) ([]SEGeoEntry, error) {
}
if geoListRes.Status.Code != SE_STATUS_OK {
return nil, fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
geoListRes.Status.Code, geoListRes.Status.Message)
return nil, newAPIError(geoListRes.Status)
}
return geoListRes.Data.Geos, nil
@@ -218,8 +214,7 @@ func (c *SEClient) Discover(ctx context.Context, requestedGeo string) ([]SEIPEnt
}
if discoverRes.Status.Code != SE_STATUS_OK {
return nil, fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
discoverRes.Status.Code, discoverRes.Status.Message)
return nil, newAPIError(discoverRes.Status)
}
return discoverRes.Data.IPs, nil
@@ -229,13 +224,12 @@ func (c *SEClient) Login(ctx context.Context) error {
c.Mux.Lock()
defer c.Mux.Unlock()
err := c.resetCookies()
if err != nil {
if err := c.resetCookies(); err != nil {
return err
}
var loginRes SESubscriberLoginResponse
err = c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{
err := c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{
"login": c.SubscriberEmail,
"password": c.SubscriberPassword,
"client_type": c.Settings.ClientType,
@@ -245,8 +239,7 @@ func (c *SEClient) Login(ctx context.Context) error {
}
if loginRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
loginRes.Status.Code, loginRes.Status.Message)
return newAPIError(loginRes.Status)
}
return nil
}
@@ -264,8 +257,7 @@ func (c *SEClient) DeviceGeneratePassword(ctx context.Context) error {
}
if genRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
genRes.Status.Code, genRes.Status.Message)
return newAPIError(genRes.Status)
}
c.DevicePassword = genRes.Data.DevicePassword
@@ -293,16 +285,12 @@ func (c *SEClient) RpcCall(ctx context.Context, endpoint string, params map[stri
}
func (c *SEClient) rpcCall(ctx context.Context, endpoint string, params map[string]string, res interface{}) error {
input := make(url.Values)
input := make(url.Values, len(params))
for k, v := range params {
input[k] = []string{v}
}
req, err := http.NewRequestWithContext(
ctx,
"POST",
endpoint,
strings.NewReader(input.Encode()),
)
req, err := http.NewRequestWithContext(ctx, "POST", endpoint,
strings.NewReader(input.Encode()))
if err != nil {
return err
}
@@ -316,26 +304,17 @@ func (c *SEClient) rpcCall(ctx context.Context, endpoint string, params map[stri
}
if resp.StatusCode != http.StatusOK {
cleanupBody(resp.Body)
return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header)
}
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(res)
err = json.NewDecoder(resp.Body).Decode(res)
cleanupBody(resp.Body)
if err != nil {
return err
}
return nil
return err
}
// Does cleanup of HTTP response in order to make it reusable by keep-alive
// logic of HTTP client
// cleanupBody drains and closes an HTTP response body to allow connection reuse.
func cleanupBody(body io.ReadCloser) {
io.Copy(ioutil.Discard, &io.LimitedReader{
R: body,
N: READ_LIMIT,
})
io.Copy(io.Discard, &io.LimitedReader{R: body, N: READ_LIMIT})
body.Close()
}