happ_local_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. package service
  2. import (
  3. "context"
  4. "crypto/rsa"
  5. "crypto/sha256"
  6. "encoding/pem"
  7. "errors"
  8. "fmt"
  9. "net"
  10. "net/http"
  11. "strings"
  12. "sync/atomic"
  13. "testing"
  14. )
  15. func TestHappGenerateLocallyWithoutNetwork(t *testing.T) {
  16. initHappTestDB(t)
  17. client := seedHappClient(t, "local-only")
  18. configureHappSubscription(t, true, "https://sub.example/sub/")
  19. configureHappLinkGate(t, true)
  20. var calls atomic.Int32
  21. previous := http.DefaultTransport
  22. // Fail before opening a socket, including clients cloned from the default transport.
  23. http.DefaultTransport = &http.Transport{DialContext: func(context.Context, string, string) (net.Conn, error) {
  24. calls.Add(1)
  25. return nil, errors.New("network is unavailable in the local-generation test")
  26. }}
  27. t.Cleanup(func() { http.DefaultTransport = previous })
  28. svc := NewHappService(&ClientService{}, &SettingService{})
  29. result, err := svc.Generate(context.Background(), client.Id, "panel.example")
  30. if err != nil || !strings.HasPrefix(result.EncryptedLink, "happ://crypt5/") {
  31. t.Fatalf("local generation = %#v, %v; network attempts = %d", result, err, calls.Load())
  32. }
  33. if calls.Load() != 0 {
  34. t.Fatalf("local generation attempted %d network connections", calls.Load())
  35. }
  36. }
  37. func TestHappEncryptPreservesUTF8AndEnforcesResourceLimit(t *testing.T) {
  38. key, err := syntheticHappKey()
  39. if err != nil {
  40. t.Fatal(err)
  41. }
  42. for _, tc := range []struct {
  43. name, source string
  44. wantError error
  45. }{
  46. {"unicode URL", "https://example.com/中文?emoji=🔒&literal=%2F&x=a+b", nil},
  47. {"501 ASCII bytes", "https://example.com/" + strings.Repeat("a", 481), nil},
  48. {"502 ASCII bytes", "https://example.com/" + strings.Repeat("a", 482), nil},
  49. {"501 UTF8 bytes", "https://example.com/" + strings.Repeat("界", 160) + "a", nil},
  50. {"502 UTF8 bytes", "https://example.com/" + strings.Repeat("界", 160) + "ab", nil},
  51. {"8192 ASCII bytes", "https://example.com/" + strings.Repeat("a", 8172), nil},
  52. {"8193 ASCII bytes", "https://example.com/" + strings.Repeat("a", 8173), ErrHappSourceTooLong},
  53. {"8192 UTF8 bytes", "https://example.com/" + strings.Repeat("界", 2724), nil},
  54. {"8193 UTF8 bytes", "https://example.com/" + strings.Repeat("界", 2724) + "a", ErrHappSourceTooLong},
  55. {"raw query and fragment", "https://example.com/s%2fb?a=one+two&b=%2B#标题", nil},
  56. {"empty URL", "", ErrHappLinkUnavailable},
  57. {"invalid URL", "not-a-url", ErrHappLinkUnavailable},
  58. {"unsupported scheme", "file:///tmp/sub", ErrHappLinkUnavailable},
  59. {"empty host", "https:///sub", ErrHappLinkUnavailable},
  60. {"control character", "https://example.com/a\nb", ErrHappLinkUnavailable},
  61. {"Unicode control", "https://example.com/a\u0085b", ErrHappLinkUnavailable},
  62. {"invalid UTF8", "https://example.com/" + string([]byte{0xff}), ErrHappLinkUnavailable},
  63. {"userinfo", "https://user:[email protected]/sub", ErrHappLinkUnavailable},
  64. {"opaque URL", "https:sub", ErrHappLinkUnavailable},
  65. } {
  66. t.Run(tc.name, func(t *testing.T) {
  67. link, err := encryptHappSource(tc.source, &key.PublicKey)
  68. if !errors.Is(err, tc.wantError) {
  69. t.Fatalf("error = %v, want %v", err, tc.wantError)
  70. }
  71. if tc.wantError != nil {
  72. if link != "" {
  73. t.Fatal("failed encryption returned a link")
  74. }
  75. return
  76. }
  77. if got := decryptHappTestLink(t, link, key); got != tc.source {
  78. t.Fatalf("source was changed or truncated: %q", got)
  79. }
  80. })
  81. }
  82. for _, invalidKey := range []*rsa.PublicKey{nil, {N: key.N, E: 0}} {
  83. link, err := encryptHappSource("https://example.com/sub", invalidKey)
  84. if !errors.Is(err, ErrHappLinkUnavailable) || link != "" {
  85. t.Fatalf("invalid key result = %q, %v", link, err)
  86. }
  87. }
  88. }
  89. func TestHappEncryptUsesClientValidatedPublicKey(t *testing.T) {
  90. block, _ := pem.Decode([]byte(happPublicKeyPEM))
  91. if block == nil {
  92. t.Fatal("missing public key")
  93. }
  94. // Pin the marker's public key from the accepted Android/Windows Crypt5 probe.
  95. if got := fmt.Sprintf("%x", sha256.Sum256(block.Bytes)); got != "22319c7b13647897bf5fd4f827ba92bf3946d738007a0054ccd931c31f221768" {
  96. t.Fatalf("unvalidated public key: %s", got)
  97. }
  98. first, err := encryptHappLink("https://example.com/sub")
  99. if err != nil {
  100. t.Fatal(err)
  101. }
  102. second, err := encryptHappLink("https://example.com/sub")
  103. if err != nil || len(first) != 795 || !strings.HasPrefix(first, "happ://crypt5/") || first == second {
  104. t.Fatalf("expected fresh crypt5 ciphertext: length=%d, err=%v", len(first), err)
  105. }
  106. }
  107. func TestHappEncryptUsesFreshSessionKeysAndNonces(t *testing.T) {
  108. key, err := syntheticHappKey()
  109. if err != nil {
  110. t.Fatal(err)
  111. }
  112. keys, nonces := map[string]bool{}, map[string]bool{}
  113. for range 8 {
  114. link, err := encryptHappSource("https://example.com/sub", &key.PublicKey)
  115. if err != nil {
  116. t.Fatal(err)
  117. }
  118. decoded := decodeHappTestLink(t, link, key)
  119. if keys[string(decoded.key)] || nonces[string(decoded.nonce)] {
  120. t.Fatal("generation reused a session key or nonce")
  121. }
  122. keys[string(decoded.key)], nonces[string(decoded.nonce)] = true, true
  123. }
  124. }