Reverse Proxy in Go
October 22, 2024
What’s a reverse proxy?
Nah, let’s skip this part. Knowing you’ve landed here means that you’ve understand what you’d want to build.
Reverse Proxy in httputil
Go has a good collection of standard library, this includes a Reverse Proxy.
With this, we could build a simple configurable reverse proxy.
Getting Started
Create an empty project.
$ mkdir reverse-proxy; cd reverse-proxy;
$ go mod init reverse-proxy;
$ go work init .;
Let’s read the configuration file first!
Here’s a sample configuration file we’d like to read.
config.yml
host: localhost
port: 3000
resources:
- name: google
path: /search
dest: https://www.google.com/
To read the YML file, we’ll be using yaml.v3
go get gopkg.in/yaml.v3
main.go
package main;
import (
"log"
"os"
"gopkg.in/yaml.v3"
)
type Endpoint struct {
Name string `yaml:name`
Path string `yaml:path`
Dest string `yaml:dest`
}
type Config struct {
Host string `yaml:host`
Port int `yaml:port`
Resources []Endpoint `yaml:endpoints`
}
func main() {
t := Config{}
config, err := os.ReadFile("config.yml")
if err != nil {
log.Fatalln("Could not found configuration file")
}
err = yaml.Unmarshal([]byte(config), &t)
if err != nil {
log.Fatalln("Invalid configuration file")
}
log.Println(t.Host)
}
Creating our HTTP server
Started in Go 1.22, Go offers an enhanced routing. Learn more here.
import (
+ "fmt",
+ "net/http",
"log"
"os"
"gopkg.in/yaml.v3"
)
......................................................
func main() {
t := Config{}
config, err := os.ReadFile("config.yml")
if err != nil {
log.Fatalln("Could not found configuration file")
}
err = yaml.Unmarshal([]byte(config), &t)
if err != nil {
log.Fatalln("Invalid configuration file")
}
+ mux := http.NewServeMux()
+
+ for i := 0; i < len(t.Resources); i++ {
+ curr := t.Resources[i]
+
+ log.Printf("Adding Proxy for [%s] Path \"%s\" to \"%s\"", curr.Name, curr.Path, curr.Dest)
+
+ mux.HandleFunc(curr.Path, func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, curr.Dest)
+ })
+ }
+
+ http.ListenAndServe(fmt.Sprintf("%s:%d", t.Host, t.Port), mux)
log.Println(t.Host)
}
These changes should give us something like this
Adding the proxy handler
import (
"fmt"
"log"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
"os"
+ "strings"
"gopkg.in/yaml.v3"
)
......................................................
for i := 0; i < len(t.Resources); i++ {
curr := t.Resources[i]
log.Printf("Adding Proxy for [%s] Path \"%s\" to \"%s\"", curr.Name, curr.Path, curr.Dest)
mux.HandleFunc(curr.Path, func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, curr.Dest)
+ proxy := httputil.ReverseProxy{
+ Rewrite: func(req *httputil.ProxyRequest) {
+ p, _ := url.Parse(curr.Dest)
+ p.Path = strings.TrimRight(strings.TrimLeft(r.URL.Path, curr.Path), "/")
+
+ req.SetXForwarded()
+ req.Out.Header.Add("Custom-Header", "Value")
+
+ log.Printf("[%s] %s -> %s", curr.Dest, r.URL, p.Path)
+ req.SetURL(p)
+ },
+ }
+
+ proxy.ServeHTTP(w, r)
+ })
}
Let’s try it now!
Conclusion
This is a very simple kickstart project to start of your journey into Reverse Proxy. I hope you’d find this useful. Next step is to introduce a dynamic path matching as this currently throws page not founds for non-matching paths.
To see the full project in this tutorial click here.