httpd: add support for listening over a Unix-domain socket

Fixes #266
This commit is contained in:
Nicola Murino 2020-12-29 19:02:56 +01:00
parent 40e759c983
commit 0966d44c0f
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
8 changed files with 102 additions and 13 deletions

View file

@ -175,7 +175,7 @@ The configuration file contains the following sections:
- `update_mode`, integer. Defines how the database will be initialized/updated. 0 means automatically. 1 means manually using the initprovider sub-command.
- **"httpd"**, the configuration for the HTTP server used to serve REST API and to expose the built-in web interface
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
- `bind_address`, string. Leave blank to listen on all available network interfaces. On \*NIX you can specify an absolute path to listen on a Unix-domain socket. Default: "127.0.0.1"
- `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
- `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled
- `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons
@ -184,7 +184,7 @@ The configuration file contains the following sections:
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- **"telemetry"**, the configuration for the telemetry server, more details [below](#telemetry-server)
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 10000
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
- `bind_address`, string. Leave blank to listen on all available network interfaces. On \*NIX you can specify an absolute path to listen on a Unix-domain socket. Default: "127.0.0.1"
- `enable_profiler`, boolean. Enable the built-in profiler. Default `false`
- `auth_user_file`, string. Path to a file used to store usernames and passwords for basic authentication. This can be an absolute path or a path relative to the config dir. We support HTTP basic authentication, and the file format must conform to the one generated using the Apache `htpasswd` tool. The supported password formats are bcrypt (`$2y$` prefix) and md5 crypt (`$apr1$` prefix). If empty, HTTP authentication is disabled. Authentication will be always disabled for the `/healthz` endpoint.
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.

View file

@ -11,6 +11,7 @@ import (
"fmt"
"net/http"
"path/filepath"
"runtime"
"time"
"github.com/go-chi/chi"
@ -50,6 +51,7 @@ const (
// MaxRestoreSize defines the max size for the loaddata input file
MaxRestoreSize = 10485760 // 10 MB
maxRequestSize = 1048576 // 1MB
osWindows = "windows"
)
var (
@ -99,6 +101,17 @@ type apiResponse struct {
Message string `json:"message"`
}
// ShouldBind returns true if there service must be started
func (c Conf) ShouldBind() bool {
if c.BindPort > 0 {
return true
}
if filepath.IsAbs(c.BindAddress) && runtime.GOOS != osWindows {
return true
}
return false
}
// Initialize configures and starts the HTTP server
func (c Conf) Initialize(configDir string) error {
var err error
@ -128,7 +141,6 @@ func (c Conf) Initialize(configDir string) error {
}
initializeRouter(staticFilesPath, enableWebAdmin)
httpServer := &http.Server{
Addr: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort),
Handler: router,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
@ -145,9 +157,9 @@ func (c Conf) Initialize(configDir string) error {
MinVersion: tls.VersionTLS12,
}
httpServer.TLSConfig = config
return httpServer.ListenAndServeTLS("", "")
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true)
}
return httpServer.ListenAndServe()
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false)
}
// ReloadTLSCertificate reloads the TLS certificate and key from the configured paths

View file

@ -16,6 +16,7 @@ import (
"github.com/go-chi/chi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
@ -29,6 +30,21 @@ const (
inactiveURL = "http://127.0.0.1:12345"
)
func TestShouldBind(t *testing.T) {
c := Conf{
BindPort: 10000,
}
require.True(t, c.ShouldBind())
c.BindPort = 0
require.False(t, c.ShouldBind())
if runtime.GOOS != osWindows {
c.BindAddress = "/absolute/path"
require.True(t, c.ShouldBind())
}
}
func TestGetRespStatus(t *testing.T) {
var err error
err = &dataprovider.MethodDisabledError{}
@ -631,7 +647,7 @@ func TestBasicAuth(t *testing.T) {
SetBaseURLAndCredentials(httpBaseURL, "test3", "password2")
_, _, err = GetVersion(http.StatusUnauthorized)
assert.NoError(t, err)
if runtime.GOOS != "windows" {
if runtime.GOOS != osWindows {
authUserData = append(authUserData, []byte("test5:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\n")...)
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
assert.NoError(t, err)

View file

@ -1,6 +1,6 @@
#!/bin/bash
NFPM_VERSION=2.1.0
NFPM_VERSION=2.1.1
NFPM_ARCH=${NFPM_ARCH:-amd64}
if [ -z ${SFTPGO_VERSION} ]
then

View file

@ -143,7 +143,7 @@ func (s *Service) startServices() {
logger.Debug(logSender, "", "SFTP server not started, disabled in config file")
}
if httpdConf.BindPort > 0 {
if httpdConf.ShouldBind() {
go func() {
if err := httpdConf.Initialize(s.ConfigDir); err != nil {
logger.Error(logSender, "", "could not start HTTP server: %v", err)
@ -182,7 +182,7 @@ func (s *Service) startServices() {
} else {
logger.Debug(logSender, "", "WebDAV server not started, disabled in config file")
}
if telemetryConf.BindPort > 0 {
if telemetryConf.ShouldBind() {
go func() {
if err := telemetryConf.Initialize(s.ConfigDir); err != nil {
logger.Error(logSender, "", "could not start telemetry server: %v", err)

View file

@ -6,9 +6,9 @@ package telemetry
import (
"crypto/tls"
"fmt"
"net/http"
"path/filepath"
"runtime"
"time"
"github.com/go-chi/chi"
@ -53,6 +53,17 @@ type Conf struct {
CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
}
// ShouldBind returns true if there service must be started
func (c Conf) ShouldBind() bool {
if c.BindPort > 0 {
return true
}
if filepath.IsAbs(c.BindAddress) && runtime.GOOS != "windows" {
return true
}
return false
}
// Initialize configures and starts the telemetry server.
func (c Conf) Initialize(configDir string) error {
var err error
@ -66,7 +77,6 @@ func (c Conf) Initialize(configDir string) error {
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
initializeRouter(c.EnableProfiler)
httpServer := &http.Server{
Addr: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort),
Handler: router,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
@ -83,9 +93,9 @@ func (c Conf) Initialize(configDir string) error {
MinVersion: tls.VersionTLS12,
}
httpServer.TLSConfig = config
return httpServer.ListenAndServeTLS("", "")
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true)
}
return httpServer.ListenAndServe()
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false)
}
// ReloadTLSCertificate reloads the TLS certificate and key from the configured paths

View file

@ -6,6 +6,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/require"
@ -84,6 +85,22 @@ func TestInitialization(t *testing.T) {
require.NoError(t, err)
}
func TestShouldBind(t *testing.T) {
c := Conf{
BindPort: 10000,
EnableProfiler: false,
}
require.True(t, c.ShouldBind())
c.BindPort = 0
require.False(t, c.ShouldBind())
if runtime.GOOS != "windows" {
c.BindAddress = "/absolute/path"
require.True(t, c.ShouldBind())
}
}
func TestRouter(t *testing.T) {
authUserFile := filepath.Join(os.TempDir(), "http_users.txt")
authUserData := []byte("test1:$2y$05$bcHSED7aO1cfLto6ZdDBOOKzlwftslVhtpIkRhAtSa4GuLmk5mola\n")

View file

@ -18,6 +18,7 @@ import (
"io"
"io/ioutil"
"net"
"net/http"
"os"
"path"
"path/filepath"
@ -381,3 +382,36 @@ func createDirPathIfMissing(file string, perm os.FileMode) error {
}
return nil
}
// HTTPListenAndServe is a wrapper for ListenAndServe that support both tcp
// and Unix-domain sockets
func HTTPListenAndServe(srv *http.Server, address string, port int, isTLS bool) error {
var listener net.Listener
var err error
if filepath.IsAbs(address) && runtime.GOOS != "windows" {
if !IsFileInputValid(address) {
return fmt.Errorf("invalid socket address %#v", address)
}
err = createDirPathIfMissing(address, os.ModePerm)
if err != nil {
logger.ErrorToConsole("error creating Unix-domain socket parent dir: %v", err)
logger.Error(logSender, "", "error creating Unix-domain socket parent dir: %v", err)
}
os.Remove(address)
listener, err = net.Listen("unix", address)
} else {
listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", address, port))
}
if err != nil {
return err
}
defer listener.Close()
if isTLS {
return srv.ServeTLS(listener, "", "")
}
return srv.Serve(listener)
}