From 566e1faaa29197e428f437eadb30b46c013d256f Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 13 Jan 2023 14:59:13 +0100 Subject: [PATCH] Add code --- Makefile | 15 +++++ go.mod | 12 ++++ go.sum | 8 +++ server.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++++ server_test.go | 85 +++++++++++++++++++++++++++ 5 files changed, 276 insertions(+) create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 server.go create mode 100644 server_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cb46230 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: test build deploy + +GOOS := plan9 +GOARCH := amd64 + +test: + SDF_PROXY_ALLOWED_HOSTS=teapot-dummy-target.example.com \ + go test -v + +build: test + GOOS=$(GOOS) GOARCH=$(GOARCH) \ + go build server.go + +deploy: build + rsync -avz server iceland.sdf.org:html/pub/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ab8a997 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module server + +go 1.19 + +require ( + golang.org/x/crypto v0.4.0 +) + +require ( + golang.org/x/net v0.3.0 // indirect + golang.org/x/text v0.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..82eafe4 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= diff --git a/server.go b/server.go new file mode 100644 index 0000000..9e2238c --- /dev/null +++ b/server.go @@ -0,0 +1,156 @@ +package main + +import ( + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "golang.org/x/crypto/acme/autocert" + "log" + "net" + "net/http" + "net/http/httputil" + "net/mail" + "os" + "strconv" + "strings" + "time" +) + +var ( + targetHost = os.Getenv("SDF_PROXY_TARGET_HOST") + targetScheme = os.Getenv("SDF_PROXY_TARGET_SCHEME") + myIp string + myPtrAddr string + allowedHosts = strings.Split(os.Getenv("SDF_PROXY_ALLOWED_HOSTS"), ",") + tlsLetsEncryptEmail = os.Getenv("SDF_PROXY_LETS_ENCRYPT_EMAIL") + tlsCertManager = autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache("certs"), + Email: tlsLetsEncryptEmail, + HostPolicy: autocert.HostWhitelist(allowedHosts...), + } +) + +func init() { + setMyIpAndPtrAddr(&myIp, &myPtrAddr) +} + +// setMyIpAndPtrAddr attempts to set myIp and myPtrAddr +func setMyIpAndPtrAddr(ip, addr *string) { + resolver := &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: time.Duration(10) * time.Second, + } + return d.DialContext(ctx, network, "ns1.google.com:53") + }, + } + ips, errTxtLookup := resolver.LookupTXT(context.Background(), "o-o.myaddr.l.google.com") + if errTxtLookup != nil { + log.Printf("%v\n", errTxtLookup) + return + } + if len(ips) > 0 { + *ip = ips[len(ips)-1] + addrs, errAddrLookup := net.LookupAddr(*ip) + if errAddrLookup != nil { + log.Printf("%v\n", errAddrLookup) + return + } + if len(addrs) > 0 { + *addr = strings.TrimSuffix(addrs[len(addrs)-1], ".") + } + } +} + +type httpReverseProxy struct { + http.Server + serveTls bool +} + +func newHttpReverseProxy(addr string, serveTls bool) *httpReverseProxy { + server := new(httpReverseProxy) + server.Addr = addr + server.serveTls = serveTls + if server.serveTls { + server.TLSConfig = &tls.Config{GetCertificate: tlsCertManager.GetCertificate} + } + + proxy := new(httputil.ReverseProxy) + proxy.Transport = &http.Transport{} + proxy.Director = func(req *http.Request) { + req.Header.Set("X-Forwarded-For", req.RemoteAddr) + req.Header.Set("X-Forwarded-Host", req.Host) + req.Host = targetHost + req.URL.Host = targetHost + req.URL.Scheme = targetScheme + fmt.Print("Handling request: ") + fmt.Println(req.Header) + } + + mux := http.NewServeMux() + // Hostnames specified in the variable allowedHosts for which the reverse proxy should direct requests to the target host + for _, host := range allowedHosts { + mux.Handle(host+"/", proxy) + mux.Handle(host+"/.well-known/acme-challenge/", tlsCertManager.HTTPHandler(nil)) + } + // First hostname in the PTR record of the IP address which is being used by the server to connect to the internet + // This is an easter egg and will only work if the server is also reachable by the IP address from the internet + if len(myPtrAddr) > 0 { + mux.HandleFunc(myPtrAddr+"/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/html") + content, _ := base64.StdEncoding.DecodeString("PCFkb2N0eXBlIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgo8bWV0YSBjaGFyc2V0PXV0Zi04Pgo8dGl0bGU+8J+Qh/CfpZo8L3RpdGxlPgo8c3R5bGU+CmJvZHkge2JhY2tncm91bmQtY29sb3I6ICM3MDc0NzA7fQpodG1sIHtjdXJzb3I6IHVybChkYXRhOmltYWdlL3BuZztiYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQUJBQUFBQVFDQVlBQUFBZjgvOWhBQUFBQVhOU1IwSUFyczRjNlFBQUFBWmlTMGRFQVA4QS93RC9vTDJua3dBQUFBbHdTRmx6QUFBTEV3QUFDeE1CQUpxY0dBQUFBQWQwU1UxRkIrQUtIUUFKRDNNdWdCa0FBQUI1U1VSQlZEakxwVk5CRHNBZ0NDdUdRLy8vV2c0bTdLS0dtRzBxa25DQlVBb0ZzV3FPQzFNQW9ESlZiTlZRc3AydEdxZ1V6UlEyMWpKR09PMGFZd1dYZGdSQUpXYlZqaG5NSUtrUklraDZCeDNrK2c3S1c2SnJ2U1ZubThVQmVGeE9qRWZmVm9GS21abThIZExvOVBXVnF6eXNtcTllK2kvL0FQUW9aZmJneUlxQkFBQUFBRWxGVGtTdVFtQ0MpLCBhdXRvO30KPC9zdHlsZT4K") + if _, err := w.Write(content); err != nil { + fmt.Println(err) + return + } + }) + mux.Handle(myPtrAddr+"/.well-known/acme-challenge/", tlsCertManager.HTTPHandler(nil)) + } + // All other hostnames, which are being ignored + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusGatewayTimeout) + }) + + server.Handler = mux + + return server +} + +// listenAndServe will call ListenAndServe or ListenAndServeTLS on httpReverseProxy to handle requests on incoming connections +func (t *httpReverseProxy) listenAndServe() { + fmt.Printf("Listening on %s\n", t.Addr) + var err error + if t.serveTls { + err = t.ListenAndServeTLS("", "") + } else { + err = t.ListenAndServe() + } + if err != nil { + log.Fatalf("listenAndServe, Addr: %s, serveTls: %s, error: %s", t.Addr, strconv.FormatBool(t.serveTls), err) + } +} + +func main() { + if _, err := mail.ParseAddress(tlsLetsEncryptEmail); err != nil { + log.Fatalf("Invalid Let's Encrypt account email: %v\n", err) + } + + if len(myPtrAddr) > 0 { + tlsCertManager.HostPolicy = autocert.HostWhitelist(append(allowedHosts, myPtrAddr)...) + } + + fmt.Print("Allowed hosts: ") + fmt.Println(allowedHosts) + fmt.Print("PTR record (if any): ") + fmt.Println(myPtrAddr) + + httpProxy := newHttpReverseProxy(":http", false) + httpsProxy := newHttpReverseProxy(":https", true) + + go httpProxy.listenAndServe() + httpsProxy.listenAndServe() +} diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..18d77c5 --- /dev/null +++ b/server_test.go @@ -0,0 +1,85 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHttpReverseProxy(t *testing.T) { + var header map[string][]string + + teapot := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + header = r.Header.Clone() + w.WriteHeader(http.StatusTeapot) + })) + defer teapot.Close() + + targetHost = teapot.Listener.Addr().String() + targetScheme = "http" + reverseProxy := newHttpReverseProxy(":8081", false) + + fmt.Println("Proxy server listening on " + reverseProxy.Addr) + fmt.Println("Dummy target server listening on " + targetHost) + + var tests = []struct { + host string + wantXForwardedHostHeader string + wantResponseCode int + }{ + { + "teapot-dummy-target.example.com", + "teapot-dummy-target.example.com", + http.StatusTeapot, + }, + { + myPtrAddr, + "", + http.StatusOK, + }, + { + reverseProxy.Addr, + "", + http.StatusGatewayTimeout, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("requesting host %q returns status %d and header X-Forwarded-Host set to %q", + test.host, + test.wantResponseCode, + test.wantXForwardedHostHeader), + func(t *testing.T) { + request, _ := http.NewRequest("GET", "/", nil) + request.Host = test.host + response := httptest.NewRecorder() + reverseProxy.Handler.ServeHTTP(response, request) + + got := header + header = nil + + assertStatus(t, response.Code, test.wantResponseCode) + assertHeader(t, got, "X-Forwarded-Host", test.wantXForwardedHostHeader) + }) + } +} + +func assertHeader(t *testing.T, gotHeader map[string][]string, headerName, want string) { + t.Helper() + got := "" + lookup, ok := gotHeader[headerName] + if ok { + got = lookup[0] + } + if got != want { + t.Errorf("got %s, want %s", got, want) + } +} + +func assertStatus(t *testing.T, got, want int) { + t.Helper() + if got != want { + t.Errorf("got %d, want %d", got, want) + } +}