add the lerproxy first
This commit is contained in:
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal 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
|
||||||
22
lerproxy/LICENSE
Executable file
22
lerproxy/LICENSE
Executable 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
114
lerproxy/README.md
Executable 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
15
lerproxy/buf/bufpool.go
Executable 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
19
lerproxy/buf/util.go
Executable 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
12
lerproxy/go.mod
Executable 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
23
lerproxy/go.sum
Executable 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
14
lerproxy/hsts/proxy.go
Executable 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
19
lerproxy/hsts/util.go
Executable 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
353
lerproxy/main.go
Executable 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
32
lerproxy/reverse/proxy.go
Executable 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
19
lerproxy/reverse/util.go
Executable 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
|
||||||
|
)
|
||||||
37
lerproxy/tcpkeepalive/listener.go
Executable file
37
lerproxy/tcpkeepalive/listener.go
Executable 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
19
lerproxy/tcpkeepalive/util.go
Executable 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
30
lerproxy/timeout/conn.go
Executable 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
19
lerproxy/timeout/util.go
Executable 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
19
lerproxy/util.go
Executable 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
23
lerproxy/util/util.go
Executable 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user