mirror of
https://github.com/Alexey71/opera-proxy.git
synced 2026-05-14 22:50:59 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f4651508c | |||
| e7d9ae4706 | |||
| 7546402687 | |||
| 63b9203be4 | |||
| b99a41e040 |
@@ -83,11 +83,23 @@ You can also download the proxy list from a URL. If the download fails, the app
|
|||||||
```
|
```
|
||||||
$ ./opera-proxy -api-proxy-list-url https://example.com/proxies.txt -country EU
|
$ ./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
|
$ ./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:
|
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:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -102,40 +114,41 @@ If SurfEasy discover returns API error `801`, the app also automatically tries `
|
|||||||
|
|
||||||
| Argument | Type | Description |
|
| Argument | Type | Description |
|
||||||
| -------- | ---- | ----------- |
|
| -------- | ---- | ----------- |
|
||||||
| api-address | String | override IP address of api2.sec-tunnel.com |
|
| -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-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-client-version | String | client version reported to SurfEasy API (default "Stable 114.0.5282.21") |
|
||||||
| api-login | String | SurfEasy API login (default "se0316") |
|
| -api-login | String | SurfEasy API login (default "se0316") |
|
||||||
| api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") |
|
| -api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") |
|
||||||
| api-proxy | String | additional proxy server used to access SurfEasy API |
|
| -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-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-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-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") |
|
| -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") |
|
| -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/`) |
|
| -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 |
|
| -cafile | String | use custom CA certificate bundle file |
|
||||||
| config | String | read configuration from file with space-separated keys and values |
|
| -config | String | read configuration from file with space-separated keys and values |
|
||||||
| country | String | desired proxy location (default "EU") |
|
| -country | String | desired proxy location (default "EU") |
|
||||||
| discover-csv | String | read proxy endpoints from CSV instead of SurfEasy discover API |
|
| -discover-csv | String | read proxy endpoints from CSV instead of SurfEasy discover API |
|
||||||
| dp-export | - | export configuration for dumbproxy |
|
| -dp-export | - | export configuration for dumbproxy |
|
||||||
| fake-SNI | String | domain name to use as SNI in outbound TLS and in tunneled TLS ClientHello when possible |
|
| -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` |
|
||||||
| init-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
|
| -fake-SNI | String | domain name to use as SNI in communications with servers |
|
||||||
| init-retry-interval | Duration | delay between initialization retries (default 5s) |
|
| -init-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
|
||||||
| list-countries | - | list available countries and exit |
|
| -init-retry-interval | Duration | delay between initialization retries (default 5s) |
|
||||||
| list-proxies | - | output proxy list and exit |
|
| -list-countries | - | list available countries and exit |
|
||||||
| override-proxy-address | string | use fixed proxy address instead of server address returned by SurfEasy API |
|
| -list-proxies | - | output proxy list and exit |
|
||||||
| 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 |
|
| -override-proxy-address | string | use fixed proxy address instead of server address returned by SurfEasy API |
|
||||||
| proxy-blacklist | String | path to file with blacklisted proxy addresses, one `host[:port]` per line |
|
| -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 | 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 |
|
||||||
| refresh | Duration | login refresh interval (default 4h0m0s) |
|
| -proxy-blacklist | String | path to file with blacklisted proxy addresses, one `host[:port]` per line |
|
||||||
| refresh-retry | Duration | login refresh retry interval (default 5s) |
|
| -refresh | Duration | login refresh interval (default 4h0m0s) |
|
||||||
| server-selection | Enum | server selection policy (first/random/fastest) (default fastest) |
|
| -refresh-retry | Duration | login refresh retry interval (default 5s) |
|
||||||
| server-selection-dl-limit | Number | restrict amount of downloaded data per connection by fastest server selection |
|
| -server-selection | Enum | server selection policy (first/random/fastest) (default fastest) |
|
||||||
| 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-dl-limit | Number | restrict amount of downloaded data per connection by fastest server selection |
|
||||||
| server-selection-timeout | Duration | timeout given for server selection function to produce result (default 30s) |
|
| -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`) |
|
||||||
| timeout | Duration | timeout for network operations (default 10s) |
|
| -server-selection-timeout | Duration | timeout given for server selection function to produce result (default 30s) |
|
||||||
| verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
|
| -socks-mode | - | listen for SOCKS requests instead of HTTP |
|
||||||
| version | - | show program version and exit |
|
| -timeout | Duration | timeout for network operations (default 10s) |
|
||||||
| socks-mode | - | listen for SOCKS requests instead of HTTP |
|
| -verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
|
||||||
|
| -version | - | show program version and exit |
|
||||||
|
|
||||||
|
|||||||
+14
-3
@@ -115,9 +115,14 @@ func (d *ProxyDialer) DialContext(ctx context.Context, network, address string)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if uTLSServerName != "" {
|
if uTLSServerName != "" {
|
||||||
// Custom cert verification logic:
|
// Custom TLS verification strategy:
|
||||||
// DO NOT send SNI extension of TLS ClientHello
|
// - Do NOT send SNI in ClientHello (use fakeSNI, may be empty string).
|
||||||
// DO peer certificate verification against specified servername
|
// - 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{
|
conn = tls.Client(conn, &tls.Config{
|
||||||
ServerName: fakeSNI,
|
ServerName: fakeSNI,
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
@@ -186,6 +191,12 @@ func (d *ProxyDialer) Address() (string, error) {
|
|||||||
return d.address()
|
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) {
|
func readResponse(r io.Reader, req *http.Request) (*http.Response, error) {
|
||||||
endOfResponse := []byte("\r\n\r\n")
|
endOfResponse := []byte("\r\n\r\n")
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ module github.com/Alexey71/opera-proxy
|
|||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
toolchain go1.26.2
|
toolchain go1.26.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Alexey71/go-http-digest-auth-client v1.1.3
|
github.com/Alexey71/go-http-digest-auth-client v1.1.3
|
||||||
github.com/Alexey71/go-multierror v1.1.3
|
github.com/Alexey71/go-multierror v1.1.3
|
||||||
github.com/ncruces/go-dns v1.3.3
|
github.com/ncruces/go-dns v1.3.3
|
||||||
github.com/things-go/go-socks5 v0.1.1
|
github.com/things-go/go-socks5 v0.1.1
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260423152011-b9e53593a607
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2
|
||||||
golang.org/x/net v0.53.0
|
golang.org/x/net v0.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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/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 h1:48hy9cHEXPKeG91G/g4n8zW4uynzPUQy/FkcrJ7r5AY=
|
||||||
github.com/things-go/go-socks5 v0.1.1/go.mod h1:1YBHVYG7Oli5ae+Pwkp630cPAwY1pjUPmohO1n0Emg0=
|
github.com/things-go/go-socks5 v0.1.1/go.mod h1:1YBHVYG7Oli5ae+Pwkp630cPAwY1pjUPmohO1n0Emg0=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260423152011-b9e53593a607 h1:WlWLkLEVGjGS9LziRuLo1Ut+UkpzbXxFQh94eUUVjeg=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2 h1:7Y5FZkvYs5XMyG0VS/pONmKIgD9+9eqcm1DGar541SA=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260423152011-b9e53593a607/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
+40
-27
@@ -19,19 +19,35 @@ import (
|
|||||||
const (
|
const (
|
||||||
COPY_BUF = 128 * 1024
|
COPY_BUF = 128 * 1024
|
||||||
BAD_REQ_MSG = "Bad Request\n"
|
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 {
|
type ProxyHandler struct {
|
||||||
logger *clog.CondLogger
|
logger *clog.CondLogger
|
||||||
dialer dialer.ContextDialer
|
dialer dialer.ContextDialer
|
||||||
httptransport http.RoundTripper
|
httptransport http.RoundTripper
|
||||||
fakeSNI string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger, fakeSNI string) *ProxyHandler {
|
func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger) *ProxyHandler {
|
||||||
httptransport := &http.Transport{
|
httptransport := &http.Transport{
|
||||||
MaxIdleConns: 100,
|
MaxIdleConns: TRANSPORT_MAX_IDLE_CONNS,
|
||||||
IdleConnTimeout: 90 * time.Second,
|
MaxIdleConnsPerHost: TRANSPORT_MAX_IDLE_CONNS_PER_HOST,
|
||||||
|
IdleConnTimeout: TRANSPORT_IDLE_CONN_TIMEOUT,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
DialContext: dialer.DialContext,
|
DialContext: dialer.DialContext,
|
||||||
@@ -40,7 +56,6 @@ func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger, fakeS
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
httptransport: httptransport,
|
httptransport: httptransport,
|
||||||
fakeSNI: fakeSNI,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +70,7 @@ func (s *ProxyHandler) HandleTunnel(wr http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
if req.ProtoMajor == 0 || req.ProtoMajor == 1 {
|
if req.ProtoMajor == 0 || req.ProtoMajor == 1 {
|
||||||
// Upgrade client connection
|
// Upgrade client connection
|
||||||
localconn, rw, err := hijack(wr)
|
localconn, _, err := hijack(wr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Can't hijack client connection: %v", err)
|
s.logger.Error("Can't hijack client connection: %v", err)
|
||||||
http.Error(wr, "Can't hijack client connection", http.StatusInternalServerError)
|
http.Error(wr, "Can't hijack client connection", http.StatusInternalServerError)
|
||||||
@@ -66,16 +81,12 @@ func (s *ProxyHandler) HandleTunnel(wr http.ResponseWriter, req *http.Request) {
|
|||||||
// Inform client connection is built
|
// Inform client connection is built
|
||||||
fmt.Fprintf(localconn, "HTTP/%d.%d 200 OK\r\n\r\n", req.ProtoMajor, req.ProtoMinor)
|
fmt.Fprintf(localconn, "HTTP/%d.%d 200 OK\r\n\r\n", req.ProtoMajor, req.ProtoMinor)
|
||||||
|
|
||||||
clientReader := io.Reader(localconn)
|
proxy(req.Context(), localconn, conn)
|
||||||
if rw != nil && rw.Reader.Buffered() > 0 {
|
|
||||||
clientReader = io.MultiReader(rw.Reader, localconn)
|
|
||||||
}
|
|
||||||
proxy(req.Context(), localconn, clientReader, conn, s.fakeSNI)
|
|
||||||
} else if req.ProtoMajor == 2 {
|
} else if req.ProtoMajor == 2 {
|
||||||
wr.Header()["Date"] = nil
|
wr.Header()["Date"] = nil
|
||||||
wr.WriteHeader(http.StatusOK)
|
wr.WriteHeader(http.StatusOK)
|
||||||
flush(wr)
|
flush(wr)
|
||||||
proxyh2(req.Context(), req.Body, wr, conn, s.fakeSNI)
|
proxyh2(req.Context(), req.Body, wr, conn)
|
||||||
} else {
|
} else {
|
||||||
s.logger.Error("Unsupported protocol version: %s", req.Proto)
|
s.logger.Error("Unsupported protocol version: %s", req.Proto)
|
||||||
http.Error(wr, "Unsupported protocol version.", http.StatusBadRequest)
|
http.Error(wr, "Unsupported protocol version.", http.StatusBadRequest)
|
||||||
@@ -121,21 +132,19 @@ func (s *ProxyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxy(ctx context.Context, left net.Conn, leftReader io.Reader, right net.Conn, fakeSNI string) {
|
func proxy(ctx context.Context, left, right net.Conn) {
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
ltr := func(dst net.Conn, src io.Reader) {
|
cpy := func(dst, src net.Conn) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
copyWithSNIRewrite(dst, src, fakeSNI)
|
// Grab a pooled buffer for this copy direction.
|
||||||
dst.Close()
|
bufp := copyBufPool.Get().(*[]byte)
|
||||||
}
|
defer copyBufPool.Put(bufp)
|
||||||
rtl := func(dst, src net.Conn) {
|
io.CopyBuffer(dst, src, *bufp)
|
||||||
defer wg.Done()
|
|
||||||
io.Copy(dst, src)
|
|
||||||
dst.Close()
|
dst.Close()
|
||||||
}
|
}
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
go ltr(right, leftReader)
|
go cpy(left, right)
|
||||||
go rtl(left, right)
|
go cpy(right, left)
|
||||||
groupdone := make(chan struct{})
|
groupdone := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -152,11 +161,13 @@ func proxy(ctx context.Context, left net.Conn, leftReader io.Reader, right net.C
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer, right net.Conn, fakeSNI string) {
|
func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer, right net.Conn) {
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
ltr := func(dst net.Conn, src io.Reader) {
|
ltr := func(dst net.Conn, src io.Reader) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
copyWithSNIRewrite(dst, src, fakeSNI)
|
bufp := copyBufPool.Get().(*[]byte)
|
||||||
|
defer copyBufPool.Put(bufp)
|
||||||
|
io.CopyBuffer(dst, src, *bufp)
|
||||||
dst.Close()
|
dst.Close()
|
||||||
}
|
}
|
||||||
rtl := func(dst io.Writer, src io.Reader) {
|
rtl := func(dst io.Writer, src io.Reader) {
|
||||||
@@ -237,12 +248,14 @@ func flush(flusher interface{}) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyBody(wr io.Writer, body io.Reader) {
|
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 {
|
for {
|
||||||
bread, read_err := body.Read(buf)
|
bread, read_err := body.Read(*bufp)
|
||||||
var write_err error
|
var write_err error
|
||||||
if bread > 0 {
|
if bread > 0 {
|
||||||
_, write_err = wr.Write(buf[:bread])
|
_, write_err = wr.Write((*bufp)[:bread])
|
||||||
flush(wr)
|
flush(wr)
|
||||||
}
|
}
|
||||||
if read_err != nil || write_err != nil {
|
if read_err != nil || write_err != nil {
|
||||||
|
|||||||
+4
-4
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/things-go/go-socks5/statute"
|
"github.com/things-go/go-socks5/statute"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger, fakeSNI string) (*socks5.Server, error) {
|
func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger) (*socks5.Server, error) {
|
||||||
opts := []socks5.Option{
|
opts := []socks5.Option{
|
||||||
socks5.WithLogger(socks5.NewLogger(logger)),
|
socks5.WithLogger(socks5.NewLogger(logger)),
|
||||||
socks5.WithRule(
|
socks5.WithRule(
|
||||||
@@ -23,13 +23,13 @@ func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger, fakeSNI str
|
|||||||
),
|
),
|
||||||
socks5.WithResolver(DummySocksResolver{}),
|
socks5.WithResolver(DummySocksResolver{}),
|
||||||
socks5.WithConnectHandle(func(ctx context.Context, writer io.Writer, request *socks5.Request) error {
|
socks5.WithConnectHandle(func(ctx context.Context, writer io.Writer, request *socks5.Request) error {
|
||||||
return handleSocksConnect(ctx, writer, request, dialer, fakeSNI)
|
return handleSocksConnect(ctx, writer, request, dialer)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
return socks5.NewServer(opts...), nil
|
return socks5.NewServer(opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSocksConnect(ctx context.Context, writer io.Writer, request *socks5.Request, upstream dialer.ContextDialer, fakeSNI string) error {
|
func handleSocksConnect(ctx context.Context, writer io.Writer, request *socks5.Request, upstream dialer.ContextDialer) error {
|
||||||
target, err := upstream.DialContext(ctx, "tcp", request.DestAddr.String())
|
target, err := upstream.DialContext(ctx, "tcp", request.DestAddr.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reply := statute.RepHostUnreachable
|
reply := statute.RepHostUnreachable
|
||||||
@@ -56,7 +56,7 @@ func handleSocksConnect(ctx context.Context, writer io.Writer, request *socks5.R
|
|||||||
return fmt.Errorf("writer is %T, expected net.Conn", writer)
|
return fmt.Errorf("writer is %T, expected net.Conn", writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy(ctx, clientConn, request.Reader, target, fakeSNI)
|
proxy(ctx, clientConn, target)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,227 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tlsRecordHeaderLen = 5
|
|
||||||
tlsHandshakeHeaderLen = 4
|
|
||||||
tlsRecordTypeHandshake = 0x16
|
|
||||||
tlsHandshakeTypeClientHello = 0x01
|
|
||||||
tlsExtensionServerName = 0x0000
|
|
||||||
)
|
|
||||||
|
|
||||||
func copyWithSNIRewrite(dst io.Writer, src io.Reader, fakeSNI string) error {
|
|
||||||
fakeSNI = strings.TrimSpace(fakeSNI)
|
|
||||||
if fakeSNI == "" {
|
|
||||||
_, err := io.Copy(dst, src)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
br, ok := src.(*bufio.Reader)
|
|
||||||
if !ok {
|
|
||||||
br = bufio.NewReader(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
header, err := br.Peek(tlsRecordHeaderLen)
|
|
||||||
if err != nil {
|
|
||||||
_, copyErr := io.Copy(dst, br)
|
|
||||||
if copyErr != nil {
|
|
||||||
return copyErr
|
|
||||||
}
|
|
||||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !looksLikeTLSClientHelloRecord(header) {
|
|
||||||
_, err = io.Copy(dst, br)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
recordLen := int(binary.BigEndian.Uint16(header[3:5]))
|
|
||||||
record := make([]byte, tlsRecordHeaderLen+recordLen)
|
|
||||||
n, err := io.ReadFull(br, record)
|
|
||||||
if err != nil {
|
|
||||||
if n > 0 {
|
|
||||||
if _, writeErr := dst.Write(record[:n]); writeErr != nil {
|
|
||||||
return writeErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
|
||||||
_, copyErr := io.Copy(dst, br)
|
|
||||||
return copyErr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if rewritten, ok := rewriteTLSClientHelloRecordServerName(record, fakeSNI); ok {
|
|
||||||
record = rewritten
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := dst.Write(record); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = io.Copy(dst, br)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func looksLikeTLSClientHelloRecord(header []byte) bool {
|
|
||||||
return len(header) >= tlsRecordHeaderLen &&
|
|
||||||
header[0] == tlsRecordTypeHandshake &&
|
|
||||||
header[1] == 0x03 &&
|
|
||||||
header[2] <= 0x04
|
|
||||||
}
|
|
||||||
|
|
||||||
func rewriteTLSClientHelloRecordServerName(record []byte, fakeSNI string) ([]byte, bool) {
|
|
||||||
if len(record) < tlsRecordHeaderLen+tlsHandshakeHeaderLen {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if !looksLikeTLSClientHelloRecord(record[:tlsRecordHeaderLen]) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := record[tlsRecordHeaderLen:]
|
|
||||||
if len(payload) < tlsHandshakeHeaderLen || payload[0] != tlsHandshakeTypeClientHello {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
handshakeLen := readUint24(payload[1:4])
|
|
||||||
if handshakeLen > len(payload)-tlsHandshakeHeaderLen {
|
|
||||||
// ClientHello is fragmented across multiple TLS records.
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
hello := payload[tlsHandshakeHeaderLen : tlsHandshakeHeaderLen+handshakeLen]
|
|
||||||
offset := 0
|
|
||||||
if !skipLen(hello, &offset, 2+32) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if !skipOpaque8(hello, &offset) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if !skipOpaque16(hello, &offset) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if !skipOpaque8(hello, &offset) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if offset == len(hello) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if offset+2 > len(hello) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionsLenOffset := offset
|
|
||||||
extensionsLen := int(binary.BigEndian.Uint16(hello[offset : offset+2]))
|
|
||||||
offset += 2
|
|
||||||
if offset+extensionsLen > len(hello) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionsEnd := offset + extensionsLen
|
|
||||||
for offset+4 <= extensionsEnd {
|
|
||||||
extStart := offset
|
|
||||||
extType := binary.BigEndian.Uint16(hello[offset : offset+2])
|
|
||||||
extLen := int(binary.BigEndian.Uint16(hello[offset+2 : offset+4]))
|
|
||||||
offset += 4
|
|
||||||
if offset+extLen > extensionsEnd {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if extType != tlsExtensionServerName {
|
|
||||||
offset += extLen
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
extDataStart := offset
|
|
||||||
extDataEnd := offset + extLen
|
|
||||||
extData := hello[extDataStart:extDataEnd]
|
|
||||||
if len(extData) < 5 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
serverNameListLen := int(binary.BigEndian.Uint16(extData[:2]))
|
|
||||||
if 2+serverNameListLen > len(extData) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if extData[2] != 0x00 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
nameLen := int(binary.BigEndian.Uint16(extData[3:5]))
|
|
||||||
if 5+nameLen > len(extData) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
tail := extData[5+nameLen:]
|
|
||||||
newExtData := make([]byte, 2+1+2+len(fakeSNI)+len(tail))
|
|
||||||
binary.BigEndian.PutUint16(newExtData[:2], uint16(1+2+len(fakeSNI)+len(tail)))
|
|
||||||
newExtData[2] = 0x00
|
|
||||||
binary.BigEndian.PutUint16(newExtData[3:5], uint16(len(fakeSNI)))
|
|
||||||
copy(newExtData[5:], fakeSNI)
|
|
||||||
copy(newExtData[5+len(fakeSNI):], tail)
|
|
||||||
|
|
||||||
helloStart := tlsRecordHeaderLen + tlsHandshakeHeaderLen
|
|
||||||
extLenFieldStart := helloStart + extStart + 2
|
|
||||||
extDataAbsStart := helloStart + extDataStart
|
|
||||||
extDataAbsEnd := helloStart + extDataEnd
|
|
||||||
extensionsLenFieldStart := helloStart + extensionsLenOffset
|
|
||||||
|
|
||||||
delta := len(newExtData) - len(extData)
|
|
||||||
newRecord := make([]byte, 0, len(record)+delta)
|
|
||||||
newRecord = append(newRecord, record[:extDataAbsStart]...)
|
|
||||||
newRecord = append(newRecord, newExtData...)
|
|
||||||
newRecord = append(newRecord, record[extDataAbsEnd:]...)
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(newRecord[3:5], uint16(len(payload)+delta))
|
|
||||||
writeUint24(newRecord[6:9], handshakeLen+delta)
|
|
||||||
binary.BigEndian.PutUint16(newRecord[extensionsLenFieldStart:extensionsLenFieldStart+2], uint16(extensionsLen+delta))
|
|
||||||
binary.BigEndian.PutUint16(newRecord[extLenFieldStart:extLenFieldStart+2], uint16(extLen+delta))
|
|
||||||
|
|
||||||
return newRecord, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func readUint24(b []byte) int {
|
|
||||||
return int(b[0])<<16 | int(b[1])<<8 | int(b[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeUint24(dst []byte, v int) {
|
|
||||||
dst[0] = byte(v >> 16)
|
|
||||||
dst[1] = byte(v >> 8)
|
|
||||||
dst[2] = byte(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipLen(b []byte, offset *int, n int) bool {
|
|
||||||
if *offset+n > len(b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
*offset += n
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipOpaque8(b []byte, offset *int) bool {
|
|
||||||
if *offset >= len(b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
l := int(b[*offset])
|
|
||||||
*offset++
|
|
||||||
return skipLen(b, offset, l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipOpaque16(b []byte, offset *int) bool {
|
|
||||||
if *offset+2 > len(b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
l := int(binary.BigEndian.Uint16(b[*offset : *offset+2]))
|
|
||||||
*offset += 2
|
|
||||||
return skipLen(b, offset, l)
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRewriteTLSClientHelloRecordServerName(t *testing.T) {
|
|
||||||
record := buildClientHelloRecord("example.com")
|
|
||||||
|
|
||||||
rewritten, ok := rewriteTLSClientHelloRecordServerName(record, "fake.example")
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected ClientHello record to be rewritten")
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := extractServerName(t, rewritten); got != "fake.example" {
|
|
||||||
t.Fatalf("unexpected SNI after rewrite: got %q", got)
|
|
||||||
}
|
|
||||||
if got := int(binary.BigEndian.Uint16(rewritten[3:5])); got != len(rewritten)-tlsRecordHeaderLen {
|
|
||||||
t.Fatalf("unexpected TLS record length: got %d want %d", got, len(rewritten)-tlsRecordHeaderLen)
|
|
||||||
}
|
|
||||||
if got := readUint24(rewritten[6:9]); got != len(rewritten)-tlsRecordHeaderLen-tlsHandshakeHeaderLen {
|
|
||||||
t.Fatalf("unexpected handshake length: got %d want %d", got, len(rewritten)-tlsRecordHeaderLen-tlsHandshakeHeaderLen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyWithSNIRewritePreservesTrailingBytes(t *testing.T) {
|
|
||||||
record := buildClientHelloRecord("example.com")
|
|
||||||
stream := append(append([]byte{}, record...), []byte("tail")...)
|
|
||||||
|
|
||||||
var dst bytes.Buffer
|
|
||||||
if err := copyWithSNIRewrite(&dst, bytes.NewReader(stream), "fake.example"); err != nil {
|
|
||||||
t.Fatalf("copyWithSNIRewrite returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
out := dst.Bytes()
|
|
||||||
recordLen := int(binary.BigEndian.Uint16(out[3:5])) + tlsRecordHeaderLen
|
|
||||||
if got := extractServerName(t, out[:recordLen]); got != "fake.example" {
|
|
||||||
t.Fatalf("unexpected SNI in output stream: got %q", got)
|
|
||||||
}
|
|
||||||
if tail := string(out[recordLen:]); tail != "tail" {
|
|
||||||
t.Fatalf("unexpected trailing bytes: got %q", tail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteTLSClientHelloRecordServerNameSkipsFragmentedHello(t *testing.T) {
|
|
||||||
record := buildClientHelloRecord("example.com")
|
|
||||||
record[6] = 0x7f
|
|
||||||
record[7] = 0xff
|
|
||||||
record[8] = 0xff
|
|
||||||
|
|
||||||
if _, ok := rewriteTLSClientHelloRecordServerName(record, "fake.example"); ok {
|
|
||||||
t.Fatal("expected fragmented ClientHello rewrite to be skipped")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildClientHelloRecord(serverName string) []byte {
|
|
||||||
sniExtData := make([]byte, 2+1+2+len(serverName))
|
|
||||||
binary.BigEndian.PutUint16(sniExtData[:2], uint16(1+2+len(serverName)))
|
|
||||||
sniExtData[2] = 0x00
|
|
||||||
binary.BigEndian.PutUint16(sniExtData[3:5], uint16(len(serverName)))
|
|
||||||
copy(sniExtData[5:], serverName)
|
|
||||||
|
|
||||||
sniExt := makeExtension(tlsExtensionServerName, sniExtData)
|
|
||||||
otherExt := makeExtension(0x002b, []byte{0x02, 0x03, 0x04})
|
|
||||||
extensions := append(sniExt, otherExt...)
|
|
||||||
|
|
||||||
hello := make([]byte, 0, 128)
|
|
||||||
hello = append(hello, 0x03, 0x03)
|
|
||||||
hello = append(hello, bytes.Repeat([]byte{0x11}, 32)...)
|
|
||||||
hello = append(hello, 0x00)
|
|
||||||
hello = append(hello, 0x00, 0x02, 0x13, 0x01)
|
|
||||||
hello = append(hello, 0x01, 0x00)
|
|
||||||
hello = append(hello, byte(len(extensions)>>8), byte(len(extensions)))
|
|
||||||
hello = append(hello, extensions...)
|
|
||||||
|
|
||||||
record := make([]byte, 0, tlsRecordHeaderLen+tlsHandshakeHeaderLen+len(hello))
|
|
||||||
record = append(record, tlsRecordTypeHandshake, 0x03, 0x03)
|
|
||||||
recordLen := tlsHandshakeHeaderLen + len(hello)
|
|
||||||
record = append(record, byte(recordLen>>8), byte(recordLen))
|
|
||||||
record = append(record, tlsHandshakeTypeClientHello)
|
|
||||||
record = append(record, byte(len(hello)>>16), byte(len(hello)>>8), byte(len(hello)))
|
|
||||||
record = append(record, hello...)
|
|
||||||
return record
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeExtension(extType uint16, data []byte) []byte {
|
|
||||||
ext := make([]byte, 4+len(data))
|
|
||||||
binary.BigEndian.PutUint16(ext[:2], extType)
|
|
||||||
binary.BigEndian.PutUint16(ext[2:4], uint16(len(data)))
|
|
||||||
copy(ext[4:], data)
|
|
||||||
return ext
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractServerName(t *testing.T, record []byte) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
payload := record[tlsRecordHeaderLen:]
|
|
||||||
handshakeLen := readUint24(payload[1:4])
|
|
||||||
hello := payload[tlsHandshakeHeaderLen : tlsHandshakeHeaderLen+handshakeLen]
|
|
||||||
|
|
||||||
offset := 2 + 32
|
|
||||||
sessionIDLen := int(hello[offset])
|
|
||||||
offset++
|
|
||||||
offset += sessionIDLen
|
|
||||||
|
|
||||||
cipherSuitesLen := int(binary.BigEndian.Uint16(hello[offset : offset+2]))
|
|
||||||
offset += 2 + cipherSuitesLen
|
|
||||||
|
|
||||||
compressionMethodsLen := int(hello[offset])
|
|
||||||
offset++
|
|
||||||
offset += compressionMethodsLen
|
|
||||||
|
|
||||||
extensionsLen := int(binary.BigEndian.Uint16(hello[offset : offset+2]))
|
|
||||||
offset += 2
|
|
||||||
extensionsEnd := offset + extensionsLen
|
|
||||||
for offset+4 <= extensionsEnd {
|
|
||||||
extType := binary.BigEndian.Uint16(hello[offset : offset+2])
|
|
||||||
extLen := int(binary.BigEndian.Uint16(hello[offset+2 : offset+4]))
|
|
||||||
offset += 4
|
|
||||||
if extType != tlsExtensionServerName {
|
|
||||||
offset += extLen
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
extData := hello[offset : offset+extLen]
|
|
||||||
nameLen := int(binary.BigEndian.Uint16(extData[3:5]))
|
|
||||||
return string(extData[5 : 5+nameLen])
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Fatal("server_name extension not found")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -42,6 +41,15 @@ const (
|
|||||||
PROXY_SUFFIX = "sec-tunnel.com"
|
PROXY_SUFFIX = "sec-tunnel.com"
|
||||||
DefaultDiscoverCSVFallback = "proxies.csv"
|
DefaultDiscoverCSVFallback = "proxies.csv"
|
||||||
DefaultProxyBypassFallback = "proxy-bypass.txt"
|
DefaultProxyBypassFallback = "proxy-bypass.txt"
|
||||||
|
|
||||||
|
// Default timeouts increased to reduce premature API errors on slow networks.
|
||||||
|
DEFAULT_TIMEOUT = 30 * time.Second
|
||||||
|
DEFAULT_SERVER_SELECTION_TIMEOUT = 60 * time.Second
|
||||||
|
|
||||||
|
// Reduced idle connection pool to lower resource usage on embedded/low-RAM hosts.
|
||||||
|
HTTP_MAX_IDLE_CONNS = 10
|
||||||
|
HTTP_MAX_IDLE_CONNS_PER_HOST = 3
|
||||||
|
HTTP_IDLE_CONN_TIMEOUT = 60 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func perror(msg string) {
|
func perror(msg string) {
|
||||||
@@ -188,7 +196,8 @@ func parse_args() *CLIArgs {
|
|||||||
flag.BoolVar(&args.socksMode, "socks-mode", false, "listen for SOCKS requests instead of HTTP")
|
flag.BoolVar(&args.socksMode, "socks-mode", false, "listen for SOCKS requests instead of HTTP")
|
||||||
flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+
|
flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+
|
||||||
"(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical)")
|
"(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical)")
|
||||||
flag.DurationVar(&args.timeout, "timeout", 10*time.Second, "timeout for network operations")
|
flag.DurationVar(&args.timeout, "timeout", DEFAULT_TIMEOUT,
|
||||||
|
"timeout for network operations")
|
||||||
flag.BoolVar(&args.showVersion, "version", false, "show program version and exit")
|
flag.BoolVar(&args.showVersion, "version", false, "show program version and exit")
|
||||||
flag.StringVar(&args.proxy, "proxy", "", "sets base proxy to use for all dial-outs. "+
|
flag.StringVar(&args.proxy, "proxy", "", "sets base proxy to use for all dial-outs. "+
|
||||||
"Format: <http|https|socks5|socks5h>://[login:password@]host[:port] "+
|
"Format: <http|https|socks5|socks5h>://[login:password@]host[:port] "+
|
||||||
@@ -221,10 +230,13 @@ func parse_args() *CLIArgs {
|
|||||||
flag.DurationVar(&args.proxySpeedTimeout, "proxy-speed-timeout", 15*time.Second, "timeout for a single proxy speed measurement")
|
flag.DurationVar(&args.proxySpeedTimeout, "proxy-speed-timeout", 15*time.Second, "timeout for a single proxy speed measurement")
|
||||||
flag.Int64Var(&args.proxySpeedDLLimit, "proxy-speed-dl-limit", 262144, "limit of downloaded bytes for proxy speed measurement")
|
flag.Int64Var(&args.proxySpeedDLLimit, "proxy-speed-dl-limit", 262144, "limit of downloaded bytes for proxy speed measurement")
|
||||||
flag.Var(&args.serverSelection, "server-selection", "server selection policy (first/random/fastest)")
|
flag.Var(&args.serverSelection, "server-selection", "server selection policy (first/random/fastest)")
|
||||||
flag.DurationVar(&args.serverSelectionTimeout, "server-selection-timeout", 30*time.Second, "timeout given for server selection function to produce result")
|
flag.DurationVar(&args.serverSelectionTimeout, "server-selection-timeout", DEFAULT_SERVER_SELECTION_TIMEOUT,
|
||||||
flag.StringVar(&args.serverSelectionTestURL, "server-selection-test-url", "https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js",
|
"timeout given for server selection function to produce result")
|
||||||
|
flag.StringVar(&args.serverSelectionTestURL, "server-selection-test-url",
|
||||||
|
"https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js",
|
||||||
"URL used for download benchmark by fastest server selection policy")
|
"URL used for download benchmark by fastest server selection policy")
|
||||||
flag.Int64Var(&args.serverSelectionDLLimit, "server-selection-dl-limit", 0, "restrict amount of downloaded data per connection by fastest server selection")
|
flag.Int64Var(&args.serverSelectionDLLimit, "server-selection-dl-limit", 0,
|
||||||
|
"restrict amount of downloaded data per connection by fastest server selection")
|
||||||
flag.Func("config", "read configuration from file with space-separated keys and values", readConfig)
|
flag.Func("config", "read configuration from file with space-separated keys and values", readConfig)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
flag.Visit(func(f *flag.Flag) {
|
flag.Visit(func(f *flag.Flag) {
|
||||||
@@ -276,6 +288,58 @@ func proxyFromURLWrapper(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error)
|
|||||||
return dialer.ProxyDialerFromURL(u, cdialer)
|
return dialer.ProxyDialerFromURL(u, cdialer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildAPITransport returns an http.Transport tuned for infrequent API calls:
|
||||||
|
// reduced idle pool (saves goroutines/sockets), no forced HTTP/2.
|
||||||
|
func buildAPITransport(
|
||||||
|
dialCtx func(context.Context, string, string) (net.Conn, error),
|
||||||
|
dialTLSCtx func(context.Context, string, string) (net.Conn, error),
|
||||||
|
) *http.Transport {
|
||||||
|
return &http.Transport{
|
||||||
|
DialContext: dialCtx,
|
||||||
|
DialTLSContext: dialTLSCtx,
|
||||||
|
ForceAttemptHTTP2: false,
|
||||||
|
MaxIdleConns: HTTP_MAX_IDLE_CONNS,
|
||||||
|
MaxIdleConnsPerHost: HTTP_MAX_IDLE_CONNS_PER_HOST,
|
||||||
|
IdleConnTimeout: HTTP_IDLE_CONN_TIMEOUT,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildCAPool constructs the x509 cert pool used for all TLS verification.
|
||||||
|
// When -cafile is given, only that file is loaded (useful for custom/corporate CAs).
|
||||||
|
// Otherwise the bundled Mozilla NSS root store is used, which includes all
|
||||||
|
// major roots and supports AddCertWithConstraint for name-constrained CAs —
|
||||||
|
// strictly better than a plain PEM file.
|
||||||
|
func buildCAPool(caFile string, logger *clog.CondLogger) (*x509.CertPool, int) {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if caFile != "" {
|
||||||
|
certs, err := os.ReadFile(caFile)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Can't load CA file: %v", err)
|
||||||
|
return nil, 15
|
||||||
|
}
|
||||||
|
if ok := pool.AppendCertsFromPEM(certs); !ok {
|
||||||
|
logger.Error("Can't load certificates from CA file")
|
||||||
|
return nil, 15
|
||||||
|
}
|
||||||
|
return pool, 0
|
||||||
|
}
|
||||||
|
for c := range bundle.Roots() {
|
||||||
|
cert, err := x509.ParseCertificate(c.Certificate)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Unable to parse bundled certificate: %v", err)
|
||||||
|
return nil, 15
|
||||||
|
}
|
||||||
|
if c.Constraint == nil {
|
||||||
|
pool.AddCert(cert)
|
||||||
|
} else {
|
||||||
|
pool.AddCertWithConstraint(cert, c.Constraint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pool, 0
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeAPIProxy(raw string) (string, error) {
|
func normalizeAPIProxy(raw string) (string, error) {
|
||||||
raw = strings.TrimSpace(raw)
|
raw = strings.TrimSpace(raw)
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
@@ -441,27 +505,25 @@ func newSEClient(args *CLIArgs, baseDialer dialer.ContextDialer, caPool *x509.Ce
|
|||||||
seclientDialer = dialer.NewResolvingDialer(resolver, seclientDialer)
|
seclientDialer = dialer.NewResolvingDialer(resolver, seclientDialer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialing w/o SNI, receiving self-signed certificate, so skip verification.
|
// TLS config for the API connection: SNI suppressed (or faked), cert
|
||||||
// Either way we'll validate certificate of actual proxy server.
|
// verification is skipped at the TLS layer because the API endpoint uses
|
||||||
|
// a self-signed cert — actual peer verification happens in VerifyConnection
|
||||||
|
// inside ProxyDialer for the proxy connections.
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
ServerName: args.fakeSNI,
|
ServerName: args.fakeSNI,
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
seclient, err := se.NewSEClient(args.apiLogin, args.apiPassword, &http.Transport{
|
|
||||||
DialContext: seclientDialer.DialContext,
|
seclient, err := se.NewSEClient(args.apiLogin, args.apiPassword, buildAPITransport(
|
||||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
seclientDialer.DialContext,
|
||||||
|
func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
conn, err := seclientDialer.DialContext(ctx, network, addr)
|
conn, err := seclientDialer.DialContext(ctx, network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
return tls.Client(conn, tlsConfig), nil
|
return tls.Client(conn, tlsConfig), nil
|
||||||
},
|
},
|
||||||
ForceAttemptHTTP2: true,
|
))
|
||||||
MaxIdleConns: 100,
|
|
||||||
IdleConnTimeout: 90 * time.Second,
|
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to construct SEClient: %w", err)
|
return nil, fmt.Errorf("unable to construct SEClient: %w", err)
|
||||||
}
|
}
|
||||||
@@ -659,30 +721,9 @@ func run() int {
|
|||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
caPool := x509.NewCertPool()
|
caPool, exitCode := buildCAPool(args.caFile, mainLogger)
|
||||||
if args.caFile != "" {
|
if exitCode != 0 {
|
||||||
certs, err := ioutil.ReadFile(args.caFile)
|
return exitCode
|
||||||
if err != nil {
|
|
||||||
mainLogger.Error("Can't load CA file: %v", err)
|
|
||||||
return 15
|
|
||||||
}
|
|
||||||
if ok := caPool.AppendCertsFromPEM(certs); !ok {
|
|
||||||
mainLogger.Error("Can't load certificates from CA file")
|
|
||||||
return 15
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for c := range bundle.Roots() {
|
|
||||||
cert, err := x509.ParseCertificate(c.Certificate)
|
|
||||||
if err != nil {
|
|
||||||
mainLogger.Error("Unable to parse bundled certificate: %v", err)
|
|
||||||
return 15
|
|
||||||
}
|
|
||||||
if c.Constraint == nil {
|
|
||||||
caPool.AddCert(cert)
|
|
||||||
} else {
|
|
||||||
caPool.AddCertWithConstraint(cert, c.Constraint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xproxy.RegisterDialerType("http", proxyFromURLWrapper)
|
xproxy.RegisterDialerType("http", proxyFromURLWrapper)
|
||||||
@@ -793,12 +834,12 @@ func run() int {
|
|||||||
if args.apiProxy != "" {
|
if args.apiProxy != "" {
|
||||||
apiProxyURL, err := url.Parse(args.apiProxy)
|
apiProxyURL, err := url.Parse(args.apiProxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLogger.Critical("Unable to parse base proxy URL: %v", err)
|
mainLogger.Critical("Unable to parse api-proxy URL: %v", err)
|
||||||
return 6
|
return 6
|
||||||
}
|
}
|
||||||
pxDialer, err := xproxy.FromURL(apiProxyURL, seclientDialer)
|
pxDialer, err := xproxy.FromURL(apiProxyURL, seclientDialer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLogger.Critical("Unable to instantiate base proxy dialer: %v", err)
|
mainLogger.Critical("Unable to instantiate api-proxy dialer: %v", err)
|
||||||
return 7
|
return 7
|
||||||
}
|
}
|
||||||
seclientDialer = pxDialer.(dialer.ContextDialer)
|
seclientDialer = pxDialer.(dialer.ContextDialer)
|
||||||
@@ -806,12 +847,12 @@ func run() int {
|
|||||||
if args.apiAddress != "" {
|
if args.apiAddress != "" {
|
||||||
seclientDialer = dialer.NewFixedDialer(args.apiAddress, seclientDialer)
|
seclientDialer = dialer.NewFixedDialer(args.apiAddress, seclientDialer)
|
||||||
} else if len(args.bootstrapDNS.values) > 0 {
|
} else if len(args.bootstrapDNS.values) > 0 {
|
||||||
resolver, err := resolver.FastFromURLs(caPool, args.bootstrapDNS.values...)
|
res, err := resolver.FastFromURLs(caPool, args.bootstrapDNS.values...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLogger.Critical("Unable to instantiate DNS resolver: %v", err)
|
mainLogger.Critical("Unable to instantiate DNS resolver: %v", err)
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
seclientDialer = dialer.NewResolvingDialer(resolver, seclientDialer)
|
seclientDialer = dialer.NewResolvingDialer(res, seclientDialer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialing w/o SNI, receiving self-signed certificate, so skip verification.
|
// Dialing w/o SNI, receiving self-signed certificate, so skip verification.
|
||||||
@@ -959,9 +1000,7 @@ func run() int {
|
|||||||
ss = dialer.NewFastestServerSelectionFunc(
|
ss = dialer.NewFastestServerSelectionFunc(
|
||||||
args.serverSelectionTestURL,
|
args.serverSelectionTestURL,
|
||||||
args.serverSelectionDLLimit,
|
args.serverSelectionDLLimit,
|
||||||
&tls.Config{
|
&tls.Config{RootCAs: caPool},
|
||||||
RootCAs: caPool,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
panic("unhandled server selection value got past parsing")
|
panic("unhandled server selection value got past parsing")
|
||||||
@@ -1012,8 +1051,7 @@ func run() int {
|
|||||||
mainLogger.Info("Refreshing login...")
|
mainLogger.Info("Refreshing login...")
|
||||||
reqCtx, cl := context.WithTimeout(ctx, args.timeout)
|
reqCtx, cl := context.WithTimeout(ctx, args.timeout)
|
||||||
defer cl()
|
defer cl()
|
||||||
err := seclient.Login(reqCtx)
|
if err := seclient.Login(reqCtx); err != nil {
|
||||||
if err != nil {
|
|
||||||
mainLogger.Error("Login refresh failed: %v", err)
|
mainLogger.Error("Login refresh failed: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1022,8 +1060,7 @@ func run() int {
|
|||||||
mainLogger.Info("Refreshing device password...")
|
mainLogger.Info("Refreshing device password...")
|
||||||
reqCtx, cl = context.WithTimeout(ctx, args.timeout)
|
reqCtx, cl = context.WithTimeout(ctx, args.timeout)
|
||||||
defer cl()
|
defer cl()
|
||||||
err = seclient.DeviceGeneratePassword(reqCtx)
|
if err := seclient.DeviceGeneratePassword(reqCtx); err != nil {
|
||||||
if err != nil {
|
|
||||||
mainLogger.Error("Device password refresh failed: %v", err)
|
mainLogger.Error("Device password refresh failed: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1033,15 +1070,15 @@ func run() int {
|
|||||||
|
|
||||||
mainLogger.Info("Starting proxy server...")
|
mainLogger.Info("Starting proxy server...")
|
||||||
if args.socksMode {
|
if args.socksMode {
|
||||||
socks, initError := handler.NewSocksServer(handlerDialer, socksLogger, args.fakeSNI)
|
socks, initError := handler.NewSocksServer(handlerDialer, socksLogger)
|
||||||
if initError != nil {
|
if initError != nil {
|
||||||
mainLogger.Critical("Failed to start: %v", err)
|
mainLogger.Critical("Failed to start: %v", initError)
|
||||||
return 16
|
return 16
|
||||||
}
|
}
|
||||||
mainLogger.Info("Init complete.")
|
mainLogger.Info("Init complete.")
|
||||||
err = socks.ListenAndServe("tcp", args.bindAddress)
|
err = socks.ListenAndServe("tcp", args.bindAddress)
|
||||||
} else {
|
} else {
|
||||||
h := handler.NewProxyHandler(handlerDialer, proxyLogger, args.fakeSNI)
|
h := handler.NewProxyHandler(handlerDialer, proxyLogger)
|
||||||
mainLogger.Info("Init complete.")
|
mainLogger.Info("Init complete.")
|
||||||
err = http.ListenAndServe(args.bindAddress, h)
|
err = http.ListenAndServe(args.bindAddress, h)
|
||||||
}
|
}
|
||||||
@@ -1155,10 +1192,7 @@ func dpExport(ips []se.SEIPEntry, seclient *se.SEClient, sni string) int {
|
|||||||
u := url.URL{
|
u := url.URL{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
User: creds,
|
User: creds,
|
||||||
Host: net.JoinHostPort(
|
Host: net.JoinHostPort(ip.IP, strconv.Itoa(int(ip.Ports[0]))),
|
||||||
ip.IP,
|
|
||||||
strconv.Itoa(int(ip.Ports[0])),
|
|
||||||
),
|
|
||||||
RawQuery: url.Values{
|
RawQuery: url.Values{
|
||||||
"sni": []string{sni},
|
"sni": []string{sni},
|
||||||
"peername": []string{fmt.Sprintf("%s%d.%s", strings.ToLower(ip.Geo.CountryCode), i, PROXY_SUFFIX)},
|
"peername": []string{fmt.Sprintf("%s%d.%s", strings.ToLower(ip.Geo.CountryCode), i, PROXY_SUFFIX)},
|
||||||
@@ -1168,10 +1202,7 @@ func dpExport(ips []se.SEIPEntry, seclient *se.SEClient, sni string) int {
|
|||||||
if gotOne {
|
if gotOne {
|
||||||
key = "#proxy"
|
key = "#proxy"
|
||||||
}
|
}
|
||||||
wr.Write([]string{
|
wr.Write([]string{key, u.String()})
|
||||||
key,
|
|
||||||
u.String(),
|
|
||||||
})
|
|
||||||
gotOne = true
|
gotOne = true
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+26
-41
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -16,10 +15,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ANON_EMAIL_LOCALPART_BYTES = 32
|
ANON_EMAIL_LOCALPART_BYTES = 32
|
||||||
ANON_PASSWORD_BYTES = 20
|
ANON_PASSWORD_BYTES = 20
|
||||||
DEVICE_ID_BYTES = 20
|
DEVICE_ID_BYTES = 20
|
||||||
READ_LIMIT int64 = 128 * 1024
|
READ_LIMIT int64 = 128 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
type SEEndpoints struct {
|
type SEEndpoints struct {
|
||||||
@@ -73,9 +72,10 @@ type SEClient struct {
|
|||||||
|
|
||||||
type StrKV map[string]string
|
type StrKV map[string]string
|
||||||
|
|
||||||
// Instantiates SurfEasy client with default settings and given API keys.
|
// NewSEClient instantiates a SurfEasy client.
|
||||||
// Optional `transport` parameter allows to override HTTP transport used
|
// apiUsername/apiSecret are the application-level Digest Auth credentials
|
||||||
// for HTTP calls
|
// 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) {
|
func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*SEClient, error) {
|
||||||
if transport == nil {
|
if transport == nil {
|
||||||
transport = http.DefaultTransport
|
transport = http.DefaultTransport
|
||||||
@@ -83,7 +83,7 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S
|
|||||||
|
|
||||||
rng := rand.New(RandomSource)
|
rng := rand.New(RandomSource)
|
||||||
|
|
||||||
device_id, err := randomCapitalHexString(rng, DEVICE_ID_BYTES)
|
deviceID, err := randomCapitalHexString(rng, DEVICE_ID_BYTES)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -93,17 +93,15 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &SEClient{
|
return &SEClient{
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Jar: jar,
|
Jar: jar,
|
||||||
Transport: dac.NewDigestTransport(apiUsername, apiSecret, transport),
|
Transport: dac.NewDigestTransport(apiUsername, apiSecret, transport),
|
||||||
},
|
},
|
||||||
Settings: DefaultSESettings,
|
Settings: DefaultSESettings,
|
||||||
rng: rng,
|
rng: rng,
|
||||||
DeviceID: device_id,
|
DeviceID: deviceID,
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SEClient) ResetCookies() error {
|
func (c *SEClient) ResetCookies() error {
|
||||||
@@ -125,6 +123,8 @@ func (c *SEClient) AnonRegister(ctx context.Context) error {
|
|||||||
return err
|
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.SubscriberEmail = fmt.Sprintf("%s@%s.best.vpn", localPart, c.Settings.ClientType)
|
||||||
c.SubscriberPassword = capitalHexSHA1(c.SubscriberEmail)
|
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 {
|
func (c *SEClient) register(ctx context.Context) error {
|
||||||
err := c.resetCookies()
|
if err := c.resetCookies(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var regRes SERegisterSubscriberResponse
|
var regRes SERegisterSubscriberResponse
|
||||||
err = c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{
|
err := c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{
|
||||||
"email": c.SubscriberEmail,
|
"email": c.SubscriberEmail,
|
||||||
"password": c.SubscriberPassword,
|
"password": c.SubscriberPassword,
|
||||||
}, ®Res)
|
}, ®Res)
|
||||||
@@ -225,13 +224,12 @@ func (c *SEClient) Login(ctx context.Context) error {
|
|||||||
c.Mux.Lock()
|
c.Mux.Lock()
|
||||||
defer c.Mux.Unlock()
|
defer c.Mux.Unlock()
|
||||||
|
|
||||||
err := c.resetCookies()
|
if err := c.resetCookies(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginRes SESubscriberLoginResponse
|
var loginRes SESubscriberLoginResponse
|
||||||
err = c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{
|
err := c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{
|
||||||
"login": c.SubscriberEmail,
|
"login": c.SubscriberEmail,
|
||||||
"password": c.SubscriberPassword,
|
"password": c.SubscriberPassword,
|
||||||
"client_type": c.Settings.ClientType,
|
"client_type": c.Settings.ClientType,
|
||||||
@@ -287,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 {
|
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 {
|
for k, v := range params {
|
||||||
input[k] = []string{v}
|
input[k] = []string{v}
|
||||||
}
|
}
|
||||||
req, err := http.NewRequestWithContext(
|
req, err := http.NewRequestWithContext(ctx, "POST", endpoint,
|
||||||
ctx,
|
strings.NewReader(input.Encode()))
|
||||||
"POST",
|
|
||||||
endpoint,
|
|
||||||
strings.NewReader(input.Encode()),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -310,26 +304,17 @@ func (c *SEClient) rpcCall(ctx context.Context, endpoint string, params map[stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
cleanupBody(resp.Body)
|
||||||
return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header)
|
return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header)
|
||||||
}
|
}
|
||||||
|
|
||||||
decoder := json.NewDecoder(resp.Body)
|
err = json.NewDecoder(resp.Body).Decode(res)
|
||||||
err = decoder.Decode(res)
|
|
||||||
cleanupBody(resp.Body)
|
cleanupBody(resp.Body)
|
||||||
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does cleanup of HTTP response in order to make it reusable by keep-alive
|
// cleanupBody drains and closes an HTTP response body to allow connection reuse.
|
||||||
// logic of HTTP client
|
|
||||||
func cleanupBody(body io.ReadCloser) {
|
func cleanupBody(body io.ReadCloser) {
|
||||||
io.Copy(ioutil.Discard, &io.LimitedReader{
|
io.Copy(io.Discard, &io.LimitedReader{R: body, N: READ_LIMIT})
|
||||||
R: body,
|
|
||||||
N: READ_LIMIT,
|
|
||||||
})
|
|
||||||
body.Close()
|
body.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user