mirror of
https://github.com/Snawoot/hola-proxy.git
synced 2026-04-04 19:18:14 +00:00
Compare commits
96 Commits
v1.11.1+ol
...
v1.18.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cd7f71d13 | ||
|
|
ccb11a0c6a | ||
|
|
8dc4b32ce7 | ||
|
|
81bec4bcb0 | ||
|
|
a887dd0a88 | ||
|
|
ecc1159e1a | ||
|
|
f361120f7e | ||
|
|
ecabe00326 | ||
|
|
d55790c30d | ||
|
|
c0715b0244 | ||
|
|
1e2ff556c5 | ||
|
|
a34396e149 | ||
|
|
1ecf989ea0 | ||
|
|
240c9a5194 | ||
|
|
d6f3871db1 | ||
|
|
b0435c3929 | ||
|
|
9d49f99cd3 | ||
|
|
51a9f257e7 | ||
|
|
5d3aa90a0f | ||
|
|
f062f9310c | ||
|
|
c019e84bbe | ||
|
|
65fdac74bd | ||
|
|
15fea1bf78 | ||
|
|
1a526a6381 | ||
|
|
fcfc6212ee | ||
|
|
cf49895825 | ||
|
|
becf27500b | ||
|
|
b1671d4d19 | ||
|
|
2c22c3af35 | ||
|
|
55f8125044 | ||
|
|
c3691b208b | ||
|
|
bfe19d9a85 | ||
|
|
3be0ad42f6 | ||
|
|
5e00cfb499 | ||
|
|
ba52035f5a | ||
|
|
091bc05a30 | ||
|
|
ebba7bca60 | ||
|
|
99132778c9 | ||
|
|
02bfdea676 | ||
|
|
e2dbbb9ec3 | ||
|
|
81e06c2828 | ||
|
|
440bad2860 | ||
|
|
f9d2e8985f | ||
|
|
7775e5f54e | ||
|
|
8479a86de6 | ||
|
|
3fccf32f7b | ||
|
|
332db446b4 | ||
|
|
8020fba8f3 | ||
|
|
f659f26bad | ||
|
|
ce356ce015 | ||
|
|
e8cd8ba0a8 | ||
|
|
191b1e7f51 | ||
|
|
6aa4fc334a | ||
|
|
124212a5e2 | ||
|
|
b120cb5462 | ||
|
|
972771b6af | ||
|
|
49b6ba9147 | ||
|
|
1e4c6684cc | ||
|
|
569bf1d39f | ||
|
|
92fdd4fb72 | ||
|
|
d4d37bb354 | ||
|
|
458fdf2d50 | ||
|
|
010ca056b3 | ||
|
|
e28c186971 | ||
|
|
5de4f1a616 | ||
|
|
1e13bae8b3 | ||
|
|
0811bb6fa6 | ||
|
|
1bfef0287f | ||
|
|
778339ddb2 | ||
|
|
c8350cc731 | ||
|
|
5b0e1e33c6 | ||
|
|
7bc4cc57de | ||
|
|
3105cf04f1 | ||
|
|
69b582090c | ||
|
|
9a91ec21ab | ||
|
|
3d14c627bf | ||
|
|
f0ee8a0d23 | ||
|
|
ca7c13f86a | ||
|
|
c5ddc5a5f1 | ||
|
|
8680aaf470 | ||
|
|
36822781d1 | ||
|
|
1d81ddd083 | ||
|
|
37401b39f8 | ||
|
|
d4e86ad2b4 | ||
|
|
fc9e85dba0 | ||
|
|
bbd2ea4048 | ||
|
|
6d65d22a84 | ||
|
|
2be66ef0d1 | ||
|
|
7ed761c0c3 | ||
|
|
fa21f85414 | ||
|
|
6940495771 | ||
|
|
7864816161 | ||
|
|
838463b771 | ||
|
|
2ff8391919 | ||
|
|
54fa9a4831 | ||
|
|
679ac56806 |
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -17,41 +17,22 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Read tag
|
||||
id: tag
|
||||
run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 'stable'
|
||||
-
|
||||
name: Read tag
|
||||
id: tag
|
||||
run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Build
|
||||
run: >-
|
||||
make -j $(nproc)
|
||||
bin-linux-amd64 bin-linux-386 bin-linux-arm bin-linux-arm64
|
||||
bin-linux-mips bin-linux-mipsle bin-linux-mips64 bin-linux-mips64le
|
||||
bin-freebsd-amd64 bin-freebsd-386 bin-freebsd-arm
|
||||
bin-netbsd-amd64 bin-netbsd-386
|
||||
bin-openbsd-amd64 bin-openbsd-386
|
||||
bin-darwin-amd64 bin-darwin-arm64
|
||||
bin-android-arm bin-android-arm64
|
||||
make -j $(nproc) allplus
|
||||
NDK_CC_ARM64="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
|
||||
NDK_CC_ARM="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang"
|
||||
VERSION=${{steps.tag.outputs.tag}}
|
||||
-
|
||||
name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '~1.20'
|
||||
-
|
||||
name: Build
|
||||
run: >-
|
||||
make -j $(nproc)
|
||||
bin-windows-amd64 bin-windows-386 bin-windows-arm
|
||||
VERSION=${{steps.tag.outputs.tag}}
|
||||
GO120=go
|
||||
-
|
||||
name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.21 AS build
|
||||
FROM --platform=$BUILDPLATFORM golang:1 AS build
|
||||
|
||||
ARG GIT_DESC=undefined
|
||||
|
||||
|
||||
7
Makefile
7
Makefile
@@ -9,7 +9,6 @@ NDK_CC_ARM = $(abspath ../../ndk-toolchain-arm/bin/arm-linux-androideabi-gcc)
|
||||
NDK_CC_ARM64 = $(abspath ../../ndk-toolchain-arm64/bin/aarch64-linux-android21-clang)
|
||||
|
||||
GO := go
|
||||
GO120 := /usr/lib64/go/1.20/bin/go
|
||||
|
||||
src = $(wildcard *.go */*.go */*/*.go) go.mod go.sum
|
||||
|
||||
@@ -104,13 +103,13 @@ $(OUTSUFFIX).darwin-arm64: $(src)
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
|
||||
|
||||
$(OUTSUFFIX).windows-amd64.exe: $(src)
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO120) build $(BUILDOPTS) $(LDFLAGS) -o $@
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
|
||||
|
||||
$(OUTSUFFIX).windows-386.exe: $(src)
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=386 $(GO120) build $(BUILDOPTS) $(LDFLAGS) -o $@
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
|
||||
|
||||
$(OUTSUFFIX).windows-arm.exe: $(src)
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm GOARM=7 $(GO120) build $(BUILDOPTS) $(LDFLAGS) -o $@
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm GOARM=7 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
|
||||
|
||||
$(OUTSUFFIX).android-arm: $(src)
|
||||
CC=$(NDK_CC_ARM) CGO_ENABLED=1 GOOS=android GOARCH=arm GOARM=7 $(GO) build $(LDFLAGS_NATIVE) -o $@
|
||||
|
||||
@@ -7,6 +7,8 @@ By default the application listens on 127.0.0.1:8080.
|
||||
|
||||
Application is capable to forward traffic via proxies in datacenters (flag `-proxy-type direct`, default) or via peer proxies on residental IPs (consumer ISP) in that country (flag `-proxy-type lum`).
|
||||
|
||||
This alternative implementation ensures your internet connection is not shared with anyone else and everything is clean and safe.
|
||||
|
||||
---
|
||||
|
||||
:heart: :heart: :heart:
|
||||
@@ -165,14 +167,17 @@ zagent248.hola.org,165.22.65.3,22222,22223,22224,22225,22226,digitalocean
|
||||
| ext-ver | String | extension version to mimic in requests. Can be obtained from https://chrome.google.com/webstore/detail/hola-vpn-the-website-unbl/gkojfkhlekighikafcpjkiklfbnlmeio (default "999.999.999") |
|
||||
| force-port-field | Number | force specific port field/num (example 24232 or lum) |
|
||||
| hide-SNI | Boolean | hide SNI in TLS sessions with proxy server (default true) |
|
||||
| init-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
|
||||
| init-retry-interval | Duration | delay between initialization retries (default 5s) |
|
||||
| limit | Unsigned Integer (Number) | amount of proxies in retrieved list (default 3) |
|
||||
| list-countries | String | list available countries and exit |
|
||||
| list-proxies | - | output proxy list and exit |
|
||||
| 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-type | String | proxy type (Datacenter: direct) (Residential: lum) (default "direct") |
|
||||
| resolver | String | DNS/DoH/DoT resolver to workaround Hola blocked hosts. See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. (default "https://cloudflare-dns.com/dns-query") |
|
||||
| rotate | Duration | rotate user ID once per given period (default 1h0m0s) |
|
||||
| resolver | String | comma-separated list of DNS/DoH/DoT resolvers used to lookup domain names blocked by Hola. Supported schemes are: dns://, https://, tls://, tcp://. (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/`) |
|
||||
| rotate | Duration | rotate user ID once per given period (default 48h0m0s) |
|
||||
| timeout | Duration | timeout for network operations (default 35s) |
|
||||
| user-agent | String | value of User-Agent header in requests. Default: User-Agent of latest stable Chrome for Windows |
|
||||
| verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
|
||||
|
||||
## See also
|
||||
|
||||
63
chromever.go
Normal file
63
chromever.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type chromeVerResponse struct {
|
||||
Versions [1]struct {
|
||||
Version string `json:"version"`
|
||||
} `json:"versions"`
|
||||
}
|
||||
|
||||
const chromeVerURL = "https://versionhistory.googleapis.com/v1/chrome/platforms/win/channels/stable/versions?alt=json&orderBy=version+desc&pageSize=1&prettyPrint=false"
|
||||
|
||||
func GetChromeVer(ctx context.Context, dialer ContextDialer) (string, error) {
|
||||
if dialer == nil {
|
||||
dialer = &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
DialContext: dialer.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
defer transport.CloseIdleConnections()
|
||||
httpClient := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", chromeVerURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("chrome browser version request construction failed: %w", err)
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("chrome browser version request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("chrome browser version request failed: bad status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
var chromeVerResp chromeVerResponse
|
||||
if err := dec.Decode(&chromeVerResp); err != nil {
|
||||
return "", fmt.Errorf("unable to decode chrome browser version response: %w", err)
|
||||
}
|
||||
|
||||
return chromeVerResp.Versions[0].Version, nil
|
||||
}
|
||||
@@ -20,10 +20,9 @@ func CredService(interval, timeout time.Duration,
|
||||
var mux sync.Mutex
|
||||
var auth_header, user_uuid string
|
||||
auth = func() (res string) {
|
||||
(&mux).Lock()
|
||||
res = auth_header
|
||||
(&mux).Unlock()
|
||||
return
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
return auth_header
|
||||
}
|
||||
|
||||
tx_res, tx_err := EnsureTransaction(context.Background(), timeout, func(ctx context.Context, client *http.Client) bool {
|
||||
@@ -45,6 +44,9 @@ func CredService(interval, timeout time.Duration,
|
||||
return
|
||||
}
|
||||
auth_header = basic_auth_header(TemplateLogin(user_uuid), tunnels.AgentKey)
|
||||
if interval <= 0 {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
var (
|
||||
err error
|
||||
@@ -74,9 +76,9 @@ func CredService(interval, timeout time.Duration,
|
||||
logger.Critical("All rotation attempts failed.")
|
||||
continue
|
||||
}
|
||||
(&mux).Lock()
|
||||
mux.Lock()
|
||||
auth_header = basic_auth_header(TemplateLogin(user_uuid), tuns.AgentKey)
|
||||
(&mux).Unlock()
|
||||
mux.Unlock()
|
||||
logger.Info("Credentials rotated successfully.")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -86,7 +86,7 @@ func GetExtVer(ctx context.Context,
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("bad status code: %d", resp.StatusCode)
|
||||
return "", fmt.Errorf("chrome web store: bad status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
reader := io.LimitReader(resp.Body, 64*1024)
|
||||
|
||||
36
go.mod
36
go.mod
@@ -1,33 +1,21 @@
|
||||
module github.com/Snawoot/hola-proxy
|
||||
|
||||
go 1.20
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.58.0
|
||||
github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e
|
||||
github.com/cenkalti/backoff/v4 v4.2.1
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/miekg/dns v1.1.56
|
||||
golang.org/x/net v0.18.0
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/ncruces/go-dns v1.2.7
|
||||
github.com/refraction-networking/utls v1.8.0
|
||||
golang.org/x/net v0.43.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/golibs v0.17.2 // indirect
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
||||
github.com/ameshkov/dnscrypt/v2 v2.2.7 // indirect
|
||||
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.13.1 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/quic-go/quic-go v0.40.0 // indirect
|
||||
go.uber.org/mock v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.15.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.15.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
)
|
||||
|
||||
88
go.sum
88
go.sum
@@ -1,65 +1,27 @@
|
||||
github.com/AdguardTeam/dnsproxy v0.58.0 h1:1zPmDYWIc60D5Mn2idt3TcH+CQzKBvkWzJ5/u49wraw=
|
||||
github.com/AdguardTeam/dnsproxy v0.58.0/go.mod h1:ZvkbM71HwpilgkCnTubDiR4Ba6x5Qvnhy2iasMWaTDM=
|
||||
github.com/AdguardTeam/golibs v0.17.2 h1:vg6wHMjUKscnyPGRvxS5kAt7Uw4YxcJiITZliZ476W8=
|
||||
github.com/AdguardTeam/golibs v0.17.2/go.mod h1:DKhCIXHcUYtBhU8ibTLKh1paUL96n5zhQBlx763sj+U=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
|
||||
github.com/ameshkov/dnscrypt/v2 v2.2.7 h1:aEitLIR8HcxVodZ79mgRcCiC0A0I5kZPBuWGFwwulAw=
|
||||
github.com/ameshkov/dnscrypt/v2 v2.2.7/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow=
|
||||
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
|
||||
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e h1:V9a67dfYqPLAvzk5hMQOXYJlZ4SLIXgyKIE+ZiHzgGQ=
|
||||
github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
|
||||
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
|
||||
github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU=
|
||||
github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=
|
||||
github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
|
||||
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
|
||||
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/ncruces/go-dns v1.2.7 h1:NMA7vFqXUl+nBhGFlleLyo2ni3Lqv3v+qFWZidzRemI=
|
||||
github.com/ncruces/go-dns v1.2.7/go.mod h1:SqmhVMBd8Wr7hsu3q6yTt6/Jno/xLMrbse/JLOMBo1Y=
|
||||
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
|
||||
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
|
||||
@@ -19,7 +19,7 @@ type ProxyHandler struct {
|
||||
auth AuthProvider
|
||||
}
|
||||
|
||||
func NewProxyHandler(dialer, requestDialer ContextDialer, auth AuthProvider, resolver *Resolver, logger *CondLogger) *ProxyHandler {
|
||||
func NewProxyHandler(dialer, requestDialer ContextDialer, auth AuthProvider, resolver LookupNetIPer, logger *CondLogger) *ProxyHandler {
|
||||
dialer = NewRetryDialer(dialer, resolver, logger)
|
||||
httptransport := &http.Transport{
|
||||
Proxy: func(_ *http.Request) (*url.URL, error) {
|
||||
|
||||
49
holaapi.go
49
holaapi.go
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
@@ -24,23 +23,37 @@ import (
|
||||
"github.com/campoy/unique"
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/google/uuid"
|
||||
tls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
const USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
|
||||
const EXT_BROWSER = "chrome"
|
||||
const PRODUCT = "cws"
|
||||
const CCGI_URL = "https://client.hola.org/client_cgi/"
|
||||
const VPN_COUNTRIES_URL = CCGI_URL + "vpn_countries.json"
|
||||
const BG_INIT_URL = CCGI_URL + "background_init"
|
||||
const ZGETTUNNELS_URL = CCGI_URL + "zgettunnels"
|
||||
const FALLBACK_CONF_URL = "https://www.dropbox.com/s/jemizcvpmf2qb9v/cloud_failover.conf?dl=1"
|
||||
const AGENT_SUFFIX = ".hola.org"
|
||||
|
||||
var FALLBACK_CONF_URLS = []string{
|
||||
"https://www.dropbox.com/s/jemizcvpmf2qb9v/cloud_failover.conf?dl=1",
|
||||
"https://vdkd6nz8qr.s3.amazonaws.com/cloud_failover.conf",
|
||||
}
|
||||
|
||||
var LOGIN_TEMPLATE = template.Must(template.New("LOGIN_TEMPLATE").Parse("user-uuid-{{.uuid}}-is_prem-{{.prem}}"))
|
||||
var TemporaryBanError = errors.New("temporary ban detected")
|
||||
var PermanentBanError = errors.New("permanent ban detected")
|
||||
var EmptyResponseError = errors.New("empty response")
|
||||
|
||||
var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||
|
||||
func SetUserAgent(ua string) {
|
||||
userAgent = ua
|
||||
}
|
||||
|
||||
func GetUserAgent() string {
|
||||
return userAgent
|
||||
}
|
||||
|
||||
type CountryList []string
|
||||
|
||||
type BgInitResponse struct {
|
||||
@@ -158,7 +171,7 @@ func do_req(ctx context.Context, client *http.Client, method, url string, query,
|
||||
if query != nil {
|
||||
req.URL.RawQuery = query.Encode()
|
||||
}
|
||||
req.Header.Set("User-Agent", USER_AGENT)
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -247,7 +260,7 @@ func zgettunnels(ctx context.Context,
|
||||
params.Add("uuid", user_uuid)
|
||||
params.Add("session_key", strconv.FormatInt(session_key, 10))
|
||||
params.Add("is_premium", "0")
|
||||
data, err := do_req(ctx, client, "", ZGETTUNNELS_URL, params, nil)
|
||||
data, err := do_req(ctx, client, "POST", ZGETTUNNELS_URL, params, nil)
|
||||
if err != nil {
|
||||
reterr = err
|
||||
return
|
||||
@@ -265,7 +278,8 @@ func zgettunnels(ctx context.Context,
|
||||
|
||||
func fetchFallbackConfig(ctx context.Context) (*FallbackConfig, error) {
|
||||
client := httpClientWithProxy(nil)
|
||||
confRaw, err := do_req(ctx, client, "", FALLBACK_CONF_URL, nil, nil)
|
||||
fallbackConfURL := FALLBACK_CONF_URLS[rand.New(RandomSource).Intn(len(FALLBACK_CONF_URLS))]
|
||||
confRaw, err := do_req(ctx, client, "", fallbackConfURL, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -390,9 +404,7 @@ func httpClientWithProxy(agent *FallbackAgent) *http.Client {
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
var dialer ContextDialer = baseDialer
|
||||
var rootCAs *x509.CertPool
|
||||
@@ -403,6 +415,27 @@ func httpClientWithProxy(agent *FallbackAgent) *http.Client {
|
||||
dialer = NewProxyDialer(agent.NetAddr(), agent.Hostname(), rootCAs, nil, true, dialer)
|
||||
}
|
||||
t.DialContext = dialer.DialContext
|
||||
t.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hostname extraction error: %w", err)
|
||||
}
|
||||
conn, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't prepare underlying connection for TLS session: %w", err)
|
||||
}
|
||||
var cfg tls.Config
|
||||
if tlsConfig != nil {
|
||||
cfg = *tlsConfig
|
||||
}
|
||||
cfg.ServerName = host
|
||||
tlsConn := tls.UClient(conn, &cfg, tls.HelloAndroid_11_OkHttp)
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("UClient handshake failed: %w", err)
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: t,
|
||||
}
|
||||
|
||||
187
main.go
187
main.go
@@ -1,20 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
xproxy "golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
@@ -46,6 +50,37 @@ func arg_fail(msg string) {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
type CSVArg struct {
|
||||
values []string
|
||||
}
|
||||
|
||||
func (a *CSVArg) String() string {
|
||||
if len(a.values) == 0 {
|
||||
return ""
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
wr := csv.NewWriter(buf)
|
||||
wr.Write(a.values)
|
||||
wr.Flush()
|
||||
return strings.TrimRight(buf.String(), "\n")
|
||||
}
|
||||
|
||||
func (a *CSVArg) Set(line string) error {
|
||||
rd := csv.NewReader(strings.NewReader(line))
|
||||
rd.FieldsPerRecord = -1
|
||||
rd.TrimLeadingSpace = true
|
||||
values, err := rd.Read()
|
||||
if err == io.EOF {
|
||||
a.values = nil
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse comma-separated argument: %w", err)
|
||||
}
|
||||
a.values = values
|
||||
return nil
|
||||
}
|
||||
|
||||
type CLIArgs struct {
|
||||
extVer string
|
||||
country string
|
||||
@@ -55,7 +90,7 @@ type CLIArgs struct {
|
||||
verbosity int
|
||||
timeout, rotate time.Duration
|
||||
proxy_type string
|
||||
resolver string
|
||||
resolver *CSVArg
|
||||
force_port_field string
|
||||
showVersion bool
|
||||
proxy string
|
||||
@@ -64,11 +99,28 @@ type CLIArgs struct {
|
||||
maxPause time.Duration
|
||||
backoffInitial time.Duration
|
||||
backoffDeadline time.Duration
|
||||
initRetries int
|
||||
initRetryInterval time.Duration
|
||||
hideSNI bool
|
||||
userAgent *string
|
||||
}
|
||||
|
||||
func parse_args() CLIArgs {
|
||||
var args CLIArgs
|
||||
func parse_args() *CLIArgs {
|
||||
args := &CLIArgs{
|
||||
resolver: &CSVArg{
|
||||
values: []string{
|
||||
"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/",
|
||||
},
|
||||
},
|
||||
}
|
||||
flag.StringVar(&args.extVer, "ext-ver", "", "extension version to mimic in requests. "+
|
||||
"Can be obtained from https://chrome.google.com/webstore/detail/hola-vpn-the-website-unbl/gkojfkhlekighikafcpjkiklfbnlmeio")
|
||||
flag.StringVar(&args.force_port_field, "force-port-field", "", "force specific port field/num (example 24232 or lum)") // would be nice to not show in help page
|
||||
@@ -79,21 +131,30 @@ func parse_args() CLIArgs {
|
||||
flag.StringVar(&args.bind_address, "bind-address", "127.0.0.1:8080", "HTTP proxy listen address")
|
||||
flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+
|
||||
"(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.rotate, "rotate", 1*time.Hour, "rotate user ID once per given period")
|
||||
flag.DurationVar(&args.timeout, "timeout", 35*time.Second, "timeout for network operations")
|
||||
flag.DurationVar(&args.rotate, "rotate", 48*time.Hour, "rotate user ID once per given period")
|
||||
flag.DurationVar(&args.backoffInitial, "backoff-initial", 3*time.Second, "initial average backoff delay for zgettunnels (randomized by +/-50%)")
|
||||
flag.DurationVar(&args.backoffDeadline, "backoff-deadline", 5*time.Minute, "total duration of zgettunnels method attempts")
|
||||
flag.IntVar(&args.initRetries, "init-retries", 0, "number of attempts for initialization steps, zero for unlimited retry")
|
||||
flag.DurationVar(&args.initRetryInterval, "init-retry-interval", 5*time.Second, "delay between initialization retries")
|
||||
flag.StringVar(&args.proxy_type, "proxy-type", "direct", "proxy type: direct or lum") // or skip but not mentioned
|
||||
// skip would be used something like this: `./bin/hola-proxy -proxy-type skip -force-port-field 24232 -country ua.peer` for debugging
|
||||
flag.StringVar(&args.resolver, "resolver", "https://cloudflare-dns.com/dns-query",
|
||||
"DNS/DoH/DoT resolver to workaround Hola blocked hosts. "+
|
||||
"See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format.")
|
||||
flag.Var(args.resolver, "resolver",
|
||||
"comma-separated list of DNS/DoH/DoT resolvers used to lookup domain names blocked by Hola. "+
|
||||
"Supported schemes are: dns://, https://, tls://, tcp://. "+
|
||||
"Example: https://1.1.1.1/dns-query,tls://9.9.9.9:853")
|
||||
flag.BoolVar(&args.use_trial, "dont-use-trial", false, "use regular ports instead of trial ports") // would be nice to not show in help page
|
||||
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. "+
|
||||
"Format: <http|https|socks5|socks5h>://[login:password@]host[:port] "+
|
||||
"Examples: http://user:password@192.168.1.1:3128, socks5://10.0.0.1:1080")
|
||||
flag.StringVar(&args.caFile, "cafile", "", "use custom CA certificate bundle file")
|
||||
flag.Func("user-agent",
|
||||
"value of User-Agent header in requests. Default: User-Agent of latest stable Chrome for Windows",
|
||||
func(s string) error {
|
||||
args.userAgent = &s
|
||||
return nil
|
||||
})
|
||||
flag.BoolVar(&args.hideSNI, "hide-SNI", true, "hide SNI in TLS sessions with proxy server")
|
||||
flag.Parse()
|
||||
if args.country == "" {
|
||||
@@ -176,40 +237,81 @@ func run() int {
|
||||
UpdateHolaDialer(dialer)
|
||||
}
|
||||
|
||||
if args.list_countries {
|
||||
return print_countries(args.timeout)
|
||||
}
|
||||
try := retryPolicy(args.initRetries, args.initRetryInterval, mainLogger)
|
||||
|
||||
if args.extVer == "" {
|
||||
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
|
||||
defer cl()
|
||||
extVer, err := GetExtVer(ctx, nil, HolaExtStoreID, dialer)
|
||||
if err != nil {
|
||||
mainLogger.Critical("Can't detect latest API version. Try to specify -ext-ver parameter. Error: %v", err)
|
||||
return 8
|
||||
}
|
||||
args.extVer = extVer
|
||||
mainLogger.Warning("Detected latest extension version: %q. Pass -ext-ver parameter to skip resolve and speedup startup", args.extVer)
|
||||
cl()
|
||||
}
|
||||
if args.list_proxies {
|
||||
return print_proxies(mainLogger, args.extVer, args.country, args.proxy_type, args.limit, args.timeout,
|
||||
args.backoffInitial, args.backoffDeadline)
|
||||
if args.list_countries {
|
||||
return print_countries(try, args.timeout)
|
||||
}
|
||||
|
||||
mainLogger.Info("hola-proxy client version %s is starting...", version)
|
||||
|
||||
var userAgent string
|
||||
if args.userAgent == nil {
|
||||
err := try("get latest version of Chrome browser", func() error {
|
||||
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
|
||||
defer cl()
|
||||
ver, err := GetChromeVer(ctx, dialer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mainLogger.Info("latest Chrome version is %q", ver)
|
||||
majorVer, _, _ := strings.Cut(ver, ".")
|
||||
userAgent = fmt.Sprintf(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36",
|
||||
majorVer)
|
||||
mainLogger.Info("discovered latest Chrome User-Agent: %q", userAgent)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
mainLogger.Critical("Can't detect latest Chrome version. "+
|
||||
"Try to specify proper user agent with -user-agent parameter. Error: %v",
|
||||
err)
|
||||
return 8
|
||||
}
|
||||
} else {
|
||||
userAgent = *args.userAgent
|
||||
}
|
||||
SetUserAgent(userAgent)
|
||||
|
||||
if args.extVer == "" {
|
||||
err := try("get latest version of browser extension", func() error {
|
||||
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
|
||||
defer cl()
|
||||
extVer, err := GetExtVer(ctx, nil, HolaExtStoreID, dialer)
|
||||
if err == nil {
|
||||
mainLogger.Info("discovered latest browser extension version: %s", extVer)
|
||||
args.extVer = extVer
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
mainLogger.Critical("Can't detect latest browser extension version. Try to specify -ext-ver parameter. Error: %v", err)
|
||||
return 8
|
||||
}
|
||||
mainLogger.Warning("Detected latest extension version: %q. Pass -ext-ver parameter to skip resolve and speedup startup", args.extVer)
|
||||
}
|
||||
if args.list_proxies {
|
||||
return print_proxies(try, mainLogger, args.extVer, args.country, args.proxy_type, args.limit, args.timeout,
|
||||
args.backoffInitial, args.backoffDeadline)
|
||||
}
|
||||
|
||||
mainLogger.Info("Constructing fallback DNS upstream...")
|
||||
resolver, err := NewResolver(args.resolver, args.timeout)
|
||||
resolver, err := FastResolverFromURLs(args.resolver.values...)
|
||||
if err != nil {
|
||||
mainLogger.Critical("Unable to instantiate DNS resolver: %v", err)
|
||||
return 6
|
||||
}
|
||||
|
||||
mainLogger.Info("Initializing configuration provider...")
|
||||
auth, tunnels, err := CredService(args.rotate, args.timeout, args.extVer, args.country,
|
||||
args.proxy_type, credLogger, args.backoffInitial, args.backoffDeadline)
|
||||
var (
|
||||
auth AuthProvider
|
||||
tunnels *ZGetTunnelsResponse
|
||||
)
|
||||
err = try("run credentials service", func() error {
|
||||
auth, tunnels, err = CredService(args.rotate, args.timeout, args.extVer, args.country,
|
||||
args.proxy_type, credLogger, args.backoffInitial, args.backoffDeadline)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
mainLogger.Critical("Unable to instantiate credential service: %v", err)
|
||||
return 4
|
||||
}
|
||||
endpoint, err := get_endpoint(tunnels, args.proxy_type, args.use_trial, args.force_port_field)
|
||||
@@ -232,3 +334,24 @@ func run() int {
|
||||
func main() {
|
||||
os.Exit(run())
|
||||
}
|
||||
|
||||
func retryPolicy(retries int, retryInterval time.Duration, logger *CondLogger) func(string, func() error) error {
|
||||
return func(name string, f func() error) error {
|
||||
var err error
|
||||
for i := 1; retries <= 0 || i <= retries; i++ {
|
||||
if i > 1 {
|
||||
logger.Warning("Retrying action %q in %v...", name, retryInterval)
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
logger.Info("Attempting action %q, attempt #%d...", name, i)
|
||||
err = f()
|
||||
if err == nil {
|
||||
logger.Info("Action %q succeeded on attempt #%d", name, i)
|
||||
return nil
|
||||
}
|
||||
logger.Warning("Action %q failed: %v", name, err)
|
||||
}
|
||||
logger.Critical("All attempts for action %q have failed. Last error: %v", name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
12
plaintext.go
12
plaintext.go
@@ -2,10 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
type PlaintextDialer struct {
|
||||
@@ -46,7 +47,7 @@ func (d *PlaintextDialer) DialContext(ctx context.Context, network, address stri
|
||||
if d.hideSNI {
|
||||
sni = ""
|
||||
}
|
||||
conn = tls.Client(conn, &tls.Config{
|
||||
tlsConn := tls.UClient(conn, &tls.Config{
|
||||
ServerName: sni,
|
||||
InsecureSkipVerify: true,
|
||||
VerifyConnection: func(cs tls.ConnectionState) error {
|
||||
@@ -61,7 +62,12 @@ func (d *PlaintextDialer) DialContext(ctx context.Context, network, address stri
|
||||
_, err := cs.PeerCertificates[0].Verify(opts)
|
||||
return err
|
||||
},
|
||||
})
|
||||
}, tls.HelloAndroid_11_OkHttp)
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
181
resolver.go
181
resolver.go
@@ -1,83 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/ncruces/go-dns"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
upstream upstream.Upstream
|
||||
}
|
||||
|
||||
const DOT = 0x2e
|
||||
|
||||
func NewResolver(address string, timeout time.Duration) (*Resolver, error) {
|
||||
opts := &upstream.Options{Timeout: timeout}
|
||||
u, err := upstream.AddressToUpstream(address, opts)
|
||||
func FromURL(u string) (*net.Resolver, error) {
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Resolver{upstream: u}, nil
|
||||
switch strings.ToLower(parsed.Scheme) {
|
||||
case "", "dns":
|
||||
host := parsed.Hostname()
|
||||
port := parsed.Port()
|
||||
if port == "" {
|
||||
port = "53"
|
||||
}
|
||||
return NewPlainResolver(net.JoinHostPort(host, port)), nil
|
||||
case "tcp":
|
||||
host := parsed.Hostname()
|
||||
port := parsed.Port()
|
||||
if port == "" {
|
||||
port = "53"
|
||||
}
|
||||
return NewTCPResolver(net.JoinHostPort(host, port)), nil
|
||||
case "http", "https":
|
||||
return dns.NewDoHResolver(u)
|
||||
case "tls":
|
||||
host := parsed.Hostname()
|
||||
port := parsed.Port()
|
||||
if port == "" {
|
||||
port = "853"
|
||||
}
|
||||
return dns.NewDoTResolver(net.JoinHostPort(host, port))
|
||||
default:
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveA(domain string) []string {
|
||||
res := make([]string, 0)
|
||||
if len(domain) == 0 {
|
||||
return res
|
||||
type LookupNetIPer interface {
|
||||
LookupNetIP(context.Context, string, string) ([]netip.Addr, error)
|
||||
}
|
||||
|
||||
type FastResolver struct {
|
||||
upstreams []LookupNetIPer
|
||||
}
|
||||
|
||||
type lookupReply struct {
|
||||
addrs []netip.Addr
|
||||
err error
|
||||
}
|
||||
|
||||
func FastResolverFromURLs(urls ...string) (*FastResolver, error) {
|
||||
resolvers := make([]LookupNetIPer, 0, len(urls))
|
||||
for i, u := range urls {
|
||||
res, err := FromURL(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to construct resolver #%d (%q): %w", i, u, err)
|
||||
}
|
||||
resolvers = append(resolvers, res)
|
||||
}
|
||||
if domain[len(domain)-1] != DOT {
|
||||
domain = domain + "."
|
||||
return NewFastResolver(resolvers...), nil
|
||||
}
|
||||
|
||||
func NewFastResolver(resolvers ...LookupNetIPer) *FastResolver {
|
||||
return &FastResolver{
|
||||
upstreams: resolvers,
|
||||
}
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: domain, Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
|
||||
func (r FastResolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) {
|
||||
ctx, cl := context.WithCancel(ctx)
|
||||
drain := make(chan lookupReply, len(r.upstreams))
|
||||
for _, res := range r.upstreams {
|
||||
go func(res LookupNetIPer) {
|
||||
addrs, err := res.LookupNetIP(ctx, network, host)
|
||||
drain <- lookupReply{addrs, err}
|
||||
}(res)
|
||||
}
|
||||
reply, err := r.upstream.Exchange(&req)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
for _, rr := range reply.Answer {
|
||||
if a, ok := rr.(*dns.A); ok {
|
||||
res = append(res, a.A.String())
|
||||
|
||||
i := 0
|
||||
var resAddrs []netip.Addr
|
||||
var resErr error
|
||||
for ; i < len(r.upstreams); i++ {
|
||||
pair := <-drain
|
||||
if pair.err != nil {
|
||||
resErr = multierror.Append(resErr, pair.err)
|
||||
} else {
|
||||
cl()
|
||||
resAddrs = pair.addrs
|
||||
resErr = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveAAAA(domain string) []string {
|
||||
res := make([]string, 0)
|
||||
if len(domain) == 0 {
|
||||
return res
|
||||
}
|
||||
if domain[len(domain)-1] != DOT {
|
||||
domain = domain + "."
|
||||
}
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: domain, Qtype: dns.TypeAAAA, Qclass: dns.ClassINET},
|
||||
}
|
||||
reply, err := r.upstream.Exchange(&req)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
for _, rr := range reply.Answer {
|
||||
if a, ok := rr.(*dns.AAAA); ok {
|
||||
res = append(res, a.AAAA.String())
|
||||
go func() {
|
||||
for i = i + 1; i < len(r.upstreams); i++ {
|
||||
<-drain
|
||||
}
|
||||
}
|
||||
return res
|
||||
}()
|
||||
return resAddrs, resErr
|
||||
}
|
||||
|
||||
func (r *Resolver) Resolve(domain string) []string {
|
||||
res := r.ResolveA(domain)
|
||||
if len(res) == 0 {
|
||||
res = r.ResolveAAAA(domain)
|
||||
func NewPlainResolver(addr string) *net.Resolver {
|
||||
return &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
return (&net.Dialer{
|
||||
Resolver: &net.Resolver{},
|
||||
}).DialContext(ctx, network, addr)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewTCPResolver(addr string) *net.Resolver {
|
||||
return &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
dnet := "tcp"
|
||||
switch network {
|
||||
case "udp4":
|
||||
dnet = "tcp4"
|
||||
case "udp6":
|
||||
dnet = "tcp6"
|
||||
}
|
||||
return (&net.Dialer{
|
||||
Resolver: &net.Resolver{},
|
||||
}).DialContext(ctx, dnet, addr)
|
||||
},
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
31
retry.go
31
retry.go
@@ -2,16 +2,17 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type RetryDialer struct {
|
||||
dialer ContextDialer
|
||||
resolver *Resolver
|
||||
resolver LookupNetIPer
|
||||
logger *CondLogger
|
||||
}
|
||||
|
||||
func NewRetryDialer(dialer ContextDialer, resolver *Resolver, logger *CondLogger) *RetryDialer {
|
||||
func NewRetryDialer(dialer ContextDialer, resolver LookupNetIPer, logger *CondLogger) *RetryDialer {
|
||||
return &RetryDialer{
|
||||
dialer: dialer,
|
||||
resolver: resolver,
|
||||
@@ -28,12 +29,30 @@ func (d *RetryDialer) DialContext(ctx context.Context, network, address string)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
ips := d.resolver.Resolve(host)
|
||||
if len(ips) == 0 {
|
||||
return conn, err
|
||||
var resolveNetwork string
|
||||
switch network {
|
||||
case "udp4", "tcp4", "ip4":
|
||||
resolveNetwork = "ip4"
|
||||
case "udp6", "tcp6", "ip6":
|
||||
resolveNetwork = "ip6"
|
||||
case "udp", "tcp", "ip":
|
||||
resolveNetwork = "ip"
|
||||
default:
|
||||
return nil, fmt.Errorf("resolving dial %q: unsupported network %q", address, network)
|
||||
}
|
||||
resolved, err := d.resolver.LookupNetIP(ctx, resolveNetwork, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial failed on address lookup: %w", err)
|
||||
}
|
||||
|
||||
return d.dialer.DialContext(ctx, network, net.JoinHostPort(ips[0], port))
|
||||
var conn net.Conn
|
||||
for _, ip := range resolved {
|
||||
conn, err = d.dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("failed to dial %s: %w", address, err)
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
name: hola-proxy
|
||||
version: '1.11.1'
|
||||
summary: Standalone Hola proxy client.
|
||||
description: |
|
||||
Standalone Hola proxy client. Just run it and it'll start plain HTTP proxy server forwarding traffic via Hola proxies of your choice.
|
||||
|
||||
confinement: strict
|
||||
base: core18
|
||||
base: core22
|
||||
adopt-info: hola-proxy
|
||||
|
||||
parts:
|
||||
hola-proxy:
|
||||
plugin: go
|
||||
source: .
|
||||
build-snaps: [go/latest/stable]
|
||||
build-packages:
|
||||
- gcc
|
||||
- make
|
||||
- git-core
|
||||
source: https://github.com/Snawoot/hola-proxy
|
||||
source-type: git
|
||||
override-pull: |
|
||||
craftctl default
|
||||
craftctl set version="$(git describe --long --tags --always --match=v*.*.* | sed 's/v//')"
|
||||
override-build:
|
||||
make &&
|
||||
cp bin/hola-proxy "$SNAPCRAFT_PART_INSTALL"
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -14,6 +13,8 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -106,7 +107,7 @@ func (d *ProxyDialer) DialContext(ctx context.Context, network, address string)
|
||||
if d.hideSNI {
|
||||
sni = ""
|
||||
}
|
||||
conn = tls.Client(conn, &tls.Config{
|
||||
conn = tls.UClient(conn, &tls.Config{
|
||||
ServerName: sni,
|
||||
InsecureSkipVerify: true,
|
||||
VerifyConnection: func(cs tls.ConnectionState) error {
|
||||
@@ -121,7 +122,7 @@ func (d *ProxyDialer) DialContext(ctx context.Context, network, address string)
|
||||
_, err := cs.PeerCertificates[0].Verify(opts)
|
||||
return err
|
||||
},
|
||||
})
|
||||
}, tls.HelloAndroid_11_OkHttp)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
|
||||
68
utils.go
68
utils.go
@@ -106,27 +106,33 @@ func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer
|
||||
return
|
||||
}
|
||||
|
||||
func print_countries(timeout time.Duration) int {
|
||||
func print_countries(try func(string, func() error) error, timeout time.Duration) int {
|
||||
var (
|
||||
countries CountryList
|
||||
err error
|
||||
tx_res bool
|
||||
tx_err error
|
||||
)
|
||||
tx_res, tx_err := EnsureTransaction(context.Background(), timeout, func(ctx context.Context, client *http.Client) bool {
|
||||
ctx1, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
countries, err = VPNCountries(ctx1, client)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Transaction error: %v. Retrying with the fallback mechanism...\n", err)
|
||||
return false
|
||||
err = try("list VPN countries", func() error {
|
||||
tx_res, tx_err = EnsureTransaction(context.Background(), timeout, func(ctx context.Context, client *http.Client) bool {
|
||||
ctx1, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
countries, err = VPNCountries(ctx1, client)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Transaction error: %v. Retrying with the fallback mechanism...\n", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if tx_err != nil {
|
||||
return fmt.Errorf("transaction recovery mechanism failure: %v", err)
|
||||
}
|
||||
return true
|
||||
if !tx_res {
|
||||
return errors.New("all fallback proxies failed.")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if tx_err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Transaction recovery mechanism failure: %v.\n", tx_err)
|
||||
return 4
|
||||
}
|
||||
if !tx_res {
|
||||
fmt.Fprintf(os.Stderr, "All attempts failed.")
|
||||
if err != nil {
|
||||
return 3
|
||||
}
|
||||
for _, code := range countries {
|
||||
@@ -135,28 +141,34 @@ func print_countries(timeout time.Duration) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func print_proxies(logger *CondLogger, extVer, country string, proxy_type string,
|
||||
func print_proxies(try func(string, func() error) error, logger *CondLogger, extVer, country string, proxy_type string,
|
||||
limit uint, timeout time.Duration, backoffInitial time.Duration, backoffDeadline time.Duration,
|
||||
) int {
|
||||
var (
|
||||
tunnels *ZGetTunnelsResponse
|
||||
user_uuid string
|
||||
err error
|
||||
tx_res bool
|
||||
tx_err error
|
||||
)
|
||||
tx_res, tx_err := EnsureTransaction(context.Background(), timeout, func(ctx context.Context, client *http.Client) bool {
|
||||
tunnels, user_uuid, err = Tunnels(ctx, logger, client, extVer, country, proxy_type, limit, timeout, backoffInitial, backoffDeadline)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Transaction error: %v. Retrying with the fallback mechanism...\n", err)
|
||||
return false
|
||||
err = try("list proxies", func() error {
|
||||
tx_res, tx_err = EnsureTransaction(context.Background(), timeout, func(ctx context.Context, client *http.Client) bool {
|
||||
tunnels, user_uuid, err = Tunnels(ctx, logger, client, extVer, country, proxy_type, limit, timeout, backoffInitial, backoffDeadline)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Transaction error: %v. Retrying with the fallback mechanism...\n", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if tx_err != nil {
|
||||
return fmt.Errorf("transaction recovery mechanism failure: %v", err)
|
||||
}
|
||||
return true
|
||||
if !tx_res {
|
||||
return errors.New("all fallback proxies failed.")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if tx_err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Transaction recovery mechanism failure: %v.\n", tx_err)
|
||||
return 4
|
||||
}
|
||||
if !tx_res {
|
||||
fmt.Fprintf(os.Stderr, "All attempts failed.")
|
||||
if err != nil {
|
||||
return 3
|
||||
}
|
||||
wr := csv.NewWriter(os.Stdout)
|
||||
|
||||
Reference in New Issue
Block a user