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