Golang: Creating HTTPS connection via proxy
I'm writing this post because I had to read a bit of golang code to figure out how to go about doing this.
User familiar with crypto/tls/ will notice that there's already a function available to establish a TLS connection, tls.Dial. However, it doesn't have any option to specify proxy. Why? Because, TLS connection has nothing to do with proxy, TLS is available as a addon to an already existing TCP connection. It exists one level below HTTP, the application layer protocol. That's exactly the reason tls.Dial function is not present in the net/http package too.
A simple way to do a request is http.Client.Get. (). Let's see what it does.
func (c *Client) Get(url string) (resp *Response, err error) {
req, err := NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.doFollowingRedirects(req, shouldRedirectGet)
}
It simply does a request using NewRequest function. All it does is returns a proper Request struct depending on various things like type of request, URL to open, headers etc etc. Depending on various StatusCode, headers etc received, request is handled and ultimately (*Client).send is called. This function handles cookies and then actually calls the send function in the net/http package which is not available to the outside world.
func (c *Client) send(req *Request, deadline time.Time) (*Response, error) {
if c.Jar != nil {
for _, cookie := range c.Jar.Cookies(req.URL) {
req.AddCookie(cookie)
}
}
resp, err := send(req, c.transport(), deadline)
if err != nil {
return nil, err
}
if c.Jar != nil {
if rc := resp.Cookies(); len(rc) > 0 {
c.Jar.SetCookies(req.URL, rc)
}
}
return resp, err
}
Interesting thing to note here is the c.transport() that we're passing. This function returns the transport being used for the current client.
func (c *Client) transport() RoundTripper {
if c.Transport != nil {
return c.Transport
}
return DefaultTransport
}
From the docs:-
Transport is an implementation of RoundTripper that supports HTTP, HTTPS, and HTTP proxies (for either HTTP or HTTPS with CONNECT).
So, this is where we can set proxies. RoundTripper is an interface and pointer to http.Transport implements that interface.
RoundTripper is an interface representing the ability to execute a single HTTP transaction, obtaining the Response for a given Request. The http module providers an implementation of RoundTripper, http.Transport
type Transport struct {
// Proxy specifies a function to return a proxy for a given
// Request. If the function returns a non-nil error, the
// request is aborted with the provided error.
// If Proxy is nil or returns a nil *URL, no proxy is used.
Proxy func(*Request) (*url.URL, error)
We've found our Proxy. SUCCESS!
So, all we need to do is, create a custom http.Transport and then a http.Client that uses this newly created http.Transport.
Now onto the real task at hand.
- Create a custom transport using proxy
// Create proxy
proxyURL, _ := url.Parse(*proxy)
transport := http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{},
}
- Create the http.Client object
client = http.Client{
Transport: &transport,
}
- Call http.Client.Get
resp, _ := client.Get("https://www.google.co.in")
Using what we have learnt, here's the code below that connects to a live HTTPS endpoint, fetches its certificate and shows the days to expiry for that cert.
package main
import (
"crypto/tls"
"flag"
"fmt"
"net/http"
"net/url"
"time"
)
const timeout time.Duration = 10
func main() {
// Parse cmdline arguments using flag package
server := flag.String("server", "abhijeetr.com", "Server to ping")
port := flag.Uint("port", 443, "Port that has TLS")
proxy := flag.String("proxyURL", "", "Proxy to use for TLS connection")
flag.Parse()
// Prepare the client
var client http.Client
if *proxy != "" {
proxyURL, err := url.Parse(*proxy)
if err != nil {
panic("Error parsing proxy URL")
}
transport := http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{},
}
client = http.Client{
Transport: &transport,
Timeout: time.Duration(time.Millisecond * timeout),
}
} else {
client = http.Client{}
}
// Now we've proper client, with or without proxy
resp, err := client.Get(fmt.Sprintf("https://%v:%v", *server, *port))
if err != nil {
panic("failed to connect: " + err.Error())
}
fmt.Printf("Time to expiry for the certificate: %v\n", resp.TLS.PeerCertificates[0].NotAfter.Sub(time.Now()))
}
Cheers !!