148 lines
6.2 KiB
Go
148 lines
6.2 KiB
Go
package module
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) }
|
|
|
|
func TestValidRelativeAPIPath(t *testing.T) {
|
|
for _, value := range []string{"http://metadata/v1/api/info", "//other/v1/api/info", "/v1/api/../secret", "/v1/api/info?q=1", "/etc/passwd"} {
|
|
if validRelativeAPIPath(value) {
|
|
t.Fatalf("unsafe path accepted: %q", value)
|
|
}
|
|
}
|
|
if !validRelativeAPIPath("/v1/api/info") {
|
|
t.Fatal("documented Palworld endpoint was rejected")
|
|
}
|
|
}
|
|
|
|
func TestPalworldAdapterReportsBoundedFailures(t *testing.T) {
|
|
wasm, err := os.ReadFile("../../modules/palworld-rest/module.wasm")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(wasm)
|
|
tests := []struct {
|
|
name string
|
|
transport roundTripFunc
|
|
code string
|
|
}{
|
|
{name: "unauthorized", code: "unauthorized", transport: func(request *http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: http.StatusUnauthorized, Header: make(http.Header), Body: io.NopCloser(strings.NewReader("unauthorized")), Request: request}, nil
|
|
}},
|
|
{name: "malformed", code: "invalid_response", transport: func(request *http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(strings.NewReader("not-json")), Request: request}, nil
|
|
}},
|
|
{name: "offline", code: "unreachable", transport: func(*http.Request) (*http.Response, error) {
|
|
return nil, errors.New("offline")
|
|
}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
runtime, err := newWithTransport(wasm, hex.EncodeToString(digest[:]), []string{"server_info"}, Limits{MemoryMB: 32, Timeout: 10 * time.Second, MaxResponseBytes: 1 << 20, MaxConcurrentCall: 2}, Binding{InstanceID: strings.Repeat("a", 20), ContainerPort: 8212, AllowedMethods: map[string]bool{"GET": true}, Configuration: map[string]string{"username": "admin"}, Secrets: map[string]string{"admin_password": "secret"}}, test.transport)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var response struct {
|
|
OK bool `json:"ok"`
|
|
Data map[string]any `json:"data"`
|
|
Error *struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Retryable bool `json:"retryable"`
|
|
} `json:"error"`
|
|
}
|
|
if err := runtime.Call(context.Background(), "get_server_info", struct{}{}, &response); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if response.OK || response.Error == nil || response.Error.Code != test.code || strings.Contains(response.Error.Message, "secret") {
|
|
t.Fatalf("unexpected safe failure: %+v", response)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPalworldAdapterExecutesInSandbox(t *testing.T) {
|
|
wasm, err := os.ReadFile("../../modules/palworld-rest/module.wasm")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(wasm)
|
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
if request.URL.Path != "/v1/api/info" || request.Header.Get("Authorization") == "" {
|
|
t.Fatalf("unexpected adapter request: %s", request.URL)
|
|
}
|
|
return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": {"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"version":"v1","servername":"test","description":"fixture","worldguid":"world"}`)), Request: request}, nil
|
|
})
|
|
runtime, err := newWithTransport(wasm, hex.EncodeToString(digest[:]), []string{"server_info"}, Limits{MemoryMB: 32, Timeout: 10 * time.Second, MaxResponseBytes: 1 << 20, MaxConcurrentCall: 2}, Binding{InstanceID: strings.Repeat("a", 20), ContainerPort: 8212, AllowedMethods: map[string]bool{"GET": true, "POST": true}, Configuration: map[string]string{"username": "admin"}, Secrets: map[string]string{"admin_password": "secret"}}, transport)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var response struct {
|
|
OK bool `json:"ok"`
|
|
Error any `json:"error"`
|
|
Data struct {
|
|
Name string `json:"name"`
|
|
GameVersion string `json:"game_version"`
|
|
Description string `json:"description"`
|
|
WorldID string `json:"world_id"`
|
|
} `json:"data"`
|
|
}
|
|
if err := runtime.Call(context.Background(), "get_server_info", struct{}{}, &response); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !response.OK || response.Data.Name != "test" {
|
|
t.Fatalf("unexpected normalized response: %+v", response)
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsChecksumAndUnknownCapability(t *testing.T) {
|
|
limits := Limits{MemoryMB: 32, Timeout: time.Second, MaxResponseBytes: 1024, MaxConcurrentCall: 1}
|
|
binding := Binding{InstanceID: strings.Repeat("a", 20), ContainerPort: 8212}
|
|
if _, err := newWithTransport([]byte("wasm"), strings.Repeat("0", 64), nil, limits, binding, roundTripFunc(nil)); err == nil {
|
|
t.Fatal("checksum mismatch accepted")
|
|
}
|
|
digest := sha256.Sum256([]byte("wasm"))
|
|
if _, err := newWithTransport([]byte("wasm"), hex.EncodeToString(digest[:]), []string{"shell"}, limits, binding, roundTripFunc(nil)); err == nil {
|
|
t.Fatal("unknown capability accepted")
|
|
}
|
|
}
|
|
|
|
func TestHTTPRequestPinsInstanceOriginAndDisablesRedirects(t *testing.T) {
|
|
instanceID := strings.Repeat("A", 20)
|
|
seen := false
|
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
seen = true
|
|
if request.URL.String() != "http://dogama-aaaaaaaaaaaaaaaaaaaa:8212/v1/api/info" {
|
|
t.Fatalf("unexpected destination %q", request.URL)
|
|
}
|
|
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"ok":true}`)), Request: request}, nil
|
|
})
|
|
digest := sha256.Sum256([]byte("wasm"))
|
|
runtime, err := newWithTransport([]byte("wasm"), hex.EncodeToString(digest[:]), nil, Limits{MemoryMB: 32, Timeout: time.Second, MaxResponseBytes: 1024, MaxConcurrentCall: 1}, Binding{InstanceID: instanceID, ContainerPort: 8212, AllowedMethods: map[string]bool{"GET": true}}, transport)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://dogama-aaaaaaaaaaaaaaaaaaaa:8212/v1/api/info", nil)
|
|
response, err := runtime.client.Do(request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response.Body.Close()
|
|
if !seen {
|
|
t.Fatal("bound transport was not used")
|
|
}
|
|
}
|