Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

util.py 32KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044
  1. ###############################################################################
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (c) typedef int GmbH
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in
  15. # all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", fWITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. # THE SOFTWARE.
  24. #
  25. ###############################################################################
  26. import inspect
  27. import os
  28. import time
  29. import struct
  30. import sys
  31. import re
  32. import base64
  33. import math
  34. import random
  35. import binascii
  36. import socket
  37. import subprocess
  38. from collections import OrderedDict
  39. from typing import Optional
  40. from datetime import datetime, timedelta
  41. from pprint import pformat
  42. from array import array
  43. import txaio
  44. try:
  45. _TLS = True
  46. from OpenSSL import SSL
  47. except ImportError:
  48. _TLS = False
  49. __all__ = ("public",
  50. "encode_truncate",
  51. "xor",
  52. "utcnow",
  53. "utcstr",
  54. "id",
  55. "rid",
  56. "newid",
  57. "rtime",
  58. "Stopwatch",
  59. "Tracker",
  60. "EqualityMixin",
  61. "ObservableMixin",
  62. "IdGenerator",
  63. "generate_token",
  64. "generate_activation_code",
  65. "generate_serial_number",
  66. "generate_user_password",
  67. "machine_id",
  68. 'parse_keyfile',
  69. 'write_keyfile',
  70. "hl",
  71. "hltype",
  72. "hlid",
  73. "hluserid",
  74. "hlval",
  75. "hlcontract",
  76. "with_0x",
  77. "without_0x")
  78. def public(obj):
  79. """
  80. The public user API of Autobahn is marked using this decorator.
  81. Everything that is not decorated @public is library internal, can
  82. change at any time and should not be used in user program code.
  83. """
  84. try:
  85. obj._is_public = True
  86. except AttributeError:
  87. # FIXME: exceptions.AttributeError: 'staticmethod' object has no attribute '_is_public'
  88. pass
  89. return obj
  90. @public
  91. def encode_truncate(text, limit, encoding='utf8', return_encoded=True):
  92. """
  93. Given a string, return a truncated version of the string such that
  94. the UTF8 encoding of the string is smaller than the given limit.
  95. This function correctly truncates even in the presence of Unicode code
  96. points that encode to multi-byte encodings which must not be truncated
  97. in the middle.
  98. :param text: The (Unicode) string to truncate.
  99. :type text: str
  100. :param limit: The number of bytes to limit the UTF8 encoding to.
  101. :type limit: int
  102. :param encoding: Truncate the string in this encoding (default is ``utf-8``).
  103. :type encoding: str
  104. :param return_encoded: If ``True``, return the string encoded into bytes
  105. according to the specified encoding, else return the string as a string.
  106. :type return_encoded: bool
  107. :returns: The truncated string.
  108. :rtype: str or bytes
  109. """
  110. assert(text is None or type(text) == str)
  111. assert(type(limit) == int)
  112. assert(limit >= 0)
  113. if text is None:
  114. return
  115. # encode the given string in the specified encoding
  116. s = text.encode(encoding)
  117. # when the resulting byte string is longer than the given limit ..
  118. if len(s) > limit:
  119. # .. truncate, and
  120. s = s[:limit]
  121. # decode back, ignoring errors that result from truncation
  122. # in the middle of multi-byte encodings
  123. text = s.decode(encoding, 'ignore')
  124. if return_encoded:
  125. s = text.encode(encoding)
  126. if return_encoded:
  127. return s
  128. else:
  129. return text
  130. @public
  131. def xor(d1: bytes, d2: bytes) -> bytes:
  132. """
  133. XOR two binary strings of arbitrary (equal) length.
  134. :param d1: The first binary string.
  135. :param d2: The second binary string.
  136. :returns: XOR of the binary strings (``XOR(d1, d2)``)
  137. """
  138. if type(d1) != bytes:
  139. raise Exception("invalid type {} for d1 - must be binary".format(type(d1)))
  140. if type(d2) != bytes:
  141. raise Exception("invalid type {} for d2 - must be binary".format(type(d2)))
  142. if len(d1) != len(d2):
  143. raise Exception("cannot XOR binary string of differing length ({} != {})".format(len(d1), len(d2)))
  144. d1 = array('B', d1)
  145. d2 = array('B', d2)
  146. for i in range(len(d1)):
  147. d1[i] ^= d2[i]
  148. return d1.tobytes()
  149. @public
  150. def utcstr(ts=None):
  151. """
  152. Format UTC timestamp in ISO 8601 format.
  153. Note: to parse an ISO 8601 formatted string, use the **iso8601**
  154. module instead (e.g. ``iso8601.parse_date("2014-05-23T13:03:44.123Z")``).
  155. >>> txaio.time_ns()
  156. 1641121311914026419
  157. >>> int(iso8601.parse_date(utcnow()).timestamp() * 1000000000.)
  158. 1641121313209000192
  159. :param ts: The timestamp to format.
  160. :type ts: instance of :py:class:`datetime.datetime` or ``None``
  161. :returns: Timestamp formatted in ISO 8601 format.
  162. :rtype: str
  163. """
  164. assert(ts is None or isinstance(ts, datetime))
  165. if ts is None:
  166. ts = datetime.utcnow()
  167. return "{0}Z".format(ts.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3])
  168. @public
  169. def utcnow():
  170. """
  171. Get current time in UTC as ISO 8601 string.
  172. :returns: Current time as string in ISO 8601 format.
  173. :rtype: str
  174. """
  175. return utcstr()
  176. class IdGenerator(object):
  177. """
  178. ID generator for WAMP request IDs.
  179. WAMP request IDs are sequential per WAMP session, starting at 1 and
  180. wrapping around at 2**53 (both value are inclusive [1, 2**53]).
  181. The upper bound **2**53** is chosen since it is the maximum integer that can be
  182. represented as a IEEE double such that all smaller integers are representable as well.
  183. Hence, IDs can be safely used with languages that use IEEE double as their
  184. main (or only) number type (JavaScript, Lua, etc).
  185. See https://github.com/wamp-proto/wamp-proto/blob/master/spec/basic.md#ids
  186. """
  187. def __init__(self):
  188. self._next = 0 # starts at 1; next() pre-increments
  189. def next(self):
  190. """
  191. Returns next ID.
  192. :returns: The next ID.
  193. :rtype: int
  194. """
  195. self._next += 1
  196. if self._next > 9007199254740992:
  197. self._next = 1
  198. return self._next
  199. # generator protocol
  200. def __next__(self):
  201. return self.next()
  202. #
  203. # Performance comparison of IdGenerator.next(), id() and rid().
  204. #
  205. # All tests were performed on:
  206. #
  207. # - Ubuntu 14.04 LTS x86-64
  208. # - Intel Core i7 920 @ 3.3GHz
  209. #
  210. # The tests generated 100 mio. IDs and run-time was measured
  211. # as wallclock from Unix "time" command. In each run, a single CPU
  212. # core was essentially at 100% load all the time (though the sys/usr
  213. # ratio was different).
  214. #
  215. # PyPy 2.6.1:
  216. #
  217. # IdGenerator.next() 0.5s
  218. # id() 29.4s
  219. # rid() 106.1s
  220. #
  221. # CPython 2.7.10:
  222. #
  223. # IdGenerator.next() 49.0s
  224. # id() 370.5s
  225. # rid() 196.4s
  226. #
  227. #
  228. # Note on the ID range [0, 2**53]. We once reduced the range to [0, 2**31].
  229. # This lead to extremely hard to track down issues due to ID collisions!
  230. # Here: https://github.com/crossbario/autobahn-python/issues/419#issue-90483337
  231. #
  232. # 8 byte mask with 53 LSBs set (WAMP requires IDs from [0, 2**53]
  233. _WAMP_ID_MASK = struct.unpack(">Q", b"\x00\x1f\xff\xff\xff\xff\xff\xff")[0]
  234. def rid():
  235. """
  236. Generate a new random integer ID from range **[0, 2**53]**.
  237. The generated ID is uniformly distributed over the whole range, doesn't have
  238. a period (no pseudo-random generator is used) and cryptographically strong.
  239. The upper bound **2**53** is chosen since it is the maximum integer that can be
  240. represented as a IEEE double such that all smaller integers are representable as well.
  241. Hence, IDs can be safely used with languages that use IEEE double as their
  242. main (or only) number type (JavaScript, Lua, etc).
  243. :returns: A random integer ID.
  244. :rtype: int
  245. """
  246. return struct.unpack("@Q", os.urandom(8))[0] & _WAMP_ID_MASK
  247. # noinspection PyShadowingBuiltins
  248. def id():
  249. """
  250. Generate a new random integer ID from range **[0, 2**53]**.
  251. The generated ID is based on a pseudo-random number generator (Mersenne Twister,
  252. which has a period of 2**19937-1). It is NOT cryptographically strong, and
  253. hence NOT suitable to generate e.g. secret keys or access tokens.
  254. The upper bound **2**53** is chosen since it is the maximum integer that can be
  255. represented as a IEEE double such that all smaller integers are representable as well.
  256. Hence, IDs can be safely used with languages that use IEEE double as their
  257. main (or only) number type (JavaScript, Lua, etc).
  258. :returns: A random integer ID.
  259. :rtype: int
  260. """
  261. return random.randint(0, 9007199254740992)
  262. def newid(length=16):
  263. """
  264. Generate a new random string ID.
  265. The generated ID is uniformly distributed and cryptographically strong. It is
  266. hence usable for things like secret keys and access tokens.
  267. :param length: The length (in chars) of the ID to generate.
  268. :type length: int
  269. :returns: A random string ID.
  270. :rtype: str
  271. """
  272. l = int(math.ceil(float(length) * 6. / 8.))
  273. return base64.b64encode(os.urandom(l))[:length].decode('ascii')
  274. # a standard base36 character set
  275. # DEFAULT_TOKEN_CHARS = string.digits + string.ascii_uppercase
  276. # we take out the following 9 chars (leaving 27), because there
  277. # is visual ambiguity: 0/O/D, 1/I, 8/B, 2/Z
  278. DEFAULT_TOKEN_CHARS = '345679ACEFGHJKLMNPQRSTUVWXY'
  279. """
  280. Default set of characters to create rtokens from.
  281. """
  282. DEFAULT_ZBASE32_CHARS = '13456789abcdefghijkmnopqrstuwxyz'
  283. """
  284. Our choice of confusing characters to eliminate is: `0', `l', `v', and `2'. Our
  285. reasoning is that `0' is potentially mistaken for `o', that `l' is potentially
  286. mistaken for `1' or `i', that `v' is potentially mistaken for `' or `r'
  287. (especially in handwriting) and that `2' is potentially mistaken for `z'
  288. (especially in handwriting).
  289. Note that we choose to focus on typed and written transcription more than on
  290. vocal, since humans already have a well-established system of disambiguating
  291. spoken alphanumerics, such as the United States military's "Alpha Bravo Charlie
  292. Delta" and telephone operators' "Is that 'd' as in 'dog'?".
  293. * http://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
  294. """
  295. @public
  296. def generate_token(char_groups: int,
  297. chars_per_group: int,
  298. chars: Optional[str] = None,
  299. sep: Optional[str] = None,
  300. lower_case: Optional[bool] = False) -> str:
  301. """
  302. Generate cryptographically strong tokens, which are strings like `M6X5-YO5W-T5IK`.
  303. These can be used e.g. for used-only-once activation tokens or the like.
  304. The returned token has an entropy of
  305. ``math.log(len(chars), 2.) * chars_per_group * char_groups``
  306. bits.
  307. With the default charset and 4 characters per group, ``generate_token()`` produces
  308. strings with the following entropy:
  309. ================ =================== ========================================
  310. character groups entropy (at least) recommended use
  311. ================ =================== ========================================
  312. 2 38 bits
  313. 3 57 bits one-time activation or pairing code
  314. 4 76 bits secure user password
  315. 5 95 bits
  316. 6 114 bits globally unique serial / product code
  317. 7 133 bits
  318. ================ =================== ========================================
  319. Here are some examples:
  320. * token(3): ``9QXT-UXJW-7R4H``
  321. * token(4): ``LPNN-JMET-KWEP-YK45``
  322. * token(6): ``NXW9-74LU-6NUH-VLPV-X6AG-QUE3``
  323. :param char_groups: Number of character groups (or characters if chars_per_group == 1).
  324. :param chars_per_group: Number of characters per character group (or 1 to return a token with no grouping).
  325. :param chars: Characters to choose from. Default is 27 character subset
  326. of the ISO basic Latin alphabet (see: ``DEFAULT_TOKEN_CHARS``).
  327. :param sep: When separating groups in the token, the separater string.
  328. :param lower_case: If ``True``, generate token in lower-case.
  329. :returns: The generated token.
  330. """
  331. assert(type(char_groups) == int)
  332. assert(type(chars_per_group) == int)
  333. assert(chars is None or type(chars) == str), 'chars must be str, was {}'.format(type(chars))
  334. chars = chars or DEFAULT_TOKEN_CHARS
  335. if lower_case:
  336. chars = chars.lower()
  337. sep = sep or '-'
  338. rng = random.SystemRandom()
  339. token_value = ''.join(rng.choice(chars) for _ in range(char_groups * chars_per_group))
  340. if chars_per_group > 1:
  341. return sep.join(map(''.join, zip(*[iter(token_value)] * chars_per_group)))
  342. else:
  343. return token_value
  344. @public
  345. def generate_activation_code():
  346. """
  347. Generate a one-time activation code or token of the form ``'W97F-96MJ-YGJL'``.
  348. The generated value is cryptographically strong and has (at least) 57 bits of entropy.
  349. :returns: The generated activation code.
  350. :rtype: str
  351. """
  352. return generate_token(char_groups=3, chars_per_group=4, chars=DEFAULT_TOKEN_CHARS, sep='-', lower_case=False)
  353. _PAT_ACTIVATION_CODE = re.compile('^([' + DEFAULT_TOKEN_CHARS + ']{4,4})-([' + DEFAULT_TOKEN_CHARS + ']{4,4})-([' + DEFAULT_TOKEN_CHARS + ']{4,4})$')
  354. @public
  355. def parse_activation_code(code: str):
  356. """
  357. Parse an activation code generated by :func:<autobahn.util.generate_activation_code>:
  358. .. code:: console
  359. "RWCN-94NV-CEHR" -> ("RWCN", "94NV", "CEHR") | None
  360. :param code: The code to parse, e.g. ``'W97F-96MJ-YGJL'``.
  361. :return: If the string is a properly conforming activation code, return
  362. the matched pattern, otherwise return ``None``.
  363. """
  364. return _PAT_ACTIVATION_CODE.match(code)
  365. @public
  366. def generate_user_password():
  367. """
  368. Generate a secure, random user password of the form ``'kgojzi61dn5dtb6d'``.
  369. The generated value is cryptographically strong and has (at least) 76 bits of entropy.
  370. :returns: The generated password.
  371. :rtype: str
  372. """
  373. return generate_token(char_groups=16, chars_per_group=1, chars=DEFAULT_ZBASE32_CHARS, sep='-', lower_case=True)
  374. @public
  375. def generate_serial_number():
  376. """
  377. Generate a globally unique serial / product code of the form ``'YRAC-EL4X-FQQE-AW4T-WNUV-VN6T'``.
  378. The generated value is cryptographically strong and has (at least) 114 bits of entropy.
  379. :returns: The generated serial number / product code.
  380. :rtype: str
  381. """
  382. return generate_token(char_groups=6, chars_per_group=4, chars=DEFAULT_TOKEN_CHARS, sep='-', lower_case=False)
  383. # Select the most precise walltime measurement function available
  384. # on the platform
  385. #
  386. if sys.platform.startswith('win'):
  387. # On Windows, this function returns wall-clock seconds elapsed since the
  388. # first call to this function, as a floating point number, based on the
  389. # Win32 function QueryPerformanceCounter(). The resolution is typically
  390. # better than one microsecond
  391. if sys.version_info >= (3, 8):
  392. _rtime = time.perf_counter
  393. else:
  394. _rtime = time.clock
  395. _ = _rtime() # this starts wallclock
  396. else:
  397. # On Unix-like platforms, this used the first available from this list:
  398. # (1) gettimeofday() -- resolution in microseconds
  399. # (2) ftime() -- resolution in milliseconds
  400. # (3) time() -- resolution in seconds
  401. _rtime = time.time
  402. @public
  403. def rtime():
  404. """
  405. Precise, fast wallclock time.
  406. :returns: The current wallclock in seconds. Returned values are only guaranteed
  407. to be meaningful relative to each other.
  408. :rtype: float
  409. """
  410. return _rtime()
  411. class Stopwatch(object):
  412. """
  413. Stopwatch based on walltime.
  414. This can be used to do code timing and uses the most precise walltime measurement
  415. available on the platform. This is a very light-weight object,
  416. so create/dispose is very cheap.
  417. """
  418. def __init__(self, start=True):
  419. """
  420. :param start: If ``True``, immediately start the stopwatch.
  421. :type start: bool
  422. """
  423. self._elapsed = 0
  424. if start:
  425. self._started = rtime()
  426. self._running = True
  427. else:
  428. self._started = None
  429. self._running = False
  430. def elapsed(self):
  431. """
  432. Return total time elapsed in seconds during which the stopwatch was running.
  433. :returns: The elapsed time in seconds.
  434. :rtype: float
  435. """
  436. if self._running:
  437. now = rtime()
  438. return self._elapsed + (now - self._started)
  439. else:
  440. return self._elapsed
  441. def pause(self):
  442. """
  443. Pauses the stopwatch and returns total time elapsed in seconds during which
  444. the stopwatch was running.
  445. :returns: The elapsed time in seconds.
  446. :rtype: float
  447. """
  448. if self._running:
  449. now = rtime()
  450. self._elapsed += now - self._started
  451. self._running = False
  452. return self._elapsed
  453. else:
  454. return self._elapsed
  455. def resume(self):
  456. """
  457. Resumes a paused stopwatch and returns total elapsed time in seconds
  458. during which the stopwatch was running.
  459. :returns: The elapsed time in seconds.
  460. :rtype: float
  461. """
  462. if not self._running:
  463. self._started = rtime()
  464. self._running = True
  465. return self._elapsed
  466. else:
  467. now = rtime()
  468. return self._elapsed + (now - self._started)
  469. def stop(self):
  470. """
  471. Stops the stopwatch and returns total time elapsed in seconds during which
  472. the stopwatch was (previously) running.
  473. :returns: The elapsed time in seconds.
  474. :rtype: float
  475. """
  476. elapsed = self.pause()
  477. self._elapsed = 0
  478. self._started = None
  479. self._running = False
  480. return elapsed
  481. class Tracker(object):
  482. """
  483. A key-based statistics tracker.
  484. """
  485. def __init__(self, tracker, tracked):
  486. """
  487. """
  488. self.tracker = tracker
  489. self.tracked = tracked
  490. self._timings = {}
  491. self._offset = rtime()
  492. self._dt_offset = datetime.utcnow()
  493. def track(self, key):
  494. """
  495. Track elapsed for key.
  496. :param key: Key under which to track the timing.
  497. :type key: str
  498. """
  499. self._timings[key] = rtime()
  500. def diff(self, start_key, end_key, formatted=True):
  501. """
  502. Get elapsed difference between two previously tracked keys.
  503. :param start_key: First key for interval (older timestamp).
  504. :type start_key: str
  505. :param end_key: Second key for interval (younger timestamp).
  506. :type end_key: str
  507. :param formatted: If ``True``, format computed time period and return string.
  508. :type formatted: bool
  509. :returns: Computed time period in seconds (or formatted string).
  510. :rtype: float or str
  511. """
  512. if end_key in self._timings and start_key in self._timings:
  513. d = self._timings[end_key] - self._timings[start_key]
  514. if formatted:
  515. if d < 0.00001: # 10us
  516. s = "%d ns" % round(d * 1000000000.)
  517. elif d < 0.01: # 10ms
  518. s = "%d us" % round(d * 1000000.)
  519. elif d < 10: # 10s
  520. s = "%d ms" % round(d * 1000.)
  521. else:
  522. s = "%d s" % round(d)
  523. return s.rjust(8)
  524. else:
  525. return d
  526. else:
  527. if formatted:
  528. return "n.a.".rjust(8)
  529. else:
  530. return None
  531. def absolute(self, key):
  532. """
  533. Return the UTC wall-clock time at which a tracked event occurred.
  534. :param key: The key
  535. :type key: str
  536. :returns: Timezone-naive datetime.
  537. :rtype: instance of :py:class:`datetime.datetime`
  538. """
  539. elapsed = self[key]
  540. if elapsed is None:
  541. raise KeyError("No such key \"%s\"." % elapsed)
  542. return self._dt_offset + timedelta(seconds=elapsed)
  543. def __getitem__(self, key):
  544. if key in self._timings:
  545. return self._timings[key] - self._offset
  546. else:
  547. return None
  548. def __iter__(self):
  549. return self._timings.__iter__()
  550. def __str__(self):
  551. return pformat(self._timings)
  552. class EqualityMixin(object):
  553. """
  554. Mixing to add equality comparison operators to a class.
  555. Two objects are identical under this mixin, if and only if:
  556. 1. both object have the same class
  557. 2. all non-private object attributes are equal
  558. """
  559. def __eq__(self, other):
  560. """
  561. Compare this object to another object for equality.
  562. :param other: The other object to compare with.
  563. :type other: obj
  564. :returns: ``True`` iff the objects are equal.
  565. :rtype: bool
  566. """
  567. if not isinstance(other, self.__class__):
  568. return False
  569. # we only want the actual message data attributes (not eg _serialize)
  570. for k in self.__dict__:
  571. if not k.startswith('_'):
  572. if not self.__dict__[k] == other.__dict__[k]:
  573. return False
  574. return True
  575. # return (isinstance(other, self.__class__) and self.__dict__ == other.__dict__)
  576. def __ne__(self, other):
  577. """
  578. Compare this object to another object for inequality.
  579. :param other: The other object to compare with.
  580. :type other: obj
  581. :returns: ``True`` iff the objects are not equal.
  582. :rtype: bool
  583. """
  584. return not self.__eq__(other)
  585. def wildcards2patterns(wildcards):
  586. """
  587. Compute a list of regular expression patterns from a list of
  588. wildcard strings. A wildcard string uses '*' as a wildcard character
  589. matching anything.
  590. :param wildcards: List of wildcard strings to compute regular expression patterns for.
  591. :type wildcards: list of str
  592. :returns: Computed regular expressions.
  593. :rtype: list of obj
  594. """
  595. # note that we add the ^ and $ so that the *entire* string must
  596. # match. Without this, e.g. a prefix will match:
  597. # re.match('.*good\\.com', 'good.com.evil.com') # match!
  598. # re.match('.*good\\.com$', 'good.com.evil.com') # no match!
  599. return [re.compile('^' + wc.replace('.', r'\.').replace('*', '.*') + '$') for wc in wildcards]
  600. class ObservableMixin(object):
  601. """
  602. Internal utility for enabling event-listeners on particular objects
  603. """
  604. # A "helper" style composable class (as opposed to a mix-in) might
  605. # be a lot easier to deal with here. Having an __init__ method
  606. # with a "mix in" style class can be fragile and error-prone,
  607. # especially if it takes arguments. Since we don't use the
  608. # "parent" beavior anywhere, I didn't add a .set_parent() (yet?)
  609. # these are class-level globals; individual instances are
  610. # initialized as-needed (e.g. the first .on() call adds a
  611. # _listeners dict). Thus, subclasses don't have to call super()
  612. # properly etc.
  613. _parent = None
  614. _valid_events = None
  615. _listeners = None
  616. _results = None
  617. def set_valid_events(self, valid_events=None):
  618. """
  619. :param valid_events: if non-None, .on() or .fire() with an event
  620. not listed in valid_events raises an exception.
  621. """
  622. self._valid_events = list(valid_events)
  623. self._results = {k: None for k in self._valid_events}
  624. def _check_event(self, event):
  625. """
  626. Internal helper. Throws RuntimeError if we have a valid_events
  627. list, and the given event isnt' in it. Does nothing otherwise.
  628. """
  629. if self._valid_events and event not in self._valid_events:
  630. raise RuntimeError(
  631. "Invalid event '{event}'. Expected one of: {events}".format(
  632. event=event,
  633. events=', '.join(self._valid_events),
  634. )
  635. )
  636. def on(self, event, handler):
  637. """
  638. Add a handler for an event.
  639. :param event: the name of the event
  640. :param handler: a callable thats invoked when .fire() is
  641. called for this events. Arguments will be whatever are given
  642. to .fire()
  643. """
  644. # print("adding '{}' to '{}': {}".format(event, hash(self), handler))
  645. self._check_event(event)
  646. if self._listeners is None:
  647. self._listeners = dict()
  648. if event not in self._listeners:
  649. self._listeners[event] = []
  650. self._listeners[event].append(handler)
  651. def off(self, event=None, handler=None):
  652. """
  653. Stop listening for a single event, or all events.
  654. :param event: if None, remove all listeners. Otherwise, remove
  655. listeners for the single named event.
  656. :param handler: if None, remove all handlers for the named
  657. event; otherwise remove just the given handler.
  658. """
  659. if event is None:
  660. if handler is not None:
  661. # maybe this should mean "remove the given handler
  662. # from any event at all that contains it"...?
  663. raise RuntimeError(
  664. "Can't specificy a specific handler without an event"
  665. )
  666. self._listeners = dict()
  667. else:
  668. if self._listeners is None:
  669. return
  670. self._check_event(event)
  671. if event in self._listeners:
  672. if handler is None:
  673. del self._listeners[event]
  674. else:
  675. try:
  676. self._listeners[event].remove(handler)
  677. except ValueError:
  678. pass
  679. def fire(self, event, *args, **kwargs):
  680. """
  681. Fire a particular event.
  682. :param event: the event to fire. All other args and kwargs are
  683. passed on to the handler(s) for the event.
  684. :return: a Deferred/Future gathering all async results from
  685. all handlers and/or parent handlers.
  686. """
  687. # print("firing '{}' from '{}'".format(event, hash(self)))
  688. if self._listeners is None:
  689. return txaio.create_future(result=[])
  690. self._check_event(event)
  691. res = []
  692. for handler in self._listeners.get(event, []):
  693. future = txaio.as_future(handler, *args, **kwargs)
  694. res.append(future)
  695. if self._parent is not None:
  696. res.append(self._parent.fire(event, *args, **kwargs))
  697. d_res = txaio.gather(res, consume_exceptions=False)
  698. self._results[event] = d_res
  699. return d_res
  700. class _LazyHexFormatter(object):
  701. """
  702. This is used to avoid calling binascii.hexlify() on data given to
  703. log.debug() calls unless debug is active (for example). Like::
  704. self.log.debug(
  705. "Some data: {octets}",
  706. octets=_LazyHexFormatter(os.urandom(32)),
  707. )
  708. """
  709. __slots__ = ('obj',)
  710. def __init__(self, obj):
  711. self.obj = obj
  712. def __str__(self):
  713. return binascii.hexlify(self.obj).decode('ascii')
  714. def _is_tls_error(instance):
  715. """
  716. :returns: True if we have TLS support and 'instance' is an
  717. instance of :class:`OpenSSL.SSL.Error` otherwise False
  718. """
  719. if _TLS:
  720. return isinstance(instance, SSL.Error)
  721. return False
  722. def _maybe_tls_reason(instance):
  723. """
  724. :returns: a TLS error-message, or empty-string if 'instance' is
  725. not a TLS error.
  726. """
  727. if _is_tls_error(instance):
  728. ssl_error = instance.args[0][0]
  729. return "SSL error: {msg} (in {func})".format(
  730. func=ssl_error[1],
  731. msg=ssl_error[2],
  732. )
  733. return ""
  734. def machine_id() -> str:
  735. """
  736. For informational purposes, get a unique ID or serial for this machine (device).
  737. :returns: Unique machine (device) ID (serial), e.g. ``81655b901e334fc1ad59cbf2719806b7``.
  738. """
  739. from twisted.python.runtime import platform
  740. if platform.isLinux():
  741. try:
  742. # why this? see: http://0pointer.de/blog/projects/ids.html
  743. with open('/var/lib/dbus/machine-id', 'r') as f:
  744. return f.read().strip()
  745. except:
  746. # Non-dbus using Linux, get a hostname
  747. return socket.gethostname()
  748. elif platform.isMacOSX():
  749. import plistlib
  750. plist_data = subprocess.check_output(["ioreg", "-rd1", "-c", "IOPlatformExpertDevice", "-a"])
  751. return plistlib.loads(plist_data)[0]["IOPlatformSerialNumber"]
  752. else:
  753. return socket.gethostname()
  754. try:
  755. import click
  756. _HAS_CLICK = True
  757. except ImportError:
  758. _HAS_CLICK = False
  759. def hl(text, bold=False, color='yellow'):
  760. if not isinstance(text, str):
  761. text = '{}'.format(text)
  762. if _HAS_CLICK:
  763. return click.style(text, fg=color, bold=bold)
  764. else:
  765. return text
  766. def _qn(obj):
  767. if inspect.isclass(obj) or inspect.isfunction(obj) or inspect.ismethod(obj):
  768. qn = '{}.{}'.format(obj.__module__, obj.__qualname__)
  769. else:
  770. qn = 'unknown'
  771. return qn
  772. def hltype(obj):
  773. qn = _qn(obj).split('.')
  774. text = hl(qn[0], color='yellow', bold=True) + hl('.' + '.'.join(qn[1:]), color='yellow', bold=False)
  775. return '<' + text + '>'
  776. def hlid(oid):
  777. return hl('{}'.format(oid), color='blue', bold=True)
  778. def hluserid(oid):
  779. if not isinstance(oid, str):
  780. oid = '{}'.format(oid)
  781. return hl('"{}"'.format(oid), color='yellow', bold=True)
  782. def hlval(val, color='white', bold=True):
  783. return hl('{}'.format(val), color=color, bold=bold)
  784. def hlcontract(oid):
  785. if not isinstance(oid, str):
  786. oid = '{}'.format(oid)
  787. return hl('<{}>'.format(oid), color='magenta', bold=True)
  788. def with_0x(address):
  789. if address and not address.startswith('0x'):
  790. return '0x{address}'.format(address=address)
  791. return address
  792. def without_0x(address):
  793. if address and address.startswith('0x'):
  794. return address[2:]
  795. return address
  796. def write_keyfile(filepath, tags, msg):
  797. """
  798. Internal helper, write the given tags to the given file-
  799. """
  800. with open(filepath, 'w') as f:
  801. f.write(msg)
  802. for (tag, value) in tags.items():
  803. if value:
  804. f.write('{}: {}\n'.format(tag, value))
  805. def parse_keyfile(key_path: str, private: bool = True) -> OrderedDict:
  806. """
  807. Internal helper. This parses a node.pub or node.priv file and
  808. returns a dict mapping tags -> values.
  809. """
  810. if os.path.exists(key_path) and not os.path.isfile(key_path):
  811. raise Exception("Key file '{}' exists, but isn't a file".format(key_path))
  812. allowed_tags = [
  813. # common tags
  814. 'public-key-ed25519',
  815. 'public-adr-eth',
  816. 'created-at',
  817. 'creator',
  818. # user profile
  819. 'user-id',
  820. # node profile
  821. 'machine-id',
  822. 'node-authid',
  823. 'node-cluster-ip',
  824. ]
  825. if private:
  826. # private key file tags
  827. allowed_tags.extend(['private-key-ed25519', 'private-key-eth'])
  828. tags = OrderedDict() # type: ignore
  829. with open(key_path, 'r') as key_file:
  830. got_blankline = False
  831. for line in key_file.readlines():
  832. if line.strip() == '':
  833. got_blankline = True
  834. elif got_blankline:
  835. tag, value = line.split(':', 1)
  836. tag = tag.strip().lower()
  837. value = value.strip()
  838. if tag not in allowed_tags:
  839. raise Exception("Invalid tag '{}' in key file {}".format(tag, key_path))
  840. if tag in tags:
  841. raise Exception("Duplicate tag '{}' in key file {}".format(tag, key_path))
  842. tags[tag] = value
  843. return tags