Extending Golang’s Single Host Reverse Proxy
Golang provides a simple HTTP Utility called ReverseProxy
which makes it dead simple to create a ReverseProxy as such:
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
)
func main() {
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "this call was relayed by the reverse proxy")
}))
defer backendServer.Close()
rpURL, err := url.Parse(backendServer.URL)
if err != nil {
log.Fatal(err)
}
frontendProxy := httptest.NewServer(httputil.NewSingleHostReverseProxy(rpURL))
defer frontendProxy.Close()
resp, err := http.Get(frontendProxy.URL)
if err != nil {
log.Fatal(err)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", b)
}
This is all well and good, but maybe you want to use multiple backend servers. That is exactly what I wanted to
accomplish! To make things easy we are just going to assume that:
* backend servers are healthy
* backend servers have same priority weight (round robin)
* all traffic is HTTP (not SSL/TLS/HTTPS)
To accomplish this I need to be able to identify multiple backend servers, and rotate between them. So the first thing
I want to do is look at what exactly httputil.NewSingleHostReverseProxy()
is doing.
Looking at https://golang.org/pkg/net/http/httputil/#NewSingleHostReverseProxy we see a director
is created, which
manipulates the URL to conform to what the backend server is expecting. This directory is set into a ReverseProxy
struct, and sent back to the caller.
So what exactly is ReverseProxy
? ReverseProxy
is an HTTP Handler, which is an interface that dictates the existence
of the method ServeHTTP(ResponseWriter, *http.Request)
. So we know that ServeHTTP
is the meat to look at there, and
I have trimmed it down to the relevant parts for us
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
...
p.Director(outreq)
...
}
As you can see the Director is being used, so all we need to do is have multiple Directors, and swap them out each time
ServerHTTP
runs. Let’s do it!
Extend
So the first thing we need to do is extend ReverseProxy
so we do that as such:
type ReverseProxy struct {
httputil.ReverseProxy
}
Next we need to have a way to store the state of which Director is being used. So lets add a simple integer; we also
need a simple mechanism for storing the directors, so we’ll add a slice of functions!
type ReverseProxy struct {
httputil.ReverseProxy
currentDirector int
Directors []func(req *http.Request)
}
The original NewSingleHostReverseProxy()
takes a single url.URL
as its argument, but we’d like to be able to give a
bunch if we have them ready when we create the ReverseProxy
, so we create a new function to create our new
ReverseProxy
with multiple Director support:
func NewMultiHostReverseProxy(targets ...*url.URL) (reverseProxy *ReverseProxy) {
// grab a new ReverseProxy and set the currentDirector to 0
reverseProxy := &ReverseProxy{currentDirector: 0}
// loop through given targets and add as Directors
for _, target := range targets {
reverseProxy.AddHost(target)
}
// send back the
return reverseProxy
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
func (p *ReverseProxy) AddHost(target *url.URL) {
targetQuery := target.RawQuery
p.Directors = append(p.Directors, func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
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", "")
}
})
}
Above you can see the AddHost
method added which borrows the logic from NewSingleHostReverseProxy()
but places the
newly generated director into our new Directors
slice. We also had to take singleJoiningSlash()
verbatim from
reverseproxy.go
as it was created as a private function.
Lastly we need to rotate between the backend servers, so we add a new function called NextDirector
:
func (p *ReverseProxy) NextDirector() func(*http.Request) {
log.Printf("Current Director: %d", p.currentDirector)
p.currentDirector += 1
log.Printf("Next Director: %d", p.currentDirector)
if p.currentDirector >= len(p.Directors) {
p.currentDirector = 0
log.Printf("Too High; Resetting Director: %d", p.currentDirector)
}
log.Printf("Returning Next Director: %d", p.currentDirector)
return p.Directors[p.currentDirector]
}
and we need to call that new function when ServeHTTP
is called, but we don’t want to take over all the function of the
original ServeHTTP
, and so we have:
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Rotate Director
p.Director = p.NextDirector()
// Call the upstream ServeHTTP
p.ReverseProxy.ServeHTTP(rw, req)
}
and now we can use our new Multi Host Reverse Proxy as follows:
package main
import (
"github.com/utahcon/reverse-proxy/util"
"log"
"net/http"
"net/url"
)
func main() {
var targets []*url.URL
for _, host := range []string{"http://127.0.0.1:9000", "http://127.0.0.1:9500"} {
rpURL, err := url.Parse(host)
if err != nil {
log.Fatalf("Error parsing URL: %v", err)
}
targets = append(targets, rpURL)
}
rp := util.NewMultiHostReverseProxy(targets...)
feProxy := &http.Server{
Addr: ":80",
Handler: rp,
}
defer func() {
err := feProxy.Close()
if err != nil {
log.Fatalf("Error closing proxy: %v", err)
}
}()
err := feProxy.ListenAndServe()
if err != nil {
log.Fatal("Error Listening: ", err)
}
}
What’s Next?
Next up I want to add ACME support so that SSL/TLS/HTTPS is a trivial add-on.
Read other posts
Golang provides a simple HTTP Utility called ReverseProxy
which makes it dead simple to create a ReverseProxy as such:
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
)
func main() {
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "this call was relayed by the reverse proxy")
}))
defer backendServer.Close()
rpURL, err := url.Parse(backendServer.URL)
if err != nil {
log.Fatal(err)
}
frontendProxy := httptest.NewServer(httputil.NewSingleHostReverseProxy(rpURL))
defer frontendProxy.Close()
resp, err := http.Get(frontendProxy.URL)
if err != nil {
log.Fatal(err)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", b)
}
This is all well and good, but maybe you want to use multiple backend servers. That is exactly what I wanted to accomplish! To make things easy we are just going to assume that: * backend servers are healthy * backend servers have same priority weight (round robin) * all traffic is HTTP (not SSL/TLS/HTTPS)
To accomplish this I need to be able to identify multiple backend servers, and rotate between them. So the first thing
I want to do is look at what exactly httputil.NewSingleHostReverseProxy()
is doing.
Looking at https://golang.org/pkg/net/http/httputil/#NewSingleHostReverseProxy we see a director
is created, which
manipulates the URL to conform to what the backend server is expecting. This directory is set into a ReverseProxy
struct, and sent back to the caller.
So what exactly is ReverseProxy
? ReverseProxy
is an HTTP Handler, which is an interface that dictates the existence
of the method ServeHTTP(ResponseWriter, *http.Request)
. So we know that ServeHTTP
is the meat to look at there, and
I have trimmed it down to the relevant parts for us
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
...
p.Director(outreq)
...
}
As you can see the Director is being used, so all we need to do is have multiple Directors, and swap them out each time
ServerHTTP
runs. Let’s do it!
Extend
So the first thing we need to do is extend ReverseProxy
so we do that as such:
type ReverseProxy struct {
httputil.ReverseProxy
}
Next we need to have a way to store the state of which Director is being used. So lets add a simple integer; we also need a simple mechanism for storing the directors, so we’ll add a slice of functions!
type ReverseProxy struct {
httputil.ReverseProxy
currentDirector int
Directors []func(req *http.Request)
}
The original NewSingleHostReverseProxy()
takes a single url.URL
as its argument, but we’d like to be able to give a
bunch if we have them ready when we create the ReverseProxy
, so we create a new function to create our new
ReverseProxy
with multiple Director support:
func NewMultiHostReverseProxy(targets ...*url.URL) (reverseProxy *ReverseProxy) {
// grab a new ReverseProxy and set the currentDirector to 0
reverseProxy := &ReverseProxy{currentDirector: 0}
// loop through given targets and add as Directors
for _, target := range targets {
reverseProxy.AddHost(target)
}
// send back the
return reverseProxy
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
func (p *ReverseProxy) AddHost(target *url.URL) {
targetQuery := target.RawQuery
p.Directors = append(p.Directors, func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
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", "")
}
})
}
Above you can see the AddHost
method added which borrows the logic from NewSingleHostReverseProxy()
but places the
newly generated director into our new Directors
slice. We also had to take singleJoiningSlash()
verbatim from
reverseproxy.go
as it was created as a private function.
Lastly we need to rotate between the backend servers, so we add a new function called NextDirector
:
func (p *ReverseProxy) NextDirector() func(*http.Request) {
log.Printf("Current Director: %d", p.currentDirector)
p.currentDirector += 1
log.Printf("Next Director: %d", p.currentDirector)
if p.currentDirector >= len(p.Directors) {
p.currentDirector = 0
log.Printf("Too High; Resetting Director: %d", p.currentDirector)
}
log.Printf("Returning Next Director: %d", p.currentDirector)
return p.Directors[p.currentDirector]
}
and we need to call that new function when ServeHTTP
is called, but we don’t want to take over all the function of the
original ServeHTTP
, and so we have:
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Rotate Director
p.Director = p.NextDirector()
// Call the upstream ServeHTTP
p.ReverseProxy.ServeHTTP(rw, req)
}
and now we can use our new Multi Host Reverse Proxy as follows:
package main
import (
"github.com/utahcon/reverse-proxy/util"
"log"
"net/http"
"net/url"
)
func main() {
var targets []*url.URL
for _, host := range []string{"http://127.0.0.1:9000", "http://127.0.0.1:9500"} {
rpURL, err := url.Parse(host)
if err != nil {
log.Fatalf("Error parsing URL: %v", err)
}
targets = append(targets, rpURL)
}
rp := util.NewMultiHostReverseProxy(targets...)
feProxy := &http.Server{
Addr: ":80",
Handler: rp,
}
defer func() {
err := feProxy.Close()
if err != nil {
log.Fatalf("Error closing proxy: %v", err)
}
}()
err := feProxy.ListenAndServe()
if err != nil {
log.Fatal("Error Listening: ", err)
}
}
What’s Next?
Next up I want to add ACME support so that SSL/TLS/HTTPS is a trivial add-on.