proxy: WIP

This commit is contained in:
Vladislav Yarmak 2021-03-26 22:34:43 +02:00
parent e62f2e2ed7
commit c7c6bba005
6 changed files with 769 additions and 40 deletions

58
condlog.go Normal file
View 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
View 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
View 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
View file

@ -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
View 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
View 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
}
}
}