mirror of
https://github.com/Snawoot/hola-proxy.git
synced 2026-04-02 13:38:14 +00:00
390 lines
9.3 KiB
Go
390 lines
9.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"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"
|
|
const EXT_VER = "1.181.350"
|
|
const EXT_BROWSER = "chrome"
|
|
const PRODUCT = "cws"
|
|
const CCGI_URL = "https://client.hola.org/client_cgi/"
|
|
const VPN_COUNTRIES_URL = CCGI_URL + "vpn_countries.json"
|
|
const BG_INIT_URL = CCGI_URL + "background_init"
|
|
const ZGETTUNNELS_URL = CCGI_URL + "zgettunnels"
|
|
const 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")
|
|
|
|
type CountryList []string
|
|
|
|
type BgInitResponse struct {
|
|
Ver string `json:"ver"`
|
|
Key int64 `json:"key"`
|
|
Country string `json:"country"`
|
|
Blocked bool `json:"blocked,omitempty"`
|
|
Permanent bool `json:"permanent,omitempty"`
|
|
}
|
|
|
|
type PortMap struct {
|
|
Direct uint16 `json:"direct"`
|
|
Hola uint16 `json:"hola"`
|
|
Peer uint16 `json:"peer"`
|
|
Trial uint16 `json:"trial"`
|
|
TrialPeer uint16 `json:"trial_peer"`
|
|
}
|
|
|
|
type ZGetTunnelsResponse struct {
|
|
AgentKey string `json:"agent_key"`
|
|
AgentTypes map[string]string `json:"agent_types"`
|
|
IPList map[string]string `json:"ip_list"`
|
|
Port PortMap `json:"port"`
|
|
Protocol map[string]string `json:"protocol"`
|
|
Vendor map[string]string `json:"vendor"`
|
|
Ztun map[string][]string `json:"ztun"`
|
|
}
|
|
|
|
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 (
|
|
req *http.Request
|
|
err error
|
|
)
|
|
if method == "" {
|
|
method = "GET"
|
|
}
|
|
if data == nil {
|
|
req, err = http.NewRequestWithContext(ctx, method, url, nil)
|
|
} else {
|
|
req, err = http.NewRequestWithContext(ctx,
|
|
method,
|
|
url,
|
|
bytes.NewReader([]byte(data.Encode())))
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if data != nil {
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
}
|
|
if query != nil {
|
|
req.URL.RawQuery = query.Encode()
|
|
}
|
|
req.Header.Set("User-Agent", USER_AGENT)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch resp.StatusCode {
|
|
case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent:
|
|
default:
|
|
return nil, errors.New(fmt.Sprintf("Bad HTTP response: %s", resp.Status))
|
|
}
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
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, client, "", VPN_COUNTRIES_URL, params, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(data, &res)
|
|
for _, a := range res {
|
|
if a == "uk" {
|
|
res = append(res, "gb")
|
|
}
|
|
}
|
|
less := func(i, j int) bool { return res[i] < res[j] }
|
|
unique.Slice(&res, less)
|
|
return
|
|
}
|
|
|
|
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, client, "POST", BG_INIT_URL, qs, post_data)
|
|
if err != nil {
|
|
reterr = err
|
|
return
|
|
}
|
|
|
|
reterr = json.Unmarshal(resp, &res)
|
|
if reterr == nil && res.Blocked {
|
|
if res.Permanent {
|
|
reterr = PermanentBanError
|
|
} else {
|
|
reterr = TemporaryBanError
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func zgettunnels(ctx context.Context,
|
|
client *http.Client,
|
|
user_uuid string,
|
|
session_key int64,
|
|
country string,
|
|
proxy_type string,
|
|
limit uint) (res *ZGetTunnelsResponse, reterr error) {
|
|
var tunnels ZGetTunnelsResponse
|
|
params := make(url.Values)
|
|
if proxy_type == "lum" {
|
|
params.Add("country", country+".pool_lum_"+country+"_shared")
|
|
} else if proxy_type == "virt" { // seems to be for brazil and japan only
|
|
params.Add("country", country+".pool_virt_pool_"+country)
|
|
} else if proxy_type == "peer" {
|
|
//params.Add("country", country + ".peer")
|
|
params.Add("country", country)
|
|
} else if proxy_type == "pool" {
|
|
params.Add("country", country+".pool")
|
|
} else { // direct or skip
|
|
params.Add("country", country)
|
|
}
|
|
params.Add("limit", strconv.FormatInt(int64(limit), 10))
|
|
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, client, "", ZGETTUNNELS_URL, params, nil)
|
|
if err != nil {
|
|
reterr = err
|
|
return
|
|
}
|
|
reterr = json.Unmarshal(data, &tunnels)
|
|
res = &tunnels
|
|
return
|
|
}
|
|
|
|
func fetchFallbackConfig(ctx context.Context) (*FallbackConfig, error) {
|
|
client := httpClientWithProxy(nil)
|
|
confRaw, err := do_req(ctx, 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, client, user_uuid)
|
|
if err != nil {
|
|
reterr = err
|
|
return
|
|
}
|
|
res, reterr = zgettunnels(ctx, client, user_uuid, initres.Key, country, proxy_type, limit)
|
|
return
|
|
}
|
|
|
|
var baseDialer ContextDialer = &net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
}
|
|
|
|
func UpdateHolaDialer(dialer ContextDialer) {
|
|
baseDialer = dialer
|
|
}
|
|
|
|
// 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 = baseDialer
|
|
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
|
|
}
|