mirror of
https://github.com/Snawoot/opera-proxy.git
synced 2025-09-02 10:42:07 +00:00
proxy: WIP
This commit is contained in:
parent
e62f2e2ed7
commit
c7c6bba005
6 changed files with 769 additions and 40 deletions
58
condlog.go
Normal file
58
condlog.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CRITICAL = 50
|
||||||
|
ERROR = 40
|
||||||
|
WARNING = 30
|
||||||
|
INFO = 20
|
||||||
|
DEBUG = 10
|
||||||
|
NOTSET = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type CondLogger struct {
|
||||||
|
logger *log.Logger
|
||||||
|
verbosity int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *CondLogger) Log(verb int, format string, v ...interface{}) error {
|
||||||
|
if verb >= cl.verbosity {
|
||||||
|
return cl.logger.Output(2, fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *CondLogger) log(verb int, format string, v ...interface{}) error {
|
||||||
|
if verb >= cl.verbosity {
|
||||||
|
return cl.logger.Output(3, fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *CondLogger) Critical(s string, v ...interface{}) error {
|
||||||
|
return cl.log(CRITICAL, "CRITICAL "+s, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *CondLogger) Error(s string, v ...interface{}) error {
|
||||||
|
return cl.log(ERROR, "ERROR "+s, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *CondLogger) Warning(s string, v ...interface{}) error {
|
||||||
|
return cl.log(WARNING, "WARNING "+s, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *CondLogger) Info(s string, v ...interface{}) error {
|
||||||
|
return cl.log(INFO, "INFO "+s, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *CondLogger) Debug(s string, v ...interface{}) error {
|
||||||
|
return cl.log(DEBUG, "DEBUG "+s, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCondLogger(logger *log.Logger, verbosity int) *CondLogger {
|
||||||
|
return &CondLogger{verbosity: verbosity, logger: logger}
|
||||||
|
}
|
107
handler.go
Normal file
107
handler.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const BAD_REQ_MSG = "Bad Request\n"
|
||||||
|
|
||||||
|
type AuthProvider func() string
|
||||||
|
|
||||||
|
type ProxyHandler struct {
|
||||||
|
logger *CondLogger
|
||||||
|
dialer ContextDialer
|
||||||
|
httptransport http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
DialContext: dialer.DialContext,
|
||||||
|
}
|
||||||
|
return &ProxyHandler{
|
||||||
|
logger: logger,
|
||||||
|
dialer: dialer,
|
||||||
|
httptransport: httptransport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
s.logger.Error("Can't hijack client connection: %v", err)
|
||||||
|
http.Error(wr, "Can't hijack client connection", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer localconn.Close()
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
57
logwriter.go
Normal file
57
logwriter.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MAX_LOG_QLEN = 128
|
||||||
|
const QUEUE_SHUTDOWN_TIMEOUT = 500 * time.Millisecond
|
||||||
|
|
||||||
|
type LogWriter struct {
|
||||||
|
writer io.Writer
|
||||||
|
ch chan []byte
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lw *LogWriter) Write(p []byte) (int, error) {
|
||||||
|
if p == nil {
|
||||||
|
return 0, errors.New("Can't write nil byte slice")
|
||||||
|
}
|
||||||
|
buf := make([]byte, len(p))
|
||||||
|
copy(buf, p)
|
||||||
|
select {
|
||||||
|
case lw.ch <- buf:
|
||||||
|
return len(p), nil
|
||||||
|
default:
|
||||||
|
return 0, errors.New("Writer queue overflow")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogWriter(writer io.Writer) *LogWriter {
|
||||||
|
lw := &LogWriter{writer,
|
||||||
|
make(chan []byte, MAX_LOG_QLEN),
|
||||||
|
make(chan struct{})}
|
||||||
|
go lw.loop()
|
||||||
|
return lw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lw *LogWriter) loop() {
|
||||||
|
for p := range lw.ch {
|
||||||
|
if p == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lw.writer.Write(p)
|
||||||
|
}
|
||||||
|
lw.done <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lw *LogWriter) Close() {
|
||||||
|
lw.ch <- nil
|
||||||
|
timer := time.After(QUEUE_SHUTDOWN_TIMEOUT)
|
||||||
|
select {
|
||||||
|
case <-timer:
|
||||||
|
case <-lw.done:
|
||||||
|
}
|
||||||
|
}
|
173
main.go
173
main.go
|
@ -1,51 +1,144 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
se "github.com/Snawoot/opera-proxy/seclient"
|
xproxy "golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
username = "se0316"
|
version = "undefined"
|
||||||
password = "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func perror(msg string) {
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func arg_fail(msg string) {
|
||||||
|
perror(msg)
|
||||||
|
perror("Usage:")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CLIArgs struct {
|
||||||
|
country string
|
||||||
|
bind_address string
|
||||||
|
verbosity int
|
||||||
|
timeout time.Duration
|
||||||
|
resolver string
|
||||||
|
showVersion bool
|
||||||
|
proxy string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse_args() CLIArgs {
|
||||||
|
var args CLIArgs
|
||||||
|
flag.StringVar(&args.country, "country", "us", "desired proxy location")
|
||||||
|
flag.BoolVar(&args.list_countries, "list-countries", false, "list available countries and exit")
|
||||||
|
flag.BoolVar(&args.list_proxies, "list-proxies", false, "output proxy list and exit")
|
||||||
|
flag.StringVar(&args.bind_address, "bind-address", "127.0.0.1:8080", "HTTP proxy listen address")
|
||||||
|
flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+
|
||||||
|
"(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical)")
|
||||||
|
flag.DurationVar(&args.timeout, "timeout", 10*time.Second, "timeout for network operations")
|
||||||
|
flag.StringVar(&args.resolver, "resolver", "https://cloudflare-dns.com/dns-query",
|
||||||
|
"DNS/DoH/DoT resolver to workaround Hola blocked hosts. "+
|
||||||
|
"See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format.")
|
||||||
|
flag.BoolVar(&args.showVersion, "version", false, "show program version and exit")
|
||||||
|
flag.StringVar(&args.proxy, "proxy", "", "sets base proxy to use for all dial-outs. " +
|
||||||
|
"Format: <http|https|socks5|socks5h>://[login:password@]host[:port] " +
|
||||||
|
"Examples: http://user:password@192.168.1.1:3128, socks5://10.0.0.1:1080")
|
||||||
|
flag.Parse()
|
||||||
|
if args.country == "" {
|
||||||
|
arg_fail("Country can't be empty string.")
|
||||||
|
}
|
||||||
|
if args.list_countries && args.list_proxies {
|
||||||
|
arg_fail("list-countries and list-proxies flags are mutually exclusive")
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyFromURLWrapper(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error) {
|
||||||
|
cdialer, ok := next.(ContextDialer)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("only context dialers are accepted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProxyDialerFromURL(u, cdialer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() int {
|
||||||
|
args := parse_args()
|
||||||
|
if args.showVersion {
|
||||||
|
fmt.Println(version)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
logWriter := NewLogWriter(os.Stderr)
|
||||||
|
defer logWriter.Close()
|
||||||
|
|
||||||
|
mainLogger := NewCondLogger(log.New(logWriter, "MAIN : ",
|
||||||
|
log.LstdFlags|log.Lshortfile),
|
||||||
|
args.verbosity)
|
||||||
|
proxyLogger := NewCondLogger(log.New(logWriter, "PROXY : ",
|
||||||
|
log.LstdFlags|log.Lshortfile),
|
||||||
|
args.verbosity)
|
||||||
|
|
||||||
|
var dialer ContextDialer = &net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}
|
||||||
|
if args.proxy != "" {
|
||||||
|
xproxy.RegisterDialerType("http", proxyFromURLWrapper)
|
||||||
|
xproxy.RegisterDialerType("https", proxyFromURLWrapper)
|
||||||
|
proxyURL, err := url.Parse(args.proxy)
|
||||||
|
if err != nil {
|
||||||
|
mainLogger.Critical("Unable to parse base proxy URL: %v", err)
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
pxDialer, err := xproxy.FromURL(proxyURL, dialer)
|
||||||
|
if err != nil {
|
||||||
|
mainLogger.Critical("Unable to instantiate base proxy dialer: %v", err)
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
dialer = pxDialer.(ContextDialer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.list_countries {
|
||||||
|
return print_countries(args.timeout)
|
||||||
|
}
|
||||||
|
if args.list_proxies {
|
||||||
|
return print_proxies(args.country, args.proxy_type, args.limit, args.timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainLogger.Info("opera-proxy client version %s is starting...", version)
|
||||||
|
mainLogger.Info("Constructing fallback DNS upstream...")
|
||||||
|
resolver, err := NewResolver(args.resolver, args.timeout)
|
||||||
|
if err != nil {
|
||||||
|
mainLogger.Critical("Unable to instantiate DNS resolver: %v", err)
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: get creds here
|
||||||
|
handlerDialer := NewProxyDialer(endpoint.NetAddr(), endpoint.TLSName, auth, dialer)
|
||||||
|
mainLogger.Info("Endpoint: %s", endpoint.URL().String())
|
||||||
|
mainLogger.Info("Starting proxy server...")
|
||||||
|
handler := NewProxyHandler(handlerDialer, proxyLogger)
|
||||||
|
mainLogger.Info("Init complete.")
|
||||||
|
err = http.ListenAndServe(args.bind_address, handler)
|
||||||
|
mainLogger.Critical("Server terminated with a reason: %v", err)
|
||||||
|
mainLogger.Info("Shutting down...")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
seclient, err := se.NewSEClient(username, password, nil)
|
os.Exit(run())
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
log.Printf("seclient = %#v", seclient)
|
|
||||||
|
|
||||||
log.Println("------------ DOING REGISTRATION ------------")
|
|
||||||
err = seclient.AnonRegister(context.TODO())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
log.Printf("seclient = %#v", seclient)
|
|
||||||
log.Printf("jar = %#v", seclient.HttpClient.Jar)
|
|
||||||
|
|
||||||
log.Println("------------ DOING DEVICE REGISTRATION ------------")
|
|
||||||
err = seclient.RegisterDevice(context.TODO())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
log.Printf("seclient = %#v", seclient)
|
|
||||||
log.Printf("Device Password: %s", seclient.DevicePassword)
|
|
||||||
|
|
||||||
log.Println("------------ GETTING GEO LIST ------------")
|
|
||||||
geos, err := seclient.GeoList(context.TODO())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
log.Printf("Geo List: %#v", geos)
|
|
||||||
|
|
||||||
log.Println("------------ GETTING IP LIST ------------")
|
|
||||||
ips, err := seclient.Discover(context.TODO(), "\"EU\",,")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
log.Printf("IP List: %#v", ips)
|
|
||||||
}
|
}
|
||||||
|
|
189
upstream.go
Normal file
189
upstream.go
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PROXY_CONNECT_METHOD = "CONNECT"
|
||||||
|
PROXY_HOST_HEADER = "Host"
|
||||||
|
PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"
|
||||||
|
)
|
||||||
|
|
||||||
|
var UpstreamBlockedError = errors.New("blocked by upstream")
|
||||||
|
|
||||||
|
type Dialer interface {
|
||||||
|
Dial(network, address string) (net.Conn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextDialer interface {
|
||||||
|
Dialer
|
||||||
|
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 ProxyDialerFromURL(u *url.URL, next ContextDialer) (*ProxyDialer, error) {
|
||||||
|
host := u.Hostname()
|
||||||
|
port := u.Port()
|
||||||
|
tlsServerName := ""
|
||||||
|
var auth AuthProvider = nil
|
||||||
|
|
||||||
|
switch strings.ToLower(u.Scheme) {
|
||||||
|
case "http":
|
||||||
|
if port == "" {
|
||||||
|
port = "80"
|
||||||
|
}
|
||||||
|
case "https":
|
||||||
|
if port == "" {
|
||||||
|
port = "443"
|
||||||
|
}
|
||||||
|
tlsServerName = host
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported proxy type")
|
||||||
|
}
|
||||||
|
|
||||||
|
address := net.JoinHostPort(host, port)
|
||||||
|
|
||||||
|
if u.User != nil {
|
||||||
|
username := u.User.Username()
|
||||||
|
password, _ := u.User.Password()
|
||||||
|
authHeader := basic_auth_header(username, password)
|
||||||
|
auth = func() string {
|
||||||
|
return authHeader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NewProxyDialer(address, tlsServerName, auth, next), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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(fmt.Sprintf("bad response from upstream proxy server: %s", proxyResp.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProxyDialer) Dial(network, address string) (net.Conn, error) {
|
||||||
|
return d.DialContext(context.Background(), network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
225
utils.go
Normal file
225
utils.go
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/csv"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const COPY_BUF = 128 * 1024
|
||||||
|
|
||||||
|
func basic_auth_header(login, password string) string {
|
||||||
|
return "basic " + base64.StdEncoding.EncodeToString(
|
||||||
|
[]byte(login+":"+password))
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxy(ctx context.Context, left, right net.Conn) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
cpy := func(dst, src net.Conn) {
|
||||||
|
defer wg.Done()
|
||||||
|
io.Copy(dst, src)
|
||||||
|
dst.Close()
|
||||||
|
}
|
||||||
|
wg.Add(2)
|
||||||
|
go cpy(left, right)
|
||||||
|
go cpy(right, left)
|
||||||
|
groupdone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
groupdone <- struct{}{}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
left.Close()
|
||||||
|
right.Close()
|
||||||
|
case <-groupdone:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
<-groupdone
|
||||||
|
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
|
||||||
|
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 {
|
||||||
|
fmt.Printf("%v - %v\n", code, ISO3166[strings.ToUpper(code)])
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func print_proxies(country string, proxy_type string, limit uint, timeout time.Duration) int {
|
||||||
|
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)
|
||||||
|
login := LOGIN_PREFIX + user_uuid
|
||||||
|
password := tunnels.AgentKey
|
||||||
|
fmt.Println("Login:", login)
|
||||||
|
fmt.Println("Password:", password)
|
||||||
|
fmt.Println("Proxy-Authorization:",
|
||||||
|
basic_auth_header(login, password))
|
||||||
|
fmt.Println("")
|
||||||
|
wr.Write([]string{"host", "ip_address", "direct", "peer", "hola", "trial", "trial_peer", "vendor"})
|
||||||
|
for host, ip := range tunnels.IPList {
|
||||||
|
if PROTOCOL_WHITELIST[tunnels.Protocol[host]] {
|
||||||
|
wr.Write([]string{host,
|
||||||
|
ip,
|
||||||
|
strconv.FormatUint(uint64(tunnels.Port.Direct), 10),
|
||||||
|
strconv.FormatUint(uint64(tunnels.Port.Peer), 10),
|
||||||
|
strconv.FormatUint(uint64(tunnels.Port.Hola), 10),
|
||||||
|
strconv.FormatUint(uint64(tunnels.Port.Trial), 10),
|
||||||
|
strconv.FormatUint(uint64(tunnels.Port.TrialPeer), 10),
|
||||||
|
tunnels.Vendor[host]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wr.Flush()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hop-by-hop headers. These are removed when sent to the backend.
|
||||||
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
|
||||||
|
var hopHeaders = []string{
|
||||||
|
"Connection",
|
||||||
|
"Keep-Alive",
|
||||||
|
"Proxy-Authenticate",
|
||||||
|
"Proxy-Connection",
|
||||||
|
"Te", // canonicalized version of "TE"
|
||||||
|
"Trailers",
|
||||||
|
"Transfer-Encoding",
|
||||||
|
"Upgrade",
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyHeader(dst, src http.Header) {
|
||||||
|
for k, vv := range src {
|
||||||
|
for _, v := range vv {
|
||||||
|
dst.Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func delHopHeaders(header http.Header) {
|
||||||
|
for _, h := range hopHeaders {
|
||||||
|
header.Del(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hijack(hijackable interface{}) (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
hj, ok := hijackable.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("Connection doesn't support hijacking")
|
||||||
|
}
|
||||||
|
conn, rw, err := hj.Hijack()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var emptytime time.Time
|
||||||
|
err = conn.SetDeadline(emptytime)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return conn, rw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flush(flusher interface{}) bool {
|
||||||
|
f, ok := flusher.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
f.Flush()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue