Add code
This commit is contained in:
parent
e13f14ab49
commit
566e1faaa2
|
@ -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/
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue