3cx_abandoned.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. #!/usr/bin/env python3
  2. import base64
  3. import re
  4. import requests
  5. from dataclasses import dataclass
  6. URL = "https://schmid-gartenpflanzen.on3cx.de/MyPhone/MPWebService.asmx"
  7. SESSION = "b7a74b60-7987-d1ae-9189-49dd1c48803a"
  8. @dataclass
  9. class AbandonedCall:
  10. call_id: int
  11. number: str | None
  12. queue: str | None
  13. raw: bytes
  14. def read_varint(data, pos):
  15. value = 0
  16. shift = 0
  17. while pos < len(data):
  18. b = data[pos]
  19. pos += 1
  20. value |= (b & 0x7f) << shift
  21. if not b & 0x80:
  22. return value, pos
  23. shift += 7
  24. raise ValueError("unterminated varint")
  25. def fields(data):
  26. pos = 0
  27. while pos < len(data):
  28. key, pos = read_varint(data, pos)
  29. field_no = key >> 3
  30. wire = key & 7
  31. if wire == 0:
  32. value, pos = read_varint(data, pos)
  33. elif wire == 2:
  34. length, pos = read_varint(data, pos)
  35. value = data[pos:pos + length]
  36. pos += length
  37. elif wire == 1:
  38. value = data[pos:pos + 8]
  39. pos += 8
  40. elif wire == 5:
  41. value = data[pos:pos + 4]
  42. pos += 4
  43. else:
  44. raise ValueError(f"unsupported wire type {wire}")
  45. yield field_no, wire, value
  46. def strings(data):
  47. out = []
  48. for field_no, wire, value in fields(data):
  49. if wire == 2:
  50. try:
  51. text = value.decode("utf-8")
  52. if text.isprintable() and len(text) >= 2:
  53. out.append((field_no, text))
  54. except UnicodeDecodeError:
  55. pass
  56. return out
  57. def find_call_messages(data):
  58. """
  59. Command 145 liefert eine Liste verschachtelter Call-Messages.
  60. Wir suchen rekursiv nach Messages, die eine E.164-Nummer enthalten.
  61. """
  62. results = []
  63. def walk(blob):
  64. try:
  65. fs = list(fields(blob))
  66. except Exception:
  67. return
  68. text = strings(blob)
  69. numbers = [
  70. value for _, value in text
  71. if re.fullmatch(r"\+\d{6,20}", value)
  72. ]
  73. if numbers:
  74. # bekannte Struktur: irgendwo in derselben Message
  75. # liegt die numerische Call-ID als varint.
  76. varints = [
  77. value for _, wire, value in fs
  78. if wire == 0 and isinstance(value, int)
  79. ]
  80. # IDs wie 11626/11627 sind hier besonders interessant.
  81. ids = [v for v in varints if 1000 <= v <= 10000000]
  82. if ids:
  83. results.append((ids[0], numbers[0], blob))
  84. for _, wire, value in fs:
  85. if wire == 2 and isinstance(value, bytes) and len(value) > 2:
  86. walk(value)
  87. walk(data)
  88. return results
  89. def main():
  90. # Command 145, beobachteter Request:
  91. # 08 91 01 8a 09 06 10 50 18 00 22 00
  92. payload = bytes.fromhex("08 91 01 8a 09 06 10 50 18 00 22 00")
  93. r = requests.post(
  94. URL,
  95. headers={
  96. "Accept": "application/octet-stream",
  97. "Content-Type": "application/octet-stream",
  98. "MyPhoneSession": SESSION,
  99. "Origin": "https://schmid-gartenpflanzen.on3cx.de",
  100. },
  101. data=payload,
  102. verify=False,
  103. timeout=15,
  104. )
  105. r.raise_for_status()
  106. print(f"HTTP: {r.status_code}")
  107. print(f"Response: {len(r.content)} bytes")
  108. calls = find_call_messages(r.content)
  109. seen = set()
  110. for call_id, number, raw in calls:
  111. key = (call_id, number)
  112. if key in seen:
  113. continue
  114. seen.add(key)
  115. print(f"{call_id:>8} {number}")
  116. if __name__ == "__main__":
  117. main()