From 35f4665a6b592adb6fb930d95939cac58c34d8cf Mon Sep 17 00:00:00 2001 From: mleku Date: Thu, 12 Sep 2024 20:45:58 +0100 Subject: [PATCH] add the lerproxy first --- .gitignore | 56 +++++ go.mod | 3 + lerproxy/LICENSE | 22 ++ lerproxy/README.md | 114 ++++++++++ lerproxy/buf/bufpool.go | 15 ++ lerproxy/buf/util.go | 19 ++ lerproxy/go.mod | 12 + lerproxy/go.sum | 23 ++ lerproxy/hsts/proxy.go | 14 ++ lerproxy/hsts/util.go | 19 ++ lerproxy/main.go | 353 ++++++++++++++++++++++++++++++ lerproxy/reverse/proxy.go | 32 +++ lerproxy/reverse/util.go | 19 ++ lerproxy/tcpkeepalive/listener.go | 37 ++++ lerproxy/tcpkeepalive/util.go | 19 ++ lerproxy/timeout/conn.go | 30 +++ lerproxy/timeout/util.go | 19 ++ lerproxy/util.go | 19 ++ lerproxy/util/util.go | 23 ++ 19 files changed, 848 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100755 lerproxy/LICENSE create mode 100755 lerproxy/README.md create mode 100755 lerproxy/buf/bufpool.go create mode 100755 lerproxy/buf/util.go create mode 100755 lerproxy/go.mod create mode 100755 lerproxy/go.sum create mode 100755 lerproxy/hsts/proxy.go create mode 100755 lerproxy/hsts/util.go create mode 100755 lerproxy/main.go create mode 100755 lerproxy/reverse/proxy.go create mode 100755 lerproxy/reverse/util.go create mode 100755 lerproxy/tcpkeepalive/listener.go create mode 100755 lerproxy/tcpkeepalive/util.go create mode 100755 lerproxy/timeout/conn.go create mode 100755 lerproxy/timeout/util.go create mode 100755 lerproxy/util.go create mode 100755 lerproxy/util/util.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1046e5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# Allowlisting gitignore template for GO projects prevents us +# from adding various unwanted local files, such as generated +# files, developer configurations or IDE-specific files etc. +# +# Recommended: Go.AllowList.gitignore + +# Ignore everything +* +# But not these files... +!/.gitignore +!*.go +!go.sum +!go.mod +!*.md +!LICENSE +!*.sh +!Makefile +!*.json +!*.pdf +!*.csv +!*.py +!*.mediawiki +!*.did +!*.rs +!*.toml +!*.file +!.gitkeep +!pkg/eth/** +!*.h +!*.c +!*.proto +!bundleData +!*.item +!*.bin +# ...even if they are in subdirectories +!*/ +.vscode +.vscode/ +.vscode/** +**/.vscode +**/.vscode/** +.idea +.idea/ +.idea/** +**/.idea +**/.idea/ +**/.idea/** +node_modules +node_modules/ +node_modules/** +**/node_modules +**/node_modules/ +**/node_modules/** +pkg/uploader/keyfile.json +/pkg/uploader/keyfile.json +**/keyfile.json diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..af643f6 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/mleku/magnumopus + +go 1.22.7 diff --git a/lerproxy/LICENSE b/lerproxy/LICENSE new file mode 100755 index 0000000..3b0fd64 --- /dev/null +++ b/lerproxy/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2016 Artyom Pervukhin +Copyright (c) 2024 mleku npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lerproxy/README.md b/lerproxy/README.md new file mode 100755 index 0000000..db66440 --- /dev/null +++ b/lerproxy/README.md @@ -0,0 +1,114 @@ +# lerproxy + +Command lerproxy implements https reverse proxy with automatic LetsEncrypt and your own own TLS +certificates for multiple hostnames/backends including a static filesystem directory, nostr +DNS verification [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md) hosting. + +## Install + + go install lerproxy.mleku.dev@latest + +## Run + +``` +Usage: lerproxy.mleku.dev [--listen LISTEN] [--map MAP] [--rewrites REWRITES] [--cachedir CACHEDIR] [--hsts] [--email EMAIL] [--http HTTP] [--rto RTO] [--wto WTO] [--idle IDLE] [--cert CERT] + +Options: + --listen LISTEN, -l LISTEN + address to listen at [default: :https] + --map MAP, -m MAP file with host/backend mapping [default: mapping.txt] + --rewrites REWRITES, -r REWRITES [default: rewrites.txt] + --cachedir CACHEDIR, -c CACHEDIR + path to directory to cache key and certificates [default: /var/cache/letsencrypt] + --hsts, -h add Strict-Transport-Security header + --email EMAIL, -e EMAIL + contact email address presented to letsencrypt CA + --http HTTP optional address to serve http-to-https redirects and ACME http-01 challenge responses [default: :http] + --rto RTO, -r RTO maximum duration before timing out read of the request [default: 1m] + --wto WTO, -w WTO maximum duration before timing out write of the response [default: 5m] + --idle IDLE, -i IDLE how long idle connection is kept before closing (set rto, wto to 0 to use this) + --cert CERT certificates and the domain they match: eg: mleku.dev:/path/to/cert - this will indicate to load two, one with extension .key and one with .crt, each expected to be PEM encoded TLS private and public keys, respectively + --help, -h display this help and exit +``` + +`mapping.txt` contains host-to-backend mapping, where backend can be specified +as: + +* http/https url for http(s) connections to backend *without* passing "Host" + header from request; +* host:port for http over TCP connections to backend; +* absolute path for http over unix socket connections; +* @name for http over abstract unix socket connections (linux only); +* absolute path with a trailing slash to serve files from a given directory; +* path to a nostr.json file containing a + [nip-05](https://github.com/nostr-protocol/nips/blob/master/05.md) and + hosting it at `https://example.com/.well-known/nostr.json` +* using the prefix `git+` and a full web address path after it, generate html + with the necessary meta tags that indicate to the `go` tool when fetching + dependencies from the address found after the `+`. +* in the launch parameters for `lerproxy` you can now add any number of `--cert` parameters with + the domain (including for wildcards), and the path to the `.crt`/`.key` files: + + lerproxy.mleku.dev --cert :/path/to/TLS_cert + + this will then, if found, load and parse the TLS certificate and secret key if the suffix of + the domain matches. The certificate path is expanded to two files with the above filename + extensions and become active in place of the LetsEncrypt certificates + + > Note that the match is greedy, so you can explicitly separately give a subdomain + certificate and it will be selected even if there is a wildcard that also matches. + +## example mapping.txt + + nostr.example.com: /path/to/nostr.json + subdomain1.example.com: 127.0.0.1:8080 + subdomain2.example.com: /var/run/http.socket + subdomain3.example.com: @abstractUnixSocket + uploads.example.com: https://uploads-bucket.s3.amazonaws.com + # this is a comment, it can only start on a new line + static.example.com: /var/www/ + awesome-go-project.example.com: git+https://github.com/crappy-name/crappy-go-project-name + +Note that when `@name` backend is specified, connection to abstract unix socket +is made in a manner compatible with some other implementations like uWSGI, that +calculate addrlen including trailing zero byte despite [documentation not +requiring that](http://man7.org/linux/man-pages/man7/unix.7.html). It won't +work with other implementations that calculate addrlen differently (i.e. by +taking into account only `strlen(addr)` like Go, or even `UNIX_PATH_MAX`). + +## systemd service file + +``` +[Unit] +Description=lerproxy + +[Service] +Type=simple +User=username +ExecStart=/usr/local/bin/lerproxy.mleku.dev -m /path/to/mapping.txt -l xxx.xxx.xxx.xxx:443 --http xxx.xxx.xxx.6:80 -m /path/to/mapping.txt -e email@example.com -c /path/to/letsencrypt/cache --cert example.com:/path/to/tls/certs +Restart=on-failure +Wants=network-online.target +After=network.target network-online.target wg-quick@wg0.service + +[Install] +WantedBy=multi-user.target +``` + +If your VPS has wireguard running and you want to be able to host services from the other end of +a tunnel, such as your dev machine (something I do for nostr relay development) add the +`wg-quick@wg0` or whatever wg-quick configuration you are using to ensure when it boots, +`lerproxy` does not run until the tunnel is active. + +## privileged port binding + +The simplest way to allow `lerproxy` to bind to port 80 and 443 is as follows: + + setcap 'cap_net_bind_service=+ep' /path/to/lerproxy.mleku.dev + +## todo + +- add url rewriting such as flipping addresses such as a gitea instance + `example.com/gituser/reponame` to `reponame.example.com` by funneling all + `example.com/gituser` into be rewritten to be the only accessible user account on the gitea + instance. or for other things like a dynamic subscription based hosting service subdomain + instead of path \ No newline at end of file diff --git a/lerproxy/buf/bufpool.go b/lerproxy/buf/bufpool.go new file mode 100755 index 0000000..81a0dae --- /dev/null +++ b/lerproxy/buf/bufpool.go @@ -0,0 +1,15 @@ +package buf + +import "sync" + +var bufferPool = &sync.Pool{ + New: func() interface{} { + buf := make([]byte, 32*1024) + return &buf + }, +} + +type Pool struct{} + +func (bp Pool) Get() []byte { return *(bufferPool.Get().(*[]byte)) } +func (bp Pool) Put(b []byte) { bufferPool.Put(&b) } diff --git a/lerproxy/buf/util.go b/lerproxy/buf/util.go new file mode 100755 index 0000000..83f75e8 --- /dev/null +++ b/lerproxy/buf/util.go @@ -0,0 +1,19 @@ +package buf + +import ( + "bytes" + "os" + + "ec.mleku.dev/v2/lol" +) + +type ( + B = []byte + S = string + E = error +) + +var ( + log, chk, errorf = lol.New(os.Stderr) + equals = bytes.Equal +) diff --git a/lerproxy/go.mod b/lerproxy/go.mod new file mode 100755 index 0000000..7f344c4 --- /dev/null +++ b/lerproxy/go.mod @@ -0,0 +1,12 @@ +module lerproxy.mleku.dev + +go 1.22.5 + +toolchain go1.22.7 + +require ( + ec.mleku.dev/v2 v2.3.5 + github.com/alexflint/go-arg v1.5.1 + golang.org/x/crypto v0.27.0 + golang.org/x/sync v0.8.0 +) diff --git a/lerproxy/go.sum b/lerproxy/go.sum new file mode 100755 index 0000000..1849857 --- /dev/null +++ b/lerproxy/go.sum @@ -0,0 +1,23 @@ +ec.mleku.dev/v2 v2.3.5 h1:95cTJn4EemxzvdejpdZSse6w79pYx1Ty3nL2zDLKqQU= +ec.mleku.dev/v2 v2.3.5/go.mod h1:4hK39Si4F2Aav4H4jBtzSUR7xlFxeuS4pPK3t0Ol8VQ= +github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= +github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lerproxy/hsts/proxy.go b/lerproxy/hsts/proxy.go new file mode 100755 index 0000000..133788a --- /dev/null +++ b/lerproxy/hsts/proxy.go @@ -0,0 +1,14 @@ +package hsts + +import "net/http" + +type Proxy struct { + http.Handler +} + +func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header(). + Set("Strict-Transport-Security", + "max-age=31536000; includeSubDomains; preload") + p.ServeHTTP(w, r) +} diff --git a/lerproxy/hsts/util.go b/lerproxy/hsts/util.go new file mode 100755 index 0000000..04e9a8d --- /dev/null +++ b/lerproxy/hsts/util.go @@ -0,0 +1,19 @@ +package hsts + +import ( + "bytes" + "os" + + "ec.mleku.dev/v2/lol" +) + +type ( + B = []byte + S = string + E = error +) + +var ( + log, chk, errorf = lol.New(os.Stderr) + equals = bytes.Equal +) diff --git a/lerproxy/main.go b/lerproxy/main.go new file mode 100755 index 0000000..4bdf67b --- /dev/null +++ b/lerproxy/main.go @@ -0,0 +1,353 @@ +// Command lerproxy implements https reverse proxy with automatic LetsEncrypt +// usage for multiple hostnames/backends, and URL rewriting capability. +package main + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + stdLog "log" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/signal" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "github.com/alexflint/go-arg" + "golang.org/x/crypto/acme/autocert" + "golang.org/x/sync/errgroup" + "lerproxy.mleku.dev/buf" + "lerproxy.mleku.dev/hsts" + "lerproxy.mleku.dev/reverse" + "lerproxy.mleku.dev/tcpkeepalive" + "lerproxy.mleku.dev/util" +) + +type runArgs struct { + Addr string `arg:"-l,--listen" default:":https" help:"address to listen at"` + Conf string `arg:"-m,--map" default:"mapping.txt" help:"file with host/backend mapping"` + // Rewrites string `arg:"-r,--rewrites" default:"rewrites.txt"` + Cache string `arg:"-c,--cachedir" default:"/var/cache/letsencrypt" help:"path to directory to cache key and certificates"` + HSTS bool `arg:"-h,--hsts" help:"add Strict-Transport-Security header"` + Email string `arg:"-e,--email" help:"contact email address presented to letsencrypt CA"` + HTTP string `arg:"--http" default:":http" help:"optional address to serve http-to-https redirects and ACME http-01 challenge responses"` + RTO time.Duration `arg:"-r,--rto" default:"1m" help:"maximum duration before timing out read of the request"` + WTO time.Duration `arg:"-w,--wto" default:"5m" help:"maximum duration before timing out write of the response"` + Idle time.Duration `arg:"-i,--idle" help:"how long idle connection is kept before closing (set rto, wto to 0 to use this)"` + Certs []string `arg:"--cert,separate" help:"certificates and the domain they match: eg: mleku.dev:/path/to/cert - this will indicate to load two, one with extension .key and one with .crt, each expected to be PEM encoded TLS private and public keys, respectively"` +} + +var args runArgs + +func main() { + arg.MustParse(&args) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + if err := run(ctx, args); err != nil { + log.F.Ln(err) + } +} + +func run(ctx context.Context, args runArgs) (err error) { + + if args.Cache == "" { + err = log.E.Err("no cache specified") + return + } + + var srv *http.Server + var httpHandler http.Handler + if srv, httpHandler, err = setupServer(args); chk.E(err) { + return + } + srv.ReadHeaderTimeout = 5 * time.Second + if args.RTO > 0 { + srv.ReadTimeout = args.RTO + } + if args.WTO > 0 { + srv.WriteTimeout = args.WTO + } + group, ctx := errgroup.WithContext(ctx) + if args.HTTP != "" { + httpServer := http.Server{ + Addr: args.HTTP, + Handler: httpHandler, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + group.Go(func() (err error) { + chk.E(httpServer.ListenAndServe()) + return + }) + group.Go(func() error { + <-ctx.Done() + ctx, cancel := context.WithTimeout(context.Background(), + time.Second) + defer cancel() + return httpServer.Shutdown(ctx) + }) + } + if srv.ReadTimeout != 0 || srv.WriteTimeout != 0 || args.Idle == 0 { + group.Go(func() (err error) { + chk.E(srv.ListenAndServeTLS("", "")) + return + }) + } else { + group.Go(func() (err error) { + var ln net.Listener + if ln, err = net.Listen("tcp", srv.Addr); chk.E(err) { + return + } + defer ln.Close() + ln = tcpkeepalive.Listener{ + Duration: args.Idle, + TCPListener: ln.(*net.TCPListener), + } + err = srv.ServeTLS(ln, "", "") + chk.E(err) + return + }) + } + group.Go(func() error { + <-ctx.Done() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + return srv.Shutdown(ctx) + }) + return group.Wait() +} + +// TLSConfig returns a TLSConfig that works with a LetsEncrypt automatic SSL cert issuer as well +// as any provided .pem certificates from providers. +// +// The certs are provided in the form "example.com:/path/to/cert.pem" +func TLSConfig(m *autocert.Manager, certs ...string) (tc *tls.Config) { + certMap := make(map[S]*tls.Certificate) + var mx sync.Mutex + for _, cert := range certs { + split := strings.Split(cert, ":") + if len(split) != 2 { + log.E.F("invalid certificate parameter format: `%s`", cert) + continue + } + var err E + var c tls.Certificate + if c, err = tls.LoadX509KeyPair(split[1]+".crt", split[1]+".key"); chk.E(err) { + continue + } + certMap[split[0]] = &c + } + tc = m.TLSConfig() + tc.GetCertificate = func(helo *tls.ClientHelloInfo) (cert *tls.Certificate, err E) { + mx.Lock() + var own S + for i := range certMap { + // to also handle explicit subdomain certs, prioritize over a root wildcard. + if helo.ServerName == i { + own = i + break + } + // if it got to us and ends in the same name dot tld assume the subdomain was + // redirected or it's a wildcard certificate, thus only the ending needs to match. + if strings.HasSuffix(helo.ServerName, i) { + own = i + break + } + } + if own != "" { + defer mx.Unlock() + return certMap[own], nil + } + mx.Unlock() + return m.GetCertificate(helo) + } + return +} + +func setupServer(a runArgs) (s *http.Server, h http.Handler, err error) { + var mapping map[string]string + if mapping, err = readMapping(a.Conf); chk.E(err) { + return + } + var proxy http.Handler + if proxy, err = setProxy(mapping); chk.E(err) { + return + } + if a.HSTS { + proxy = &hsts.Proxy{Handler: proxy} + } + if err = os.MkdirAll(a.Cache, 0700); chk.E(err) { + err = fmt.Errorf("cannot create cache directory %q: %v", + a.Cache, err) + chk.E(err) + return + } + m := autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(a.Cache), + HostPolicy: autocert.HostWhitelist(util.GetKeys(mapping)...), + Email: a.Email, + } + s = &http.Server{ + Handler: proxy, + Addr: a.Addr, + TLSConfig: TLSConfig(&m, a.Certs...), + } + h = m.HTTPHandler(nil) + return +} + +type NostrJSON struct { + Names map[string]string `json:"names"` + Relays map[string][]string `json:"relays"` +} + +func setProxy(mapping map[string]string) (h http.Handler, err error) { + if len(mapping) == 0 { + return nil, fmt.Errorf("empty mapping") + } + mux := http.NewServeMux() + for hostname, backendAddr := range mapping { + hn, ba := hostname, backendAddr + if strings.ContainsRune(hn, os.PathSeparator) { + err = log.E.Err("invalid hostname: %q", hn) + return + } + network := "tcp" + if ba != "" && ba[0] == '@' && runtime.GOOS == "linux" { + // append \0 to address so addrlen for connect(2) is calculated in a + // way compatible with some other implementations (i.e. uwsgi) + network, ba = "unix", ba+string(byte(0)) + } else if strings.HasPrefix(ba, "git+") { + split := strings.Split(ba, "git+") + if len(split) != 2 { + log.E.Ln("invalid go vanity redirect: %s: %s", hn, ba) + continue + } + redirector := fmt.Sprintf( + `redirecting to %s`, + hn, split[1], split[1], split[1], split[1]) + mux.HandleFunc(hn+"/", func(writer http.ResponseWriter, request *http.Request) { + writer.Header().Set("Access-Control-Allow-Methods", + "GET,HEAD,PUT,PATCH,POST,DELETE") + writer.Header().Set("Access-Control-Allow-Origin", "*") + writer.Header().Set("Content-Type", "text/html") + writer.Header().Set("Content-Length", fmt.Sprint(len(redirector))) + writer.Header().Set("strict-transport-security", "max-age=0; includeSubDomains") + fmt.Fprint(writer, redirector) + }) + continue + } else if filepath.IsAbs(ba) { + network = "unix" + switch { + case strings.HasSuffix(ba, string(os.PathSeparator)): + // path specified as directory with explicit trailing slash; add + // this path as static site + fs := http.FileServer(http.Dir(ba)) + mux.Handle(hn+"/", fs) + continue + case strings.HasSuffix(ba, "nostr.json"): + log.I.Ln(hn, ba) + var fb []byte + if fb, err = os.ReadFile(ba); chk.E(err) { + continue + } + var v NostrJSON + if err = json.Unmarshal(fb, &v); chk.E(err) { + continue + } + var jb []byte + if jb, err = json.Marshal(v); chk.E(err) { + continue + } + nostrJSON := string(jb) + mux.HandleFunc(hn+"/.well-known/nostr.json", + func(writer http.ResponseWriter, request *http.Request) { + log.I.Ln("serving nostr json to", hn) + writer.Header().Set("Access-Control-Allow-Methods", + "GET,HEAD,PUT,PATCH,POST,DELETE") + writer.Header().Set("Access-Control-Allow-Origin", "*") + writer.Header().Set("Content-Type", "application/json") + writer.Header().Set("Content-Length", fmt.Sprint(len(nostrJSON))) + writer.Header().Set("strict-transport-security", + "max-age=0; includeSubDomains") + fmt.Fprint(writer, nostrJSON) + }) + continue + } + } else if u, err := url.Parse(ba); err == nil { + switch u.Scheme { + case "http", "https": + rp := reverse.NewSingleHostReverseProxy(u) + modifyCORSResponse := func(res *http.Response) error { + res.Header.Set("Access-Control-Allow-Methods", + "GET,HEAD,PUT,PATCH,POST,DELETE") + // res.Header.Set("Access-Control-Allow-Credentials", "true") + res.Header.Set("Access-Control-Allow-Origin", "*") + return nil + } + rp.ModifyResponse = modifyCORSResponse + rp.ErrorLog = stdLog.New(os.Stderr, "lerproxy", stdLog.Llongfile) + rp.BufferPool = buf.Pool{} + mux.Handle(hn+"/", rp) + continue + } + } + rp := &httputil.ReverseProxy{ + Director: func(req *http.Request) { + req.URL.Scheme = "http" + req.URL.Host = req.Host + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-For", req.RemoteAddr) + req.Header.Set("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE") + // req.Header.Set("Access-Control-Allow-Credentials", "true") + req.Header.Set("Access-Control-Allow-Origin", "*") + log.D.Ln(req.URL, req.RemoteAddr) + }, + Transport: &http.Transport{ + DialContext: func(ctx context.Context, n, addr string) (net.Conn, error) { + return net.DialTimeout(network, ba, 5*time.Second) + }, + }, + ErrorLog: stdLog.New(io.Discard, "", 0), + BufferPool: buf.Pool{}, + } + mux.Handle(hn+"/", rp) + } + return mux, nil +} + +func readMapping(file string) (m map[string]string, err error) { + var f *os.File + if f, err = os.Open(file); chk.E(err) { + return + } + m = make(map[string]string) + sc := bufio.NewScanner(f) + for sc.Scan() { + if b := sc.Bytes(); len(b) == 0 || b[0] == '#' { + continue + } + s := strings.SplitN(sc.Text(), ":", 2) + if len(s) != 2 { + err = fmt.Errorf("invalid line: %q", sc.Text()) + log.E.Ln(err) + chk.E(f.Close()) + return + } + m[strings.TrimSpace(s[0])] = strings.TrimSpace(s[1]) + } + err = sc.Err() + chk.E(err) + chk.E(f.Close()) + return +} diff --git a/lerproxy/reverse/proxy.go b/lerproxy/reverse/proxy.go new file mode 100755 index 0000000..1d022b0 --- /dev/null +++ b/lerproxy/reverse/proxy.go @@ -0,0 +1,32 @@ +package reverse + +import ( + "net/http" + "net/http/httputil" + "net/url" + + "lerproxy.mleku.dev/util" +) + +// NewSingleHostReverseProxy is a copy of httputil.NewSingleHostReverseProxy +// with addition of "X-Forwarded-Proto" header. +func NewSingleHostReverseProxy(target *url.URL) (rp *httputil.ReverseProxy) { + targetQuery := target.RawQuery + director := func(req *http.Request) { + log.D.S(req) + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.URL.Path = util.SingleJoiningSlash(target.Path, req.URL.Path) + if targetQuery == "" || req.URL.RawQuery == "" { + req.URL.RawQuery = targetQuery + req.URL.RawQuery + } else { + req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery + } + if _, ok := req.Header["User-Agent"]; !ok { + req.Header.Set("User-Agent", "") + } + req.Header.Set("X-Forwarded-Proto", "https") + } + rp = &httputil.ReverseProxy{Director: director} + return +} diff --git a/lerproxy/reverse/util.go b/lerproxy/reverse/util.go new file mode 100755 index 0000000..817db99 --- /dev/null +++ b/lerproxy/reverse/util.go @@ -0,0 +1,19 @@ +package reverse + +import ( + "bytes" + "os" + + "ec.mleku.dev/v2/lol" +) + +type ( + B = []byte + S = string + E = error +) + +var ( + log, chk, errorf = lol.New(os.Stderr) + equals = bytes.Equal +) diff --git a/lerproxy/tcpkeepalive/listener.go b/lerproxy/tcpkeepalive/listener.go new file mode 100755 index 0000000..4c39f17 --- /dev/null +++ b/lerproxy/tcpkeepalive/listener.go @@ -0,0 +1,37 @@ +package tcpkeepalive + +import ( + "net" + "time" + + "lerproxy.mleku.dev/timeout" +) + +// Period can be changed prior to opening a Listener to alter its' +// KeepAlivePeriod. +var Period = 3 * time.Minute + +// Listener sets TCP keep-alive timeouts on accepted connections. +// It's used by ListenAndServe and ListenAndServeTLS so dead TCP connections +// (e.g. closing laptop mid-download) eventually go away. +type Listener struct { + time.Duration + *net.TCPListener +} + +func (ln Listener) Accept() (conn net.Conn, e error) { + var tc *net.TCPConn + if tc, e = ln.AcceptTCP(); chk.E(e) { + return + } + if e = tc.SetKeepAlive(true); chk.E(e) { + return + } + if e = tc.SetKeepAlivePeriod(Period); chk.E(e) { + return + } + if ln.Duration != 0 { + return timeout.Conn{Duration: ln.Duration, TCPConn: tc}, nil + } + return tc, nil +} diff --git a/lerproxy/tcpkeepalive/util.go b/lerproxy/tcpkeepalive/util.go new file mode 100755 index 0000000..5768ee3 --- /dev/null +++ b/lerproxy/tcpkeepalive/util.go @@ -0,0 +1,19 @@ +package tcpkeepalive + +import ( + "bytes" + "os" + + "ec.mleku.dev/v2/lol" +) + +type ( + B = []byte + S = string + E = error +) + +var ( + log, chk, errorf = lol.New(os.Stderr) + equals = bytes.Equal +) diff --git a/lerproxy/timeout/conn.go b/lerproxy/timeout/conn.go new file mode 100755 index 0000000..3e08418 --- /dev/null +++ b/lerproxy/timeout/conn.go @@ -0,0 +1,30 @@ +package timeout + +import ( + "net" + "time" +) + +// Conn extends deadline after successful read or write operations +type Conn struct { + time.Duration + *net.TCPConn +} + +func (c Conn) Read(b []byte) (n int, e error) { + if n, e = c.TCPConn.Read(b); !chk.E(e) { + if e = c.SetDeadline(c.getTimeout()); chk.E(e) { + } + } + return +} + +func (c Conn) Write(b []byte) (n int, e error) { + if n, e = c.TCPConn.Write(b); !chk.E(e) { + if e = c.SetDeadline(c.getTimeout()); chk.E(e) { + } + } + return +} + +func (c Conn) getTimeout() (t time.Time) { return time.Now().Add(c.Duration) } diff --git a/lerproxy/timeout/util.go b/lerproxy/timeout/util.go new file mode 100755 index 0000000..31828de --- /dev/null +++ b/lerproxy/timeout/util.go @@ -0,0 +1,19 @@ +package timeout + +import ( + "bytes" + "os" + + "ec.mleku.dev/v2/lol" +) + +type ( + B = []byte + S = string + E = error +) + +var ( + log, chk, errorf = lol.New(os.Stderr) + equals = bytes.Equal +) diff --git a/lerproxy/util.go b/lerproxy/util.go new file mode 100755 index 0000000..67eaed3 --- /dev/null +++ b/lerproxy/util.go @@ -0,0 +1,19 @@ +package main + +import ( + "bytes" + "os" + + "ec.mleku.dev/v2/lol" +) + +type ( + B = []byte + S = string + E = error +) + +var ( + log, chk, errorf = lol.New(os.Stderr) + equals = bytes.Equal +) diff --git a/lerproxy/util/util.go b/lerproxy/util/util.go new file mode 100755 index 0000000..04a47de --- /dev/null +++ b/lerproxy/util/util.go @@ -0,0 +1,23 @@ +package util + +import "strings" + +func GetKeys(m map[string]string) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} + +func SingleJoiningSlash(a, b string) string { + suffixSlash := strings.HasSuffix(a, "/") + prefixSlash := strings.HasPrefix(b, "/") + switch { + case suffixSlash && prefixSlash: + return a + b[1:] + case !suffixSlash && !prefixSlash: + return a + "/" + b + } + return a + b +}