File: mocking.py

package info (click to toggle)
python-stem 1.2.2-1.1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 4,568 kB
  • ctags: 2,036
  • sloc: python: 20,108; makefile: 127; sh: 3
file content (759 lines) | stat: -rw-r--r-- 26,165 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
# Copyright 2012-2014, Damian Johnson and The Tor Project
# See LICENSE for licensing information

"""
Helper functions for creating mock objects.

::

  get_all_combinations - provides all combinations of attributes

  Instance Constructors
    get_message                     - stem.response.ControlMessage
    get_protocolinfo_response       - stem.response.protocolinfo.ProtocolInfoResponse

    stem.descriptor.server_descriptor
      get_relay_server_descriptor  - RelayDescriptor
      get_bridge_server_descriptor - BridgeDescriptor

    stem.descriptor.microdescriptor
      get_microdescriptor - Microdescriptor

    stem.descriptor.extrainfo_descriptor
      get_relay_extrainfo_descriptor  - RelayExtraInfoDescriptor
      get_bridge_extrainfo_descriptor - BridgeExtraInfoDescriptor

    stem.descriptor.networkstatus
      get_directory_authority        - DirectoryAuthority
      get_key_certificate            - KeyCertificate
      get_network_status_document_v2 - NetworkStatusDocumentV2
      get_network_status_document_v3 - NetworkStatusDocumentV3

    stem.descriptor.router_status_entry
      get_router_status_entry_v2       - RouterStatusEntryV2
      get_router_status_entry_v3       - RouterStatusEntryV3
      get_router_status_entry_micro_v3 - RouterStatusEntryMicroV3
"""

import base64
import hashlib
import itertools
import re

import stem.descriptor.extrainfo_descriptor
import stem.descriptor.microdescriptor
import stem.descriptor.networkstatus
import stem.descriptor.router_status_entry
import stem.descriptor.server_descriptor
import stem.prereq
import stem.response
import stem.util.str_tools

CRYPTO_BLOB = """
MIGJAoGBAJv5IIWQ+WDWYUdyA/0L8qbIkEVH/cwryZWoIaPAzINfrw1WfNZGtBmg
skFtXhOHHqTRN4GPPrZsAIUOQGzQtGb66IQgT4tO/pj+P6QmSCCdTfhvGfgTCsC+
WPi4Fl2qryzTb3QO5r5x7T8OsG2IBUET1bLQzmtbC560SYR49IvVAgMBAAE=
"""

DOC_SIG = stem.descriptor.networkstatus.DocumentSignature(
  'sha1',
  '14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4',
  'BF112F1C6D5543CFD0A32215ACABD4197B5279AD',
  '-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----' % CRYPTO_BLOB)

RELAY_SERVER_HEADER = (
  ('router', 'caerSidi 71.35.133.197 9001 0 0'),
  ('published', '2012-03-01 17:15:27'),
  ('bandwidth', '153600 256000 104590'),
  ('reject', '*:*'),
  ('onion-key', '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % CRYPTO_BLOB),
  ('signing-key', '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % CRYPTO_BLOB),
)

RELAY_SERVER_FOOTER = (
  ('router-signature', '\n-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----' % CRYPTO_BLOB),
)

BRIDGE_SERVER_HEADER = (
  ('router', 'Unnamed 10.45.227.253 9001 0 0'),
  ('router-digest', '006FD96BA35E7785A6A3B8B75FE2E2435A13BDB4'),
  ('published', '2012-03-22 17:34:38'),
  ('bandwidth', '409600 819200 5120'),
  ('reject', '*:*'),
)

RELAY_EXTRAINFO_HEADER = (
  ('extra-info', 'ninja B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48'),
  ('published', '2012-05-05 17:03:50'),
)

RELAY_EXTRAINFO_FOOTER = (
  ('router-signature', '\n-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----' % CRYPTO_BLOB),
)

BRIDGE_EXTRAINFO_HEADER = (
  ('extra-info', 'ec2bridgereaac65a3 1EC248422B57D9C0BD751892FE787585407479A4'),
  ('published', '2012-05-05 17:03:50'),
)

BRIDGE_EXTRAINFO_FOOTER = (
  ('router-digest', '006FD96BA35E7785A6A3B8B75FE2E2435A13BDB4'),
)

MICRODESCRIPTOR = (
  ('onion-key', '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % CRYPTO_BLOB),
)

ROUTER_STATUS_ENTRY_V2_HEADER = (
  ('r', 'caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0'),
)

ROUTER_STATUS_ENTRY_V3_HEADER = (
  ('r', 'caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0'),
  ('s', 'Fast Named Running Stable Valid'),
)

ROUTER_STATUS_ENTRY_MICRO_V3_HEADER = (
  ('r', 'Konata ARIJF2zbqirB9IwsW0mQznccWww 2012-09-24 13:40:40 69.64.48.168 9001 9030'),
  ('m', 'aiUklwBrua82obG5AsTX+iEpkjQA2+AQHxZ7GwMfY70'),
  ('s', 'Fast Guard HSDir Named Running Stable V2Dir Valid'),
)

AUTHORITY_HEADER = (
  ('dir-source', 'turtles 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090'),
  ('contact', 'Mike Perry <email>'),
)

KEY_CERTIFICATE_HEADER = (
  ('dir-key-certificate-version', '3'),
  ('fingerprint', '27B6B5996C426270A5C95488AA5BCEB6BCC86956'),
  ('dir-key-published', '2011-11-28 21:51:04'),
  ('dir-key-expires', '2012-11-28 21:51:04'),
  ('dir-identity-key', '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % CRYPTO_BLOB),
  ('dir-signing-key', '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % CRYPTO_BLOB),
)

KEY_CERTIFICATE_FOOTER = (
  ('dir-key-certification', '\n-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----' % CRYPTO_BLOB),
)

NETWORK_STATUS_DOCUMENT_HEADER_V2 = (
  ('network-status-version', '2'),
  ('dir-source', '18.244.0.114 18.244.0.114 80'),
  ('fingerprint', '719BE45DE224B607C53707D0E2143E2D423E74CF'),
  ('contact', 'arma at mit dot edu'),
  ('published', '2005-12-16 00:13:46'),
  ('dir-signing-key', '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % CRYPTO_BLOB),
)

NETWORK_STATUS_DOCUMENT_FOOTER_V2 = (
  ('directory-signature', 'moria2\n-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----' % CRYPTO_BLOB),
)

NETWORK_STATUS_DOCUMENT_HEADER = (
  ('network-status-version', '3'),
  ('vote-status', 'consensus'),
  ('consensus-methods', None),
  ('consensus-method', None),
  ('published', None),
  ('valid-after', '2012-09-02 22:00:00'),
  ('fresh-until', '2012-09-02 22:00:00'),
  ('valid-until', '2012-09-02 22:00:00'),
  ('voting-delay', '300 300'),
  ('client-versions', None),
  ('server-versions', None),
  ('known-flags', 'Authority BadExit Exit Fast Guard HSDir Named Running Stable Unnamed V2Dir Valid'),
  ('params', None),
)

NETWORK_STATUS_DOCUMENT_FOOTER = (
  ('directory-footer', ''),
  ('bandwidth-weights', None),
  ('directory-signature', '%s %s\n%s' % (DOC_SIG.identity, DOC_SIG.key_digest, DOC_SIG.signature)),
)


def get_all_combinations(attr, include_empty = False):
  """
  Provides an iterator for all combinations of a set of attributes. For
  instance...

  ::

    >>> list(test.mocking.get_all_combinations(['a', 'b', 'c']))
    [('a',), ('b',), ('c',), ('a', 'b'), ('a', 'c'), ('b', 'c'), ('a', 'b', 'c')]

  :param list attr: attributes to provide combinations for
  :param bool include_empty: includes an entry with zero items if True
  :returns: iterator for all combinations
  """

  # Makes an itertools.product() call for 'i' copies of attr...
  #
  # * itertools.product(attr) => all one-element combinations
  # * itertools.product(attr, attr) => all two-element combinations
  # * ... etc

  if include_empty:
    yield ()

  seen = set()
  for index in xrange(1, len(attr) + 1):
    product_arg = [attr for _ in xrange(index)]

    for item in itertools.product(*product_arg):
      # deduplicate, sort, and only provide if we haven't seen it yet
      item = tuple(sorted(set(item)))

      if not item in seen:
        seen.add(item)
        yield item


def get_message(content, reformat = True):
  """
  Provides a ControlMessage with content modified to be parsable. This makes
  the following changes unless 'reformat' is false...

  * ensures the content ends with a newline
  * newlines are replaced with a carriage return and newline pair

  :param str content: base content for the controller message
  :param str reformat: modifies content to be more accommodating to being parsed

  :returns: stem.response.ControlMessage instance
  """

  if reformat:
    if not content.endswith('\n'):
      content += '\n'

    content = re.sub('([\r]?)\n', '\r\n', content)

  return stem.response.ControlMessage.from_str(content)


def get_protocolinfo_response(**attributes):
  """
  Provides a ProtocolInfoResponse, customized with the given attributes. The
  base instance is minimal, with its version set to one and everything else
  left with the default.

  :param dict attributes: attributes to customize the response with

  :returns: stem.response.protocolinfo.ProtocolInfoResponse instance
  """

  protocolinfo_response = get_message('250-PROTOCOLINFO 1\n250 OK')
  stem.response.convert('PROTOCOLINFO', protocolinfo_response)

  for attr in attributes:
    setattr(protocolinfo_response, attr, attributes[attr])

  return protocolinfo_response


def _get_descriptor_content(attr = None, exclude = (), header_template = (), footer_template = ()):
  """
  Constructs a minimal descriptor with the given attributes. The content we
  provide back is of the form...

  * header_template (with matching attr filled in)
  * unused attr entries
  * footer_template (with matching attr filled in)

  So for instance...

  ::

    get_descriptor_content(
      attr = {'nickname': 'caerSidi', 'contact': 'atagar'},
      header_template = (
        ('nickname', 'foobar'),
        ('fingerprint', '12345'),
      ),
    )

  ... would result in...

  ::

    nickname caerSidi
    fingerprint 12345
    contact atagar

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param tuple header_template: key/value pairs for mandatory fields before unrecognized content
  :param tuple footer_template: key/value pairs for mandatory fields after unrecognized content

  :returns: str with the requested descriptor content
  """

  header_content, footer_content = [], []

  if attr is None:
    attr = {}

  attr = dict(attr)  # shallow copy since we're destructive

  for content, template in ((header_content, header_template),
                           (footer_content, footer_template)):
    for keyword, value in template:
      if keyword in exclude:
        continue
      elif keyword in attr:
        value = attr[keyword]
        del attr[keyword]

      if value is None:
        continue
      elif value == '':
        content.append(keyword)
      elif keyword == 'onion-key' or keyword == 'signing-key' or keyword == 'router-signature':
        content.append('%s%s' % (keyword, value))
      else:
        content.append('%s %s' % (keyword, value))

  remainder = []

  for k, v in attr.items():
    if v:
      remainder.append('%s %s' % (k, v))
    else:
      remainder.append(k)

  return stem.util.str_tools._to_bytes('\n'.join(header_content + remainder + footer_content))


def get_relay_server_descriptor(attr = None, exclude = (), content = False, sign_content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.server_descriptor.RelayDescriptor

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True
  :param bool sign_content: sets a proper digest value if True

  :returns: RelayDescriptor for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, RELAY_SERVER_HEADER, RELAY_SERVER_FOOTER)

  if content:
    return desc_content
  else:
    if sign_content:
      desc_content = sign_descriptor_content(desc_content)

    return stem.descriptor.server_descriptor.RelayDescriptor(desc_content, validate = True)


def get_bridge_server_descriptor(attr = None, exclude = (), content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.server_descriptor.BridgeDescriptor

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: BridgeDescriptor for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, BRIDGE_SERVER_HEADER)

  if content:
    return desc_content
  else:
    return stem.descriptor.server_descriptor.BridgeDescriptor(desc_content, validate = True)


def get_relay_extrainfo_descriptor(attr = None, exclude = (), content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.extrainfo_descriptor.RelayExtraInfoDescriptor

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: RelayExtraInfoDescriptor for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, RELAY_EXTRAINFO_HEADER, RELAY_EXTRAINFO_FOOTER)

  if content:
    return desc_content
  else:
    return stem.descriptor.extrainfo_descriptor.RelayExtraInfoDescriptor(desc_content, validate = True)


def get_bridge_extrainfo_descriptor(attr = None, exclude = (), content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.extrainfo_descriptor.BridgeExtraInfoDescriptor

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: BridgeExtraInfoDescriptor for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, BRIDGE_EXTRAINFO_HEADER, BRIDGE_EXTRAINFO_FOOTER)

  if content:
    return desc_content
  else:
    return stem.descriptor.extrainfo_descriptor.BridgeExtraInfoDescriptor(desc_content, validate = True)


def get_microdescriptor(attr = None, exclude = (), content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.microdescriptor.Microdescriptor

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: Microdescriptor for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, MICRODESCRIPTOR)

  if content:
    return desc_content
  else:
    return stem.descriptor.microdescriptor.Microdescriptor(desc_content, validate = True)


def get_router_status_entry_v2(attr = None, exclude = (), content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.router_status_entry.RouterStatusEntryV2

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: RouterStatusEntryV2 for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, ROUTER_STATUS_ENTRY_V2_HEADER)

  if content:
    return desc_content
  else:
    return stem.descriptor.router_status_entry.RouterStatusEntryV2(desc_content, validate = True)


def get_router_status_entry_v3(attr = None, exclude = (), content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.router_status_entry.RouterStatusEntryV3

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: RouterStatusEntryV3 for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, ROUTER_STATUS_ENTRY_V3_HEADER)

  if content:
    return desc_content
  else:
    return stem.descriptor.router_status_entry.RouterStatusEntryV3(desc_content, validate = True)


def get_router_status_entry_micro_v3(attr = None, exclude = (), content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.router_status_entry.RouterStatusEntryMicroV3

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: RouterStatusEntryMicroV3 for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, ROUTER_STATUS_ENTRY_MICRO_V3_HEADER)

  if content:
    return desc_content
  else:
    return stem.descriptor.router_status_entry.RouterStatusEntryMicroV3(desc_content, validate = True)


def get_directory_authority(attr = None, exclude = (), is_vote = False, content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.networkstatus.DirectoryAuthority

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool is_vote: True if this is for a vote, False if it's for a consensus
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: DirectoryAuthority for the requested descriptor content
  """

  if attr is None:
    attr = {}

  if not is_vote:
    # entries from a consensus also have a mandatory 'vote-digest' field
    if not ('vote-digest' in attr or (exclude and 'vote-digest' in exclude)):
      attr['vote-digest'] = '0B6D1E9A300B895AA2D0B427F92917B6995C3C1C'

  desc_content = _get_descriptor_content(attr, exclude, AUTHORITY_HEADER)

  if is_vote:
    desc_content += b'\n' + get_key_certificate(content = True)

  if content:
    return desc_content
  else:
    return stem.descriptor.networkstatus.DirectoryAuthority(desc_content, validate = True, is_vote = is_vote)


def get_key_certificate(attr = None, exclude = (), content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.networkstatus.KeyCertificate

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: KeyCertificate for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, KEY_CERTIFICATE_HEADER, KEY_CERTIFICATE_FOOTER)

  if content:
    return desc_content
  else:
    return stem.descriptor.networkstatus.KeyCertificate(desc_content, validate = True)


def get_network_status_document_v2(attr = None, exclude = (), content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.networkstatus.NetworkStatusDocumentV2

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: NetworkStatusDocumentV2 for the requested descriptor content
  """

  desc_content = _get_descriptor_content(attr, exclude, NETWORK_STATUS_DOCUMENT_HEADER_V2, NETWORK_STATUS_DOCUMENT_FOOTER_V2)

  if content:
    return desc_content
  else:
    return stem.descriptor.networkstatus.NetworkStatusDocumentV2(desc_content, validate = True)


def get_network_status_document_v3(attr = None, exclude = (), authorities = None, routers = None, content = False):
  """
  Provides the descriptor content for...
  stem.descriptor.networkstatus.NetworkStatusDocumentV3

  :param dict attr: keyword/value mappings to be included in the descriptor
  :param list exclude: mandatory keywords to exclude from the descriptor
  :param list authorities: directory authorities to include in the document
  :param list routers: router status entries to include in the document
  :param bool content: provides the str content of the descriptor rather than the class if True

  :returns: NetworkStatusDocumentV3 for the requested descriptor content
  """

  if attr is None:
    attr = {}

  # add defaults only found in a vote, consensus, or microdescriptor

  if attr.get('vote-status') == 'vote':
    extra_defaults = {
      'consensus-methods': '1 9',
      'published': '2012-09-02 22:00:00',
    }

    # votes need an authority to be valid

    if authorities is None:
      authorities = [get_directory_authority(is_vote = True)]
  else:
    extra_defaults = {
      'consensus-method': '9',
    }

  for k, v in extra_defaults.items():
    if not (k in attr or (exclude and k in exclude)):
      attr[k] = v

  desc_content = _get_descriptor_content(attr, exclude, NETWORK_STATUS_DOCUMENT_HEADER, NETWORK_STATUS_DOCUMENT_FOOTER)

  # inject the authorities and/or routers between the header and footer
  if authorities:
    if b'directory-footer' in desc_content:
      footer_div = desc_content.find(b'\ndirectory-footer') + 1
    elif b'directory-signature' in desc_content:
      footer_div = desc_content.find(b'\ndirectory-signature') + 1
    else:
      if routers:
        desc_content += b'\n'

      footer_div = len(desc_content) + 1

    authority_content = stem.util.str_tools._to_bytes('\n'.join([str(a) for a in authorities]) + '\n')
    desc_content = desc_content[:footer_div] + authority_content + desc_content[footer_div:]

  if routers:
    if b'directory-footer' in desc_content:
      footer_div = desc_content.find(b'\ndirectory-footer') + 1
    elif b'directory-signature' in desc_content:
      footer_div = desc_content.find(b'\ndirectory-signature') + 1
    else:
      if routers:
        desc_content += b'\n'

      footer_div = len(desc_content) + 1

    router_content = stem.util.str_tools._to_bytes('\n'.join([str(r) for r in routers]) + '\n')
    desc_content = desc_content[:footer_div] + router_content + desc_content[footer_div:]

  if content:
    return desc_content
  else:
    return stem.descriptor.networkstatus.NetworkStatusDocumentV3(desc_content, validate = True)


def sign_descriptor_content(desc_content):
  """
  Add a valid signature to the supplied descriptor string.

  If pycrypto is available the function will generate a key pair, and use it to
  sign the descriptor string. Any existing fingerprint, signing-key or
  router-signature data will be overwritten. If the library's unavailable the
  code will return the unaltered descriptor.

  :param str desc_content: the descriptor string to sign
  :returns: a descriptor string, signed if crypto available and unaltered otherwise
  """

  if not stem.prereq.is_crypto_available():
    return desc_content
  else:
    from Crypto.PublicKey import RSA
    from Crypto.Util import asn1
    from Crypto.Util.number import long_to_bytes

    # generate a key
    private_key = RSA.generate(1024)

    # get a string representation of the public key
    seq = asn1.DerSequence()
    seq.append(private_key.n)
    seq.append(private_key.e)
    seq_as_string = seq.encode()
    public_key_string = base64.b64encode(seq_as_string)

    # split public key into lines 64 characters long
    public_key_string = b'\n'.join([
      public_key_string[:64],
      public_key_string[64:128],
      public_key_string[128:],
    ])

    # generate the new signing key string

    signing_key_token = b'\nsigning-key\n'  # note the trailing '\n' is important here so as not to match the string elsewhere
    signing_key_token_start = b'-----BEGIN RSA PUBLIC KEY-----\n'
    signing_key_token_end = b'\n-----END RSA PUBLIC KEY-----\n'
    new_sk = signing_key_token + signing_key_token_start + public_key_string + signing_key_token_end

    # update the descriptor string with the new signing key

    skt_start = desc_content.find(signing_key_token)
    skt_end = desc_content.find(signing_key_token_end, skt_start)
    desc_content = desc_content[:skt_start] + new_sk + desc_content[skt_end + len(signing_key_token_end):]

    # generate the new fingerprint string

    key_hash = stem.util.str_tools._to_bytes(hashlib.sha1(seq_as_string).hexdigest().upper())
    grouped_fingerprint = b''

    for x in range(0, len(key_hash), 4):
      grouped_fingerprint += b' ' + key_hash[x:x + 4]
      fingerprint_token = b'\nfingerprint'
      new_fp = fingerprint_token + grouped_fingerprint

    # update the descriptor string with the new fingerprint

    ft_start = desc_content.find(fingerprint_token)
    if ft_start < 0:
      fingerprint_token = b'\nopt fingerprint'
      ft_start = desc_content.find(fingerprint_token)

    # if the descriptor does not already contain a fingerprint do not add one

    if ft_start >= 0:
      ft_end = desc_content.find(b'\n', ft_start + 1)
      desc_content = desc_content[:ft_start] + new_fp + desc_content[ft_end:]

    # create a temporary object to use to calculate the digest

    tempDesc = stem.descriptor.server_descriptor.RelayDescriptor(desc_content, validate=False)

    # calculate the new digest for the descriptor

    new_digest_hex = tempDesc.digest().lower()

    # remove the hex encoding

    if stem.prereq.is_python_3():
      new_digest = bytes.fromhex(new_digest_hex)
    else:
      new_digest = new_digest_hex.decode('hex_codec')

    # Generate the digest buffer.
    #  block is 128 bytes in size
    #  2 bytes for the type info
    #  1 byte for the separator

    padding = b''

    for x in range(125 - len(new_digest)):
      padding += b'\xFF'
      digestBuffer = b'\x00\x01' + padding + b'\x00' + new_digest

    # generate a new signature by signing the digest buffer with the private key

    (signature, ) = private_key.sign(digestBuffer, None)
    signature_as_bytes = long_to_bytes(signature, 128)
    signature_base64 = base64.b64encode(signature_as_bytes)

    signature_base64 = b'b'.join([
      signature_base64[:64],
      signature_base64[64:128],
      signature_base64[128:],
    ])

    # update the descriptor string with the new signature

    router_signature_token = b'\nrouter-signature\n'
    router_signature_start = b'-----BEGIN SIGNATURE-----\n'
    router_signature_end = b'\n-----END SIGNATURE-----\n'
    rst_start = desc_content.find(router_signature_token)
    desc_content = desc_content[:rst_start] + router_signature_token + router_signature_start + signature_base64 + router_signature_end

    return desc_content