Backend: Support listening on Unix Socket #2337 #3595

When HTTP listening address starts with unix: and contains /, listen
at given path instead of a TCP socket.

TLS or AutoTLS will not work since there is no TLS layer when using
the unix domain socket.
This commit is contained in:
Silver Bullet 2023-08-14 16:00:35 +08:00 committed by GitHub
parent 77b5996640
commit 2bf50082f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 61 additions and 7 deletions

View file

@ -3,6 +3,7 @@ package commands
import (
"fmt"
"io"
"net"
"net/http"
"time"
@ -32,6 +33,15 @@ func statusAction(ctx *cli.Context) error {
// interrupt reading of the Response.Body.
client := &http.Client{Timeout: 10 * time.Second}
// make a dial function for unix socket
if unixSocketPath := conf.HttpHostAsSocketPath(); unixSocketPath != "" {
client.Transport = &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return net.Dial("unix", unixSocketPath)
},
}
}
url := fmt.Sprintf("http://%s:%d/api/v1/status", conf.HttpHost(), conf.HttpPort())
req, err := http.NewRequest(http.MethodGet, url, nil)

View file

@ -104,7 +104,8 @@ func (c *Config) HttpCachePublic() bool {
// HttpHost returns the built-in HTTP server host name or IP address (empty for all interfaces).
func (c *Config) HttpHost() string {
if c.options.HttpHost == "" {
// when unix socket used as host, make host as default value. or http client will act weirdly.
if c.options.HttpHost == "" || c.HttpHostAsSocketPath() != "" {
return "0.0.0.0"
}
@ -120,6 +121,15 @@ func (c *Config) HttpPort() int {
return c.options.HttpPort
}
// HttpHostAsSocketPath tries to parse the HttpHost as unix socket path. If failed, return empty string.
func (c *Config) HttpHostAsSocketPath() string {
host := c.options.HttpHost
if strings.HasPrefix(host, "unix:") && strings.Contains(host, "/") {
return strings.TrimPrefix(host, "unix:")
}
return ""
}
// TemplatesPath returns the server templates path.
func (c *Config) TemplatesPath() string {
return filepath.Join(c.AssetsPath(), "templates")

View file

@ -8,12 +8,22 @@ import (
"github.com/photoprism/photoprism/internal/thumb"
)
func TestConfig_HttpHostAsSocketPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.HttpHostAsSocketPath())
c.options.HttpHost = "unix:/tmp/photoprism.sock"
assert.Equal(t, "/tmp/photoprism.sock", c.HttpHostAsSocketPath())
}
func TestConfig_HttpServerHost2(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "0.0.0.0", c.HttpHost())
c.options.HttpHost = "test"
assert.Equal(t, "test", c.HttpHost())
c.options.HttpHost = "unix:/tmp/photoprism.sock"
assert.Equal(t, "0.0.0.0", c.HttpHost())
}
func TestConfig_HttpServerPort2(t *testing.T) {

View file

@ -503,7 +503,7 @@ var Flags = CliFlags{
}}, {
Flag: cli.StringFlag{
Name: "http-host, ip",
Usage: "Web server `IP` address",
Usage: "Web server `IP` address. If start with unix:, path followed is treated as unix socket path for listening.",
EnvVar: EnvVar("HTTP_HOST"),
}}, {
Flag: cli.IntFlag{

View file

@ -16,6 +16,8 @@ func AutoTLS(conf *config.Config) (*autocert.Manager, error) {
// Enable automatic HTTPS via Let's Encrypt?
if !conf.SiteHttps() {
return nil, fmt.Errorf("disabled tls")
} else if conf.HttpHostAsSocketPath() != "" {
return nil, fmt.Errorf("unix socket not work with auto https")
} else if siteDomain = conf.SiteDomain(); !strings.Contains(siteDomain, ".") {
return nil, fmt.Errorf("fully qualified domain required to enable tls")
} else if tlsEmail = conf.TLSEmail(); tlsEmail == "" {

View file

@ -3,6 +3,7 @@ package server
import (
"context"
"fmt"
"net"
"net/http"
"time"
@ -86,6 +87,9 @@ func Start(ctx context.Context, conf *config.Config) {
go StartAutoTLS(server, tlsManager, conf)
} else if publicCert, privateKey := conf.TLS(); publicCert != "" && privateKey != "" {
log.Infof("server: starting in tls mode")
if unixSocketPath := conf.HttpHostAsSocketPath(); unixSocketPath != "" {
log.Errorf("both unix socket and tls cert provided")
}
server = &http.Server{
Addr: fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()),
Handler: router,
@ -94,12 +98,30 @@ func Start(ctx context.Context, conf *config.Config) {
go StartTLS(server, publicCert, privateKey)
} else {
log.Infof("server: %s", tlsErr)
var listener net.Listener
var listenPath string
var err error
if unixSocketPath := conf.HttpHostAsSocketPath(); unixSocketPath != "" {
var unixAddr *net.UnixAddr
unixAddr, err = net.ResolveUnixAddr("unix", unixSocketPath)
if err != nil {
log.Errorf("server: resolve unix address failed (%s)", err)
}
listenPath = unixSocketPath
listener, err = net.ListenUnix("unix", unixAddr)
} else {
listenPath = fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())
listener, err = net.Listen("tcp", listenPath)
}
if err != nil {
log.Errorf("server: listen unix address failed (%s)", err)
}
server = &http.Server{
Addr: fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()),
Addr: listenPath,
Handler: router,
}
log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))
go StartHttp(server)
log.Infof("server: listening on %s [%s]", listenPath, time.Since(start))
go StartHttp(server, listener)
}
// Graceful HTTP server shutdown.
@ -112,8 +134,8 @@ func Start(ctx context.Context, conf *config.Config) {
}
// StartHttp starts the web server in http mode.
func StartHttp(s *http.Server) {
if err := s.ListenAndServe(); err != nil {
func StartHttp(s *http.Server, l net.Listener) {
if err := s.Serve(l); err != nil {
if err == http.ErrServerClosed {
log.Info("server: shutdown complete")
} else {