add the lerproxy first

This commit is contained in:
2024-09-12 20:45:58 +01:00
parent d97b60dc02
commit 35f4665a6b
19 changed files with 848 additions and 0 deletions

56
.gitignore vendored Normal file
View File

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

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/mleku/magnumopus
go 1.22.7

22
lerproxy/LICENSE Executable file
View File

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

114
lerproxy/README.md Executable file
View File

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

15
lerproxy/buf/bufpool.go Executable file
View File

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

19
lerproxy/buf/util.go Executable file
View File

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

12
lerproxy/go.mod Executable file
View File

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

23
lerproxy/go.sum Executable file
View File

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

14
lerproxy/hsts/proxy.go Executable file
View File

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

19
lerproxy/hsts/util.go Executable file
View File

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

353
lerproxy/main.go Executable file
View File

@@ -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(
`<html><head><meta name="go-import" content="%s git %s"/><meta http-equiv = "refresh" content = " 3 ; url = %s"/></head><body>redirecting to <a href="%s">%s</a></body></html>`,
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
}

32
lerproxy/reverse/proxy.go Executable file
View File

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

19
lerproxy/reverse/util.go Executable file
View File

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

View File

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

19
lerproxy/tcpkeepalive/util.go Executable file
View File

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

30
lerproxy/timeout/conn.go Executable file
View File

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

19
lerproxy/timeout/util.go Executable file
View File

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

19
lerproxy/util.go Executable file
View File

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

23
lerproxy/util/util.go Executable file
View File

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