Skip to content

Commit

Permalink
Merge pull request 'Change address syntax to be a URL' (#3) from addr…
Browse files Browse the repository at this point in the history
…-url into main

Reviewed-on: https://gitea.balki.me/balki/anyhttp/pulls/3
  • Loading branch information
balki committed Dec 19, 2024
2 parents 917f340 + 74efe7c commit 94c737a
Show file tree
Hide file tree
Showing 3 changed files with 305 additions and 76 deletions.
59 changes: 31 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 and auto shutdown if no requests received in last 30 minutes
sysd?name=myapp.socket&idle_timeout=30m

# Using default name
sysd/fdname/myapp.socket
| 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 |

### TCP port
### TCP

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
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
Expand All @@ -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
190 changes: 142 additions & 48 deletions anyhttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
// Serve creates and serve a http server.
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
}

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)
listener, err := func() (net.Listener, error) {
if usc != nil {
return usc.GetListener()
} else if sysc != nil {
return sysc.GetListener()
}
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"
}
}

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)
return net.Listen("tcp", 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)
}()
return addrType, srv, done, nil
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
}
Expand All @@ -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
}
Loading

0 comments on commit 94c737a

Please sign in to comment.