Compare commits

...

21 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
10 changed files with 445 additions and 351 deletions

View File

@@ -5,13 +5,17 @@ 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
@@ -20,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)
go build $(LDFLAGS_NATIVE) -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

@@ -1,152 +1,48 @@
package main
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"strings"
"time"
)
const BAD_REQ_MSG = "Bad Request\n"
type AuthProvider func() string
type ProxyHandler struct {
auth AuthProvider
upstreamAddr string
tlsName string
logger *CondLogger
dialer *net.Dialer
dialer ContextDialer
httptransport http.RoundTripper
resolver *Resolver
}
func NewProxyHandler(upstream *Endpoint, auth AuthProvider, resolver *Resolver, logger *CondLogger) *ProxyHandler {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
netaddr := net.JoinHostPort(upstream.Host, fmt.Sprintf("%d", upstream.Port))
func NewProxyHandler(dialer ContextDialer, resolver *Resolver, logger *CondLogger) *ProxyHandler {
dialer = NewRetryDialer(dialer, resolver, logger)
httptransport := &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
Proxy: http.ProxyURL(upstream.URL()),
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", netaddr)
},
DialContext: dialer.DialContext,
}
return &ProxyHandler{
auth: auth,
upstreamAddr: netaddr,
tlsName: upstream.TLSName,
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 := s.dialer.DialContext(req.Context(), "tcp", s.upstreamAddr)
if err != nil {
s.logger.Error("Can't dial upstream: %v", err)
http.Error(wr, "Can't dial upstream", http.StatusBadGateway)
return
}
defer conn.Close()
if s.tlsName != "" {
conn = tls.Client(conn, &tls.Config{
ServerName: s.tlsName,
})
defer conn.Close()
}
_, err = conn.Write(rawreq)
if err != nil {
s.logger.Error("Can't write upstream: %v", err)
http.Error(wr, "Can't write 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, err = s.dialer.DialContext(req.Context(), "tcp", s.upstreamAddr)
if err != nil {
s.logger.Error("Can't dial upstream: %v", err)
http.Error(wr, "Can't dial upstream", http.StatusBadGateway)
return
}
defer conn.Close()
if s.tlsName != "" {
conn = tls.Client(conn, &tls.Config{
ServerName: s.tlsName,
})
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 {
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 {
@@ -156,102 +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 conn
conn, err := s.dialer.DialContext(req.Context(), "tcp", s.upstreamAddr)
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()
if s.tlsName != "" {
conn = tls.Client(conn, &tls.Config{
ServerName: s.tlsName,
})
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

@@ -120,6 +120,14 @@ func (a *FallbackAgent) ToProxy() *url.URL {
}
}
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 (
req *http.Request
@@ -328,19 +336,14 @@ func httpClientWithProxy(agent *FallbackAgent) *http.Client {
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
dialer := &net.Dialer{
var dialer ContextDialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
if agent == nil {
t.DialContext = dialer.DialContext
} else {
t.Proxy = http.ProxyURL(agent.ToProxy())
addr := net.JoinHostPort(agent.IP, fmt.Sprintf("%d", agent.Port))
t.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", addr)
}
if agent != nil {
dialer = NewProxyDialer(agent.NetAddr(), agent.Hostname(), nil, dialer)
}
t.DialContext = dialer.DialContext
return &http.Client{
Transport: t,
}

11
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
}
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.5'
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.

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)
}

120
utils.go
View File

@@ -18,6 +18,8 @@ import (
"time"
)
const COPY_BUF = 128 * 1024
type Endpoint struct {
Host string
Port uint16
@@ -38,6 +40,10 @@ func (e *Endpoint) URL() *url.URL {
}
}
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))
@@ -69,6 +75,36 @@ 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 {
var (
countries CountryList
@@ -233,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
}