HTTP API per the recorded contract, bridge WebSocket, SNI relay with pass-through tunnel TLS, local CA issuance, JSON store. Verified end-to-end with the official opentunnel CLI.
188 lines
4.7 KiB
Go
188 lines
4.7 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// The spec vectors are shared with the reference implementation
|
|
// (.reference/opentunnel/spec/vectors).
|
|
|
|
type frameVector struct {
|
|
Conn uint32 `json:"conn"`
|
|
Payload string `json:"payload"`
|
|
Frame string `json:"frame"`
|
|
}
|
|
|
|
func TestFrameVectors(t *testing.T) {
|
|
var vectors []frameVector
|
|
readVector(t, "frames.json", &vectors)
|
|
for i, v := range vectors {
|
|
payload, err := hex.DecodeString(v.Payload)
|
|
if err != nil {
|
|
t.Fatalf("vector %d payload: %v", i, err)
|
|
}
|
|
want, err := hex.DecodeString(v.Frame)
|
|
if err != nil {
|
|
t.Fatalf("vector %d frame: %v", i, err)
|
|
}
|
|
if got := buildFrame(v.Conn, payload); !equalBytes(got, want) {
|
|
t.Errorf("vector %d: buildFrame = %x, want %x", i, got, want)
|
|
}
|
|
conn, gotPayload, ok := parseFrame(want)
|
|
if !ok || conn != v.Conn || !equalBytes(gotPayload, payload) {
|
|
t.Errorf("vector %d: parseFrame = (%d, %x, %v)", i, conn, gotPayload, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
type controlVector struct {
|
|
Client []json.RawMessage `json:"client"`
|
|
Server []json.RawMessage `json:"server"`
|
|
Ignored []json.RawMessage `json:"ignored"`
|
|
Invalid []json.RawMessage `json:"invalid"`
|
|
}
|
|
|
|
func TestControlVectors(t *testing.T) {
|
|
var vectors controlVector
|
|
readVector(t, "control.json", &vectors)
|
|
for i, raw := range vectors.Client {
|
|
msg, err := parseClientControl(raw)
|
|
if err != nil || msg.Type == "" {
|
|
t.Errorf("client vector %d: %v", i, err)
|
|
}
|
|
}
|
|
for i, raw := range vectors.Server {
|
|
var probe struct {
|
|
Type string `json:"type"`
|
|
}
|
|
if err := json.Unmarshal(raw, &probe); err != nil || probe.Type == "" {
|
|
t.Errorf("server vector %d should decode", i)
|
|
}
|
|
}
|
|
for i, raw := range vectors.Ignored {
|
|
if _, err := parseClientControl(raw); err != nil {
|
|
t.Errorf("ignored vector %d should still decode: %v", i, err)
|
|
}
|
|
}
|
|
// The invalid vectors either fail to decode or miss required fields.
|
|
for i, raw := range vectors.Invalid {
|
|
msg, err := parseClientControl(raw)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
switch msg.Type {
|
|
case "": // {"conn":1} has no type
|
|
case "end": // {"type":"end"} misses conn
|
|
if msg.Conn != nil {
|
|
t.Errorf("invalid vector %d: end with conn?", i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAttachedVectorShape(t *testing.T) {
|
|
// Our attached message must marshal to exactly the vector's key order.
|
|
var vectors controlVector
|
|
readVector(t, "control.json", &vectors)
|
|
var want map[string]any
|
|
json.Unmarshal(vectors.Server[0], &want)
|
|
raw, err := json.Marshal(attachedMessage{
|
|
Type: "attached", Session: "sess_1", Routes: []string{"api"},
|
|
HeartbeatMs: 15000, IdleTimeoutMs: 45000,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got map[string]any
|
|
json.Unmarshal(raw, &got)
|
|
if len(got) != len(want) {
|
|
t.Fatalf("attached fields = %v, want %v", got, want)
|
|
}
|
|
for key := range want {
|
|
if _, ok := got[key]; !ok {
|
|
t.Errorf("attached misses %q", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
type namesVector struct {
|
|
Route []struct {
|
|
Value string `json:"value"`
|
|
Valid bool `json:"valid"`
|
|
} `json:"route"`
|
|
}
|
|
|
|
func TestRouteNameVectors(t *testing.T) {
|
|
var vectors namesVector
|
|
readVector(t, "names.json", &vectors)
|
|
for _, v := range vectors.Route {
|
|
if got := validRoute(v.Value); got != v.Valid {
|
|
t.Errorf("validRoute(%q) = %v, want %v", v.Value, got, v.Valid)
|
|
}
|
|
}
|
|
}
|
|
|
|
type routeVector struct {
|
|
Hostname string `json:"hostname"`
|
|
SNI string `json:"sni"`
|
|
Route *string `json:"route"`
|
|
}
|
|
|
|
func TestRouteVectors(t *testing.T) {
|
|
var vectors []routeVector
|
|
readVector(t, "routes.json", &vectors)
|
|
const domain = "opentunnel.xyz"
|
|
for _, v := range vectors {
|
|
id, route, ok := routeForSNI(domain, v.SNI)
|
|
// The vectors know which tunnel exists; the server closes visitors of
|
|
// unknown tunnels at lookup, so a foreign ID resolves to no route.
|
|
tunnelID := v.Hostname[:len(v.Hostname)-len("."+domain)]
|
|
if ok && id != tunnelID {
|
|
ok = false
|
|
}
|
|
if v.Route == nil {
|
|
if ok {
|
|
t.Errorf("sni %q: got route %q, want no match", v.SNI, route)
|
|
}
|
|
continue
|
|
}
|
|
if !ok || route != *v.Route {
|
|
t.Errorf("sni %q: got (%q, %q, %v), want route %q", v.SNI, id, route, ok, *v.Route)
|
|
}
|
|
}
|
|
}
|
|
|
|
func readVector(t *testing.T, name string, out any) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join(vectorDir(t), name))
|
|
if err != nil {
|
|
t.Skipf("spec vectors not available: %v", err)
|
|
}
|
|
if err := json.Unmarshal(raw, out); err != nil {
|
|
t.Fatalf("parsing %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
// vectorDir locates the shared vectors: the reference checkout lives in
|
|
// .reference/opentunnel at the repository root.
|
|
func vectorDir(t *testing.T) string {
|
|
t.Helper()
|
|
return filepath.Join("..", "..", ".reference", "opentunnel", "spec", "vectors")
|
|
}
|
|
|
|
func equalBytes(a, b []byte) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|