firewall.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. # Copyright (C) 2018 Boris Bobrov
  2. #
  3. # This program is free software: you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation, either version 3 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  15. import json
  16. import logging
  17. import daiquiri
  18. daiquiri.setup(level=logging.INFO)
  19. logger = daiquiri.getLogger(__name__)
  20. FIREWALL_URL = ('https://www.nfoservers.com/control/firewall.pl?'
  21. 'name={name}&typeofserver={typeofserver}')
  22. DEFAULT_FIELDS_NUM = 25
  23. class FirewallIsFullError(Exception):
  24. pass
  25. class NoMatchingDescriptionError(Exception):
  26. pass
  27. class FirewallRules:
  28. def __init__(self, rules, fields_num=DEFAULT_FIELDS_NUM):
  29. self.rules = rules
  30. self.fields_num = fields_num
  31. def get_idx_by_description(self, description):
  32. for i, r in enumerate(self.rules):
  33. if r['description'] == description:
  34. return i
  35. else:
  36. raise NoMatchingDescriptionError('No such description')
  37. def sanity_check(self):
  38. self._check_number()
  39. self._check_numberoption()
  40. def _check_numberoption(self):
  41. numberoptions = [r['firewall_numberoption'] for r in self.rules]
  42. if (list(sorted(int(x) for x in numberoptions)) !=
  43. list(range(1, self.fields_num + 1))):
  44. raise AssertionError('Something is wrong with numberoptions')
  45. def _check_number(self):
  46. # rules should be deleted by setting 'firewall_easychoice' to 'choose'
  47. if len(self.rules) != self.fields_num:
  48. raise AssertionError('Not enough rules')
  49. def prepare_new_rule_idx(self, number):
  50. '''Safely prepare new rule.
  51. Creating rules is hard. A number for it needs to be picked and the
  52. number of the next rule needs to be bumped. This method does that.
  53. For convenience and due to implementation details of the firewall page,
  54. we will always edit the last rule in the python list. If the last rule
  55. is already occupied, the method will raise an error.
  56. Returns index of the rule to edit, which is always the last one.
  57. '''
  58. last_rule = self.rules[-1]
  59. if (last_rule['firewall_easychoice'] != 'choose' or
  60. last_rule['firewall_filterid']):
  61. raise FirewallIsFull
  62. for r in self.rules:
  63. numberoption = int(r['firewall_numberoption'])
  64. if numberoption >= number:
  65. r['firewall_numberoption'] = str(numberoption+1)
  66. self.rules[-1]['firewall_numberoption'] = str(number)
  67. return len(self.rules) - 1
  68. class Firewall:
  69. def __init__(self, server, fields_num=DEFAULT_FIELDS_NUM):
  70. self.server = server
  71. self.fields_num = fields_num
  72. def fetch_rules(self):
  73. url = FIREWALL_URL.format(name=self.server.servername,
  74. typeofserver=self.server.typeofserver)
  75. self.server.go(url)
  76. self.server.g.doc.choose_form(name='rules_form')
  77. fields = self.server.g.doc.form_fields()
  78. return self._fields_to_rules(fields)
  79. def update_rules(self, rules, sanity_check=True, idxs_updated=None):
  80. fields = self._rules_to_fields(rules, idxs=idxs_updated,
  81. sanity_check=sanity_check)
  82. url = FIREWALL_URL.format(name=self.server.servername,
  83. typeofserver=self.server.typeofserver)
  84. self.server.go(url)
  85. self.server.g.doc.choose_form(name='rules_form')
  86. fields_backup = self.server.g.doc.form_fields()
  87. def _do_submit_fields(fields):
  88. for k, v in fields.items():
  89. self.server.g.doc.set_input(k, v)
  90. self.server.submit()
  91. expected = ('Your changes have been saved and should take effect'
  92. ' shortly')
  93. try:
  94. self.server.g.doc.text_assert(expected)
  95. except Exception as e:
  96. logger.error(self.server.g.doc(
  97. '//div[@class="errormessage"]').text())
  98. raise
  99. try:
  100. _do_submit_fields(fields)
  101. except Exception as e:
  102. logger.exception('Error happened on upload, restoring original'
  103. ' rules')
  104. try:
  105. _do_submit_fields(fields_backup)
  106. logger.info('Original rules restored')
  107. except Exception as e:
  108. logger.exception('Error restoring original rules')
  109. logger.error('Here are rules that we could not restore:')
  110. import json
  111. logger.error(json.dumps(fields_backup))
  112. raise e
  113. def _fields_to_rules(self, fields: dict) -> FirewallRules:
  114. rules = [{} for i in range(self.fields_num)]
  115. for k, v in fields.items():
  116. # typical fields look like 'firewall_easychoice_9' with
  117. # the number in the end as rule number
  118. key_parts = k.split('_')
  119. try:
  120. idx = int(key_parts[-1])
  121. except ValueError:
  122. # the field is not a filter rule, but probably a service
  123. # field. Skip it.
  124. continue
  125. field_name = '_'.join(key_parts[:-1])
  126. rules[idx][field_name] = v
  127. return FirewallRules(rules)
  128. def _rules_to_fields(self, rules: FirewallRules,
  129. sanity_check: bool, idxs=None) -> dict:
  130. fields = {}
  131. if sanity_check:
  132. rules.sanity_check()
  133. for i, rule in enumerate(rules.rules):
  134. if idxs and i not in idxs:
  135. continue
  136. for k, v in rule.items():
  137. fields['{}_{}'.format(k, i)] = v
  138. return fields