Compare commits

..

38 Commits

Author SHA1 Message Date
Vladislav Yarmak
776411c2d4 bump snap version 2021-03-23 18:39:51 +02:00
Snawoot
f2c7f73548 Merge pull request #41 from Snawoot/no_sni
Major network stack rework
2021-03-23 18:34:13 +02:00
Vladislav Yarmak
af12cee5f0 reworked resolve&tunnel workaround as a stackable dialer 2021-03-23 18:13:31 +02:00
Vladislav Yarmak
05ac2bc146 rework proxy handler on new stack 2021-03-23 17:34:12 +02:00
Vladislav Yarmak
1511ed333a no SNI for upstream proxy dial-outs 2021-03-23 16:03:55 +02:00
Vladislav Yarmak
af955a6cd1 switch fallback to new dialer stack 2021-03-23 15:47:34 +02:00
Vladislav Yarmak
cfd3474af3 WIP: proxy tcp dialer 2021-03-23 14:48:19 +02:00
Snawoot
eb2ae460da Merge pull request #40 from Snawoot/improvements
Makefile improvements
2021-03-20 01:29:02 +02:00
Vladislav Yarmak
dbb9b29c81 experimental build for Windows on ARM 2021-03-20 01:24:03 +02:00
Vladislav Yarmak
839499313b extend *BSD build support 2021-03-20 01:11:57 +02:00
Vladislav Yarmak
58006c4f6b introduce Apple M1 support 2021-03-19 20:57:49 +02:00
Vladislav Yarmak
b23dc2f63f allow go tool override 2021-03-19 17:46:21 +02:00
Snawoot
55788d8188 Merge pull request #39 from rany2/master
Add new helper script
2021-03-17 13:09:10 +02:00
rany
bc0b0df26a Use $(...) notation instead of legacy backticked 2021-03-17 12:27:58 +02:00
rany
700ae4a4f4 try to locate hola-proxy in ~/go/bin/hola-proxy also 2021-03-17 12:26:37 +02:00
rany
8db460ff99 Add new helper script 2021-03-17 10:15:03 +02:00
Snawoot
9efb99cf8e Merge pull request #36 from aarivex/patch-1
Rework README.md
2021-03-16 01:30:19 +02:00
aarivex
af2f1e40d6 Update README.md
Co-authored-by: Snawoot <vladislav@vm-0.com>
2021-03-16 00:26:23 +01:00
aarivex
f6d3de8488 Update README.md
Co-authored-by: Snawoot <vladislav@vm-0.com>
2021-03-16 00:26:17 +01:00
aarivex
fde7f1516b Rework README.md
- Changed `~/go/bin/hola-proxy` to `./hola-proxy`
- Slight improvement of grammar, rewrote some sentences
- Reworked the help output to an arguments table
2021-03-16 00:12:29 +01:00
Vladislav Yarmak
3f92cecac9 makefile: use dynamic linking for install target 2021-03-16 01:12:21 +02:00
Vladislav Yarmak
6bfb8d0aee bump snap version 2021-03-16 00:57:34 +02:00
Snawoot
4faf6aa04b Merge pull request #35 from Snawoot/skip_agent_resolve
Skip upstream agent resolve
2021-03-16 00:56:47 +02:00
Vladislav Yarmak
edd723079d fix endpoint display 2021-03-16 00:49:59 +02:00
Vladislav Yarmak
3cb79059b2 skip upstream agent resolve 2021-03-16 00:46:49 +02:00
Snawoot
3b09f31616 Merge pull request #34 from Snawoot/makefile_upd
makefile: use dynamic linking for native target
2021-03-15 15:45:56 +02:00
Vladislav Yarmak
8d9285c00b makefile: use dynamic linking for native target 2021-03-15 15:42:20 +02:00
Vladislav Yarmak
528e2b2a71 bump snap version 2021-03-15 04:13:04 +02:00
Snawoot
0a473f9662 Merge pull request #32 from Snawoot/fallback
feat: Hola API communication failover
2021-03-15 04:12:15 +02:00
Vladislav Yarmak
17860682be do not resolve fallback agent IP addr, use provided in config 2021-03-15 04:07:03 +02:00
Vladislav Yarmak
1f6c87a797 fix typos 2021-03-15 02:21:23 +02:00
Vladislav Yarmak
72beef10c9 feat: Hola API communication failover 2021-03-15 02:13:42 +02:00
Vladislav Yarmak
ead89d5245 snap: remove unnecessary go-importpath 2021-03-13 06:32:48 +02:00
Vladislav Yarmak
6edd098c82 embed version into snap image 2021-03-13 06:30:34 +02:00
Vladislav Yarmak
752d2ba789 fix dockerfile version embedding 2021-03-13 05:28:49 +02:00
Snawoot
311d1ad74d Merge pull request #30 from Snawoot/docker_builds
embed version into docker image
2021-03-13 05:14:54 +02:00
Vladislav Yarmak
6ac04587cb embed version into docker image 2021-03-13 05:14:19 +02:00
Vladislav Yarmak
8c3538ab4c bump snap version 2021-03-13 04:42:20 +02:00
14 changed files with 779 additions and 369 deletions

View File

@@ -1,8 +1,10 @@
FROM golang AS build
ARG GIT_DESC=undefined
WORKDIR /go/src/github.com/Snawoot/hola-proxy
COPY . .
RUN CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w -extldflags "-static"'
RUN CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w -extldflags "-static" -X main.version='"$GIT_DESC"
ADD https://curl.haxx.se/ca/cacert.pem /certs.crt
RUN chmod 0644 /certs.crt

View File

@@ -3,14 +3,19 @@ OUTSUFFIX = bin/$(PROGNAME)
VERSION := $(shell git describe)
BUILDOPTS = -a -tags netgo
LDFLAGS = -ldflags '-s -w -extldflags "-static" -X main.version=$(VERSION)'
LDFLAGS_NATIVE = -ldflags '-s -w -X main.version=$(VERSION)'
GO := go
src = $(wildcard *.go)
native: bin-native
all: bin-linux-amd64 bin-linux-386 bin-linux-arm \
bin-freebsd-amd64 bin-freebsd-386 bin-freebsd-arm \
bin-darwin-amd64 \
bin-windows-amd64 bin-windows-386
bin-netbsd-amd64 bin-netbsd-386 \
bin-openbsd-amd64 bin-openbsd-386 \
bin-darwin-amd64 bin-darwin-arm64 \
bin-windows-amd64 bin-windows-386 bin-windows-arm
bin-native: $(OUTSUFFIX)
bin-linux-amd64: $(OUTSUFFIX).linux-amd64
@@ -19,51 +24,75 @@ bin-linux-arm: $(OUTSUFFIX).linux-arm
bin-freebsd-amd64: $(OUTSUFFIX).freebsd-amd64
bin-freebsd-386: $(OUTSUFFIX).freebsd-386
bin-freebsd-arm: $(OUTSUFFIX).freebsd-arm
bin-netbsd-amd64: $(OUTSUFFIX).netbsd-amd64
bin-netbsd-386: $(OUTSUFFIX).netbsd-386
bin-openbsd-amd64: $(OUTSUFFIX).openbsd-amd64
bin-openbsd-386: $(OUTSUFFIX).openbsd-386
bin-darwin-amd64: $(OUTSUFFIX).darwin-amd64
bin-darwin-arm64: $(OUTSUFFIX).darwin-arm64
bin-windows-amd64: $(OUTSUFFIX).windows-amd64.exe
bin-windows-386: $(OUTSUFFIX).windows-386.exe
bin-windows-arm: $(OUTSUFFIX).windows-arm.exe
$(OUTSUFFIX): $(src)
CGO_ENABLED=0 go build $(BUILDOPTS) $(LDFLAGS) -o $@
$(GO) build $(LDFLAGS_NATIVE) -o $@
$(OUTSUFFIX).linux-amd64: $(src)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(BUILDOPTS) $(LDFLAGS) -o $@
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).linux-386: $(src)
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build $(BUILDOPTS) $(LDFLAGS) -o $@
CGO_ENABLED=0 GOOS=linux GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).linux-arm: $(src)
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build $(BUILDOPTS) $(LDFLAGS) -o $@
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).freebsd-amd64: $(src)
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build $(BUILDOPTS) $(LDFLAGS) -o $@
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).freebsd-386: $(src)
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build $(BUILDOPTS) $(LDFLAGS) -o $@
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).freebsd-arm: $(src)
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm go build $(BUILDOPTS) $(LDFLAGS) -o $@
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).netbsd-amd64: $(src)
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).netbsd-386: $(src)
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).openbsd-amd64: $(src)
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).openbsd-386: $(src)
CGO_ENABLED=0 GOOS=openbsd GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(OUTSUFFIX).darwin-amd64: $(src)
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(BUILDOPTS) $(LDFLAGS) -o $@
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
$(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 go 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 go 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 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
clean:
rm -f bin/*
fmt:
go fmt ./...
$(GO) fmt ./...
run:
go run $(LDFLAGS) .
$(GO) run $(LDFLAGS) .
install:
go install $(BUILDOPTS) $(LDFLAGS) .
$(GO) install $(LDFLAGS_NATIVE) .
.PHONY: clean all native fmt install \
bin-native \

View File

@@ -2,7 +2,8 @@
[![hola-proxy](https://snapcraft.io//hola-proxy/badge.svg)](https://snapcraft.io/hola-proxy)
Standalone Hola proxy client. Just run it and it'll start plain HTTP proxy server forwarding traffic via Hola proxies of your choice. By default application listens port on 127.0.0.1:8080.
Standalone Hola proxy client. Just run it and it'll start a plain HTTP proxy server forwarding traffic through Hola proxies of your choice.
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 pool` or `-proxy-type lum`).
@@ -31,17 +32,18 @@ git clone https://ipfs.io/ipns/k51qzi5uqu5dkrgx0hozpy1tlggw5o0whtquyrjlc6pprhvbm
* Cross-platform (Windows/Mac OS/Linux/Android (via shell)/\*BSD)
* Uses TLS for secure communication with upstream proxies
* Zero-configuration
* Zero configuration
* Simple and straight forward
## Installation
#### Binary download
#### Binaries
Pre-built binaries available on [releases](https://github.com/Snawoot/hola-proxy/releases/latest) page.
Pre-built binaries are available [here](https://github.com/Snawoot/hola-proxy/releases/latest).
#### From source
#### Build from source
Alternatively, you may install hola-proxy from source. Run within source directory
Alternatively, you may install hola-proxy from source. Run the following within the source directory:
```
make install
@@ -49,7 +51,7 @@ make install
#### Docker
Docker image is available as well. Here is an example for running proxy via DE as a background service:
A docker image is available as well. Here is an example of running hola-proxy via DE as a background service:
```sh
docker run -d \
@@ -73,7 +75,7 @@ sudo snap install hola-proxy
List available countries:
```
$ ~/go/bin/hola-proxy -list-countries
$ ./hola-proxy -list-countries
ar - Argentina
at - Austria
au - Australia
@@ -121,19 +123,19 @@ us - United States of America
Run proxy via country of your choice:
```
$ ~/go/bin/hola-proxy -country de
$ ./hola-proxy -country de
```
Or run proxy on residental IP:
Or run proxy on residential IP:
```
$ ~/go/bin/hola-proxy -country de -proxy-type lum
$ ./hola-proxy -proxy-type lum
```
Also it is possible to export proxy addresses and credentials:
```
$ ~/go/bin/hola-proxy -country de -list-proxies -limit 3
$ ./hola-proxy -country de -list-proxies -limit 3
Login: user-uuid-0a67c797b3214cbdb432b089c4b801cd
Password: cd123c465901
Proxy-Authorization: basic dXNlci11dWlkLTBhNjdjNzk3YjMyMTRjYmRiNDMyYjA4OWM0YjgwMWNkOmNkMTIzYzQ2NTkwMQ==
@@ -144,33 +146,19 @@ zagent830.hola.org,104.248.24.64,22222,22223,22224,22225,22226,digitalocean
zagent248.hola.org,165.22.65.3,22222,22223,22224,22225,22226,digitalocean
```
## Synopsis
## List of arguments
```
$ ~/go/bin/hola-proxy -h
Usage of /home/user/go/bin/hola-proxy:
-bind-address string
HTTP proxy listen address (default "127.0.0.1:8080")
-country string
desired proxy location (default "us")
-dont-use-trial
use regular ports instead of trial ports
-force-port-field string
force specific port field/num (example 24232 or lum)
-limit uint
amount of proxies in retrieved list (default 3)
-list-countries
list available countries and exit
-list-proxies
output proxy list and exit
-proxy-type string
proxy type: direct or peer or lum or virt or pool (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)
-timeout duration
timeout for network operations (default 10s)
-verbosity int
logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20)
```
| Argument | Type | Description |
| -------- | ---- | ----------- |
| bind-address | String | HTTP proxy address to listen to (default "127.0.0.1:8080") |
| country | String | desired proxy location (default "us") |
| dont-use-trial | - | use regular ports instead of trial ports |
| force-port-field | Number | force specific port field/num (example 24232 or lum) |
| 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-type | String | proxy type (Datacenter: direct, virt) (Residential: peer, lum, pool) (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) |
| timeout | Duration | timeout for network operations (default 10s) |
| verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |

View File

@@ -2,12 +2,12 @@ package main
import (
"context"
"net/http"
"sync"
"time"
)
const DEFAULT_LIST_LIMIT = 3
const API_CALL_ATTEMPTS = 3
func CredService(interval, timeout time.Duration,
country string,
@@ -24,15 +24,21 @@ func CredService(interval, timeout time.Duration,
return
}
for i := 0; i < API_CALL_ATTEMPTS; i++ {
ctx, _ := context.WithTimeout(context.Background(), timeout)
tunnels, user_uuid, err = Tunnels(ctx, country, proxytype, DEFAULT_LIST_LIMIT)
if err == nil {
break
tx_res, tx_err := EnsureTransaction(context.Background(), timeout, func(ctx context.Context, client *http.Client) bool {
tunnels, user_uuid, err = Tunnels(ctx, client, country, proxytype, DEFAULT_LIST_LIMIT)
if err != nil {
logger.Error("Configuration bootstrap error: %v. Retrying with the fallback mechanism...", err)
return false
}
return true
})
if tx_err != nil {
logger.Critical("Transaction recovery mechanism failure: %v", tx_err)
err = tx_err
return
}
if err != nil {
logger.Critical("Configuration bootstrap failed: %v", err)
if !tx_res {
logger.Critical("All attempts failed.")
return
}
auth_header = basic_auth_header(LOGIN_PREFIX+user_uuid,
@@ -48,23 +54,28 @@ func CredService(interval, timeout time.Duration,
for {
<-ticker.C
logger.Info("Rotating credentials...")
for i := 0; i < API_CALL_ATTEMPTS; i++ {
ctx, _ := context.WithTimeout(context.Background(), timeout)
tuns, user_uuid, err = Tunnels(ctx, country, proxytype, DEFAULT_LIST_LIMIT)
if err == nil {
break
tx_res, tx_err := EnsureTransaction(context.Background(), timeout, func(ctx context.Context, client *http.Client) bool {
tuns, user_uuid, err = Tunnels(ctx, client, country, proxytype, DEFAULT_LIST_LIMIT)
if err != nil {
logger.Error("Credential rotation error: %v. Retrying with the fallback mechanism...", err)
return false
}
return true
})
if tx_err != nil {
logger.Critical("Transaction recovery mechanism failure: %v", tx_err)
err = tx_err
continue
}
if err != nil {
logger.Error("Credential rotation failed after %d attempts. Error: %v",
API_CALL_ATTEMPTS, err)
} else {
(&mux).Lock()
auth_header = basic_auth_header(LOGIN_PREFIX+user_uuid,
tuns.AgentKey)
(&mux).Unlock()
logger.Info("Credentials rotated successfully.")
if !tx_res {
logger.Critical("All rotation attempts failed.")
continue
}
(&mux).Lock()
auth_header = basic_auth_header(LOGIN_PREFIX+user_uuid,
tuns.AgentKey)
(&mux).Unlock()
logger.Info("Credentials rotated successfully.")
}
}()
return

23
csrand.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
crand "crypto/rand"
"math/big"
)
type secureRandomSource struct{}
var RandomSource secureRandomSource
var int63Limit = big.NewInt(0).Lsh(big.NewInt(1), 63)
func (_ secureRandomSource) Seed(_ int64) {
}
func (_ secureRandomSource) Int63() int64 {
randNum, err := crand.Int(crand.Reader, int63Limit)
if err != nil {
panic(err)
}
return randNum.Int64()
}

View File

@@ -1,122 +1,48 @@
package main
import (
"bufio"
"crypto/tls"
"io"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)
const BAD_REQ_MSG = "Bad Request\n"
type AuthProvider func() string
type ProxyHandler struct {
auth AuthProvider
upstream string
logger *CondLogger
dialer ContextDialer
httptransport http.RoundTripper
resolver *Resolver
}
func NewProxyHandler(upstream string, auth AuthProvider, resolver *Resolver, logger *CondLogger) *ProxyHandler {
proxyurl, err := url.Parse("https://" + upstream)
if err != nil {
panic(err)
}
func NewProxyHandler(dialer ContextDialer, resolver *Resolver, logger *CondLogger) *ProxyHandler {
dialer = NewRetryDialer(dialer, resolver, logger)
httptransport := &http.Transport{
Proxy: http.ProxyURL(proxyurl),
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: dialer.DialContext,
}
return &ProxyHandler{
auth: auth,
upstream: upstream,
logger: logger,
dialer: dialer,
httptransport: httptransport,
resolver: resolver,
}
}
func (s *ProxyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
s.logger.Info("Request: %v %v %v", req.RemoteAddr, req.Method, req.URL)
if strings.ToUpper(req.Method) == "CONNECT" {
req.Header.Set("Proxy-Authorization", s.auth())
rawreq, err := httputil.DumpRequest(req, false)
if err != nil {
s.logger.Error("Can't dump request: %v", err)
http.Error(wr, "Can't dump request", http.StatusInternalServerError)
return
}
conn, err := tls.Dial("tcp", s.upstream, nil)
if err != nil {
s.logger.Error("Can't dial tls upstream: %v", err)
http.Error(wr, "Can't dial tls upstream", http.StatusBadGateway)
return
}
_, err = conn.Write(rawreq)
if err != nil {
s.logger.Error("Can't write tls upstream: %v", err)
http.Error(wr, "Can't write tls upstream", http.StatusBadGateway)
return
}
bufrd := bufio.NewReader(conn)
proxyResp, err := http.ReadResponse(bufrd, req)
responseBytes := make([]byte, 0)
if err != nil {
s.logger.Error("Can't read response from upstream: %v", err)
http.Error(wr, "Can't read response from upstream", http.StatusBadGateway)
return
}
if proxyResp.StatusCode == http.StatusForbidden &&
proxyResp.Header.Get("X-Hola-Error") == "Forbidden Host" {
s.logger.Info("Request %s denied by upstream. Rescuing it with resolve&rewrite workaround.",
req.URL.String())
conn.Close()
conn, err = tls.Dial("tcp", s.upstream, nil)
if err != nil {
s.logger.Error("Can't dial tls upstream: %v", err)
http.Error(wr, "Can't dial tls upstream", http.StatusBadGateway)
return
}
defer conn.Close()
err = rewriteConnectReq(req, s.resolver)
if err != nil {
s.logger.Error("Can't rewrite request: %v", err)
http.Error(wr, "Can't rewrite request", http.StatusInternalServerError)
return
}
rawreq, err = httputil.DumpRequest(req, false)
if err != nil {
s.logger.Error("Can't dump request: %v", err)
http.Error(wr, "Can't dump request", http.StatusInternalServerError)
return
}
_, err = conn.Write(rawreq)
if err != nil {
s.logger.Error("Can't write tls upstream: %v", err)
http.Error(wr, "Can't write tls upstream", http.StatusBadGateway)
return
}
} else {
defer conn.Close()
responseBytes, err = httputil.DumpResponse(proxyResp, false)
if err != nil {
s.logger.Error("Can't dump response: %v", err)
http.Error(wr, "Can't dump response", http.StatusInternalServerError)
return
}
buffered := bufrd.Buffered()
if buffered > 0 {
trailer := make([]byte, buffered)
bufrd.Read(trailer)
responseBytes = append(responseBytes, trailer...)
}
}
bufrd = nil
func (s *ProxyHandler) HandleTunnel(wr http.ResponseWriter, req *http.Request) {
ctx := req.Context()
conn, err := s.dialer.DialContext(ctx, "tcp", req.RequestURI)
if err != nil {
s.logger.Error("Can't satisfy CONNECT request: %v", err)
http.Error(wr, "Can't satisfy CONNECT request", http.StatusBadGateway)
return
}
if req.ProtoMajor == 0 || req.ProtoMajor == 1 {
// Upgrade client connection
localconn, _, err := hijack(wr)
if err != nil {
@@ -126,95 +52,56 @@ func (s *ProxyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
}
defer localconn.Close()
if len(responseBytes) > 0 {
_, err = localconn.Write(responseBytes)
if err != nil {
return
}
}
// Inform client connection is built
fmt.Fprintf(localconn, "HTTP/%d.%d 200 OK\r\n\r\n", req.ProtoMajor, req.ProtoMinor)
proxy(req.Context(), localconn, conn)
} else if req.ProtoMajor == 2 {
wr.Header()["Date"] = nil
wr.WriteHeader(http.StatusOK)
flush(wr)
proxyh2(req.Context(), req.Body, wr, conn)
} else {
delHopHeaders(req.Header)
orig_req := req.Clone(req.Context())
req.RequestURI = ""
req.Header.Set("Proxy-Authorization", s.auth())
resp, err := s.httptransport.RoundTrip(req)
if err != nil {
s.logger.Error("HTTP fetch error: %v", err)
http.Error(wr, "Server Error", http.StatusInternalServerError)
return
}
if resp.StatusCode == http.StatusForbidden &&
resp.Header.Get("X-Hola-Error") == "Forbidden Host" {
s.logger.Info("Request %s denied by upstream. Rescuing it with resolve&tunnel workaround.",
req.URL.String())
resp.Body.Close()
// Prepare tunnel request
proxyReq, err := makeConnReq(orig_req.RequestURI, s.resolver)
if err != nil {
s.logger.Error("Can't rewrite request: %v", err)
http.Error(wr, "Can't rewrite request", http.StatusBadGateway)
return
}
proxyReq.Header.Set("Proxy-Authorization", s.auth())
rawreq, _ := httputil.DumpRequest(proxyReq, false)
// Prepare upstream TLS conn
conn, err := tls.Dial("tcp", s.upstream, nil)
if err != nil {
s.logger.Error("Can't dial tls upstream: %v", err)
http.Error(wr, "Can't dial tls upstream", http.StatusBadGateway)
return
}
defer conn.Close()
// Send proxy request
_, err = conn.Write(rawreq)
if err != nil {
s.logger.Error("Can't write tls upstream: %v", err)
http.Error(wr, "Can't write tls upstream", http.StatusBadGateway)
return
}
// Read proxy response
bufrd := bufio.NewReader(conn)
proxyResp, err := http.ReadResponse(bufrd, proxyReq)
if err != nil {
s.logger.Error("Can't read response from upstream: %v", err)
http.Error(wr, "Can't read response from upstream", http.StatusBadGateway)
return
}
if proxyResp.StatusCode != http.StatusOK {
delHopHeaders(proxyResp.Header)
copyHeader(wr.Header(), proxyResp.Header)
wr.WriteHeader(proxyResp.StatusCode)
}
// Send tunneled request
orig_req.RequestURI = ""
orig_req.Header.Set("Connection", "close")
rawreq, _ = httputil.DumpRequest(orig_req, false)
_, err = conn.Write(rawreq)
if err != nil {
s.logger.Error("Can't write tls upstream: %v", err)
http.Error(wr, "Can't write tls upstream", http.StatusBadGateway)
return
}
// Read tunneled response
resp, err = http.ReadResponse(bufrd, orig_req)
if err != nil {
s.logger.Error("Can't read response from upstream: %v", err)
http.Error(wr, "Can't read response from upstream", http.StatusBadGateway)
return
}
}
defer resp.Body.Close()
s.logger.Info("%v %v %v %v", req.RemoteAddr, req.Method, req.URL, resp.Status)
delHopHeaders(resp.Header)
copyHeader(wr.Header(), resp.Header)
wr.WriteHeader(resp.StatusCode)
io.Copy(wr, resp.Body)
s.logger.Error("Unsupported protocol version: %s", req.Proto)
http.Error(wr, "Unsupported protocol version.", http.StatusBadRequest)
return
}
}
func (s *ProxyHandler) HandleRequest(wr http.ResponseWriter, req *http.Request) {
req.RequestURI = ""
if req.ProtoMajor == 2 {
req.URL.Scheme = "http" // We can't access :scheme pseudo-header, so assume http
req.URL.Host = req.Host
}
resp, err := s.httptransport.RoundTrip(req)
if err != nil {
s.logger.Error("HTTP fetch error: %v", err)
http.Error(wr, "Server Error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
s.logger.Info("%v %v %v %v", req.RemoteAddr, req.Method, req.URL, resp.Status)
delHopHeaders(resp.Header)
copyHeader(wr.Header(), resp.Header)
wr.WriteHeader(resp.StatusCode)
flush(wr)
copyBody(wr, resp.Body)
}
func (s *ProxyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
s.logger.Info("Request: %v %v %v %v", req.RemoteAddr, req.Proto, req.Method, req.URL)
isConnect := strings.ToUpper(req.Method) == "CONNECT"
if (req.URL.Host == "" || req.URL.Scheme == "" && !isConnect) && req.ProtoMajor < 2 ||
req.Host == "" && req.ProtoMajor == 2 {
http.Error(wr, BAD_REQ_MSG, http.StatusBadRequest)
return
}
delHopHeaders(req.Header)
if isConnect {
s.HandleTunnel(wr, req)
} else {
s.HandleRequest(wr, req)
}
}

53
helper.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# arguments <country> <proxytype(default direct, needs explicit country)> <port(default autogen using country+proxytype, needs explicit proxytype)>
country=${1-us}
proxytype=${2-direct}
if [ -z "$3" ]
then
port=17160
for x in {a..z}{a..z} # loop over all possible country codes (676 possibilities)
do
port=$((port+1))
if [ "$x" == "$country" ]
then
true
break
else
false
fi
done || { echo "country code $country is invalid" >&2; exit 1;}
case $proxytype in # port range = 17160+1 -> 17160+676*5
direct) port=$((676*0+port)) ;;
peer) port=$((676*1+port)) ;;
lum) port=$((676*2+port)) ;;
virt) port=$((676*3+port)) ;;
pool) port=$((676*4+port)) ;;
*) echo "proxy-type $proxytype invalid" >&2
exit 1 ;;
esac
else
port=$3
fi
try_binary() {
for x in "${@}"
do
type -a "$x" >/dev/null 2>&1 && { echo "$x"; return 0; } || false
done || return 1
}
binary=$(try_binary "hola-proxy" "$HOME/go/bin/hola-proxy")
if [ -n "$binary" ]
then
echo "country $country"
echo "proxytype $proxytype"
echo "proxy 127.0.0.1:$port"
echo
exec "$binary" -bind-address "127.0.0.1:$port" -country "$country" -proxy-type "$proxytype" -verbosity 50
else
echo "hola-proxy binary cannot be found" >&2
exit 1
fi

View File

@@ -3,17 +3,22 @@ package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/campoy/unique"
"github.com/google/uuid"
"io/ioutil"
"math/rand"
"net"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/campoy/unique"
"github.com/google/uuid"
)
const USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36"
@@ -25,6 +30,8 @@ const VPN_COUNTRIES_URL = CCGI_URL + "vpn_countries.json"
const BG_INIT_URL = CCGI_URL + "background_init"
const ZGETTUNNELS_URL = CCGI_URL + "zgettunnels"
const LOGIN_PREFIX = "user-uuid-"
const FALLBACK_CONF_URL = "https://www.dropbox.com/s/jemizcvpmf2qb9v/cloud_failover.conf?dl=1"
const AGENT_SUFFIX = ".hola.org"
var TemporaryBanError = errors.New("temporary ban detected")
var PermanentBanError = errors.New("permanent ban detected")
@@ -57,11 +64,74 @@ type ZGetTunnelsResponse struct {
Ztun map[string][]string `json:"ztun"`
}
func do_req(ctx context.Context, method, url string, query, data url.Values) ([]byte, error) {
type FallbackAgent struct {
Name string `json:"name"`
IP string `json:"ip"`
Port uint16 `json:"port"`
}
type fallbackConfResponse struct {
Agents []FallbackAgent `json:"agents"`
UpdatedAt int64 `json:"updated_ts"`
TTL int64 `json:"ttl_ms"`
}
type FallbackConfig struct {
Agents []FallbackAgent
UpdatedAt time.Time
TTL time.Duration
}
func (c *FallbackConfig) UnmarshalJSON(data []byte) error {
r := fallbackConfResponse{}
err := json.Unmarshal(data, &r)
if err != nil {
return err
}
c.Agents = r.Agents
c.UpdatedAt = time.Unix(r.UpdatedAt/1000, (r.UpdatedAt%1000)*1000000)
c.TTL = time.Duration(r.TTL * 1000000)
return nil
}
func (c *FallbackConfig) Expired() bool {
return time.Now().After(c.UpdatedAt.Add(c.TTL))
}
func (c *FallbackConfig) ShuffleAgents() {
rand.New(RandomSource).Shuffle(len(c.Agents), func(i, j int) {
c.Agents[i], c.Agents[j] = c.Agents[j], c.Agents[i]
})
}
func (c *FallbackConfig) Clone() *FallbackConfig {
return &FallbackConfig{
Agents: append([]FallbackAgent(nil), c.Agents...),
UpdatedAt: c.UpdatedAt,
TTL: c.TTL,
}
}
func (a *FallbackAgent) ToProxy() *url.URL {
return &url.URL{
Scheme: "https",
Host: net.JoinHostPort(a.Name+AGENT_SUFFIX,
fmt.Sprintf("%d", a.Port)),
}
}
func (a *FallbackAgent) Hostname() string {
return a.Name + AGENT_SUFFIX
}
func (a *FallbackAgent) NetAddr() string {
return net.JoinHostPort(a.IP, fmt.Sprintf("%d", a.Port))
}
func do_req(ctx context.Context, client *http.Client, method, url string, query, data url.Values) ([]byte, error) {
var (
client http.Client
req *http.Request
err error
req *http.Request
err error
)
if method == "" {
method = "GET"
@@ -101,10 +171,10 @@ func do_req(ctx context.Context, method, url string, query, data url.Values) ([]
return body, nil
}
func VPNCountries(ctx context.Context) (res CountryList, err error) {
func VPNCountries(ctx context.Context, client *http.Client) (res CountryList, err error) {
params := make(url.Values)
params.Add("browser", EXT_BROWSER)
data, err := do_req(ctx, "", VPN_COUNTRIES_URL, params, nil)
data, err := do_req(ctx, client, "", VPN_COUNTRIES_URL, params, nil)
if err != nil {
return nil, err
}
@@ -119,13 +189,13 @@ func VPNCountries(ctx context.Context) (res CountryList, err error) {
return
}
func background_init(ctx context.Context, user_uuid string) (res BgInitResponse, reterr error) {
func background_init(ctx context.Context, client *http.Client, user_uuid string) (res BgInitResponse, reterr error) {
post_data := make(url.Values)
post_data.Add("login", "1")
post_data.Add("ver", EXT_VER)
qs := make(url.Values)
qs.Add("uuid", user_uuid)
resp, err := do_req(ctx, "POST", BG_INIT_URL, qs, post_data)
resp, err := do_req(ctx, client, "POST", BG_INIT_URL, qs, post_data)
if err != nil {
reterr = err
return
@@ -143,6 +213,7 @@ func background_init(ctx context.Context, user_uuid string) (res BgInitResponse,
}
func zgettunnels(ctx context.Context,
client *http.Client,
user_uuid string,
session_key int64,
country string,
@@ -163,14 +234,14 @@ func zgettunnels(ctx context.Context,
params.Add("country", country)
}
params.Add("limit", strconv.FormatInt(int64(limit), 10))
params.Add("ping_id", strconv.FormatFloat(rand.Float64(), 'f', -1, 64))
params.Add("ping_id", strconv.FormatFloat(rand.New(RandomSource).Float64(), 'f', -1, 64))
params.Add("ext_ver", EXT_VER)
params.Add("browser", EXT_BROWSER)
params.Add("product", PRODUCT)
params.Add("uuid", user_uuid)
params.Add("session_key", strconv.FormatInt(session_key, 10))
params.Add("is_premium", "0")
data, err := do_req(ctx, "", ZGETTUNNELS_URL, params, nil)
data, err := do_req(ctx, client, "", ZGETTUNNELS_URL, params, nil)
if err != nil {
reterr = err
return
@@ -180,17 +251,132 @@ func zgettunnels(ctx context.Context,
return
}
func fetchFallbackConfig(ctx context.Context) (*FallbackConfig, error) {
confRaw, err := do_req(ctx, &http.Client{}, "", FALLBACK_CONF_URL, nil, nil)
if err != nil {
return nil, err
}
l := len(confRaw)
if l < 4 {
return nil, errors.New("bad response length from fallback conf URL")
}
buf := &bytes.Buffer{}
buf.Grow(l)
buf.Write(confRaw[l-3:])
buf.Write(confRaw[:l-3])
b64dec := base64.NewDecoder(base64.RawStdEncoding, buf)
jdec := json.NewDecoder(b64dec)
fbc := &FallbackConfig{}
err = jdec.Decode(fbc)
if err != nil {
return nil, err
}
if fbc.Expired() {
return nil, errors.New("fetched expired fallback config")
}
fbc.ShuffleAgents()
return fbc, nil
}
var (
fbcMux sync.Mutex
cachedFBC *FallbackConfig
)
func GetFallbackProxies(ctx context.Context) (*FallbackConfig, error) {
fbcMux.Lock()
defer fbcMux.Unlock()
var (
fbc *FallbackConfig
err error
)
if cachedFBC == nil || cachedFBC.Expired() {
fbc, err = fetchFallbackConfig(ctx)
if err != nil {
return nil, err
}
cachedFBC = fbc
} else {
fbc = cachedFBC
}
return fbc.Clone(), nil
}
func Tunnels(ctx context.Context,
client *http.Client,
country string,
proxy_type string,
limit uint) (res *ZGetTunnelsResponse, user_uuid string, reterr error) {
u := uuid.New()
user_uuid = hex.EncodeToString(u[:])
initres, err := background_init(ctx, user_uuid)
initres, err := background_init(ctx, client, user_uuid)
if err != nil {
reterr = err
return
}
res, reterr = zgettunnels(ctx, user_uuid, initres.Key, country, proxy_type, limit)
res, reterr = zgettunnels(ctx, client, user_uuid, initres.Key, country, proxy_type, limit)
return
}
// Returns default http client with a proxy override
func httpClientWithProxy(agent *FallbackAgent) *http.Client {
t := &http.Transport{
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
var dialer ContextDialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
if agent != nil {
dialer = NewProxyDialer(agent.NetAddr(), agent.Hostname(), nil, dialer)
}
t.DialContext = dialer.DialContext
return &http.Client{
Transport: t,
}
}
func EnsureTransaction(baseCtx context.Context, txnTimeout time.Duration, txn func(context.Context, *http.Client) bool) (bool, error) {
client := httpClientWithProxy(nil)
defer client.CloseIdleConnections()
ctx, cancel := context.WithTimeout(baseCtx, txnTimeout)
defer cancel()
if txn(ctx, client) {
return true, nil
}
// Fallback needed
fbc, err := GetFallbackProxies(baseCtx)
if err != nil {
return false, err
}
for _, agent := range fbc.Agents {
client = httpClientWithProxy(&agent)
defer client.CloseIdleConnections()
ctx, cancel = context.WithTimeout(baseCtx, txnTimeout)
defer cancel()
if txn(ctx, client) {
return true, nil
}
}
return false, nil
}

2
hooks/build Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker build --build-arg GIT_DESC="$(git describe)" -f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" .

13
main.go
View File

@@ -4,10 +4,9 @@ import (
"flag"
"fmt"
"log"
"os"
// "os/signal"
// "syscall"
"net"
"net/http"
"os"
"time"
)
@@ -126,9 +125,13 @@ func run() int {
logWriter.Close()
return 5
}
mainLogger.Info("Endpoint: %s", endpoint)
var dialer ContextDialer = NewProxyDialer(endpoint.NetAddr(), endpoint.TLSName, auth, &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
})
mainLogger.Info("Endpoint: %s", endpoint.URL().String())
mainLogger.Info("Starting proxy server...")
handler := NewProxyHandler(endpoint, auth, resolver, proxyLogger)
handler := NewProxyHandler(dialer, resolver, proxyLogger)
mainLogger.Info("Init complete.")
err = http.ListenAndServe(args.bind_address, handler)
mainLogger.Critical("Server terminated with a reason: %v", err)

39
retry.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"context"
"net"
)
type RetryDialer struct {
dialer ContextDialer
resolver *Resolver
logger *CondLogger
}
func NewRetryDialer(dialer ContextDialer, resolver *Resolver, logger *CondLogger) *RetryDialer {
return &RetryDialer{
dialer: dialer,
resolver: resolver,
logger: logger,
}
}
func (d *RetryDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
conn, err := d.dialer.DialContext(ctx, network, address)
if err == UpstreamBlockedError {
d.logger.Info("Destination %s blocked by upstream. Rescuing it with resolve&tunnel workaround.", address)
host, port, err1 := net.SplitHostPort(address)
if err1 != nil {
return conn, err
}
ips := d.resolver.Resolve(host)
if len(ips) == 0 {
return conn, err
}
return d.dialer.DialContext(ctx, network, net.JoinHostPort(ips[0], port))
}
return conn, err
}

View File

@@ -1,5 +1,5 @@
name: hola-proxy
version: '1.4.2'
version: '1.4.6'
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.
@@ -10,15 +10,18 @@ base: core18
parts:
hola-proxy:
plugin: go
go-importpath: github.com/Snawoot/hola-proxy
source: https://github.com/Snawoot/hola-proxy
source-type: git
source: .
build-packages:
- gcc
override-build:
make &&
cp bin/hola-proxy "$SNAPCRAFT_PART_INSTALL"
stage:
- hola-proxy
apps:
hola-proxy:
command: bin/hola-proxy
command: hola-proxy
plugs:
- network
- network-bind

144
upstream.go Normal file
View File

@@ -0,0 +1,144 @@
package main
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"io"
"net"
"net/http"
"net/http/httputil"
)
const (
PROXY_CONNECT_METHOD = "CONNECT"
PROXY_HOST_HEADER = "Host"
PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"
)
var UpstreamBlockedError = errors.New("blocked by upstream")
type ContextDialer interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
type ProxyDialer struct {
address string
tlsServerName string
auth AuthProvider
next ContextDialer
}
func NewProxyDialer(address, tlsServerName string, auth AuthProvider, nextDialer ContextDialer) *ProxyDialer {
return &ProxyDialer{
address: address,
tlsServerName: tlsServerName,
auth: auth,
next: nextDialer,
}
}
func (d *ProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
switch network {
case "tcp", "tcp4", "tcp6":
default:
return nil, errors.New("bad network specified for DialContext: only tcp is supported")
}
conn, err := d.next.DialContext(ctx, "tcp", d.address)
if err != nil {
return nil, err
}
if d.tlsServerName != "" {
// Custom cert verification logic:
// DO NOT send SNI extension of TLS ClientHello
// DO peer certificate verification against specified servername
conn = tls.Client(conn, &tls.Config{
ServerName: "",
InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error {
opts := x509.VerifyOptions{
DNSName: d.tlsServerName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range cs.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
_, err := cs.PeerCertificates[0].Verify(opts)
return err
},
})
}
req := &http.Request{
Method: PROXY_CONNECT_METHOD,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
RequestURI: address,
Host: address,
Header: http.Header{
PROXY_HOST_HEADER: []string{address},
},
}
if d.auth != nil {
req.Header.Set(PROXY_AUTHORIZATION_HEADER, d.auth())
}
rawreq, err := httputil.DumpRequest(req, false)
if err != nil {
return nil, err
}
_, err = conn.Write(rawreq)
if err != nil {
return nil, err
}
proxyResp, err := readResponse(conn, req)
if err != nil {
return nil, err
}
if proxyResp.StatusCode != http.StatusOK {
if proxyResp.StatusCode == http.StatusForbidden &&
proxyResp.Header.Get("X-Hola-Error") == "Forbidden Host" {
return nil, UpstreamBlockedError
}
return nil, errors.New("Bad response from upstream proxy server")
}
return conn, nil
}
func readResponse(r io.Reader, req *http.Request) (*http.Response, error) {
endOfResponse := []byte("\r\n\r\n")
buf := &bytes.Buffer{}
b := make([]byte, 1)
for {
n, err := r.Read(b)
if n < 1 && err == nil {
continue
}
buf.Write(b)
sl := buf.Bytes()
if len(sl) < len(endOfResponse) {
continue
}
if bytes.Equal(sl[len(sl)-4:], endOfResponse) {
break
}
if err != nil {
return nil, err
}
}
return http.ReadResponse(bufio.NewReader(buf), req)
}

204
utils.go
View File

@@ -18,6 +18,32 @@ import (
"time"
)
const COPY_BUF = 128 * 1024
type Endpoint struct {
Host string
Port uint16
TLSName string
}
func (e *Endpoint) URL() *url.URL {
if e.TLSName == "" {
return &url.URL{
Scheme: "http",
Host: net.JoinHostPort(e.Host, fmt.Sprintf("%d", e.Port)),
}
} else {
return &url.URL{
Scheme: "https",
Host: net.JoinHostPort(e.TLSName, fmt.Sprintf("%d", e.Port)),
}
}
}
func (e *Endpoint) NetAddr() string {
return net.JoinHostPort(e.Host, fmt.Sprintf("%d", e.Port))
}
func basic_auth_header(login, password string) string {
return "basic " + base64.StdEncoding.EncodeToString(
[]byte(login+":"+password))
@@ -49,11 +75,55 @@ func proxy(ctx context.Context, left, right net.Conn) {
return
}
func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer, right net.Conn) {
wg := sync.WaitGroup{}
ltr := func(dst net.Conn, src io.Reader) {
defer wg.Done()
io.Copy(dst, src)
dst.Close()
}
rtl := func(dst io.Writer, src io.Reader) {
defer wg.Done()
copyBody(dst, src)
}
wg.Add(2)
go ltr(right, leftreader)
go rtl(leftwriter, right)
groupdone := make(chan struct{}, 1)
go func() {
wg.Wait()
groupdone <- struct{}{}
}()
select {
case <-ctx.Done():
leftreader.Close()
right.Close()
case <-groupdone:
return
}
<-groupdone
return
}
func print_countries(timeout time.Duration) int {
ctx, _ := context.WithTimeout(context.Background(), timeout)
countries, err := VPNCountries(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
var (
countries CountryList
err error
)
tx_res, tx_err := EnsureTransaction(context.Background(), timeout, func(ctx context.Context, client *http.Client) bool {
countries, err = VPNCountries(ctx, 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 {
fmt.Fprintf(os.Stderr, "Transaction recovery mechanism failure: %v.\n", tx_err)
return 4
}
if !tx_res {
fmt.Fprintf(os.Stderr, "All attempts failed.")
return 3
}
for _, code := range countries {
@@ -63,10 +133,25 @@ func print_countries(timeout time.Duration) int {
}
func print_proxies(country string, proxy_type string, limit uint, timeout time.Duration) int {
ctx, _ := context.WithTimeout(context.Background(), timeout)
tunnels, user_uuid, err := Tunnels(ctx, country, proxy_type, limit)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
var (
tunnels *ZGetTunnelsResponse
user_uuid string
err error
)
tx_res, tx_err := EnsureTransaction(context.Background(), timeout, func(ctx context.Context, client *http.Client) bool {
tunnels, user_uuid, err = Tunnels(ctx, client, country, proxy_type, limit)
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 {
fmt.Fprintf(os.Stderr, "Transaction recovery mechanism failure: %v.\n", tx_err)
return 4
}
if !tx_res {
fmt.Fprintf(os.Stderr, "All attempts failed.")
return 3
}
wr := csv.NewWriter(os.Stdout)
@@ -94,14 +179,15 @@ func print_proxies(country string, proxy_type string, limit uint, timeout time.D
return 0
}
func get_endpoint(tunnels *ZGetTunnelsResponse, typ string, trial bool, force_port_field string) (string, error) {
var hostname string
for k := range tunnels.IPList {
func get_endpoint(tunnels *ZGetTunnelsResponse, typ string, trial bool, force_port_field string) (*Endpoint, error) {
var hostname, ip string
for k, v := range tunnels.IPList {
hostname = k
ip = v
break
}
if hostname == "" {
return "", errors.New("No tunnels found in API response")
if hostname == "" || ip == "" {
return nil, errors.New("No tunnels found in API response")
}
var port uint16
@@ -128,10 +214,14 @@ func get_endpoint(tunnels *ZGetTunnelsResponse, typ string, trial bool, force_po
port = tunnels.Port.Peer
}
} else {
return "", errors.New("Unsupported port type")
return nil, errors.New("Unsupported port type")
}
}
return net.JoinHostPort(hostname, strconv.FormatUint(uint64(port), 10)), nil
return &Endpoint{
Host: ip,
Port: port,
TLSName: hostname,
}, nil
}
// Hop-by-hop headers. These are removed when sent to the backend.
@@ -179,76 +269,26 @@ func hijack(hijackable interface{}) (net.Conn, *bufio.ReadWriter, error) {
return conn, rw, nil
}
func rewriteConnectReq(req *http.Request, resolver *Resolver) error {
origHost := req.Host
origAddr, origPort, err := net.SplitHostPort(origHost)
if err == nil {
origHost = origAddr
func flush(flusher interface{}) bool {
f, ok := flusher.(http.Flusher)
if !ok {
return false
}
addrs := resolver.Resolve(origHost)
if len(addrs) == 0 {
return errors.New("Can't resolve host")
}
if origPort == "" {
req.URL.Host = addrs[0]
req.Host = addrs[0]
req.RequestURI = addrs[0]
} else {
req.URL.Host = net.JoinHostPort(addrs[0], origPort)
req.Host = net.JoinHostPort(addrs[0], origPort)
req.RequestURI = net.JoinHostPort(addrs[0], origPort)
}
return nil
f.Flush()
return true
}
func rewriteReq(req *http.Request, resolver *Resolver) error {
origHost := req.URL.Host
origAddr, origPort, err := net.SplitHostPort(origHost)
if err == nil {
origHost = origAddr
}
addrs := resolver.Resolve(origHost)
if len(addrs) == 0 {
return errors.New("Can't resolve host")
}
if origPort == "" {
req.URL.Host = addrs[0]
req.Host = addrs[0]
} else {
req.URL.Host = net.JoinHostPort(addrs[0], origPort)
req.Host = net.JoinHostPort(addrs[0], origPort)
}
req.Header.Set("Host", origHost)
return nil
}
func makeConnReq(uri string, resolver *Resolver) (*http.Request, error) {
parsed_url, err := url.Parse(uri)
if err != nil {
return nil, err
}
origAddr, origPort, err := net.SplitHostPort(parsed_url.Host)
if err != nil {
origAddr = parsed_url.Host
switch strings.ToLower(parsed_url.Scheme) {
case "https":
origPort = "443"
case "http":
origPort = "80"
default:
return nil, errors.New("Unknown scheme")
func copyBody(wr io.Writer, body io.Reader) {
buf := make([]byte, COPY_BUF)
for {
bread, read_err := body.Read(buf)
var write_err error
if bread > 0 {
_, write_err = wr.Write(buf[:bread])
flush(wr)
}
if read_err != nil || write_err != nil {
break
}
}
addrs := resolver.Resolve(origAddr)
if len(addrs) == 0 {
return nil, errors.New("Can't resolve host")
}
new_uri := net.JoinHostPort(addrs[0], origPort)
req, err := http.NewRequest("CONNECT", "http://"+new_uri, nil)
if err != nil {
return nil, err
}
req.RequestURI = new_uri
req.Host = new_uri
return req, nil
}