mirror of
https://github.com/rclone/rclone.git
synced 2025-08-19 09:52:05 +02:00
serve http: support unix sockets and multiple listners
- add support for unix sockets (which skip the auth). - add support for multiple listeners - collapse unnecessary internal structure of lib/http so it can all be imported together - moves files in sub directories of lib/http into the main lib/http directory and reworks the code that uses them. See: https://forum.rclone.org/t/wip-rc-rcd-over-unix-socket/33619 Fixes: #6605
This commit is contained in:
committed by
Nick Craig-Wood
parent
dfd8ad2fff
commit
6d62267227
444
lib/http/server_test.go
Normal file
444
lib/http/server_test.go
Normal file
@@ -0,0 +1,444 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testEchoHandler(data []byte) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(data)
|
||||
})
|
||||
}
|
||||
|
||||
func testExpectRespBody(t *testing.T, resp *http.Response, expected []byte) {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, body)
|
||||
}
|
||||
|
||||
func testGetServerURL(t *testing.T, s Server) string {
|
||||
urls := s.URLs()
|
||||
require.GreaterOrEqual(t, len(urls), 1, "server should return at least one url")
|
||||
return urls[0]
|
||||
}
|
||||
|
||||
func testNewHTTPClientUnix(path string) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return net.Dial("unix", path)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testReadTestdataFile(t *testing.T, path string) []byte {
|
||||
data, err := os.ReadFile(filepath.Join("./testdata", path))
|
||||
require.NoError(t, err, "")
|
||||
return data
|
||||
}
|
||||
|
||||
func TestNewServerUnix(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
path := filepath.Join(tempDir, "rclone.sock")
|
||||
|
||||
cfg := DefaultHTTPCfg()
|
||||
cfg.ListenAddr = []string{path}
|
||||
|
||||
auth := AuthConfig{
|
||||
BasicUser: "test",
|
||||
BasicPass: "test",
|
||||
}
|
||||
|
||||
s, err := NewServer(ctx, WithConfig(cfg), WithAuth(auth))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, s.Shutdown())
|
||||
_, err := os.Stat(path)
|
||||
require.ErrorIs(t, err, os.ErrNotExist, "shutdown should remove socket")
|
||||
}()
|
||||
|
||||
require.Empty(t, s.URLs(), "unix socket should not appear in URLs")
|
||||
|
||||
s.Router().Use(MiddlewareCORS(""))
|
||||
|
||||
expected := []byte("hello world")
|
||||
s.Router().Mount("/", testEchoHandler(expected))
|
||||
s.Serve()
|
||||
|
||||
client := testNewHTTPClientUnix(path)
|
||||
req, err := http.NewRequest("GET", "http://unix", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
testExpectRespBody(t, resp, expected)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "unix sockets should ignore auth")
|
||||
|
||||
for _, key := range _testCORSHeaderKeys {
|
||||
require.NotContains(t, resp.Header, key, "unix sockets should not be sent CORS headers")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServerHTTP(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
cfg := DefaultHTTPCfg()
|
||||
cfg.ListenAddr = []string{"127.0.0.1:0"}
|
||||
|
||||
auth := AuthConfig{
|
||||
BasicUser: "test",
|
||||
BasicPass: "test",
|
||||
}
|
||||
|
||||
s, err := NewServer(ctx, WithConfig(cfg), WithAuth(auth))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, s.Shutdown())
|
||||
}()
|
||||
|
||||
url := testGetServerURL(t, s)
|
||||
require.True(t, strings.HasPrefix(url, "http://"), "url should have http scheme")
|
||||
|
||||
expected := []byte("hello world")
|
||||
s.Router().Mount("/", testEchoHandler(expected))
|
||||
s.Serve()
|
||||
|
||||
t.Run("StatusUnauthorized", func(t *testing.T) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode, "no basic auth creds should return unauthorized")
|
||||
})
|
||||
|
||||
t.Run("StatusOK", func(t *testing.T) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.SetBasicAuth(auth.BasicUser, auth.BasicPass)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "using basic auth creds should return ok")
|
||||
|
||||
testExpectRespBody(t, resp, expected)
|
||||
})
|
||||
}
|
||||
func TestNewServerBaseURL(t *testing.T) {
|
||||
servers := []struct {
|
||||
name string
|
||||
http HTTPConfig
|
||||
suffix string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
BaseURL: "",
|
||||
},
|
||||
suffix: "/",
|
||||
},
|
||||
{
|
||||
name: "Single/NoTrailingSlash",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
BaseURL: "/rclone",
|
||||
},
|
||||
suffix: "/rclone/",
|
||||
},
|
||||
{
|
||||
name: "Single/TrailingSlash",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
BaseURL: "/rclone/",
|
||||
},
|
||||
suffix: "/rclone/",
|
||||
},
|
||||
{
|
||||
name: "Multi/NoTrailingSlash",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
BaseURL: "/rclone/test/base/url",
|
||||
},
|
||||
suffix: "/rclone/test/base/url/",
|
||||
},
|
||||
{
|
||||
name: "Multi/TrailingSlash",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
BaseURL: "/rclone/test/base/url/",
|
||||
},
|
||||
suffix: "/rclone/test/base/url/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, ss := range servers {
|
||||
t.Run(ss.name, func(t *testing.T) {
|
||||
s, err := NewServer(context.Background(), WithConfig(ss.http))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, s.Shutdown())
|
||||
}()
|
||||
|
||||
expected := []byte("data")
|
||||
s.Router().Get("/", testEchoHandler(expected).ServeHTTP)
|
||||
s.Serve()
|
||||
|
||||
url := testGetServerURL(t, s)
|
||||
require.True(t, strings.HasPrefix(url, "http://"), "url should have http scheme")
|
||||
require.True(t, strings.HasSuffix(url, ss.suffix), "url should have the expected suffix")
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
t.Log(url, resp.Request.URL)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "should return ok")
|
||||
|
||||
testExpectRespBody(t, resp, expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServerTLS(t *testing.T) {
|
||||
certBytes := testReadTestdataFile(t, "local.crt")
|
||||
keyBytes := testReadTestdataFile(t, "local.key")
|
||||
|
||||
// TODO: generate a proper cert with SAN
|
||||
// TODO: generate CA, test mTLS
|
||||
// clientCert, err := tls.X509KeyPair(certBytes, keyBytes)
|
||||
// require.NoError(t, err, "should be testing with a valid self signed certificate")
|
||||
|
||||
servers := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
err error
|
||||
http HTTPConfig
|
||||
}{
|
||||
{
|
||||
name: "FromFile/Valid",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCert: "./testdata/local.crt",
|
||||
TLSKey: "./testdata/local.key",
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FromFile/NoCert",
|
||||
wantErr: true,
|
||||
err: ErrTLSFileMismatch,
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCert: "",
|
||||
TLSKey: "./testdata/local.key",
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FromFile/InvalidCert",
|
||||
wantErr: true,
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCert: "./testdata/local.crt.invalid",
|
||||
TLSKey: "./testdata/local.key",
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FromFile/NoKey",
|
||||
wantErr: true,
|
||||
err: ErrTLSFileMismatch,
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCert: "./testdata/local.crt",
|
||||
TLSKey: "",
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FromFile/InvalidKey",
|
||||
wantErr: true,
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCert: "./testdata/local.crt",
|
||||
TLSKey: "./testdata/local.key.invalid",
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FromBody/Valid",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCertBody: certBytes,
|
||||
TLSKeyBody: keyBytes,
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FromBody/NoCert",
|
||||
wantErr: true,
|
||||
err: ErrTLSBodyMismatch,
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCertBody: nil,
|
||||
TLSKeyBody: keyBytes,
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FromBody/InvalidCert",
|
||||
wantErr: true,
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCertBody: []byte("JUNK DATA"),
|
||||
TLSKeyBody: keyBytes,
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FromBody/NoKey",
|
||||
wantErr: true,
|
||||
err: ErrTLSBodyMismatch,
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCertBody: certBytes,
|
||||
TLSKeyBody: nil,
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FromBody/InvalidKey",
|
||||
wantErr: true,
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCertBody: certBytes,
|
||||
TLSKeyBody: []byte("JUNK DATA"),
|
||||
MinTLSVersion: "tls1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MinTLSVersion/Valid/1.1",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCertBody: certBytes,
|
||||
TLSKeyBody: keyBytes,
|
||||
MinTLSVersion: "tls1.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MinTLSVersion/Valid/1.2",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCertBody: certBytes,
|
||||
TLSKeyBody: keyBytes,
|
||||
MinTLSVersion: "tls1.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MinTLSVersion/Valid/1.3",
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCertBody: certBytes,
|
||||
TLSKeyBody: keyBytes,
|
||||
MinTLSVersion: "tls1.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MinTLSVersion/Invalid",
|
||||
wantErr: true,
|
||||
err: ErrInvalidMinTLSVersion,
|
||||
http: HTTPConfig{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
TLSCertBody: certBytes,
|
||||
TLSKeyBody: keyBytes,
|
||||
MinTLSVersion: "tls0.9",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, ss := range servers {
|
||||
t.Run(ss.name, func(t *testing.T) {
|
||||
s, err := NewServer(context.Background(), WithConfig(ss.http))
|
||||
if ss.wantErr == true {
|
||||
if ss.err != nil {
|
||||
require.ErrorIs(t, err, ss.err, "new server should return the expected error")
|
||||
} else {
|
||||
require.Error(t, err, "new server should return error for invalid TLS config")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, s.Shutdown())
|
||||
}()
|
||||
|
||||
expected := []byte("secret-page")
|
||||
s.Router().Mount("/", testEchoHandler(expected))
|
||||
s.Serve()
|
||||
|
||||
url := testGetServerURL(t, s)
|
||||
require.True(t, strings.HasPrefix(url, "https://"), "url should have https scheme")
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
dest := strings.TrimPrefix(url, "https://")
|
||||
dest = strings.TrimSuffix(dest, "/")
|
||||
return net.Dial("tcp", dest)
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
// Certificates: []tls.Certificate{clientCert},
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequest("GET", "https://dev.rclone.org", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "should return ok")
|
||||
|
||||
testExpectRespBody(t, resp, expected)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user