mirror of
https://github.com/rclone/rclone.git
synced 2024-11-22 15:30:06 +08:00
6d62267227
- 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
445 lines
10 KiB
Go
445 lines
10 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|