Add code
This commit is contained in:
parent
e13f14ab49
commit
566e1faaa2
15
Makefile
Normal file
15
Makefile
Normal 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
12
go.mod
Normal 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
8
go.sum
Normal 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
156
server.go
Normal 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
85
server_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user