Change address syntax to be a URL #3

Merged
balki merged 7 commits from addr-url into main 2024-12-19 10:06:03 -05:00
3 changed files with 307 additions and 78 deletions

View File

@ -17,56 +17,57 @@ Just replace `http.ListenAndServe` with `anyhttp.ListenAndServe`.
Syntax Syntax
unix/<path to socket> unix?path=<socket_path>&mode=<socket file mode>&remove_existing=<yes|no>
Examples Examples
unix/relative/path.sock unix?path=relative/path.sock
unix//var/run/app/absolutepath.sock unix?path=/var/run/app/absolutepath.sock
unix?path=/run/app.sock&mode=600&remove_existing=no
| option | description | default |
|-----------------|------------------------------------------------|----------|
| path | path to unix socket | Required |
| mode | socket file mode | 666 |
| remove_existing | Whether to remove existing socket file or fail | true |
### Systemd Socket activated fd: ### Systemd Socket activated fd:
Syntax Syntax
sysd/fdidx/<fd index starting at 0> sysd?idx=<fd index>&name=<fd name>&check_pid=<yes|no>&unset_env=<yes|no>&idle_timeout=<duration>
sysd/fdname/<fd name set using FileDescriptorName socket setting >
Only one of `idx` or `name` has to be set
Examples: Examples:
# First (or only) socket fd passed to app # First (or only) socket fd passed to app
sysd/fdidx/0 sysd?idx=0
# Socket with FileDescriptorName # Socket with FileDescriptorName
sysd/fdname/myapp sysd?name=myapp
# Using default name # Using default name and auto shutdown if no requests received in last 30 minutes
sysd/fdname/myapp.socket sysd?name=myapp.socket&idle_timeout=30m
### TCP port | option | description | default |
|--------------|--------------------------------------------------------------------------------------------|------------------|
| name | Name configured via FileDescriptorName or socket file name | Required |
| idx | FD Index. Actual fd num will be 3 + idx | Required |
| idle_timeout | time to wait before shutdown. [syntax][0] | no auto shutdown |
| check_pid | Check process PID matches LISTEN_PID | true |
| unset_env | Unsets the LISTEN\* environment variables, so they don't get passed to any child processes | true |
If the address is a number less than 65536, it is assumed as a port and passed ### TCP
as `http.ListenAndServe(":<port>",...)` Anything else is directly passed to
`http.ListenAndServe` as well. Below examples should work If the address is not one of above, it is assumed to be tcp and passed to `http.ListenAndServe`.
Examples:
:http :http
:8888 :8888
127.0.0.1:8080 127.0.0.1:8080
## Idle server auto shutdown
When using systemd socket activation, idle servers can be shut down to save on
resources. They will be restarted with socket activation when new request
arrives. Quick example for the case. (Error checking skipped for brevity)
```go
addrType, httpServer, done, _ := anyhttp.Serve(addr, idle.WrapHandler(nil))
if addrType == anyhttp.SystemdFD {
idle.Wait(30 * time.Minute)
httpServer.Shutdown(context.TODO())
}
<-done
```
## Documentation ## Documentation
https://pkg.go.dev/go.balki.me/anyhttp https://pkg.go.dev/go.balki.me/anyhttp
@ -75,3 +76,5 @@ https://pkg.go.dev/go.balki.me/anyhttp
* https://gist.github.com/teknoraver/5ffacb8757330715bcbcc90e6d46ac74#file-unixhttpd-go * https://gist.github.com/teknoraver/5ffacb8757330715bcbcc90e6d46ac74#file-unixhttpd-go
* https://github.com/coreos/go-systemd/tree/main/activation * https://github.com/coreos/go-systemd/tree/main/activation
[0]: https://pkg.go.dev/time#ParseDuration

View File

@ -2,16 +2,21 @@
package anyhttp package anyhttp
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"time"
"go.balki.me/anyhttp/idle"
) )
// AddressType of the address passed // AddressType of the address passed
@ -97,6 +102,8 @@ type SysdConfig struct {
CheckPID bool CheckPID bool
// Unsets the LISTEN* environment variables, so they don't get passed to any child processes // Unsets the LISTEN* environment variables, so they don't get passed to any child processes
UnsetEnv bool UnsetEnv bool
// Shutdown http server if no requests received for below timeout
IdleTimeout *time.Duration
} }
// DefaultSysdConfig has the default values for SysdConfig // DefaultSysdConfig has the default values for SysdConfig
@ -196,65 +203,57 @@ func (s *SysdConfig) GetListener() (net.Listener, error) {
return nil, errors.New("neither FDIndex nor FDName set") return nil, errors.New("neither FDIndex nor FDName set")
} }
// GetListener gets a unix or systemd socket listener // Serve creates and serve a http server.
func GetListener(addr string) (AddressType, net.Listener, error) { func Serve(addr string, h http.Handler) (addrType AddressType, srv *http.Server, idler idle.Idler, done <-chan error, err error) {
if strings.HasPrefix(addr, "unix/") { addrType, usc, sysc, err := parseAddress(addr)
usc := NewUnixSocketConfig(strings.TrimPrefix(addr, "unix/"))
l, err := usc.GetListener()
return UnixSocket, l, err
}
if strings.HasPrefix(addr, "sysd/fdidx/") {
idx, err := strconv.Atoi(strings.TrimPrefix(addr, "sysd/fdidx/"))
if err != nil { if err != nil {
return Unknown, nil, fmt.Errorf("invalid fdidx, addr:%q err: %w", addr, err) return
}
sysdc := NewSysDConfigWithFDIdx(idx)
l, err := sysdc.GetListener()
return SystemdFD, l, err
} }
if strings.HasPrefix(addr, "sysd/fdname/") { listener, err := func() (net.Listener, error) {
sysdc := NewSysDConfigWithFDName(strings.TrimPrefix(addr, "sysd/fdname/")) if usc != nil {
l, err := sysdc.GetListener() return usc.GetListener()
return SystemdFD, l, err } else if sysc != nil {
return sysc.GetListener()
} }
if port, err := strconv.Atoi(addr); err == nil {
if port > 0 && port < 65536 {
addr = fmt.Sprintf(":%v", port)
} else {
return Unknown, nil, fmt.Errorf("invalid port: %v", port)
}
}
if addr == "" { if addr == "" {
addr = ":http" addr = ":http"
} }
return net.Listen("tcp", addr)
l, err := net.Listen("tcp", addr)
return TCP, l, err
}
// Serve creates and serve a http server.
func Serve(addr string, h http.Handler) (AddressType, *http.Server, <-chan error, error) {
addrType, listener, err := GetListener(addr)
if err != nil {
return addrType, nil, nil, err
}
srv := &http.Server{Handler: h}
done := make(chan error)
go func() {
done <- srv.Serve(listener)
close(done)
}() }()
return addrType, srv, done, nil if err != nil {
return
}
errChan := make(chan error)
done = errChan
if addrType == SystemdFD && sysc.IdleTimeout != nil {
idler = idle.CreateIdler(*sysc.IdleTimeout)
srv = &http.Server{Handler: idle.WrapIdlerHandler(idler, h)}
waitErrChan := make(chan error)
go func() {
waitErrChan <- srv.Serve(listener)
}()
go func() {
select {
case err := <-waitErrChan:
errChan <- err
case <-idler.Chan():
errChan <- srv.Shutdown(context.TODO())
}
}()
} else {
srv = &http.Server{Handler: h}
go func() {
errChan <- srv.Serve(listener)
}()
}
return
} }
// ListenAndServe is the drop-in replacement for `http.ListenAndServe`. // ListenAndServe is the drop-in replacement for `http.ListenAndServe`.
// Supports unix and systemd sockets in addition // Supports unix and systemd sockets in addition
func ListenAndServe(addr string, h http.Handler) error { func ListenAndServe(addr string, h http.Handler) error {
_, _, done, err := Serve(addr, h) _, _, _, done, err := Serve(addr, h)
if err != nil { if err != nil {
return err return err
} }
@ -267,3 +266,98 @@ func UnsetSystemdListenVars() {
_ = os.Unsetenv("LISTEN_FDS") _ = os.Unsetenv("LISTEN_FDS")
_ = os.Unsetenv("LISTEN_FDNAMES") _ = os.Unsetenv("LISTEN_FDNAMES")
} }
func parseAddress(addr string) (addrType AddressType, usc *UnixSocketConfig, sysc *SysdConfig, err error) {
usc = nil
sysc = nil
err = nil
u, err := url.Parse(addr)
if err != nil {
return TCP, nil, nil, nil
}
if u.Path == "unix" {
duc := DefaultUnixSocketConfig
usc = &duc
addrType = UnixSocket
for key, val := range u.Query() {
if len(val) != 1 {
err = fmt.Errorf("unix socket address error. Multiple %v found: %v", key, val)
return
}
if key == "path" {
usc.SocketPath = val[0]
} else if key == "mode" {
if _, serr := fmt.Sscanf(val[0], "%o", &usc.SocketMode); serr != nil {
err = fmt.Errorf("unix socket address error. Bad mode: %v, err: %w", val, serr)
return
}
} else if key == "remove_existing" {
if removeExisting, berr := strconv.ParseBool(val[0]); berr == nil {
usc.RemoveExisting = removeExisting
} else {
err = fmt.Errorf("unix socket address error. Bad remove_existing: %v, err: %w", val, berr)
return
}
} else {
err = fmt.Errorf("unix socket address error. Bad option; key: %v, val: %v", key, val)
return
}
}
if usc.SocketPath == "" {
err = fmt.Errorf("unix socket address error. Missing path; addr: %v", addr)
return
}
} else if u.Path == "sysd" {
dsc := DefaultSysdConfig
sysc = &dsc
addrType = SystemdFD
for key, val := range u.Query() {
if len(val) != 1 {
err = fmt.Errorf("systemd socket fd address error. Multiple %v found: %v", key, val)
return
}
if key == "name" {
sysc.FDName = &val[0]
} else if key == "idx" {
if idx, ierr := strconv.Atoi(val[0]); ierr == nil {
sysc.FDIndex = &idx
} else {
err = fmt.Errorf("systemd socket fd address error. Bad idx: %v, err: %w", val, ierr)
return
}
} else if key == "check_pid" {
if checkPID, berr := strconv.ParseBool(val[0]); berr == nil {
sysc.CheckPID = checkPID
} else {
err = fmt.Errorf("systemd socket fd address error. Bad check_pid: %v, err: %w", val, berr)
return
}
} else if key == "unset_env" {
if unsetEnv, berr := strconv.ParseBool(val[0]); berr == nil {
sysc.UnsetEnv = unsetEnv
} else {
err = fmt.Errorf("systemd socket fd address error. Bad unset_env: %v, err: %w", val, berr)
return
}
} else if key == "idle_timeout" {
if timeout, terr := time.ParseDuration(val[0]); terr == nil {
sysc.IdleTimeout = &timeout
} else {
err = fmt.Errorf("systemd socket fd address error. Bad idle_timeout: %v, err: %w", val, terr)
return
}
} else {
err = fmt.Errorf("systemd socket fd address error. Bad option; key: %v, val: %v", key, val)
return
}
}
if (sysc.FDIndex == nil) == (sysc.FDName == nil) {
err = fmt.Errorf("systemd socket fd address error. Exactly only one of name and idx has to be set. name: %v, idx: %v", sysc.FDName, sysc.FDIndex)
return
}
} else {
// Just assume as TCP address
return TCP, nil, nil, nil
}
return
}

132
anyhttp_test.go Normal file
View File

@ -0,0 +1,132 @@
package anyhttp
import (
"encoding/json"
"testing"
"time"
)
func Test_parseAddress(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
addr string
wantAddrType AddressType
wantUsc *UnixSocketConfig
wantSysc *SysdConfig
wantErr bool
}{
{
name: "tcp port",
addr: ":8080",
wantAddrType: TCP,
wantUsc: nil,
wantSysc: nil,
wantErr: false,
},
{
name: "unix address",
addr: "unix?path=/run/foo.sock&mode=660",
wantAddrType: UnixSocket,
wantUsc: &UnixSocketConfig{
SocketPath: "/run/foo.sock",
SocketMode: 0660,
RemoveExisting: true,
},
wantSysc: nil,
wantErr: false,
},
{
name: "systemd address",
addr: "sysd?name=foo.socket",
wantAddrType: SystemdFD,
wantUsc: nil,
wantSysc: &SysdConfig{
FDIndex: nil,
FDName: ptr("foo.socket"),
CheckPID: true,
UnsetEnv: true,
IdleTimeout: nil,
},
wantErr: false,
},
{
name: "systemd address with index",
addr: "sysd?idx=0&idle_timeout=30m",
wantAddrType: SystemdFD,
wantUsc: nil,
wantSysc: &SysdConfig{
FDIndex: ptr(0),
FDName: nil,
CheckPID: true,
UnsetEnv: true,
IdleTimeout: ptr(30 * time.Minute),
},
wantErr: false,
},
{
name: "systemd address. Bad example",
addr: "sysd?idx=0&idle_timeout=30m&name=foo",
wantAddrType: SystemdFD,
wantUsc: nil,
wantSysc: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotAddrType, gotUsc, gotSysc, gotErr := parseAddress(tt.addr)
if gotErr != nil {
if !tt.wantErr {
t.Errorf("parseAddress() failed: %v", gotErr)
}
return
}
if tt.wantErr {
t.Fatal("parseAddress() succeeded unexpectedly")
}
if gotAddrType != tt.wantAddrType {
t.Errorf("parseAddress() addrType = %v, want %v", gotAddrType, tt.wantAddrType)
}
if !check(gotUsc, tt.wantUsc) {
t.Errorf("parseAddress() Usc = %v, want %v", gotUsc, tt.wantUsc)
}
if !check(gotSysc, tt.wantSysc) {
if (gotSysc == nil || tt.wantSysc == nil) ||
!(check(gotSysc.FDIndex, tt.wantSysc.FDIndex) &&
check(gotSysc.FDName, tt.wantSysc.FDName) &&
check(gotSysc.IdleTimeout, tt.wantSysc.IdleTimeout)) {
t.Errorf("parseAddress() Sysc = %v, want %v", asJSON(gotSysc), asJSON(tt.wantSysc))
}
}
})
}
}
// Helpers
// print value instead of pointer
func asJSON[T any](val T) string {
op, err := json.Marshal(val)
if err != nil {
return err.Error()
}
return string(op)
}
func ptr[T any](val T) *T {
return &val
}
// nil safe equal check
func check[T comparable](got, want *T) bool {
if (got == nil) != (want == nil) {
return false
}
if got == nil {
return true
}
return *got == *want
}