Skip to content

Instantly share code, notes, and snippets.

@dmitshur
Last active March 16, 2020 10:59
Show Gist options
  • Select an option

  • Save dmitshur/29ceb007de7fc553227129b523b720f1 to your computer and use it in GitHub Desktop.

Select an option

Save dmitshur/29ceb007de7fc553227129b523b720f1 to your computer and use it in GitHub Desktop.

Revisions

  1. dmitshur revised this gist Sep 7, 2019. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions main.go
    Original file line number Diff line number Diff line change
    @@ -6,7 +6,7 @@
    //
    // • redirects all HTTP requests to HTTPS
    //
    // • gates certain endpoints via bcrypt-hashed basic auth passwords
    // • gates certain endpoints with basic auth, using bcrypt-hashed passwords
    //
    package main

    @@ -128,7 +128,7 @@ func (h customHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    case "private1.example.com", "private2.example.com":
    _, pw, ok := req.BasicAuth()
    if !ok {
    w.Header().Set("WWW-Authenticate", "Basic")
    w.Header().Set("Www-Authenticate", "Basic")
    http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
    return
    }
  2. dmitshur revised this gist Sep 7, 2019. 1 changed file with 14 additions and 0 deletions.
    14 changes: 14 additions & 0 deletions main.go
    Original file line number Diff line number Diff line change
    @@ -109,6 +109,20 @@ type customHandler struct {
    }

    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":
  3. dmitshur created this gist Sep 7, 2019.
    165 changes: 165 additions & 0 deletions main.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,165 @@
    // 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 via bcrypt-hashed basic auth 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) {
    // 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)
    }