Merge pull request 'Change address syntax to be a URL' (#3) from addr-url into main
Reviewed-on: #3
This commit is contained in:
commit
94c737a02b
59
README.md
59
README.md
@ -17,56 +17,57 @@ Just replace `http.ListenAndServe` with `anyhttp.ListenAndServe`.
|
||||
|
||||
Syntax
|
||||
|
||||
unix/<path to socket>
|
||||
unix?path=<socket_path>&mode=<socket file mode>&remove_existing=<yes|no>
|
||||
|
||||
Examples
|
||||
|
||||
unix/relative/path.sock
|
||||
unix//var/run/app/absolutepath.sock
|
||||
unix?path=relative/path.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:
|
||||
|
||||
Syntax
|
||||
|
||||
sysd/fdidx/<fd index starting at 0>
|
||||
sysd/fdname/<fd name set using FileDescriptorName socket setting >
|
||||
sysd?idx=<fd index>&name=<fd name>&check_pid=<yes|no>&unset_env=<yes|no>&idle_timeout=<duration>
|
||||
|
||||
Only one of `idx` or `name` has to be set
|
||||
|
||||
Examples:
|
||||
|
||||
# First (or only) socket fd passed to app
|
||||
sysd/fdidx/0
|
||||
sysd?idx=0
|
||||
|
||||
# Socket with FileDescriptorName
|
||||
sysd/fdname/myapp
|
||||
sysd?name=myapp
|
||||
|
||||
# Using default name
|
||||
sysd/fdname/myapp.socket
|
||||
# Using default name and auto shutdown if no requests received in last 30 minutes
|
||||
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
|
||||
as `http.ListenAndServe(":<port>",...)` Anything else is directly passed to
|
||||
`http.ListenAndServe` as well. Below examples should work
|
||||
### TCP
|
||||
|
||||
If the address is not one of above, it is assumed to be tcp and passed to `http.ListenAndServe`.
|
||||
|
||||
Examples:
|
||||
|
||||
:http
|
||||
:8888
|
||||
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
|
||||
|
||||
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://github.com/coreos/go-systemd/tree/main/activation
|
||||
|
||||
[0]: https://pkg.go.dev/time#ParseDuration
|
||||
|
194
anyhttp.go
194
anyhttp.go
@ -2,16 +2,21 @@
|
||||
package anyhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go.balki.me/anyhttp/idle"
|
||||
)
|
||||
|
||||
// AddressType of the address passed
|
||||
@ -97,6 +102,8 @@ type SysdConfig struct {
|
||||
CheckPID bool
|
||||
// Unsets the LISTEN* environment variables, so they don't get passed to any child processes
|
||||
UnsetEnv bool
|
||||
// Shutdown http server if no requests received for below timeout
|
||||
IdleTimeout *time.Duration
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// GetListener gets a unix or systemd socket listener
|
||||
func GetListener(addr string) (AddressType, net.Listener, error) {
|
||||
if strings.HasPrefix(addr, "unix/") {
|
||||
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 {
|
||||
return Unknown, nil, fmt.Errorf("invalid fdidx, addr:%q err: %w", addr, err)
|
||||
}
|
||||
sysdc := NewSysDConfigWithFDIdx(idx)
|
||||
l, err := sysdc.GetListener()
|
||||
return SystemdFD, l, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(addr, "sysd/fdname/") {
|
||||
sysdc := NewSysDConfigWithFDName(strings.TrimPrefix(addr, "sysd/fdname/"))
|
||||
l, err := sysdc.GetListener()
|
||||
return SystemdFD, l, err
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
addr = ":http"
|
||||
}
|
||||
|
||||
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)
|
||||
func Serve(addr string, h http.Handler) (addrType AddressType, srv *http.Server, idler idle.Idler, done <-chan error, err error) {
|
||||
addrType, usc, sysc, err := parseAddress(addr)
|
||||
if err != nil {
|
||||
return addrType, nil, nil, err
|
||||
return
|
||||
}
|
||||
srv := &http.Server{Handler: h}
|
||||
done := make(chan error)
|
||||
go func() {
|
||||
done <- srv.Serve(listener)
|
||||
close(done)
|
||||
|
||||
listener, err := func() (net.Listener, error) {
|
||||
if usc != nil {
|
||||
return usc.GetListener()
|
||||
} else if sysc != nil {
|
||||
return sysc.GetListener()
|
||||
}
|
||||
if addr == "" {
|
||||
addr = ":http"
|
||||
}
|
||||
return net.Listen("tcp", addr)
|
||||
}()
|
||||
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`.
|
||||
// Supports unix and systemd sockets in addition
|
||||
func ListenAndServe(addr string, h http.Handler) error {
|
||||
_, _, done, err := Serve(addr, h)
|
||||
_, _, _, done, err := Serve(addr, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -267,3 +266,98 @@ func UnsetSystemdListenVars() {
|
||||
_ = os.Unsetenv("LISTEN_FDS")
|
||||
_ = 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
132
anyhttp_test.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user