go lang simple reverse proxy

Few months ago, I coded on hackerrank by learning Go language. Tests there can be very tricky but you should take a look at them and practice sometimes. Today, I will show your a more concrete example with a reverse proxy implementation.

Main role of this pattern is to filter network request by inspecting (modifying...) and route it (or not) somewhere else.

For that case, we will play with http protocol but Go packages provide everything you may need to play with network other layers, TCP, UDP, IP...

Learn Go lang

Simply start with the tour of Go here and examples

This language is very intuitive, not so far from C language with some object oriented pattern like interface.

Packages

Go provides all the necessary packages to build you own application.

Http

net/http's package exposes what we do in most part of our daily life on the web: play with client, server, request, response..

If you deep look at Go documentation which is not really readable according to me, you should find this sub httputil package and NewSingleHostReverseProxy function

func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy

You provide a target URL and it returns a ReverseProxy instance which will do the work for you, OMG it is great.

Then a simple call to

func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)

Will forward the request, it sounds very good, let's try it.

Scenario

You have a backend service somewhere accessible with an http interface. And you do not want it to be exposed directly or you want to put some custom rules to access it, as authentication, or with some arbitrary logic. Thus why not place a reverse proxy in front of your backend services.

In this example, we will demonstrate role of our reverse proxy by forwarding all web request from server one (running on arbitrary 80 port) to another server somewhere, let's say http://127.0.0.1:8080

Resume

  • our program will start a webserver running on 80
  • all request made to this server will be transparently forward to targeted webserver and response sent to first server.

Forward trafic

When you start, do not worry about making things simple, with no struct, here I have just created a simple struct called Prox which is responsible to make business logic of reverse proxy.

Here are some examples of struct usage and pointers method receivers with Go receivers

Simple proxy


// our RerverseProxy object
type Prox struct {
  // target url of reverse proxy
    target *url.URL
  // instance of Go ReverseProxy thatwill do the job for us
    proxy  *httputil.ReverseProxy
}

// small factory
func New(target string) *Prox {
  url, _ := url.Parse(target)
  // you should handle error on parsing
  return &Prox{target: url,proxy: httputil.NewSingleHostReverseProxy(url)}
}

func (p *Prox) handle(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-GoProxy", "GoProxy")
  // call to magic method from ReverseProxy object
    p.proxy.ServeHTTP(w, r)
}

Main example

Flag Go Flag package is very nice to put some command line options.

func main() {
  // come constants and usage helper
  const (
    defaultPort = ":80"
    defaultPortUsage = "default server port, ':80', ':8080'..."
    defaultTarget = "http://127.0.0.1:8080"
    defaultTargetUsage = "default redirect url, 'http://127.0.0.1:8080'"
  )

  // flags
  port := flag.String("port", defaultPort, defaultPortUsage)
  url := flag.String("url", defaultTarget, defaultTargetUsage)

  flag.Parse()

  fmt.Println("server will run on : %s", *port)
  fmt.Println("redirecting to :%s", *url)

  // proxy
  proxy := &Prox{}
  proxy.New(*url)

  // server
  http.HandleFunc("/", proxy.handle)
  http.ListenAndServe(*port, nil)
}

Go tools can compile your code to get a binary executable program.

go run build yourfile.go

Forward trafic with logic inside

Our first example had no logic but you can easily add you own.

  • add security tests
  • check number of requests done..
  • filter query path, what we will do here.
  • ...

Let's register one regular expression which applied to all requests path allows or disallows forwarding to our target url.

Advanced proxy

type Prox struct {
  target        *url.URL
  proxy         *httputil.ReverseProxy
  routePatterns []*regexp.Regexp // add some route patterns with regexp
}

func New(target string) *Prox {
  url, _ := url.Parse(target)

  return &Prox{target: url,proxy: httputil.NewSingleHostReverseProxy(url)}
}

func (p *Prox) handle(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("X-GoProxy", "GoProxy")

  if p.routePatterns == nil || p.parseWhiteList(r) {
    p.proxy.ServeHTTP(w, r)
  }
}

func (p *Prox) parseWhiteList(r *http.Request) bool {
  for _, regexp := range p.routePatterns {
    fmt.Println(r.URL.Path)
    if regexp.MatchString(r.URL.Path) {
      // let's forward it
      return true
    }
  }
  fmt.Println("Not accepted routes %x", r.URL.Path)
  return false
}

Full main

func main() {
  const (
    defaultPort             = ":80"
    defaultPortUsage        = "default server port, ':80', ':8080'..."
    defaultTarget           = "http://127.0.0.1:8080"
    defaultTargetUsage      = "default redirect url, 'http://127.0.0.1:8080'"
    defaultWhiteRoutes      = `^\/$|[\w|/]*.js|/path|/path2`
    defaultWhiteRoutesUsage = "list of white route as regexp, '/path1*,/path2*...."
  )

  // flags
  port := flag.String("port", defaultPort, defaultPortUsage)
  url := flag.String("url", defaultTarget, defaultTargetUsage)
  routesRegexp := flag.String("routes", defaultWhiteRoutes, defaultWhiteRoutesUsage)

  flag.Parse()

  fmt.Println("server will run on : %s", *port)
  fmt.Println("redirecting to :%s", *url)
  fmt.Println("accepted routes :%s", *routesRegexp)

  //
  reg, _ := regexp.Compile(*routesRegexp)
  routes := []*regexp.Regexp{reg}

  // proxy
  proxy := New(*url)
  proxy.routePatterns = routes

  // server
  http.HandleFunc("/", proxy.handle)
  http.ListenAndServe(*port, nil)
}

Note

By looking into go server code, you can see that a go routine "A goroutine is a lightweight thread of execution." is started on every request.

func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
  done := make(chan bool, 1)
  tw := &timeoutWriter{w: w}
  go func() {
    h.handler.ServeHTTP(tw, r)
    done <- true
  }()
}

Conclusion

It was just a POC but you can easily change it here to start.

Just for fun


Tags: Go