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!