Files
Xray-core/transport/internet/xdrive/drive_test.go
T

691 lines
16 KiB
Go

package xdrive
import (
"context"
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
"time"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
)
type fakeFile struct {
id string
name string
data []byte
description string
}
type fakeDrive struct {
server *httptest.Server
mu sync.Mutex
files map[string]*fakeFile
nextID int
failOnce map[string]bool
failStatus map[string]int
failBody map[string]string
tokens int
hosts map[string]bool
}
func newFakeDrive(t *testing.T) *fakeDrive {
t.Helper()
drive := &fakeDrive{
files: make(map[string]*fakeFile),
failOnce: make(map[string]bool),
failStatus: make(map[string]int),
failBody: make(map[string]string),
hosts: make(map[string]bool),
}
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}))
record := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
drive.mu.Lock()
drive.hosts[r.Host] = true
drive.mu.Unlock()
next(w, r)
}
}
mux.HandleFunc("/token", record(drive.handleToken))
mux.HandleFunc("/upload", record(drive.handleUpload))
mux.HandleFunc("/files", record(drive.handleFiles))
mux.HandleFunc("/files/", record(drive.handleFile))
drive.server = httptest.NewServer(mux)
resetSharedStorage()
previous := []string{driveTokenURL, driveFilesURL, driveUploadURL}
previousBackoff := driveInitialBackoff
driveTokenURL = drive.server.URL + "/token"
driveFilesURL = drive.server.URL + "/files"
driveUploadURL = drive.server.URL + "/upload"
driveInitialBackoff = 5 * time.Millisecond
t.Cleanup(func() {
driveTokenURL, driveFilesURL, driveUploadURL = previous[0], previous[1], previous[2]
driveInitialBackoff = previousBackoff
drive.server.Close()
resetSharedStorage()
})
return drive
}
func (d *fakeDrive) handleToken(w http.ResponseWriter, r *http.Request) {
d.mu.Lock()
d.tokens++
d.mu.Unlock()
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": "fake-token",
"expires_in": 3600,
})
}
func (d *fakeDrive) handleUpload(w http.ResponseWriter, r *http.Request) {
if d.shouldFail("upload") {
if status, body := d.failure("upload"); status != 0 {
w.WriteHeader(status)
w.Write([]byte(body))
return
}
w.WriteHeader(http.StatusTooManyRequests)
return
}
_, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
reader := multipart.NewReader(r.Body, params["boundary"])
metaPart, err := reader.NextPart()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
var metadata struct {
Name string `json:"name"`
}
if err := json.NewDecoder(metaPart).Decode(&metadata); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
var data []byte
if dataPart, err := reader.NextPart(); err == nil {
data, _ = io.ReadAll(dataPart)
}
d.mu.Lock()
d.nextID++
id := fmt.Sprintf("id-%d", d.nextID)
d.files[id] = &fakeFile{id: id, name: metadata.Name, data: data}
d.mu.Unlock()
json.NewEncoder(w).Encode(map[string]string{"id": id, "name": metadata.Name})
}
func (d *fakeDrive) handleFiles(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
d.handleCreate(w, r)
return
}
d.handleList(w, r)
}
func (d *fakeDrive) handleCreate(w http.ResponseWriter, r *http.Request) {
if d.shouldFail("upload") {
if status, body := d.failure("upload"); status != 0 {
w.WriteHeader(status)
w.Write([]byte(body))
return
}
w.WriteHeader(http.StatusTooManyRequests)
return
}
var meta struct {
Name string `json:"name"`
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(&meta); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
d.mu.Lock()
d.nextID++
id := fmt.Sprintf("id-%d", d.nextID)
d.files[id] = &fakeFile{id: id, name: meta.Name, description: meta.Description}
d.mu.Unlock()
json.NewEncoder(w).Encode(map[string]string{"id": id, "name": meta.Name})
}
func (d *fakeDrive) handleList(w http.ResponseWriter, r *http.Request) {
if d.shouldFail("list") {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
query := r.URL.Query().Get("q")
exact, prefix := parseFakeQuery(query)
type entry struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
}
result := struct {
Files []entry `json:"files"`
}{}
d.mu.Lock()
for _, file := range d.files {
match := false
switch {
case exact != "":
match = file.name == exact
case prefix != "":
match = strings.HasPrefix(file.name, prefix)
}
if match {
result.Files = append(result.Files, entry{
ID: file.id, Name: file.name, Description: file.description,
})
}
}
d.mu.Unlock()
json.NewEncoder(w).Encode(result)
}
func (d *fakeDrive) handleFile(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/files/")
d.mu.Lock()
file, ok := d.files[id]
if ok && r.Method == http.MethodDelete {
delete(d.files, id)
}
d.mu.Unlock()
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
if r.Method == http.MethodDelete {
w.WriteHeader(http.StatusNoContent)
return
}
if strings.Contains(r.URL.RawQuery, "fields=description") {
json.NewEncoder(w).Encode(map[string]string{"description": file.description})
return
}
w.Write(file.data)
}
func (d *fakeDrive) shouldFail(kind string) bool {
d.mu.Lock()
defer d.mu.Unlock()
if d.failOnce[kind] {
d.failOnce[kind] = false
return true
}
return false
}
func (d *fakeDrive) failOnceWith(kind string, status int, body string) {
d.mu.Lock()
d.failOnce[kind] = true
d.failStatus[kind] = status
d.failBody[kind] = body
d.mu.Unlock()
}
func (d *fakeDrive) failure(kind string) (int, string) {
d.mu.Lock()
defer d.mu.Unlock()
if status, ok := d.failStatus[kind]; ok {
return status, d.failBody[kind]
}
return 0, ""
}
func (d *fakeDrive) failNext(kind string) {
d.mu.Lock()
d.failOnce[kind] = true
d.mu.Unlock()
}
func (d *fakeDrive) seenHosts() []string {
d.mu.Lock()
defer d.mu.Unlock()
out := make([]string, 0, len(d.hosts))
for h := range d.hosts {
out = append(out, h)
}
return out
}
func (d *fakeDrive) count() int {
d.mu.Lock()
defer d.mu.Unlock()
return len(d.files)
}
func parseFakeQuery(query string) (exact, prefix string) {
if value, ok := cutQuoted(query, "name = '"); ok {
return value, ""
}
if value, ok := cutQuoted(query, "name contains '"); ok {
return "", value
}
return "", ""
}
func cutQuoted(query, marker string) (string, bool) {
start := strings.Index(query, marker)
if start < 0 {
return "", false
}
rest := query[start+len(marker):]
end := strings.Index(rest, "'")
if end < 0 {
return "", false
}
return rest[:end], true
}
func driveSettings() *internet.MemoryStreamConfig {
return &internet.MemoryStreamConfig{
ProtocolName: protocolName,
ProtocolSettings: &Config{
RemoteFolder: "folder-id",
Service: "Google Drive",
Secrets: []string{"client", "secret", "refresh"},
FlushIntervalMs: 5,
PollIntervalMs: 5,
MaxPollIntervalMs: 20,
SessionTtlSeconds: 1,
},
}
}
func newDriveBackend(t *testing.T) *driveStorage {
t.Helper()
storage, err := newDriveStorage(driveSettings(), driveSettings().ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newDriveStorage: %v", err)
}
return storage
}
func TestDriveSecrets(t *testing.T) {
if _, err := newDriveStorage(nil, &Config{RemoteFolder: "f", Secrets: []string{"a", "b"}}); err == nil {
t.Fatal("accepted two secrets")
}
if _, err := newDriveStorage(nil, &Config{Secrets: []string{"a", "b", "c"}}); err == nil {
t.Fatal("accepted an empty remoteFolder")
}
if _, err := newDriveStorage(nil, &Config{RemoteFolder: "f", Secrets: []string{"a", "", "c"}}); err == nil {
t.Fatal("accepted an empty secret")
}
}
func TestDriveRoundTrip(t *testing.T) {
drive := newFakeDrive(t)
storage := newDriveBackend(t)
ctx := context.Background()
if err := storage.Put(ctx, "streams/abc/c2s/000000000.seg", []byte("hello")); err != nil {
t.Fatalf("Put: %v", err)
}
data, err := storage.Get(ctx, "streams/abc/c2s/000000000.seg")
if err != nil {
t.Fatalf("Get: %v", err)
}
if string(data) != "hello" {
t.Fatalf("Get returned %q, want %q", data, "hello")
}
names, err := storage.List(ctx, "streams/abc/c2s")
if err != nil {
t.Fatalf("List: %v", err)
}
if len(names) != 1 || names[0].Name != "000000000.seg" {
t.Fatalf("List returned %v, want 1 segment", names)
}
if _, err := storage.Get(ctx, "streams/abc/c2s/000000009.seg"); err != errNotFound {
t.Fatalf("Get returned %v, want errNotFound", err)
}
if err := storage.Delete(ctx, "streams/abc/c2s/000000000.seg"); err != nil {
t.Fatalf("Delete: %v", err)
}
if drive.count() != 0 {
t.Fatalf("fake drive still holds %d files", drive.count())
}
}
func TestDriveListChildren(t *testing.T) {
newFakeDrive(t)
storage := newDriveBackend(t)
ctx := context.Background()
for _, name := range []string{
"streams/one/c2s/000000000.seg",
"streams/one/s2c/000000000.seg",
"streams/two/c2s/000000000.seg",
} {
if err := storage.Put(ctx, name, []byte("x")); err != nil {
t.Fatalf("Put %s: %v", name, err)
}
}
names, err := storage.List(ctx, "streams")
if err != nil {
t.Fatalf("List: %v", err)
}
if len(names) != 2 {
t.Fatalf("List returned %v, want 2 sessions", names)
}
}
func TestDriveDeleteSession(t *testing.T) {
drive := newFakeDrive(t)
storage := newDriveBackend(t)
ctx := context.Background()
for _, name := range []string{
"streams/one/c2s/000000000.seg",
"streams/one/c2s/000000001.end",
"streams/one/s2c/000000000.seg",
"streams/two/c2s/000000000.seg",
} {
if err := storage.Put(ctx, name, []byte("x")); err != nil {
t.Fatalf("Put %s: %v", name, err)
}
}
if err := storage.Delete(ctx, "streams/one"); err != nil {
t.Fatalf("Delete: %v", err)
}
if drive.count() != 1 {
t.Fatalf("fake drive holds %d files, want 1", drive.count())
}
}
func TestDriveRetry(t *testing.T) {
drive := newFakeDrive(t)
storage := newDriveBackend(t)
ctx := context.Background()
drive.failNext("upload")
if err := storage.Put(ctx, "sessions/abc", nil); err != nil {
t.Fatalf("Put did not survive a 429: %v", err)
}
drive.failNext("list")
if _, err := storage.List(ctx, "sessions"); err != nil {
t.Fatalf("List did not survive a 503: %v", err)
}
}
func TestDriveTokenCache(t *testing.T) {
drive := newFakeDrive(t)
storage := newDriveBackend(t)
ctx := context.Background()
for i := 0; i < 5; i++ {
if err := storage.Put(ctx, fmt.Sprintf("sessions/s%d", i), nil); err != nil {
t.Fatalf("Put: %v", err)
}
}
if drive.tokens != 1 {
t.Fatalf("token endpoint hit %d times, want 1", drive.tokens)
}
}
func TestDriveTransport(t *testing.T) {
newFakeDrive(t)
client, server, cleanup := pairWith(t, driveSettings())
defer cleanup()
if _, err := client.Write([]byte("ping")); err != nil {
t.Fatalf("client write: %v", err)
}
expectRead(t, server, "ping")
if _, err := server.Write([]byte("pong")); err != nil {
t.Fatalf("server write: %v", err)
}
expectRead(t, client, "pong")
}
func TestDriveLargeTransfer(t *testing.T) {
newFakeDrive(t)
client, server, cleanup := pairWith(t, driveSettings())
defer cleanup()
payload := make([]byte, 300000)
for i := range payload {
payload[i] = byte(i % 251)
}
go func() {
client.Write(payload)
}()
if err := server.SetReadDeadline(time.Now().Add(60 * time.Second)); err != nil {
t.Fatalf("SetReadDeadline: %v", err)
}
got := make([]byte, len(payload))
if _, err := io.ReadFull(server, got); err != nil {
t.Fatalf("ReadFull: %v", err)
}
for i := range got {
if got[i] != payload[i] {
t.Fatalf("payload mismatch at byte %d", i)
}
}
}
func TestRateLimited(t *testing.T) {
limited := []string{
`{"error":{"code":403,"errors":[{"reason":"userRateLimitExceeded"}]}}`,
`{"error":{"code":403,"errors":[{"reason":"rateLimitExceeded"}]}}`,
`{"error":{"code":403,"errors":[{"reason":"sharingRateLimitExceeded"}]}}`,
`{"error":{"status":"RESOURCE_EXHAUSTED"}}`,
}
for _, payload := range limited {
if !rateLimited([]byte(payload)) {
t.Fatalf("rateLimited missed %s", payload)
}
}
permanent := []string{
`{"error":{"code":403,"errors":[{"reason":"insufficientFilePermissions"}]}}`,
`{"error":{"code":403,"errors":[{"reason":"storageQuotaExceeded"}]}}`,
`not json at all`,
}
for _, payload := range permanent {
if rateLimited([]byte(payload)) {
t.Fatalf("rateLimited treated %s as temporary", payload)
}
}
}
func TestDriveRetryRateLimit(t *testing.T) {
drive := newFakeDrive(t)
storage := newDriveBackend(t)
drive.failOnceWith("upload", http.StatusForbidden,
`{"error":{"code":403,"errors":[{"reason":"userRateLimitExceeded"}]}}`)
if err := storage.Put(context.Background(), "sessions/abc", nil); err != nil {
t.Fatalf("Put did not survive a 403: %v", err)
}
}
func TestDriveSharedClient(t *testing.T) {
newFakeDrive(t)
first, err := newStorage(driveSettings())
if err != nil {
t.Fatalf("newStorage: %v", err)
}
second, err := newStorage(driveSettings())
if err != nil {
t.Fatalf("newStorage: %v", err)
}
if first != second {
t.Fatal("same settings did not share one storage")
}
}
func TestDriveInlineListing(t *testing.T) {
drive := newFakeDrive(t)
storage := newDriveBackend(t)
ctx := context.Background()
payload := []byte("small enough to ride along with the listing")
if err := storage.Put(ctx, "streams/abc/c2s/000000000.seg", payload); err != nil {
t.Fatalf("Put: %v", err)
}
entries, err := storage.List(ctx, "streams/abc/c2s")
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != 1 {
t.Fatalf("List returned %v, want 1 entry", entries)
}
if string(entries[0].Inline) != string(payload) {
t.Fatalf("listing carried %q, want %q", entries[0].Inline, payload)
}
d := drive
d.mu.Lock()
var stored *fakeFile
for _, f := range d.files {
stored = f
}
d.mu.Unlock()
if len(stored.data) != 0 {
t.Fatal("small payload was uploaded as content")
}
}
func TestDriveLargeContent(t *testing.T) {
drive := newFakeDrive(t)
storage := newDriveBackend(t)
ctx := context.Background()
payload := make([]byte, driveInlineLimit+1)
for i := range payload {
payload[i] = byte(i)
}
if err := storage.Put(ctx, "streams/abc/c2s/000000000.seg", payload); err != nil {
t.Fatalf("Put: %v", err)
}
entries, err := storage.List(ctx, "streams/abc/c2s")
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != 1 || entries[0].Inline != nil {
t.Fatalf("large payload was inlined, got %v", entries)
}
got, err := storage.Get(ctx, "streams/abc/c2s/000000000.seg")
if err != nil {
t.Fatalf("Get: %v", err)
}
if len(got) != len(payload) {
t.Fatalf("Get returned %d bytes, want %d", len(got), len(payload))
}
_ = drive
}
func TestDriveInlineGet(t *testing.T) {
newFakeDrive(t)
storage := newDriveBackend(t)
ctx := context.Background()
payload := []byte("only in the description")
if err := storage.Put(ctx, "sessions/abc", payload); err != nil {
t.Fatalf("Put: %v", err)
}
got, err := storage.Get(ctx, "sessions/abc")
if err != nil {
t.Fatalf("Get: %v", err)
}
if string(got) != string(payload) {
t.Fatalf("Get returned %q, want %q", got, payload)
}
}
func TestDriveFronting(t *testing.T) {
drive := newFakeDrive(t)
fake, err := url.Parse(drive.server.URL)
if err != nil {
t.Fatalf("parse: %v", err)
}
port, err := net.PortFromString(fake.Port())
if err != nil {
t.Fatalf("port: %v", err)
}
driveTokenURL = "http://www.googleapis.com/token"
driveFilesURL = "http://www.googleapis.com/files"
driveUploadURL = "http://www.googleapis.com/upload"
settings := driveSettings()
settings.Destination = &net.Destination{
Address: net.ParseAddress(fake.Hostname()),
Port: port,
Network: net.Network_TCP,
}
storage, err := newDriveStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newDriveStorage: %v", err)
}
if err := storage.Put(context.Background(), "sessions/fronted", []byte("x")); err != nil {
t.Fatalf("Put: %v", err)
}
if drive.count() != 1 {
t.Fatalf("fake drive holds %d files, want 1", drive.count())
}
for _, host := range drive.seenHosts() {
if host != "www.googleapis.com" {
t.Fatalf("inner host was %q, want www.googleapis.com regardless of address", host)
}
}
}