phone.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. package vowifi
  2. import (
  3. "net/url"
  4. "strings"
  5. "unicode"
  6. )
  7. // ExtractAssociatedMSISDN accepts only identities explicitly associated by
  8. // IMS. It never examines SIMIdentity.IMSI and deliberately rejects a bare SIP
  9. // user that could be an IMSI.
  10. func ExtractAssociatedMSISDN(evidence IMSEvidence) (number string, source string, ok bool) {
  11. if number, ok := normalizeAssociatedIdentity(evidence.AssociatedMSISDN, true); ok {
  12. return number, PhoneSourceAssociatedMSISDN, true
  13. }
  14. for _, associatedURI := range evidence.PAssociatedURI {
  15. if number, ok := normalizeAssociatedIdentity(associatedURI, false); ok {
  16. return number, PhoneSourcePAssociatedURI, true
  17. }
  18. }
  19. return "", "", false
  20. }
  21. func normalizeAssociatedIdentity(raw string, explicit bool) (string, bool) {
  22. value := strings.TrimSpace(raw)
  23. value = strings.Trim(value, "<>\"'")
  24. if value == "" {
  25. return "", false
  26. }
  27. lower := strings.ToLower(value)
  28. typedURI := false
  29. switch {
  30. case strings.HasPrefix(lower, "tel:"):
  31. typedURI = true
  32. value = value[len("tel:"):]
  33. case strings.HasPrefix(lower, "sip:"):
  34. typedURI = true
  35. value = value[len("sip:"):]
  36. if at := strings.IndexByte(value, '@'); at >= 0 {
  37. value = value[:at]
  38. }
  39. // A bare, non-E.164 SIP user is commonly an IMSI-derived IMPU.
  40. if !strings.HasPrefix(strings.TrimSpace(value), "+") {
  41. return "", false
  42. }
  43. case strings.HasPrefix(lower, "sips:"):
  44. typedURI = true
  45. value = value[len("sips:"):]
  46. if at := strings.IndexByte(value, '@'); at >= 0 {
  47. value = value[:at]
  48. }
  49. if !strings.HasPrefix(strings.TrimSpace(value), "+") {
  50. return "", false
  51. }
  52. default:
  53. if at := strings.IndexByte(value, '@'); at >= 0 {
  54. value = value[:at]
  55. } else if !explicit {
  56. // P-Associated-URI entries must be typed URIs; accepting arbitrary
  57. // digit strings here risks treating an IMSI as an MSISDN.
  58. return "", false
  59. }
  60. }
  61. if semicolon := strings.IndexByte(value, ';'); semicolon >= 0 {
  62. if !typedURI {
  63. return "", false
  64. }
  65. value = value[:semicolon]
  66. }
  67. if question := strings.IndexByte(value, '?'); question >= 0 {
  68. if !typedURI {
  69. return "", false
  70. }
  71. value = value[:question]
  72. }
  73. if decoded, err := url.PathUnescape(value); err == nil {
  74. value = decoded
  75. }
  76. var normalized strings.Builder
  77. for _, character := range strings.TrimSpace(value) {
  78. switch {
  79. case character == '+' && normalized.Len() == 0:
  80. normalized.WriteRune(character)
  81. case character >= '0' && character <= '9':
  82. normalized.WriteRune(character)
  83. case unicode.IsSpace(character), character == '-', character == '(', character == ')':
  84. // Human formatting is harmless once the identity source is trusted.
  85. default:
  86. return "", false
  87. }
  88. }
  89. number := normalized.String()
  90. if !strings.HasPrefix(number, "+") {
  91. return "", false
  92. }
  93. digits := strings.TrimPrefix(number, "+")
  94. if !isNDigits(digits, 5, 15) {
  95. return "", false
  96. }
  97. return number, true
  98. }