diff --git a/condlog.go b/condlog.go new file mode 100644 index 0000000..96a18f3 --- /dev/null +++ b/condlog.go @@ -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} +} diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..278866b --- /dev/null +++ b/handler.go @@ -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) + } +} diff --git a/logwriter.go b/logwriter.go new file mode 100644 index 0000000..657c2f3 --- /dev/null +++ b/logwriter.go @@ -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: + } +} diff --git a/main.go b/main.go index 5442946..5b70878 100644 --- a/main.go +++ b/main.go @@ -1,51 +1,144 @@ package main import ( - "context" + "errors" + "flag" + "fmt" "log" + "net" + "net/http" + "net/url" + "os" + "time" - se "github.com/Snawoot/opera-proxy/seclient" + xproxy "golang.org/x/net/proxy" ) -const ( - username = "se0316" - password = "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II" +var ( + version = "undefined" ) +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: ://[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() { - seclient, err := se.NewSEClient(username, password, nil) - 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) + os.Exit(run()) } diff --git a/upstream.go b/upstream.go new file mode 100644 index 0000000..ff2a0a0 --- /dev/null +++ b/upstream.go @@ -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) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..df487fd --- /dev/null +++ b/utils.go @@ -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 + } + } +}