93 lines
3.8 KiB
Markdown
93 lines
3.8 KiB
Markdown
# lightmux-contrib
|
|
|
|
Opinionated middleware collection for [lightmux](https://git.juancwu.dev/juancwu/lightmux). Each middleware lives in its own sub-package so consumers only pull in the dependencies they actually use.
|
|
|
|
## Installation
|
|
|
|
```sh
|
|
go get git.juancwu.dev/juancwu/lightmux-contrib
|
|
```
|
|
|
|
## Packages
|
|
|
|
### `realip`
|
|
|
|
Replaces `r.RemoteAddr` with the originating client IP from `CF-Connecting-IP`, `True-Client-IP`, `X-Real-IP`, or `X-Forwarded-For` (in that order).
|
|
|
|
```go
|
|
import "git.juancwu.dev/juancwu/lightmux-contrib/realip"
|
|
|
|
mux.Use(realip.New()) // always trust headers
|
|
mux.Use(realip.New(netip.MustParsePrefix("10.0.0.0/8"))) // gated by peer CIDR
|
|
```
|
|
|
|
With no arguments, `realip.New()` always honors the proxy headers — only register it when the service sits behind a trusted proxy. With one or more `netip.Prefix` arguments, the headers are honored only when the immediate peer's IP falls within one of them.
|
|
|
|
### `requestlog`
|
|
|
|
Emits a structured `http.request` record (method, path, status, duration, client) per request via [splinter](https://git.juancwu.dev/juancwu/splinter).
|
|
|
|
```go
|
|
import "git.juancwu.dev/juancwu/lightmux-contrib/requestlog"
|
|
|
|
mux.Use(requestlog.New(nil)) // splinter.Default() resolved at request time
|
|
mux.Use(requestlog.New(custom)) // custom *splinter.Logger
|
|
```
|
|
|
|
When pairing with `realip`, register `realip` first so the `client` field is the resolved client IP rather than the proxy peer:
|
|
|
|
```go
|
|
mux.Use(realip.New(), requestlog.New(nil))
|
|
```
|
|
|
|
### `recoverer`
|
|
|
|
Catches panics inside handlers, wraps the value with [errx](https://git.juancwu.dev/juancwu/errx) under op `recoverer`, logs it with stack via the standard `log` package, and writes a 500 response.
|
|
|
|
```go
|
|
import "git.juancwu.dev/juancwu/lightmux-contrib/recoverer"
|
|
|
|
mux.Use(recoverer.New())
|
|
```
|
|
|
|
### `ipinfo`
|
|
|
|
Looks up the client IP against the [ipinfo.io](https://ipinfo.io) API via the [official Go SDK](https://github.com/ipinfo/go) and attaches the `*ipinfo.Core` result to the request context. Pair with `realip` upstream so the lookup uses the originating client IP rather than the proxy peer.
|
|
|
|
```go
|
|
import (
|
|
sdk "github.com/ipinfo/go/v2/ipinfo"
|
|
"git.juancwu.dev/juancwu/lightmux-contrib/ipinfo"
|
|
"git.juancwu.dev/juancwu/lightmux-contrib/realip"
|
|
)
|
|
|
|
client := sdk.NewClient(nil, nil, "YOUR_TOKEN")
|
|
mux.Use(realip.New(), ipinfo.New(client, nil))
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if info, ok := ipinfo.From(r.Context()); ok {
|
|
fmt.Fprintf(w, "hello from %s, %s\n", info.City, info.Country)
|
|
}
|
|
})
|
|
```
|
|
|
|
Loopback, private, link-local, and unspecified addresses are skipped to preserve API quota. Lookup failures are logged at warn level via [splinter](https://git.juancwu.dev/juancwu/splinter) (pass `nil` for `splinter.Default()` resolved at request time, or supply a custom `*splinter.Logger`) and let the request through with no context value — handlers should treat the `From` lookup as optional.
|
|
|
|
### `iplimit`
|
|
|
|
Per-IP token-bucket rate limiter backed by [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate). Each unique IP gets its own bucket; rejected requests get a 429 with a `Retry-After` header (whole seconds, ceiling).
|
|
|
|
```go
|
|
import (
|
|
"time"
|
|
|
|
"git.juancwu.dev/juancwu/lightmux-contrib/iplimit"
|
|
"git.juancwu.dev/juancwu/lightmux-contrib/realip"
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
// 5 req/s steady state, bursts of 10
|
|
mux.Use(realip.New(), iplimit.New(rate.Every(time.Second/5), 10))
|
|
```
|
|
|
|
Pair with `realip` upstream so the limiter keys on the originating client IP rather than the proxy peer. Loopback, private, link-local, and unspecified addresses pass through unlimited — they are typically internal callers (health checks, dev) that should not be rate limited. Idle buckets are evicted after 10 minutes of inactivity by an inline sweep, so no background goroutines are spawned.
|