VI. My Own Reverse Proxy

Let's get started making our own proxy. We'll just steal the contents of NewSingleHostReverseProxy(), instantiate our own ReverseProxy, and get that working.

package main

import (
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
    "strings"
)

func main() {
    // Define the backend server we proxy to
    target, err := url.Parse("http://localhost:8000")

    if err != nil {
        log.Fatal(err)
    }

    // Stolen from `httputil.NewSingleHostReverseProxy()`
    targetQuery := target.RawQuery
    director := func(req *http.Request) {
        req.URL.Scheme = target.Scheme
        req.URL.Host = target.Host
        req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
        if targetQuery == "" || req.URL.RawQuery == "" {
            req.URL.RawQuery = targetQuery + req.URL.RawQuery
        } else {
            req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
        }
        if _, ok := req.Header["User-Agent"]; !ok {
            // explicitly disable User-Agent so it's not set to default value
            req.Header.Set("User-Agent", "")
        }
    }

    // Create the proxy object
    proxy := &httputil.ReverseProxy{Director: director}

    // Listen for http:// connections on port 80
    srv := http.Server{Addr: ":80", Handler: proxy}

    // Start the server
    srv.ListenAndServe()
}

// helper functions

func singleJoiningSlash(a, b string) string {
    // snip
}

func joinURLPath(a, b *url.URL) (path, rawpath string) {
    // snip
}

We literally just ripped out the NewSingleHostReverseProxy() method directly. Unfortunately, it calls some non-exportable helper functions, so we ripped those out too. They're mine now.

All they do is add slashes to URL's in way that ensures a slash exists if needed, and that double slashes do not exist. You can find them in stdlib http.httputil.reverseproxy.go.

Instantiating a new httputil.ReverseProxy object lets us keep all the fancy logic of the Reverse Proxy, but then modify / customize what we need later.

Some Refactoring

Let's do something fun. First, I hate having this mess of code in the main namespace. Let's make our own module and tuck away some of this trash. It'll make this a tad harder to write about, but the examples will be simpler.

Here's the new project layout:

.
├── go.mod
├── go.sum
├── main.go
└── reverseproxy
    └── reverseproxy.go

The first thing we're going to do is wrap the httputil.ReverseProxy into our own ReverseProxy struct. This helps us tuck code away into our own modules, and then later we can more easily add some functionality to it.

File reverseproxy.go can have this:

package reverseproxy

import (
    "net/http"
    "net/http/httputil"
    "net/url"
    "strings"
)

type ReverseProxy struct {
    Target *url.URL
    proxy  *httputil.ReverseProxy
}

// Start will listen on configured listeners
func (r *ReverseProxy) Start() error {
    r.proxy = &httputil.ReverseProxy{
        Director: r.Director(),
    }

    // Hard-coding port 80 for now
    srv := &http.Server{Addr: ":80", Handler: r.proxy}

    return srv.ListenAndServe()
}

// Director returns a function for use in http.ReverseProxy.Director.
// The function matches the incoming request to a specific target and
// sets the request object to be sent to the matched upstream server.
func (r *ReverseProxy) Director() func(req *http.Request) {
    return func(req *http.Request) {
        targetQuery := r.Target.RawQuery
        req.URL.Scheme = r.Target.Scheme
        req.URL.Host = r.Target.Host
        req.URL.Path, req.URL.RawPath = joinURLPath(r.Target, req.URL)
        if targetQuery == "" || req.URL.RawQuery == "" {
            req.URL.RawQuery = targetQuery + req.URL.RawQuery
        } else {
            req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
        }
        if _, ok := req.Header["User-Agent"]; !ok {
            // explicitly disable User-Agent so it's not set to default value
            req.Header.Set("User-Agent", "")
        }
    }
}

// Helper functions moved here

func singleJoiningSlash(a, b string) string {
    // snip
}

func joinURLPath(a, b *url.URL) (path, rawpath string) {
    // snip
}

So, I created my own ReverseProxy and added a Start() method. The Start() method has some code smells in it, but it's simple to see what's going on so we'll keep it.

I also added a Director() method onto my proxy struct. This generates a Director function for us, which is used by httputil.ReverseProxy. For now, we just copied, pasted, and tweaked the stdlib Director method to get it working. There are only minor tweaks here. Specifiaclly, we don't pass it a target *url.URL, but instead use the Target *url.URL that's defined in our own ReverseProxy struct.

We also moved the helper functions into this file, and I'm still hiding their boring contents for brevity (// snip!).

The main.go file, which uses our ReverseProxy, instantiates and starts the server:

package main

import (
    "github.com/fideloper/someproxy/reverseproxy"
    "log"
    "net/url"
)

func main() {
    target, err := url.Parse("http://localhost:8000")

    if err != nil {
        log.Fatal(err)
    }

    proxy := &reverseproxy.ReverseProxy{
        Target: target,
    }

    proxy.Start()
}

Simple! But there's no features yet. Let's add some!