2017/11/25

Minimal reverse proxy in go

During the last Copenhagen Gophers meetup, we had some talks about:

  1. Writing applications on google appengine
  2. A personal top 5 of projects in golang.org/x repository where “acme/autocert” was top 1.

The talk about google appengine was good and the speaker made me re-remember that when doing side projects, we do not want to spend all our time setting up complex systems for getting our side project out there. Here i’m thinking, configuring webservers, setting up lets-encrypt, configuring databases, containers, orchestration, etc. etc. Unless that is what your side project is about of course, or that is what scratches our itch. Sometimes all we want is a solution with “batteries included” to get it out there quick.

For the kind of side projects i like to work on, i would love to have someting in between. I’d like to host it myself on the same server but i don’t want to bother with configuring nginx every time i want to put a new side project online.

So how hard could it be to replace nginx with my own home written reverse proxy that fetches lets-encrypt certificates on the fly for the domains that i add. I wanted a solution with minimal configuration.

I came up with a a solution less than 100 lines of code and that does what i need it to do.

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"sync"

	"golang.org/x/crypto/acme/autocert"
)

var (
	proxies map[string]*httputil.ReverseProxy
	statics map[string]http.Handler
)

type Config struct {
	Host   string
	Server string
}

func main() {

	cfgs := []Config{
		Config{
			Host:   "blog.sketchground.dk",
			Server: "http://127.0.0.1:9900",
		},
		Config{
			Host:   "journal.sketchground.dk",
			Server: "http://127.0.0.1:9900",
		},
		Config{
			Host:   "www.ikurven.dk",
			Server: "static:///var/www/ikurvendk",
		},
	}
	hosts := []string{}

	// Load services...
	proxies = map[string]*httputil.ReverseProxy{}
	statics = map[string]http.Handler{}
	for _, cfg := range cfgs {
		u, _ := url.Parse(cfg.Server)
		if u.Scheme == "static" {
			log.Printf("Initializing static server for %v -> %v\n", cfg.Host, cfg.Server)
			statics[cfg.Host] = http.FileServer(http.Dir(u.Path))
		} else {
			log.Printf("Initializing proxy connection for %v -> %v\n", cfg.Host, cfg.Server)
			proxies[cfg.Host] = httputil.NewSingleHostReverseProxy(u)
		}
		hosts = append(hosts, cfg.Host)
	}

	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		log.Println("Starting reverse proxy for ssl connections")
		log.Fatal(http.Serve(autocert.NewListener(hosts...), &P{secure: true}))
		wg.Done()
	}()
	wg.Add(1)
	go func() {
		log.Println("Starting reverse proxy for http connections")
		log.Fatal(http.ListenAndServe(":80", &P{})) // port 80
		wg.Done()
	}()
	wg.Wait()
}

type P struct {
	secure bool
}

func (p *P) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	if !p.secure { // Redirect always if not secure.
		u := fmt.Sprintf("https://%v%v", req.Host, req.URL.Path)
		http.Redirect(rw, req, u, http.StatusFound)
		return
	}

	if h, ok := proxies[req.Host]; ok { // Check if we have proxies
		h.ServeHTTP(rw, req)
		return
	}
	if h, ok := statics[req.Host]; ok { // Check if we have statics
		h.ServeHTTP(rw, req)
		return
	}

	fmt.Fprintf(rw, "Nothing here. Go elsewhere.") // Return if no hosts match
	return
}

This code is also available at github

When it’s time for some more complex, it’s always possible to replace it with nginx/apache/etc. again.

If you enjoyed reading this, you are more than welcome to support my writing/work/whatever on patreon.