elazarl / goproxy

An HTTP proxy library for Go
BSD 3-Clause "New" or "Revised" License
6.06k stars 1.1k forks source link

How to chain HTTPS proxy #230

Open matejvelikonja opened 7 years ago

matejvelikonja commented 7 years ago

I use GoProxy as an intermediate proxy. So, just transferring traffic from GoProxy to some other proxy.

It works as intended for the HTTP protocol, but HTTPS does not. It connects directly to the website, instead of connecting first to parent proxy and than to the website. I don't need / want the MITM, so basically, just forwarding of CONNECT protocol to the parent proxy (with auth).

How can I achieve this?

The following code works for HTTP, but not for HTTPS.

handler.Tr = &http.Transport{Proxy: func(req *http.Request) (*url.URL, error) {
        userInfo := url.UserPassword(fwdProxy.NodeProxy.Username, fwdProxy.NodeProxy.Password)
        parsedUrl, err := url.Parse(fwdProxy.NodeProxy.Url)

        if err == nil {
            parsedUrl.User = userInfo
        }

        return parsedUrl, err
    }}
SmashGuy2 commented 7 years ago

Are you using goproxy as a classic proxy or as a transparent proxy?

I was able to run the basic example and it works fine for both http and https. If the client is configured to use a proxy server, everything works through the http port. If the proxy is a transparent intercept proxy (ie: using iptable rules or the equivalent to forward ports), the sample also worked just fine.

matejvelikonja commented 7 years ago

As a classic.

Which example exactly are you talking about?

SmashGuy2 commented 7 years ago

Hm, that worked perfectly for me as written.

The example is found in /examples/goproxy-basic.

matejvelikonja commented 7 years ago

Well, that is not what I would like. What I need:

curl -> goproxy -> squid (or any other proxy) -> website

and i cant figure out how to do it for https. the http example is in the above comment.

SmashGuy2 commented 7 years ago

Oh I see. I think you could implement some iptables rules for that. Not sure how it could be implemented in goproxy, if that is possible.

matejvelikonja commented 7 years ago

the iptables won't work, since I need to add auth in goproxy. the squid proxy is behind auth and i want to connect to it via goproxy, but the client (curl) does not know the credentials.

vedhavyas commented 7 years ago

@matejvelikonja did you figure how to route https though auth proxy

matejvelikonja commented 7 years ago

@vedhavyas unfortunately not yet

geekhckr commented 6 years ago

Have you tried using NewConnectDialToProxy. Like below.

handler.Tr = &http.Transport{Proxy: func(req *http.Request) (*url.URL, error) {
            return url.Parse("http://username:password@proxy.com/")
        }}
handler.ConnectDial = handler.NewConnectDialToProxy("http://username:password@proxy.com/")
ianldgs commented 5 years ago

I am trying to do the same thing. Anyone succeeded with @geekhckr's answer?

onegithuber commented 5 years ago

I am trying to do the same thing. Anyone succeeded with @geekhckr's answer? are you successed with his answer?

jlcd commented 5 years ago

You basically need something like this:

    proxy := goproxy.NewProxyHttpServer()

[...]

    proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
    var proxyUrl *url.URL
    proxyUrl, _ = url.Parse("http://PROXY_USER:PROXY_PASSW@PROXY_HOST:PROXY_PORT")
        proxy.Tr = &http.Transport{
            Proxy: http.ProxyURL(proxyUrl),
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }
        return goproxy.MitmConnect, host
    })

[...]

    log.Fatal(http.ListenAndServe(":8080", proxy))

Replacing http://PROXY_USER:PROXY_PASSW@PROXY_HOST:PROXY_PORT with the actual proxy credentials/address.


Quick edit:

this will work only for https targets, if you also need to connect to http targets, you should also replace the http.Transport on proxy.OnRequest().DoFunc(...) or on the proxy.Tr, eg:

    proxy := goproxy.NewProxyHttpServer()
    var proxyUrl *url.URL
    proxyUrl, _ = url.Parse("http://PROXY_USER:PROXY_PASSW@PROXY_HOST:PROXY_PORT")
    proxy.Tr = &http.Transport{
        Proxy: http.ProxyURL(proxyUrl),
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
ianldgs commented 5 years ago

@onegithuber

I am trying to do the same thing. Anyone succeeded with @geekhckr's answer?

are you successed with his answer?

I actually created my own implementation from scratch to make it work.

alexelisenko commented 5 years ago

Hi,

I am trying to chain 2 forward proxies. I have goproxy with the following setup on the machine, and the machine has another goproxy that I would like to forward to. They both need to support HTTP + HTTPS.

If I run the following code, then use curl to test it, it works for HTTP, but not HTTPS:

Use case explanation: The machine has multiple ssh reverse port forwards that have a forward proxy listening on the other end. I need to have a "gateway" forward proxy listening on a static ip:port that will check credentials, pick the next forward proxy and forward to it.

client -> goproxy on 127.0.0.1:8080 (checks creds) -> http://127.0.x.x:xxxx (no credentials)

curl --proxy http://127.0.0.1:8080 https://wtfismyip.com/text
curl: (60) server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none
More details here: http://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.
curl --proxy http://127.0.0.1:8080 http://wtfismyip.com/text
xxx.xxx.xxx.xxx
package main

import (
    "crypto/tls"
    "net/http"
    "net/url"

    "github.com/elazarl/goproxy"
)

func startProxy(addr string) {
    proxy := goproxy.NewProxyHttpServer()

    proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
        var proxyURL *url.URL
        proxyURL, _ = url.Parse("http://127.0.1.1:10001")
        proxy.Tr = &http.Transport{
            Proxy:           http.ProxyURL(proxyURL),
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }
        return goproxy.MitmConnect, host
    })

    //proxy.Verbose = true
    http.ListenAndServe(addr, proxy)
}

func main() {

    startProxy("127.0.0.1:8080")

}

Any insight into what I have wrong would be greatly appreciated!

alexelisenko commented 5 years ago

Update:

This seems to be working for me. Is this the correct approach, or does anyone see an issue with doing this?

func startProxy(addr string) {
    proxy := goproxy.NewProxyHttpServer()

    proxy.Tr = &http.Transport{Proxy: func(req *http.Request) (*url.URL, error) {
        return url.Parse("http://127.0.1.1:10001")
    }}
    proxy.ConnectDial = proxy.NewConnectDialToProxy("http://127.0.1.1:10001")

    http.ListenAndServe(addr, proxy)
}
dylankilkenny commented 4 years ago

@alexelisenko did you ever get it working for https?

alexelisenko commented 4 years ago

@dylankilkenny Do you mean to to have the proxy listen and serve via TLS? or dial an HTTPS target?

dank commented 4 years ago

The Proxy-Authorization header is not set when using NewConnectDialToProxy.

ngeorgiadis commented 4 years ago

Hi everybody, did someone find a solution for this issue?

uzxmx commented 2 years ago

For the latest version of goproxy, there are two ways to implement chaining http(s) proxy.

One simple way is to use NewConnectDialToProxy or NewConnectDialToProxyWithHandler provided by goproxy. Below shows a working example. Note the example supports both HTTP or HTTPS proxy to be forwared to, and with or without proxy authorization. Just use a correct urlString format.

package main

import (
    "crypto/tls"
    "encoding/base64"
    "github.com/elazarl/goproxy"
    "log"
    "net/http"
    "net/url"
)

const urlString = "https://username:password@another-proxy-host:another-proxy-port"
// const urlString = "http://username:password@another-proxy-host:another-proxy-port"
// const urlString = "https://another-proxy-host:another-proxy-port"
// const urlString = "http://another-proxy-host:another-proxy-port"

func main() {
    proxyURL, err := url.Parse(urlString)
    if err != nil {
        log.Fatal(err)
    }

    proxy := goproxy.NewProxyHttpServer()
    proxy.Tr = &http.Transport{
        Proxy:           http.ProxyURL(proxyURL),
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }

    username := proxyURL.User.Username()
    password, _ := proxyURL.User.Password()
    var proxyAuthorization string
    if len(username) > 0 || len(password) > 0 {
        proxyAuthorization = "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
    }
    proxy.ConnectDial = proxy.NewConnectDialToProxyWithHandler(urlString, func(req *http.Request) {
        if len(proxyAuthorization) > 0 {
            req.Header.Set("Proxy-Authorization", proxyAuthorization)
        }
    })

    proxy.Verbose = true
    log.Fatal(http.ListenAndServe(":8080", proxy))
}

Another way is to use hijack, which means we can implement customized behavior for CONNECT request. A complete example is shown in this gist.

xzycn commented 2 years ago

@uzxmx The code seems that cannot connect to multiple targets, you have to specify a target in NewConnectDialToProxyWithHandler :(

lawlielt commented 10 months ago

@xzycn Maybe this will be work

connectDialer := newConnectDialer(proxy)
    proxy.ConnectDialWithReq = func(req *http.Request, network string, addr string) (net.Conn, error) {
        return connectDialer(req, network, addr)
}
func newConnectDialer(proxy *goproxy.ProxyHttpServer) func(req *http.Request, network, addr string) (net.Conn, error) {
    return func(req *http.Request, network, addr string) (net.Conn, error) {
        // proxyService have multi targets, you can chose a proxy per req dynamic
        u, err := proxyService.GetProxy(req.Context())
        if err != nil {
            return nil, err
        }
        proxyAddr := getAddrFromURL(u)
        username := u.User.Username()
        password, _ := u.User.Password()
        var proxyAuthorization string
        if len(username) > 0 || len(password) > 0 {
            proxyAuthorization = "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
        }
        isTLS := u.Scheme == "https" || u.Scheme == "wss"
        ///
        connectReq := &http.Request{
            Method: "CONNECT",
            URL:    &url.URL{Opaque: addr},
            Host:   addr,
            Header: make(http.Header),
        }
        connectReq = connectReq.WithContext(req.Context())
        if len(proxyAuthorization) > 0 {
            connectReq.Header.Set("Proxy-Authorization", proxyAuthorization)
        }
        c, err := net.Dial(network, proxyAddr)
        if err != nil {
            return nil, err
        }
        if isTLS {
            c = tls.Client(c, proxy.Tr.TLSClientConfig)
        }

        err = connectReq.Write(c)
        if err != nil {
            return nil, err
        }
        br := bufio.NewReader(c)
        resp, err := http.ReadResponse(br, connectReq)
        if err != nil {
            _ = c.Close()
            return nil, err
        }
        defer resp.Body.Close()
        if resp.StatusCode != 200 {
            resp, err := io.ReadAll(resp.Body)
            if err != nil {
                return nil, err
            }
            _ = c.Close()
            return nil, errors.New("proxy refused connection" + string(resp))
        }
        return c, nil
    }
}

func getAddrFromURL(u *url.URL) string {
    addr := u.Host
    if !strings.ContainsRune(addr, ':') {
        if u.Scheme == "" || u.Scheme == "http" {
            addr += ":80"
        } else if u.Scheme == "https" {
            addr += ":443"
        }
    }
    return addr
}