This commit is contained in:
marco 2023-01-13 14:59:13 +01:00
parent e13f14ab49
commit 566e1faaa2
5 changed files with 276 additions and 0 deletions

15
Makefile Normal file
View File

@ -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/

12
go.mod Normal file
View File

@ -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
)

8
go.sum Normal file
View File

@ -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=

156
server.go Normal file
View File

@ -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()
}

85
server_test.go Normal file
View File

@ -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)
}
}