mirror of
https://github.com/Snawoot/hola-proxy.git
synced 2026-04-04 15:28:12 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
776411c2d4 | ||
|
|
f2c7f73548 | ||
|
|
af12cee5f0 | ||
|
|
05ac2bc146 | ||
|
|
1511ed333a | ||
|
|
af955a6cd1 | ||
|
|
cfd3474af3 | ||
|
|
eb2ae460da | ||
|
|
dbb9b29c81 | ||
|
|
839499313b | ||
|
|
58006c4f6b | ||
|
|
b23dc2f63f | ||
|
|
55788d8188 | ||
|
|
bc0b0df26a | ||
|
|
700ae4a4f4 | ||
|
|
8db460ff99 | ||
|
|
9efb99cf8e | ||
|
|
af2f1e40d6 | ||
|
|
f6d3de8488 | ||
|
|
fde7f1516b | ||
|
|
3f92cecac9 |
58
Makefile
58
Makefile
@@ -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 \
|
||||
|
||||
70
README.md
70
README.md
@@ -2,7 +2,8 @@
|
||||
|
||||
[](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) |
|
||||
|
||||
278
handler.go
278
handler.go
@@ -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
53
helper.sh
Executable 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
|
||||
21
holaapi.go
21
holaapi.go
@@ -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
11
main.go
@@ -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
39
retry.go
Normal 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
|
||||
}
|
||||
@@ -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
144
upstream.go
Normal 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
120
utils.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user