The new cmd/tunnel client keeps the API URL and tunnel identity in ~/.config/tunnel/config.json (0600), provisions the key/CSR and certificate, terminates TLS, proxies to local routes, reloads routes without a restart, and runs as a background service. `tunnel import` takes over an existing official CLI profile, preserving its tunnel ID, token, key and certificate. Server hardening: per-IP and per-tunnel rate limits for creates and binds, a CA cool-down after repeated ACME failures, a startup sweep for stale _acme-challenge records, admin metrics, a 60-second half-close timeout, and separate prioritized control / ordered data bridge queues (with `end` ordered after the final bytes). The dashboard shows the new metrics.
118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPorkbunDNS(t *testing.T) {
|
|
var createPath, createBody, deletePath string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, _ := io.ReadAll(r.Body)
|
|
switch {
|
|
case strings.HasPrefix(r.URL.Path, "/dns/create/"):
|
|
createPath, createBody = r.URL.Path, string(body)
|
|
w.Write([]byte(`{"status":"SUCCESS","id":"42"}`))
|
|
case strings.HasPrefix(r.URL.Path, "/dns/deleteByNameType/"):
|
|
deletePath = r.URL.Path
|
|
w.Write([]byte(`{"status":"SUCCESS"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := &PorkbunDNS{APIKey: "pk", SecretKey: "sk", Zone: "tzgyn.dev", BaseURL: srv.URL, HTTP: srv.Client()}
|
|
ctx := context.Background()
|
|
record := "_acme-challenge.abc123.tunnel.tzgyn.dev"
|
|
|
|
if err := p.CreateTXT(ctx, record, "challenge-value"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if createPath != "/dns/create/tzgyn.dev" {
|
|
t.Errorf("create path = %q", createPath)
|
|
}
|
|
var body map[string]string
|
|
if err := json.Unmarshal([]byte(createBody), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for key, want := range map[string]string{
|
|
"apikey": "pk", "secretapikey": "sk",
|
|
"name": "_acme-challenge.abc123.tunnel", "type": "TXT", "content": "challenge-value",
|
|
} {
|
|
if body[key] != want {
|
|
t.Errorf("create body %s = %q, want %q", key, body[key], want)
|
|
}
|
|
}
|
|
|
|
if err := p.DeleteTXT(ctx, record); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if deletePath != "/dns/deleteByNameType/tzgyn.dev/TXT/_acme-challenge.abc123.tunnel" {
|
|
t.Errorf("delete path = %q", deletePath)
|
|
}
|
|
|
|
// A record outside the zone fails before any request.
|
|
if err := p.CreateTXT(ctx, "other.example.com", "v"); err == nil {
|
|
t.Error("expected an error for a record outside the zone")
|
|
}
|
|
|
|
// API errors surface.
|
|
bad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(`{"status":"ERROR","message":"nope"}`))
|
|
}))
|
|
defer bad.Close()
|
|
p2 := &PorkbunDNS{APIKey: "pk", SecretKey: "sk", Zone: "tzgyn.dev", BaseURL: bad.URL, HTTP: bad.Client()}
|
|
if err := p2.CreateTXT(ctx, record, "v"); err == nil || !strings.Contains(err.Error(), "nope") {
|
|
t.Errorf("expected the API error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPorkbunSweepChallenges(t *testing.T) {
|
|
var deletes []string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.URL.Path == "/dns/retrieve/tzgyn.dev":
|
|
w.Write([]byte(`{"status":"SUCCESS","records":[
|
|
{"name":"_acme-challenge.a.tunnel.tzgyn.dev","type":"TXT","content":"one"},
|
|
{"name":"_acme-challenge.a.tunnel.tzgyn.dev","type":"TXT","content":"two"},
|
|
{"name":"_acme-challenge.b.tunnel.tzgyn.dev","type":"TXT","content":"three"},
|
|
{"name":"www.tzgyn.dev","type":"TXT","content":"keep"},
|
|
{"name":"_acme-challenge.other.example.com","type":"TXT","content":"outside"}
|
|
]}`))
|
|
case strings.HasPrefix(r.URL.Path, "/dns/deleteByNameType/"):
|
|
deletes = append(deletes, r.URL.Path)
|
|
w.Write([]byte(`{"status":"SUCCESS"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := &PorkbunDNS{APIKey: "pk", SecretKey: "sk", Zone: "tzgyn.dev", BaseURL: srv.URL, HTTP: srv.Client()}
|
|
count, err := p.SweepChallenges(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 2 {
|
|
t.Errorf("count = %d, want 2", count)
|
|
}
|
|
want := []string{
|
|
"/dns/deleteByNameType/tzgyn.dev/TXT/_acme-challenge.a.tunnel",
|
|
"/dns/deleteByNameType/tzgyn.dev/TXT/_acme-challenge.b.tunnel",
|
|
}
|
|
if len(deletes) != len(want) {
|
|
t.Fatalf("deletes = %v, want %v", deletes, want)
|
|
}
|
|
for i := range want {
|
|
if deletes[i] != want[i] {
|
|
t.Errorf("delete[%d] = %q, want %q", i, deletes[i], want[i])
|
|
}
|
|
}
|
|
}
|