Skip to content

Instantly share code, notes, and snippets.

@dmitshur
Last active March 16, 2020 10:59
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmitshur/29ceb007de7fc553227129b523b720f1 to your computer and use it in GitHub Desktop.
Save dmitshur/29ceb007de7fc553227129b523b720f1 to your computer and use it in GitHub Desktop.
A simple server for HTTPS and HTTP protocols.
// A simple server for HTTPS and HTTP protocols. It implements these behaviors:
//
// • uses Let's Encrypt to acquire and automatically refresh HTTPS certificates
//
// • redirects HTTPS requests to canonical hosts, reverse proxies requests to internal backing servers
//
// • redirects all HTTP requests to HTTPS
//
// • gates certain endpoints with basic auth, using bcrypt-hashed passwords
//
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"net/http/httputil"
"os"
"os/signal"
"path/filepath"
"time"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/crypto/bcrypt"
)
func main() {
flag.Parse()
int := make(chan os.Signal, 1)
signal.Notify(int, os.Interrupt)
ctx, cancel := context.WithCancel(context.Background())
go func() { <-int; cancel() }()
err := run(ctx)
if err != nil {
log.Fatalln(err)
}
}
func run(ctx context.Context) error {
cacheDir, err := os.UserCacheDir()
if err != nil {
return err
}
m := &autocert.Manager{
Cache: autocert.DirCache(filepath.Join(cacheDir, "golang-autocert")),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(
"example.com",
"anotherdomain.example", "www.anotherdomain.example", "foobar.anotherdomain.example",
"private1.example.com", "private2.example.com",
),
Email: "you@example.com",
}
var basicAuthHashes map[string][]byte // Host -> Hash.
err = jsonDecodeFile(filepath.Join("...", "basicauth.json"), &basicAuthHashes)
if err != nil {
return err
}
httpsServer := &http.Server{
Addr: ":https",
TLSConfig: m.TLSConfig(),
Handler: customHandler{Router: newRouter(), BasicAuthHashes: basicAuthHashes},
}
httpServer := &http.Server{
Addr: ":http",
Handler: m.HTTPHandler(nil),
}
errCh := make(chan error)
go func() {
log.Println("Starting HTTPS server.")
err := httpsServer.ListenAndServeTLS("", "")
log.Println("Ended HTTPS server.")
errCh <- fmt.Errorf("httpsServer.ListenAndServeTLS: %v", err)
}()
go func() {
log.Println("Starting HTTP server.")
err := httpServer.ListenAndServe()
log.Println("Ended HTTP server.")
errCh <- fmt.Errorf("httpServer.ListenAndServe: %v", err)
}()
select {
case <-ctx.Done():
err := httpsServer.Close()
if err != nil {
log.Println("httpsServer.Close:", err)
}
err = httpServer.Close()
if err != nil {
log.Println("httpServer.Close:", err)
}
return nil
case err := <-errCh:
return err
}
}
type customHandler struct {
Router http.Handler
BasicAuthHashes map[string][]byte // Host -> Hash.
}
func (h customHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Redirect to canonical host, if needed.
var canonicalHost string
switch req.Host {
case "anotherdomain.example", "www.anotherdomain.example", "foobar.anotherdomain.example":
canonicalHost = "anotherdomain.example"
}
if canonicalHost != "" && req.Host != canonicalHost {
u := *req.URL
u.Scheme = "https" // Needs to be set explicitly because incoming request provides relative path.
u.Host = canonicalHost
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
return
}
// Basic auth.
switch req.Host {
case "private1.example.com", "private2.example.com":
_, pw, ok := req.BasicAuth()
if !ok {
w.Header().Set("Www-Authenticate", "Basic")
http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
return
}
hash, ok := h.BasicAuthHashes[req.Host]
if !ok {
http.Error(w, "403 Forbidden", http.StatusForbidden)
return
}
if err := bcrypt.CompareHashAndPassword(hash, []byte(pw)); err != nil {
http.Error(w, "403 Forbidden", http.StatusForbidden)
return
}
}
h.Router.ServeHTTP(w, req)
}
func newRouter() http.Handler {
director := func(req *http.Request) {
switch req.Host {
default: // Primary domain.
req.URL.Scheme = "http"
req.URL.Host = "127.0.0.1:10000"
case "anotherdomain.example", "www.anotherdomain.example", "foobar.anotherdomain.example":
req.URL.Scheme = "http"
req.URL.Host = "127.0.0.1:10001"
case "private1.example.com", "private2.example.com":
req.URL.Scheme = "http"
req.URL.Host = "127.0.0.1:10002"
}
// Pass req.Host through unmodified, so the target server has access
// to the original req.Host value.
}
return &httputil.ReverseProxy{
Director: director,
FlushInterval: 1 * time.Second,
}
}
// jsonDecodeFile decodes contents of file at path into v.
func jsonDecodeFile(path string, v interface{}) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
return json.NewDecoder(f).Decode(v)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment