stoobly-agent 0.29.1__py3-none-any.whl → 0.30.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/configs_controller.py +5 -2
  3. stoobly_agent/app/cli/config_cli.py +64 -14
  4. stoobly_agent/app/cli/endpoint_cli.py +1 -1
  5. stoobly_agent/app/cli/helpers/endpoint_facade.py +1 -1
  6. stoobly_agent/app/cli/helpers/handle_config_update_service.py +44 -5
  7. stoobly_agent/app/cli/helpers/openapi_endpoint_adapter.py +19 -3
  8. stoobly_agent/app/cli/helpers/replay_facade.py +2 -2
  9. stoobly_agent/app/cli/intercept_cli.py +14 -5
  10. stoobly_agent/app/cli/request_cli.py +2 -2
  11. stoobly_agent/app/cli/scenario_cli.py +2 -2
  12. stoobly_agent/app/cli/types/endpoint.py +1 -1
  13. stoobly_agent/app/models/adapters/raw_http_request_adapter.py +1 -1
  14. stoobly_agent/app/models/adapters/raw_http_response_adapter.py +1 -1
  15. stoobly_agent/app/proxy/context.py +18 -0
  16. stoobly_agent/app/proxy/handle_mock_service.py +1 -1
  17. stoobly_agent/app/proxy/handle_replay_service.py +2 -1
  18. stoobly_agent/app/proxy/intercept_handler.py +14 -3
  19. stoobly_agent/app/proxy/intercept_settings.py +28 -21
  20. stoobly_agent/app/proxy/mitmproxy/request_facade.py +41 -35
  21. stoobly_agent/app/proxy/mitmproxy/response_facade.py +0 -11
  22. stoobly_agent/app/proxy/mock/context.py +4 -11
  23. stoobly_agent/app/proxy/record/context.py +4 -12
  24. stoobly_agent/app/proxy/record/join_request_service.py +1 -1
  25. stoobly_agent/app/proxy/replay/context.py +4 -11
  26. stoobly_agent/app/proxy/replay/replay_request_service.py +5 -5
  27. stoobly_agent/app/proxy/replay/rewrite_params_service.py +1 -1
  28. stoobly_agent/app/proxy/run.py +3 -0
  29. stoobly_agent/app/proxy/test/context.py +2 -2
  30. stoobly_agent/app/proxy/test/context_abc.py +1 -1
  31. stoobly_agent/app/proxy/test/matchers/custom.py +1 -1
  32. stoobly_agent/app/settings/rewrite_rule.py +13 -0
  33. stoobly_agent/app/settings/types/__init__.py +1 -6
  34. stoobly_agent/app/settings/types/proxy_settings.py +6 -0
  35. stoobly_agent/app/settings/url_rule.py +33 -0
  36. stoobly_agent/cli.py +4 -0
  37. stoobly_agent/config/constants/custom_headers.py +1 -1
  38. stoobly_agent/config/constants/env_vars.py +1 -0
  39. stoobly_agent/config/constants/lifecycle_hooks.py +2 -0
  40. stoobly_agent/config/schema.yml +7 -1
  41. stoobly_agent/lib/orm/request.py +8 -4
  42. stoobly_agent/public/18-es2015.46d337c47cb41abec8ad.js +1 -0
  43. stoobly_agent/public/18-es5.46d337c47cb41abec8ad.js +1 -0
  44. stoobly_agent/public/dashboard.agent-alpha-0.30.0.tar.gz +0 -0
  45. stoobly_agent/public/index.html +1 -1
  46. stoobly_agent/public/runtime-es2015.3a15c6ce90c8f8cce796.js +1 -0
  47. stoobly_agent/public/runtime-es5.3a15c6ce90c8f8cce796.js +1 -0
  48. stoobly_agent-0.30.0.dist-info/METADATA +26 -0
  49. {stoobly_agent-0.29.1.dist-info → stoobly_agent-0.30.0.dist-info}/RECORD +53 -51
  50. {stoobly_agent-0.29.1.dist-info → stoobly_agent-0.30.0.dist-info}/WHEEL +1 -1
  51. stoobly_agent/public/18-es2015.750ef954bde422debcb6.js +0 -1
  52. stoobly_agent/public/18-es5.750ef954bde422debcb6.js +0 -1
  53. stoobly_agent/public/dashboard.agent-alpha-0.29.0.tar.gz +0 -0
  54. stoobly_agent/public/runtime-es2015.b392473c492ce4cf1cb5.js +0 -1
  55. stoobly_agent/public/runtime-es5.b392473c492ce4cf1cb5.js +0 -1
  56. stoobly_agent-0.29.1.dist-info/METADATA +0 -26
  57. {stoobly_agent-0.29.1.dist-info → stoobly_agent-0.30.0.dist-info}/LICENSE +0 -0
  58. {stoobly_agent-0.29.1.dist-info → stoobly_agent-0.30.0.dist-info}/entry_points.txt +0 -0
  59. {stoobly_agent-0.29.1.dist-info → stoobly_agent-0.30.0.dist-info}/top_level.txt +0 -0
stoobly_agent/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '0.29.1'
2
+ VERSION = '0.30.0'
@@ -3,7 +3,9 @@ import pdb
3
3
  from mergedeep import merge
4
4
 
5
5
  from stoobly_agent.app.api.simple_http_request_handler import SimpleHTTPRequestHandler
6
- from stoobly_agent.app.cli.helpers.handle_config_update_service import context as handle_context, handle_project_update, handle_policy_update, handle_scenario_update
6
+ from stoobly_agent.app.cli.helpers.handle_config_update_service import (
7
+ context as handle_context, handle_intercept_active_update, handle_policy_update, handle_project_update, handle_scenario_update
8
+ )
7
9
  from stoobly_agent.app.models.scenario_model import ScenarioModel
8
10
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
9
11
  from stoobly_agent.app.settings import Settings
@@ -106,8 +108,9 @@ class ConfigsController:
106
108
 
107
109
  _handle_context = handle_context()
108
110
 
109
- handle_project_update(settings, _handle_context)
111
+ handle_intercept_active_update(settings, _handle_context)
110
112
  handle_policy_update(settings, _handle_context)
113
+ handle_project_update(settings, _handle_context)
111
114
  handle_scenario_update(settings, _handle_context)
112
115
 
113
116
  settings.write(settings.to_dict())
@@ -8,7 +8,7 @@ from stoobly_agent.app.settings import Settings
8
8
  from stoobly_agent.app.settings.constants import firewall_action, request_component
9
9
  from stoobly_agent.app.settings.firewall_rule import FirewallRule
10
10
  from stoobly_agent.app.settings.match_rule import MatchRule
11
- from stoobly_agent.app.settings.rewrite_rule import ParameterRule, RewriteRule
11
+ from stoobly_agent.app.settings.rewrite_rule import ParameterRule, RewriteRule, UrlRule
12
12
  from stoobly_agent.config.constants import mode
13
13
  from stoobly_agent.config.data_dir import DataDir
14
14
  from stoobly_agent.lib.api.keys import ProjectKey, ScenarioKey
@@ -146,6 +146,7 @@ def rewrite(ctx):
146
146
  @rewrite.command(
147
147
  help="Set rewrite rule."
148
148
  )
149
+ @click.option('--host', help='Request URL host.')
149
150
  @click.option(
150
151
  '--method',
151
152
  multiple=True,
@@ -159,17 +160,30 @@ def rewrite(ctx):
159
160
  required=True,
160
161
  type=click.Choice([mode.MOCK, mode.RECORD, mode.REPLAY] + ([mode.TEST] if is_remote else []))
161
162
  )
162
- @click.option('--name', required=True, help='Name of the request component.')
163
+ @click.option('--name', help='Name of the request component.')
163
164
  @click.option('--pattern', required=True, help='URLs pattern.')
165
+ @click.option('--port', help='Request URL port.')
164
166
  @click.option('--project-key', help='Project to add rewrite rule to.')
165
167
  @click.option(
166
168
  '--type',
167
- required=True,
168
169
  type=click.Choice([request_component.BODY_PARAM, request_component.HEADER, request_component.QUERY_PARAM]),
169
170
  help='Request component type.'
170
171
  )
171
- @click.option('--value', required=True, help='Rewrite value.')
172
+ @click.option('--value', help='Rewrite value.')
172
173
  def set(**kwargs):
174
+ if kwargs['name'] or kwargs['value'] or kwargs['type']:
175
+ if kwargs['name'] == None:
176
+ print("Error: missing option '--name'", file=sys.stderr)
177
+ sys.exit(1)
178
+
179
+ if kwargs['value'] == None:
180
+ print("Error: missing option '--value'", file=sys.stderr)
181
+ sys.exit(1)
182
+
183
+ if kwargs['type'] == None:
184
+ print("Error: missing option '--type'", file=sys.stderr)
185
+ sys.exit(1)
186
+
173
187
  settings = Settings.instance()
174
188
  project_key_str = resolve_project_key_and_validate(kwargs, settings)
175
189
  project_key = ProjectKey(project_key_str)
@@ -183,27 +197,50 @@ def set(**kwargs):
183
197
  filtered_rewrite_rules: List[RewriteRule] = list(filter(rewrite_rule_filter, rewrite_rules))
184
198
 
185
199
  if len(filtered_rewrite_rules) == 0:
200
+ parameter_rules = list(filter(lambda r: r != None, [__select_parameter_rule(kwargs)]))
201
+ url_rules = list(filter(lambda r: r != None , [__select_url_rule(kwargs)]))
186
202
  rewrite_rule = RewriteRule({
187
203
  'methods': methods,
188
204
  'pattern': kwargs['pattern'],
189
- 'parameter_rules': [__select_parameter_rule(kwargs)]
205
+ 'parameter_rules': parameter_rules,
206
+ 'url_rules': url_rules,
190
207
  })
191
208
  rewrite_rules.append(rewrite_rule)
192
209
  settings.proxy.rewrite.set_rewrite_rules(project_key.id, rewrite_rules)
193
210
  else:
194
211
  parameter_rule_filter = lambda rule: rule.name == kwargs['name'] and rule.type == kwargs['type'] and rule.modes == modes
212
+ url_rule_filter = lambda rule: rule.host == kwargs['host'] and rule.modes == modes
213
+
195
214
  for rewrite_rule in filtered_rewrite_rules:
196
- parameter_rules = rewrite_rule.parameter_rules
197
- filtered_parameter_rules: List[ParameterRule] = list(filter(parameter_rule_filter, parameter_rules))
215
+ # Parameter rules
198
216
  parameter_rule_dict = __select_parameter_rule(kwargs)
199
217
 
200
- if len(filtered_parameter_rules) == 0:
201
- parameter_rule = ParameterRule(parameter_rule_dict)
202
- parameter_rules.append(parameter_rule)
203
- rewrite_rule.parameter_rules = parameter_rules
204
- else:
205
- for parameter_rule in filtered_parameter_rules:
206
- parameter_rule.update(parameter_rule_dict)
218
+ if parameter_rule_dict:
219
+ parameter_rules = rewrite_rule.parameter_rules
220
+ filtered_parameter_rules: List[ParameterRule] = list(filter(parameter_rule_filter, parameter_rules))
221
+
222
+ if len(filtered_parameter_rules) == 0:
223
+ parameter_rule = ParameterRule(parameter_rule_dict)
224
+ parameter_rules.append(parameter_rule)
225
+ rewrite_rule.parameter_rules = parameter_rules
226
+ else:
227
+ for parameter_rule in filtered_parameter_rules:
228
+ parameter_rule.update(parameter_rule_dict)
229
+
230
+ # URL rules
231
+ url_rule_dict = __select_url_rule(kwargs)
232
+
233
+ if url_rule_dict:
234
+ url_rules = rewrite_rule.url_rules
235
+ filtered_url_rules: List[UrlRule] = list(filter(url_rule_filter, url_rules))
236
+
237
+ if len(filtered_url_rules) == 0:
238
+ url_rule = UrlRule(url_rule_dict)
239
+ url_rules.append(url_rule)
240
+ rewrite_rule.url_rules = url_rules
241
+ else:
242
+ for url_rule in filtered_url_rules:
243
+ url_rule.update(url_rule_dict)
207
244
 
208
245
  settings.commit()
209
246
 
@@ -437,6 +474,9 @@ config.add_command(rewrite)
437
474
  config.add_command(scenario)
438
475
 
439
476
  def __select_parameter_rule(kwargs):
477
+ if kwargs['name'] == None or kwargs['value'] == None or kwargs['type'] == None:
478
+ return
479
+
440
480
  return {
441
481
  'modes': list(kwargs['mode']),
442
482
  'name': kwargs['name'],
@@ -444,6 +484,16 @@ def __select_parameter_rule(kwargs):
444
484
  'type': kwargs['type'],
445
485
  }
446
486
 
487
+ def __select_url_rule(kwargs):
488
+ if kwargs['host'] == None or kwargs['port'] == None:
489
+ return
490
+
491
+ return {
492
+ 'host': kwargs['host'],
493
+ 'modes': list(kwargs['mode']),
494
+ 'port': kwargs['port'],
495
+ }
496
+
447
497
  def __project_key(settings):
448
498
  project_key = settings.proxy.intercept.project_key
449
499
  validate_project_key(project_key)
@@ -26,7 +26,7 @@ def endpoint(ctx):
26
26
  )
27
27
  @click.option('--format', required=True, type=click.Choice([OPENAPI_FORMAT]), help='File format')
28
28
  @click.option('--scenario-key', help='Which scenario to import to. If none then all requests will be imported to.')
29
- @click.option('--lifecycle-hooks-script-path', help='Path to lifecycle hooks script.')
29
+ @click.option('--lifecycle-hooks-path', help='Path to lifecycle hooks script.')
30
30
  @ConditionalDecorator(lambda f: click.option('--project-key', help='Project to create endpoint in.')(f), is_remote)
31
31
  @click.argument('path')
32
32
  def _import(**kwargs: EndpointCreateCliOptions):
@@ -23,7 +23,7 @@ class EndpointFacade():
23
23
  lifecycle_hooks = {}
24
24
 
25
25
  try:
26
- lifecycle_hooks = run_path(kwargs['lifecycle_hooks_script_path'])
26
+ lifecycle_hooks = run_path(kwargs['lifecycle_hooks_path'])
27
27
  except Exception as e:
28
28
  pass
29
29
 
@@ -19,6 +19,44 @@ def context() -> Context:
19
19
  'current_proxy_settings': __current_proxy_settings()
20
20
  }
21
21
 
22
+ def handle_intercept_active_update(new_settings: Settings, context: Context = None):
23
+ data_rule = __data_rule(new_settings.proxy)
24
+ is_active = new_settings.proxy.intercept.active
25
+ old_proxy_settings = __current_proxy_settings(context)
26
+ was_active = old_proxy_settings.intercept.active
27
+ _mode = context['mode'] if context and context.get('mode') else new_settings.proxy.intercept.mode
28
+
29
+ if not was_active and is_active:
30
+ if _mode == intercept_mode.RECORD:
31
+ new_policy = data_rule.record_policy
32
+ scenario_key = data_rule.scenario_key
33
+ _scenario_key = __parse_scenario_key(scenario_key)
34
+
35
+ if _scenario_key:
36
+ scenario_model = ScenarioModel(new_settings)
37
+
38
+ if new_policy == record_policy.OVERWRITE:
39
+ # If policy is overwrite when recording, whenever intercept is enabled,
40
+ # set active scenario to be overwritable
41
+ scenario_model.update(_scenario_key.id, **{ 'overwritable': True })[1]
42
+ else:
43
+ scenario_model.update(_scenario_key.id, **{ 'overwritable': False })[1]
44
+ elif was_active and not is_active:
45
+ if _mode == intercept_mode.RECORD:
46
+ old_proxy_settings = __current_proxy_settings(context)
47
+ old_policy = __data_rule(old_proxy_settings).record_policy
48
+
49
+ if old_policy == record_policy.OVERWRITE:
50
+ scenario_key = data_rule.scenario_key
51
+ _scenario_key = __parse_scenario_key(scenario_key)
52
+
53
+ if _scenario_key:
54
+ scenario_model = ScenarioModel(new_settings)
55
+
56
+ # If policy is overwrite when recording, whenever intercept is disabled,
57
+ # set active scenario to not be overwritable
58
+ scenario_model.update(_scenario_key.id, **{ 'overwritable': False })[1]
59
+
22
60
  def handle_scenario_update(new_settings: Settings, context = None):
23
61
  new_scenario_key = __scenario_key(new_settings.proxy)
24
62
  if not new_scenario_key:
@@ -27,9 +65,10 @@ def handle_scenario_update(new_settings: Settings, context = None):
27
65
  old_proxy_settings = __current_proxy_settings(context)
28
66
  old_scenario_key = __scenario_key(old_proxy_settings)
29
67
 
30
- data_rule = __data_rule(new_settings.proxy)
31
- if data_rule.record_policy == record_policy.OVERWRITE:
32
- if old_scenario_key != new_scenario_key:
68
+ if old_scenario_key != new_scenario_key:
69
+ data_rule = __data_rule(new_settings.proxy)
70
+
71
+ if data_rule.record_policy == record_policy.OVERWRITE:
33
72
  scenario_model = ScenarioModel(new_settings)
34
73
 
35
74
  _old_scenario_key = __parse_scenario_key(old_scenario_key)
@@ -75,13 +114,13 @@ def handle_policy_update(new_settings: Settings, context: Context = None):
75
114
  new_policy = data_rule.record_policy
76
115
  old_policy = old_data_rule.record_policy
77
116
 
78
- if new_policy != old_policy and new_policy == record_policy.OVERWRITE:
117
+ if new_policy != old_policy and old_policy == record_policy.OVERWRITE:
79
118
  scenario_key = data_rule.scenario_key
80
119
  _scenario_key = __parse_scenario_key(scenario_key)
81
120
 
82
121
  if _scenario_key:
83
122
  scenario_model = ScenarioModel(new_settings)
84
- scenario_model.update(_scenario_key.id, **{ 'overwritable': True })[1]
123
+ scenario_model.update(_scenario_key.id, **{ 'overwritable': False })[1]
85
124
 
86
125
  def __current_proxy_settings(context: Context = None):
87
126
  if context and context.get('current_proxy_settings'):
@@ -1,13 +1,13 @@
1
1
  import copy
2
2
  from functools import reduce
3
3
  import itertools
4
- import pdb
5
4
  from pprint import pprint
6
5
  import re
7
6
  from typing import Dict, List, Union
8
7
  from urllib.parse import urlparse
9
8
 
10
9
  from openapi_core import Spec
10
+ import yaml
11
11
 
12
12
  from stoobly_agent.lib.api.interfaces.endpoints import (
13
13
  Alias,
@@ -23,7 +23,14 @@ class OpenApiEndpointAdapter():
23
23
  return
24
24
 
25
25
  def adapt_from_file(self, file_path) -> List[EndpointShowResponse]:
26
- spec = Spec.from_file_path(file_path)
26
+ spec = {}
27
+
28
+ with open(file_path, "r") as stream:
29
+ file_data: Dict = yaml.safe_load(stream)
30
+ if 'info' not in file_data:
31
+ self.__add_info(file_data)
32
+ spec = Spec.from_dict(file_data)
33
+
27
34
  return self.adapt(spec)
28
35
 
29
36
  def adapt(self, spec: Spec) -> List[EndpointShowResponse]:
@@ -33,7 +40,7 @@ class OpenApiEndpointAdapter():
33
40
  schemas = components.get("schemas", {})
34
41
  paths = spec.getkey('paths')
35
42
 
36
- servers_spec = spec / "servers"
43
+ servers_spec = spec.get("servers")
37
44
  servers = self.__evaluate_servers(servers_spec)
38
45
 
39
46
  for _, server in enumerate(servers):
@@ -231,6 +238,15 @@ class OpenApiEndpointAdapter():
231
238
 
232
239
  return endpoints
233
240
 
241
+ def __add_info(self, file_data: Dict):
242
+ if 'info' in file_data:
243
+ return
244
+
245
+ file_data['info'] = {
246
+ 'version': '0.0.1',
247
+ 'title': ''
248
+ }
249
+
234
250
  def __get_most_recent_param(self, literal_params: dict):
235
251
  return list(literal_params)[-1] if literal_params else None
236
252
 
@@ -14,7 +14,7 @@ class ReplayCliOptions(TypedDict):
14
14
  assign: List[str]
15
15
  group_by: str
16
16
  host: str
17
- lifecycle_hooks_script_path: str
17
+ lifecycle_hooks_path: str
18
18
  on_response: Callable
19
19
  project_key: str
20
20
  record: bool
@@ -54,7 +54,7 @@ class ReplayFacade():
54
54
  'before_replay': cli_options.get('before_replay'),
55
55
  'group_by': cli_options.get('group_by'),
56
56
  'host': cli_options.get('host'),
57
- 'lifecycle_hooks_script_path': cli_options.get('lifecycle_hooks_script_path'),
57
+ 'lifecycle_hooks_path': cli_options.get('lifecycle_hooks_path'),
58
58
  'overwrite': cli_options.get('overwrite'),
59
59
  'request_origin': request_origin.CLI,
60
60
  'save': cli_options.get('save'),
@@ -6,7 +6,7 @@ from stoobly_agent.app.settings import Settings
6
6
  from stoobly_agent.config.constants import mode, mock_policy, record_policy, replay_policy
7
7
  from stoobly_agent.lib.api.keys.project_key import ProjectKey
8
8
 
9
- from .helpers.handle_config_update_service import handle_policy_update
9
+ from .helpers.handle_config_update_service import handle_intercept_active_update, handle_policy_update
10
10
 
11
11
  settings = Settings.instance()
12
12
 
@@ -43,6 +43,8 @@ def intercept(ctx):
43
43
  def enable(**kwargs):
44
44
  settings.proxy.intercept.active = True
45
45
 
46
+ handle_intercept_active_update(settings)
47
+
46
48
  settings.commit()
47
49
 
48
50
  print("Intercept enabled!")
@@ -55,6 +57,8 @@ def disable(**kwargs):
55
57
 
56
58
  settings.proxy.intercept.active = False
57
59
 
60
+ handle_intercept_active_update(settings)
61
+
58
62
  settings.commit()
59
63
 
60
64
  print("Intercept disabled!")
@@ -72,9 +76,14 @@ def configure(**kwargs):
72
76
  sys.exit(1)
73
77
 
74
78
  if kwargs['mode']:
75
- settings.proxy.intercept.mode = kwargs['mode']
79
+ if settings.proxy.intercept.mode != kwargs['mode']:
80
+ if settings.proxy.intercept.active:
81
+ settings.proxy.intercept.active = False
82
+ handle_intercept_active_update(settings)
76
83
 
77
- print(f"Updating intercept mode to {kwargs['mode']}")
84
+ settings.proxy.intercept.mode = kwargs['mode']
85
+
86
+ print(f"Updating intercept mode to {kwargs['mode']}")
78
87
 
79
88
  _mode = kwargs['mode'] or settings.proxy.intercept.mode
80
89
 
@@ -98,9 +107,9 @@ def configure(**kwargs):
98
107
  elif active_mode == mode.TEST:
99
108
  data_rule.test_policy = kwargs['policy']
100
109
 
110
+ handle_policy_update(settings)
111
+
101
112
  print(f"Updating {_mode} policy to {kwargs['policy']}")
102
-
103
- handle_policy_update(settings)
104
113
 
105
114
  settings.commit()
106
115
 
@@ -116,7 +116,7 @@ def list(**kwargs):
116
116
  @click.option('--format', type=click.Choice([BODY_FORMAT, JSON_FORMAT]), help='Format replay response.')
117
117
  @click.option('--host', help='Rewrite request host.')
118
118
  @ConditionalDecorator(lambda f: click.option('--group-by', help='Repeat for each alias name.')(f), is_remote)
119
- @click.option('--lifecycle-hooks-script-path', help='Path to lifecycle hooks script.')
119
+ @click.option('--lifecycle-hooks-path', help='Path to lifecycle hooks script.')
120
120
  @click.option(
121
121
  '--log-level', default=logger.WARNING, type=click.Choice(log_levels),
122
122
  help='''
@@ -215,7 +215,7 @@ if is_remote:
215
215
  @click.option('--format', type=click.Choice([BODY_FORMAT, JSON_FORMAT]), help='Format replay response.')
216
216
  @click.option('--group-by', help='Repeat for each alias name.')
217
217
  @click.option('--host', help='Rewrite request host.')
218
- @click.option('--lifecycle-hooks-script-path', help='Path to lifecycle hooks script.')
218
+ @click.option('--lifecycle-hooks-path', help='Path to lifecycle hooks script.')
219
219
  @click.option(
220
220
  '--log-level', default=logger.WARNING, type=click.Choice(log_levels),
221
221
  help='''
@@ -65,7 +65,7 @@ def create(**kwargs):
65
65
  @click.option('--format', type=click.Choice([JSON_FORMAT]), help='Format replay response.')
66
66
  @ConditionalDecorator(lambda f: click.option('--group-by', help='Repeat for each alias name.')(f), is_remote)
67
67
  @click.option('--host', help='Rewrite request host.')
68
- @click.option('--lifecycle-hooks-script-path', help='Path to lifecycle hooks script.')
68
+ @click.option('--lifecycle-hooks-path', help='Path to lifecycle hooks script.')
69
69
  @click.option(
70
70
  '--log-level', default=logger.WARNING, type=click.Choice([logger.DEBUG, logger.INFO, logger.WARNING, logger.ERROR]),
71
71
  help='''
@@ -261,7 +261,7 @@ if is_remote:
261
261
  @click.option('--format', type=click.Choice([JSON_FORMAT]), help='Format replay response.')
262
262
  @click.option('--group-by', help='Repeat for each alias name.')
263
263
  @click.option('--host', help='Rewrite request host.')
264
- @click.option('--lifecycle-hooks-script-path', help='Path to lifecycle hooks script.')
264
+ @click.option('--lifecycle-hooks-path', help='Path to lifecycle hooks script.')
265
265
  @click.option(
266
266
  '--log-level', default=logger.WARNING, type=click.Choice([logger.DEBUG, logger.INFO, logger.WARNING, logger.ERROR]),
267
267
  help='''
@@ -4,7 +4,7 @@ from stoobly_agent.app.models.types import OPENAPI_FORMAT
4
4
 
5
5
  class EndpointCreateCliOptions(TypedDict):
6
6
  format: Optional[Literal[f"{OPENAPI_FORMAT}"]]
7
- lifecycle_hooks_script_path: Optional[str]
7
+ lifecycle_hooks_path: Optional[str]
8
8
  path: str
9
9
  project_key: Optional[str]
10
10
  scenario_key: Optional[str]
@@ -28,7 +28,7 @@ class RawHttpRequestAdapter():
28
28
 
29
29
  if decoded_key in self.headers:
30
30
  separator = HEADER_COOKIE_SEPARATOR if decoded_key.lower() == HEADER_COOKIE else HEADER_SEPARATOR
31
- self.headers[decoded_key] = separator.join(self.headers[decoded_key], decoded_value)
31
+ self.headers[decoded_key] = separator.join([self.headers[decoded_key], decoded_value])
32
32
  else:
33
33
  self.headers[decoded_key] = decoded_value
34
34
 
@@ -35,7 +35,7 @@ class RawHttpResponseAdapter():
35
35
  decoded_key = decode(header_key)
36
36
  decoded_value = decode(header_value).strip()
37
37
  if decoded_key in self.headers:
38
- self.headers[decoded_key] = HEADER_SEPARATOR.join(self.headers[decoded_key], decoded_value)
38
+ self.headers[decoded_key] = HEADER_SEPARATOR.join([self.headers[decoded_key], decoded_value])
39
39
  else:
40
40
  self.headers[decoded_key] = decoded_value
41
41
 
@@ -0,0 +1,18 @@
1
+ from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
2
+
3
+ from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
4
+
5
+ class InterceptContext():
6
+
7
+ def __init__(self, flow: MitmproxyHTTPFlow, intercept_settings: InterceptSettings):
8
+ self.__flow = flow
9
+ self.__intercept_settings = intercept_settings
10
+
11
+ @property
12
+ def flow(self):
13
+ return self.__flow
14
+
15
+ @property
16
+ def intercept_settings(self):
17
+ return self.__intercept_settings
18
+
@@ -41,7 +41,7 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
41
41
  # Rewrite request with paramter rules for mock
42
42
  request: MitmproxyRequest = context.flow.request
43
43
  request_facade = MitmproxyRequestFacade(request)
44
- request_facade.with_rewrite_rules(rewrite_rules).rewrite()
44
+ request_facade.with_parameter_rules(rewrite_rules).rewrite()
45
45
 
46
46
  __mock_hook(lifecycle_hooks.BEFORE_MOCK, context)
47
47
 
@@ -28,10 +28,11 @@ def __replay_request(replay_context: ReplayContext):
28
28
  """
29
29
  intercept_settings: InterceptSettings = replay_context.intercept_settings
30
30
  rewrite_rules = intercept_settings.rewrite_rules
31
+
31
32
  if len(rewrite_rules) > 0:
32
33
  request: MitmproxyRequest = replay_context.flow.request
33
34
  request_facade = MitmproxyRequestFacade(request)
34
- request_facade.with_rewrite_rules(rewrite_rules).rewrite()
35
+ request_facade.with_parameter_rules(rewrite_rules).with_url_rules(rewrite_rules).rewrite()
35
36
 
36
37
  __replay_hook(lifecycle_hooks.BEFORE_REPLAY, replay_context)
37
38
 
@@ -2,8 +2,9 @@ import os
2
2
  import pdb
3
3
 
4
4
  from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
5
- from mitmproxy.http import Headers, Request as MitmproxyRequest, Response as MitmproxyResponse
5
+ from mitmproxy.http import Headers, Request as MitmproxyRequest
6
6
 
7
+ from stoobly_agent.app.proxy.context import InterceptContext
7
8
  from stoobly_agent.app.proxy.handle_mock_service import handle_request_mock
8
9
  from stoobly_agent.app.proxy.handle_replay_service import handle_request_replay, handle_response_replay
9
10
  from stoobly_agent.app.proxy.handle_record_service import handle_response_record
@@ -14,7 +15,7 @@ from stoobly_agent.app.proxy.replay.context import ReplayContext
14
15
  from stoobly_agent.app.proxy.record.context import RecordContext
15
16
  from stoobly_agent.app.proxy.utils.response_handler import bad_request
16
17
  from stoobly_agent.app.settings import Settings
17
- from stoobly_agent.config.constants import mode
18
+ from stoobly_agent.config.constants import lifecycle_hooks, mode
18
19
  from stoobly_agent.lib.logger import Logger
19
20
 
20
21
  # Disable proxy settings in urllib
@@ -29,6 +30,8 @@ def request(flow: MitmproxyHTTPFlow):
29
30
 
30
31
  intercept_settings = InterceptSettings(Settings.instance(), request)
31
32
 
33
+ __intercept_hook(lifecycle_hooks.BEFORE_REQUEST, flow, intercept_settings)
34
+
32
35
  active_mode = intercept_settings.mode
33
36
  Logger.instance().debug(f"{LOG_ID}:ProxyMode: {active_mode}")
34
37
 
@@ -56,6 +59,8 @@ def response(flow: MitmproxyHTTPFlow):
56
59
  intercept_settings = InterceptSettings(Settings.instance(), request)
57
60
  intercept_settings.for_response()
58
61
 
62
+ __intercept_hook(lifecycle_hooks.BEFORE_RESPONSE, flow, intercept_settings)
63
+
59
64
  active_mode = intercept_settings.mode
60
65
 
61
66
  if active_mode == mode.RECORD:
@@ -90,4 +95,10 @@ def __patch_cookie(request: MitmproxyRequest):
90
95
 
91
96
  def __combine_header(headers: Headers, header_name: str, delimitter: str):
92
97
  values = headers.get_all(header_name)
93
- headers[header_name] = delimitter.join(values)
98
+ headers[header_name] = delimitter.join(values)
99
+
100
+ def __intercept_hook(hook: str, flow: MitmproxyHTTPFlow, intercept_settings: InterceptSettings):
101
+ lifecycle_hooks_module = intercept_settings.lifecycle_hooks
102
+
103
+ if hook in lifecycle_hooks_module:
104
+ lifecycle_hooks_module[hook](InterceptContext(flow, intercept_settings))
@@ -10,8 +10,8 @@ from stoobly_agent.app.settings.constants import firewall_action, intercept_mode
10
10
  from stoobly_agent.app.settings.rewrite_rule import RewriteRule
11
11
  from stoobly_agent.app.settings.firewall_rule import FirewallRule
12
12
  from stoobly_agent.app.settings import Settings
13
- from stoobly_agent.app.settings.types import IgnoreRule, MatchRule, RedactRule
14
- from stoobly_agent.config.constants import custom_headers, mode, request_origin, test_filter
13
+ from stoobly_agent.app.settings.types import IgnoreRule, MatchRule
14
+ from stoobly_agent.config.constants import custom_headers, env_vars, mode, request_origin, test_filter
15
15
  from stoobly_agent.lib.api.keys.project_key import InvalidProjectKey, ProjectKey
16
16
  from stoobly_agent.lib.logger import Logger
17
17
 
@@ -62,9 +62,12 @@ class InterceptSettings:
62
62
  return custom_headers.PROXY_MODE in self.__headers
63
63
 
64
64
  @property
65
- def lifecycle_hooks_script_path(self):
66
- if self.__headers and custom_headers.LIFECYCLE_HOOKS_SCRIPT_PATH in self.__headers:
67
- return self.__headers[custom_headers.LIFECYCLE_HOOKS_SCRIPT_PATH]
65
+ def lifecycle_hooks_path(self):
66
+ if self.__headers and custom_headers.LIFECYCLE_HOOKS_PATH in self.__headers:
67
+ return self.__headers[custom_headers.LIFECYCLE_HOOKS_PATH]
68
+
69
+ if os.environ.get(env_vars.AGENT_LIFECYCLE_HOOKS_PATH):
70
+ return os.environ[env_vars.AGENT_LIFECYCLE_HOOKS_PATH]
68
71
 
69
72
  @property
70
73
  def lifecycle_hooks(self):
@@ -151,11 +154,11 @@ class InterceptSettings:
151
154
  return self.__select_rewrite_rules(_mode)
152
155
 
153
156
  @property
154
- def record_rewrite_rules(self) -> List[RedactRule]:
157
+ def record_rewrite_rules(self) -> List[RewriteRule]:
155
158
  return self.__select_rewrite_rules(mode.RECORD)
156
159
 
157
160
  @property
158
- def mock_rewrite_rules(self) -> List[RedactRule]:
161
+ def mock_rewrite_rules(self) -> List[RewriteRule]:
159
162
  return self.__select_rewrite_rules(mode.MOCK)
160
163
 
161
164
  @property
@@ -207,21 +210,18 @@ class InterceptSettings:
207
210
 
208
211
  # Filter only parameters matching active intercept mode
209
212
  for rewrite_rule in self.__rewrite_rules:
210
- parameter_rules = self.__select_parameter_rules(rewrite_rule, mode)
213
+ # If url rule applies, then update .url_rules with url_rule
214
+ url_rules = self.__select_url_rules(rewrite_rule)
211
215
 
212
- # If no parameters rules were found, then this filter rule is not applied
213
- if len(parameter_rules) == 0:
214
- continue
215
-
216
- # Build a new RewriteRule object contain only parameter rules matching intercept mode
217
- rewrite_rule = RewriteRule({
218
- 'methods': rewrite_rule.methods,
219
- 'pattern': rewrite_rule.pattern,
220
- 'parameters_rules': [], # Has to be dict form, manually set it
221
- })
222
- rewrite_rule.parameter_rules = parameter_rules
216
+ # If parameters rules apply, then update .parameter_rules with applicable parameter_rules
217
+ parameter_rules = self.__select_parameter_rules(rewrite_rule, mode)
223
218
 
224
- rules.append(rewrite_rule)
219
+ if len(url_rules) > 0 or len(parameter_rules) > 0:
220
+ # Build a new RewriteRule object contain only parameter rules matching intercept mode
221
+ rewrite_rule = RewriteRule(rewrite_rule.to_dict())
222
+ rewrite_rule.url_rules = url_rules
223
+ rewrite_rule.parameter_rules = parameter_rules
224
+ rules.append(rewrite_rule)
225
225
 
226
226
  return rules
227
227
 
@@ -232,8 +232,15 @@ class InterceptSettings:
232
232
  rewrite_rule.parameter_rules or []
233
233
  ))
234
234
 
235
+ def __select_url_rules(self, rewrite_rule: RewriteRule, mode = None):
236
+ mode = mode or self.mode
237
+ return list(filter(
238
+ lambda url: mode in url.modes,
239
+ rewrite_rule.url_rules or []
240
+ ))
241
+
235
242
  def __initialize_lifecycle_hooks(self):
236
- script_path = self.lifecycle_hooks_script_path
243
+ script_path = self.lifecycle_hooks_path
237
244
 
238
245
  if not script_path:
239
246
  return