stoobly-agent 1.8.5__py3-none-any.whl → 1.9.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 (43) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/configs_controller.py +3 -3
  3. stoobly_agent/app/cli/helpers/handle_config_update_service.py +12 -12
  4. stoobly_agent/app/cli/intercept_cli.py +30 -7
  5. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  6. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +6 -0
  7. stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/.overwrite +13 -0
  8. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +16 -0
  9. stoobly_agent/app/models/adapters/joined_request_adapter.py +1 -1
  10. stoobly_agent/app/models/helpers/apply.py +2 -2
  11. stoobly_agent/app/proxy/handle_mock_service.py +19 -6
  12. stoobly_agent/app/proxy/handle_record_service.py +16 -7
  13. stoobly_agent/app/proxy/intercept_settings.py +12 -1
  14. stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
  15. stoobly_agent/app/proxy/record/request_string.py +15 -9
  16. stoobly_agent/app/proxy/replay/body_parser_service.py +15 -4
  17. stoobly_agent/app/proxy/replay/replay_request_service.py +1 -0
  18. stoobly_agent/app/settings/data_rules.py +16 -1
  19. stoobly_agent/config/constants/custom_headers.py +1 -0
  20. stoobly_agent/config/constants/lifecycle_hooks.py +1 -0
  21. stoobly_agent/config/constants/record_order.py +6 -0
  22. stoobly_agent/config/constants/record_policy.py +2 -2
  23. stoobly_agent/public/18-es2015.beb31fe4a4dee3007cb2.js +1 -0
  24. stoobly_agent/public/18-es5.beb31fe4a4dee3007cb2.js +1 -0
  25. stoobly_agent/public/index.html +2 -2
  26. stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +1 -0
  27. stoobly_agent/public/main-es5.089b46f303768fbe864f.js +1 -0
  28. stoobly_agent/public/{runtime-es2015.b13c22b834b51724d30a.js → runtime-es2015.f8c814b38b27708e91c1.js} +1 -1
  29. stoobly_agent/public/{runtime-es5.b13c22b834b51724d30a.js → runtime-es5.f8c814b38b27708e91c1.js} +1 -1
  30. stoobly_agent/public/{styles.ab281309cf423b2cdcb0.css → styles.817f011ab81b18b0e5c2.css} +1 -1
  31. stoobly_agent/test/app/cli/config/scenario/config_scenario_set_test.py +3 -3
  32. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +5 -7
  33. stoobly_agent/test/app/cli/intercept/intercept_enable_test.py +3 -5
  34. stoobly_agent/test/cli/record_test.py +3 -3
  35. {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.0.dist-info}/METADATA +1 -1
  36. {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.0.dist-info}/RECORD +39 -37
  37. stoobly_agent/public/18-es2015.d07dd29def7e2574c5b7.js +0 -1
  38. stoobly_agent/public/18-es5.d07dd29def7e2574c5b7.js +0 -1
  39. stoobly_agent/public/main-es2015.ce00115b0520fa030f01.js +0 -1
  40. stoobly_agent/public/main-es5.ce00115b0520fa030f01.js +0 -1
  41. {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.0.dist-info}/LICENSE +0 -0
  42. {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.0.dist-info}/WHEEL +0 -0
  43. {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.0.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '1.8.5'
2
+ VERSION = '1.9.0'
@@ -4,7 +4,7 @@ from mergedeep import merge
4
4
 
5
5
  from stoobly_agent.app.api.simple_http_request_handler import SimpleHTTPRequestHandler
6
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
7
+ context as handle_context, handle_intercept_active_update, handle_order_update, handle_project_update, handle_scenario_update
8
8
  )
9
9
  from stoobly_agent.app.models.scenario_model import ScenarioModel
10
10
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
@@ -41,7 +41,7 @@ class ConfigsController:
41
41
  )
42
42
  elif active_mode == mode.RECORD:
43
43
  context.render(
44
- json = [record_policy.ALL, record_policy.FOUND, record_policy.NOT_FOUND, record_policy.OVERWRITE],
44
+ json = [record_policy.ALL, record_policy.API, record_policy.FOUND, record_policy.NOT_FOUND],
45
45
  status = 200
46
46
  )
47
47
  elif active_mode == mode.REPLAY:
@@ -112,7 +112,7 @@ class ConfigsController:
112
112
  _handle_context = handle_context()
113
113
 
114
114
  handle_intercept_active_update(settings, _handle_context)
115
- handle_policy_update(settings, _handle_context)
115
+ handle_order_update(settings, _handle_context)
116
116
  handle_project_update(settings, _handle_context)
117
117
  handle_scenario_update(settings, _handle_context)
118
118
 
@@ -6,7 +6,7 @@ from stoobly_agent.app.models.scenario_model import ScenarioModel
6
6
  from stoobly_agent.app.settings.constants import intercept_mode
7
7
  from stoobly_agent.app.settings.constants.intercept_mode import Mode
8
8
  from stoobly_agent.app.settings import ProxySettings, Settings
9
- from stoobly_agent.config.constants import record_policy
9
+ from stoobly_agent.config.constants import record_order
10
10
  from stoobly_agent.lib.api.keys.project_key import ProjectKey
11
11
  from stoobly_agent.lib.api.keys.scenario_key import ScenarioKey
12
12
 
@@ -28,15 +28,15 @@ def handle_intercept_active_update(new_settings: Settings, context: Context = No
28
28
 
29
29
  if not was_active and is_active:
30
30
  if _mode == intercept_mode.RECORD:
31
- new_policy = data_rule.record_policy
31
+ new_order = data_rule.record_order
32
32
  scenario_key = data_rule.scenario_key
33
33
  _scenario_key = __parse_scenario_key(scenario_key)
34
34
 
35
35
  if _scenario_key:
36
36
  scenario_model = ScenarioModel(new_settings)
37
37
 
38
- if new_policy == record_policy.OVERWRITE:
39
- # If policy is overwrite when recording, whenever intercept is enabled,
38
+ if new_order == record_order.OVERWRITE:
39
+ # If order is overwrite when recording, whenever intercept is enabled,
40
40
  # set active scenario to be overwritable
41
41
  scenario_model.update(_scenario_key.id, **{ 'overwritable': True })[1]
42
42
  else:
@@ -44,16 +44,16 @@ def handle_intercept_active_update(new_settings: Settings, context: Context = No
44
44
  elif was_active and not is_active:
45
45
  if _mode == intercept_mode.RECORD:
46
46
  old_proxy_settings = __current_proxy_settings(context)
47
- old_policy = __data_rule(old_proxy_settings).record_policy
47
+ old_order = __data_rule(old_proxy_settings).record_order
48
48
 
49
- if old_policy == record_policy.OVERWRITE:
49
+ if old_order == record_order.OVERWRITE:
50
50
  scenario_key = data_rule.scenario_key
51
51
  _scenario_key = __parse_scenario_key(scenario_key)
52
52
 
53
53
  if _scenario_key:
54
54
  scenario_model = ScenarioModel(new_settings)
55
55
 
56
- # If policy is overwrite when recording, whenever intercept is disabled,
56
+ # If order is overwrite when recording, whenever intercept is disabled,
57
57
  # set active scenario to not be overwritable
58
58
  scenario_model.update(_scenario_key.id, **{ 'overwritable': False })[1]
59
59
  elif _mode == intercept_mode.MOCK:
@@ -73,7 +73,7 @@ def handle_scenario_update(new_settings: Settings, context = None):
73
73
  if old_scenario_key != new_scenario_key:
74
74
  data_rule = __data_rule(new_settings.proxy)
75
75
 
76
- if data_rule.record_policy == record_policy.OVERWRITE:
76
+ if data_rule.record_order == record_order.OVERWRITE:
77
77
  scenario_model = ScenarioModel(new_settings)
78
78
 
79
79
  _old_scenario_key = __parse_scenario_key(old_scenario_key)
@@ -108,7 +108,7 @@ def handle_project_update(new_settings: Settings, context: Context = None):
108
108
  if _project_key.id != _new_scenario_key.project_id:
109
109
  data_rule.scenario_key = None
110
110
 
111
- def handle_policy_update(new_settings: Settings, context: Context = None):
111
+ def handle_order_update(new_settings: Settings, context: Context = None):
112
112
  data_rule = __data_rule(new_settings.proxy)
113
113
  _mode = context['mode'] if context and context.get('mode') else new_settings.proxy.intercept.mode
114
114
 
@@ -116,10 +116,10 @@ def handle_policy_update(new_settings: Settings, context: Context = None):
116
116
  old_data_rule = __data_rule(old_proxy_settings)
117
117
 
118
118
  if _mode == intercept_mode.RECORD:
119
- new_policy = data_rule.record_policy
120
- old_policy = old_data_rule.record_policy
119
+ new_order = data_rule.record_order
120
+ old_order = old_data_rule.record_order
121
121
 
122
- if new_policy != old_policy and old_policy == record_policy.OVERWRITE:
122
+ if new_order != old_order and old_order == record_order.OVERWRITE:
123
123
  scenario_key = data_rule.scenario_key
124
124
  _scenario_key = __parse_scenario_key(scenario_key)
125
125
 
@@ -3,10 +3,10 @@ import pdb
3
3
  import sys
4
4
 
5
5
  from stoobly_agent.app.settings import Settings
6
- from stoobly_agent.config.constants import mode, mock_policy, record_policy, replay_policy
6
+ from stoobly_agent.config.constants import mode, mock_policy, record_order, 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_intercept_active_update, handle_policy_update
9
+ from .helpers.handle_config_update_service import handle_intercept_active_update, handle_order_update
10
10
 
11
11
  settings = Settings.instance()
12
12
 
@@ -17,16 +17,25 @@ if settings.cli.features.remote:
17
17
 
18
18
  active_mode = settings.proxy.intercept.mode
19
19
 
20
+ def __get_order_options(active_mode):
21
+ if active_mode == mode.RECORD:
22
+ return [record_order.APPEND, record_order.OVERWRITE]
23
+ else:
24
+ return []
25
+
20
26
  def __get_policy_options(active_mode):
21
27
  if active_mode == mode.MOCK:
22
28
  return [mock_policy.ALL, mock_policy.FOUND]
23
29
  elif active_mode == mode.RECORD:
24
- return [record_policy.ALL, record_policy.FOUND, record_policy.NOT_FOUND,record_policy.OVERWRITE]
30
+ return [record_policy.ALL, record_policy.API, record_policy.FOUND, record_policy.NOT_FOUND]
25
31
  elif active_mode == mode.REPLAY:
26
32
  return [replay_policy.ALL]
27
33
  elif active_mode == mode.TEST:
28
34
  return [mock_policy.FOUND]
35
+ else:
36
+ return []
29
37
 
38
+ order_options = __get_order_options(active_mode)
30
39
  policy_options = __get_policy_options(active_mode)
31
40
 
32
41
  @click.group(
@@ -69,11 +78,12 @@ def disable(**kwargs):
69
78
  help="Configure intercept"
70
79
  )
71
80
  @click.option('--mode', type=click.Choice(mode_options))
81
+ @click.option('--order', type=click.Choice(order_options))
72
82
  @click.option('--policy', type=click.Choice(policy_options))
73
83
  def configure(**kwargs):
74
- settings = Settings.instance()
84
+ settings: Settings = Settings.instance()
75
85
 
76
- if not kwargs['mode'] and not kwargs['policy']:
86
+ if not kwargs['mode'] and not kwargs['order'] and not kwargs['policy']:
77
87
  print("Error: Missing an option")
78
88
  sys.exit(1)
79
89
 
@@ -89,6 +99,21 @@ def configure(**kwargs):
89
99
 
90
100
  _mode = kwargs['mode'] or settings.proxy.intercept.mode
91
101
 
102
+ if kwargs['order']:
103
+ active_mode = settings.proxy.intercept.mode
104
+
105
+ if active_mode == mode.RECORD:
106
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
107
+ data_rule = settings.proxy.data.data_rules(project_key.id)
108
+ data_rule.record_order = kwargs['order']
109
+ else:
110
+ print("Error: set --mode to a intercept mode that supports the order option", file=sys.stderr)
111
+ sys.exit(1)
112
+
113
+ handle_order_update(settings)
114
+
115
+ print(f"Updating {_mode} order to {kwargs['order']}")
116
+
92
117
  if kwargs['policy']:
93
118
  active_mode = settings.proxy.intercept.mode
94
119
  valid_policies = __get_policy_options(active_mode)
@@ -109,8 +134,6 @@ def configure(**kwargs):
109
134
  elif active_mode == mode.TEST:
110
135
  data_rule.test_policy = kwargs['policy']
111
136
 
112
- handle_policy_update(settings)
113
-
114
137
  print(f"Updating {_mode} policy to {kwargs['policy']}")
115
138
 
116
139
  settings.commit()
@@ -1,4 +1,4 @@
1
- FROM stoobly/agent:1.8
1
+ FROM stoobly/agent:1.9
2
2
 
3
3
  ARG USER_ID
4
4
 
@@ -143,6 +143,12 @@ scenario/list:
143
143
  @export EXEC_COMMAND=.list && \
144
144
  export EXEC_OPTIONS="$(options)" && \
145
145
  $(stoobly_exec)
146
+ scenario/overwrite:
147
+ # Overwrite a scenario
148
+ @export EXEC_COMMAND=.overwrite && \
149
+ export EXEC_OPTIONS="$(options)" && \
150
+ export EXEC_ARGS="$(key)" && \
151
+ $(stoobly_exec)
146
152
  scenario/reset:
147
153
  # Resets a scenario to its last snapshot
148
154
  @export EXEC_COMMAND=.reset && \:
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+
3
+ scenario_key=$1
4
+
5
+ if [ ! "$scenario_key" ]; then
6
+ echo "Missing argument 'scenario_key'"
7
+ stoobly-agent scenario list
8
+ exit
9
+ fi
10
+
11
+ stoobly-agent config scenario set "$scenario_key"
12
+ stoobly-agent intercept configure --mode record --order overwrite
13
+ stoobly-agent intercept disable
@@ -22,6 +22,14 @@ stoobly-agent config firewall set \
22
22
 
23
23
  # Rewrite
24
24
  echo "Configuring rewrite rules..."
25
+ stoobly-agent config rewrite set \
26
+ --method GET --method POST --method OPTIONS --method PUT --method DELETE \
27
+ --mode record \
28
+ --name authorization \
29
+ --pattern "$origin/?.*?" \
30
+ --type Header \
31
+ --value '' \
32
+
25
33
  stoobly-agent config rewrite set \
26
34
  --method GET --method POST --method OPTIONS --method PUT --method DELETE \
27
35
  --mode record \
@@ -29,3 +37,11 @@ stoobly-agent config rewrite set \
29
37
  --pattern "$origin/?.*?" \
30
38
  --type Header \
31
39
  --value '' \
40
+
41
+ stoobly-agent config rewrite set \
42
+ --method GET --method POST --method OPTIONS --method PUT --method DELETE \
43
+ --mode record \
44
+ --name set-cookie \
45
+ --pattern "$origin/?.*?" \
46
+ --type 'Response Header' \
47
+ --value '' \
@@ -46,7 +46,7 @@ class JoinedRequestAdapter():
46
46
  def build_request_string(self):
47
47
  request_string = RequestString(None)
48
48
 
49
- delimitter = RequestStringCLRF.encode()
49
+ delimitter = RequestStringCLRF
50
50
  request_string_toks = self.__split_joined_request_string[0].split(delimitter)
51
51
  request_string.set(self.raw_request_string or delimitter.join(request_string_toks[1:]))
52
52
  request_string.control = request_string_toks[0]
@@ -3,7 +3,7 @@ import pdb
3
3
  from stoobly_agent.app.models.factories.resource.local_db.helpers.log import Log
4
4
  from stoobly_agent.app.models.factories.resource.local_db.helpers.request_snapshot import RequestSnapshot
5
5
  from stoobly_agent.app.models.factories.resource.local_db.helpers.scenario_snapshot import ScenarioSnapshot
6
- from stoobly_agent.app.proxy.record import REQUEST_STRING_CLRF, RequestStringControl
6
+ from stoobly_agent.app.proxy.record import REQUEST_STRING_CLRF
7
7
  from stoobly_agent.app.settings import Settings
8
8
  from stoobly_agent.lib.logger import bcolors
9
9
 
@@ -222,7 +222,7 @@ class Apply():
222
222
  for request_snapshot in request_snapshots:
223
223
  raw_request = request_snapshot.request
224
224
 
225
- toks = raw_request.split(REQUEST_STRING_CLRF.encode(), 1)
225
+ toks = raw_request.split(REQUEST_STRING_CLRF, 1)
226
226
 
227
227
  if len(toks) != 2:
228
228
  return f"{snapshot.requests_path} contains an invalid request", 400
@@ -3,7 +3,7 @@ import pdb
3
3
  import requests
4
4
  import time
5
5
 
6
- from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow, Request as MitmproxyRequest
6
+ from mitmproxy.http import Request as MitmproxyRequest
7
7
  from typing import Callable, TypedDict
8
8
 
9
9
  from stoobly_agent.app.models.request_model import RequestModel
@@ -64,7 +64,7 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
64
64
 
65
65
  return bad_request(
66
66
  context.flow,
67
- "Valid env MOCK_POLICY: %s, Got: %s" %
67
+ "Valid mock policies: %s, Got: %s" %
68
68
  ([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
69
69
  )
70
70
 
@@ -88,18 +88,20 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
88
88
  res = eval_request_with_retry(context, eval_request, **options)
89
89
 
90
90
  context.with_response(res)
91
+ res = __after_mock_not_found(context)
91
92
  elif policy == mock_policy.FOUND:
92
93
  res = eval_request_with_retry(context, eval_request, **options)
93
94
 
94
95
  context.with_response(res)
96
+ res = __after_mock_not_found(context)
95
97
 
96
- if res.status_code in [custom_response_codes.NOT_FOUND, custom_response_codes.IGNORE_COMPONENTS]:
98
+ if res.status_code == custom_response_codes.NOT_FOUND:
97
99
  try:
98
100
  return __handle_found_policy(context) # Continue proxying the request
99
101
  except RuntimeError:
100
102
  # Do nothing, return custom error response
101
- pass
102
-
103
+ pass
104
+
103
105
  if res.status_code == custom_response_codes.NOT_FOUND:
104
106
  if handle_failure:
105
107
  res = handle_failure(context) or res
@@ -241,4 +243,15 @@ def __mock_hook(hook: str, context: MockContext):
241
243
  lifecycle_hooks_module = intercept_settings.lifecycle_hooks
242
244
 
243
245
  if hook in lifecycle_hooks_module:
244
- lifecycle_hooks_module[hook](context)
246
+ lifecycle_hooks_module[hook](context)
247
+
248
+ def __after_mock_not_found(context: MockContext):
249
+ res = context.response
250
+
251
+ if res.status_code == custom_response_codes.NOT_FOUND:
252
+ __mock_hook(lifecycle_hooks.AFTER_MOCK_NOT_FOUND, context)
253
+
254
+ # context.response may have been modified by the hook
255
+ res = context.response
256
+
257
+ return res
@@ -9,7 +9,7 @@ from stoobly_agent.app.settings.constants.mode import TEST
9
9
  from stoobly_agent.app.models.request_model import RequestModel
10
10
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
11
11
  from stoobly_agent.config.constants.env_vars import ENV
12
- from stoobly_agent.config.constants import lifecycle_hooks, record_policy
12
+ from stoobly_agent.config.constants import lifecycle_hooks, record_order, record_policy
13
13
  from stoobly_agent.lib.logger import Logger
14
14
 
15
15
  from .constants import custom_response_codes
@@ -17,6 +17,7 @@ from .mock.eval_request_service import inject_eval_request
17
17
  from .record.context import RecordContext
18
18
  from .record.overwrite_scenario_service import overwrite_scenario
19
19
  from .record.upload_request_service import inject_upload_request
20
+ from .replay.body_parser_service import is_json, is_xml
20
21
  from .utils.allowed_request_service import get_active_mode_policy
21
22
  from .utils.response_handler import bad_request, disable_transfer_encoding
22
23
  from .utils.rewrite import rewrite_request_response
@@ -42,6 +43,13 @@ def handle_response_record(context: RecordContext):
42
43
 
43
44
  if active_record_policy == record_policy.ALL:
44
45
  __record_request(context, request_model)
46
+ elif active_record_policy == record_policy.API:
47
+ response = flow.response
48
+ content_type: str = response.headers.get('content-type')
49
+
50
+ if content_type:
51
+ if is_json(content_type) or is_xml(content_type) or content_type.startswith('text/plain'):
52
+ __record_request(context, request_model)
45
53
  elif active_record_policy == record_policy.FOUND:
46
54
  res = inject_eval_request(request_model, intercept_settings)(request, [])
47
55
 
@@ -52,16 +60,12 @@ def handle_response_record(context: RecordContext):
52
60
 
53
61
  if res.status_code == custom_response_codes.NOT_FOUND:
54
62
  __record_request(context, request_model)
55
- elif active_record_policy == record_policy.OVERWRITE:
56
- overwrite_scenario(intercept_settings.scenario_key)
57
-
58
- __record_request(context, request_model)
59
63
  else:
60
64
  if active_record_policy != record_policy.NONE:
61
65
  return bad_request(
62
66
  flow,
63
- "Valid env RECORD_POLICY: %s, Got: %s" %
64
- ([record_policy.ALL, record_policy.FOUND, record_policy.NOT_FOUND], active_record_policy)
67
+ "Valid record policies: %s, Got: %s" %
68
+ ([record_policy.ALL, record_policy.API, record_policy.FOUND, record_policy.NOT_FOUND], active_record_policy)
65
69
  )
66
70
 
67
71
  def __record_handler(context: RecordContext, request_model: RequestModel):
@@ -79,6 +83,11 @@ def __record_handler(context: RecordContext, request_model: RequestModel):
79
83
  context.flow = flow # Reset flow
80
84
 
81
85
  def __record_request(context: RecordContext, request_model: RequestModel):
86
+ intercept_settings = context.intercept_settings
87
+
88
+ if intercept_settings.order == record_order.OVERWRITE:
89
+ overwrite_scenario(intercept_settings.scenario_key)
90
+
82
91
  if os.environ.get(ENV) == TEST:
83
92
  __record_handler(context, request_model)
84
93
  else:
@@ -166,6 +166,10 @@ class InterceptSettings:
166
166
  if self.__headers and custom_headers.REPORT_KEY in self.__headers:
167
167
  return self.__headers[custom_headers.REPORT_KEY]
168
168
 
169
+ @property
170
+ def order(self):
171
+ return self.__order(self.mode)
172
+
169
173
  @property
170
174
  def policy(self):
171
175
  return self.__policy(self.mode)
@@ -339,7 +343,11 @@ class InterceptSettings:
339
343
  self.__response_fixtures = yaml.safe_load(stream)
340
344
  except yaml.YAMLError as exc:
341
345
  Logger.instance().error(exc)
342
-
346
+
347
+ def __order(self, mode):
348
+ if mode == intercept_mode.RECORD:
349
+ return self.__data_rules.record_order
350
+
343
351
  def __policy(self, mode):
344
352
  if mode == intercept_mode.MOCK:
345
353
  if self.__headers and custom_headers.MOCK_POLICY in self.__headers:
@@ -352,6 +360,9 @@ class InterceptSettings:
352
360
 
353
361
  return self.__data_rules.record_policy
354
362
  elif mode == intercept_mode.TEST:
363
+ if self.__headers and custom_headers.TEST_POLICY in self.__headers:
364
+ return self.__headers[custom_headers.TEST_POLICY]
365
+
355
366
  return self.__data_rules.test_policy
356
367
  elif mode == intercept_mode.REPLAY:
357
368
  return self.__data_rules.replay_policy
@@ -72,7 +72,7 @@ class MitmproxyRequestFacade(Request):
72
72
  def body(self):
73
73
  content = self.request.raw_content or b''
74
74
 
75
- return decode(content)
75
+ return content
76
76
 
77
77
  @property
78
78
  def parsed_body(self):
@@ -9,7 +9,7 @@ from stoobly_agent.lib.utils.decode import decode
9
9
  from .proxy_request import ProxyRequest
10
10
  from .request_string_control import RequestStringControl
11
11
 
12
- CLRF = "\r\n"
12
+ CLRF = b"\r\n"
13
13
 
14
14
  class RequestString:
15
15
  ENCODING = 'utf-8'
@@ -36,20 +36,19 @@ class RequestString:
36
36
 
37
37
  def get(self, **kwargs):
38
38
  if kwargs.get('control'):
39
- return CLRF.join([self.control] + self.lines).encode(self.ENCODING)
39
+ return CLRF.join([self.control] + self.lines)
40
40
  else:
41
- return CLRF.join(self.lines).encode(self.ENCODING)
41
+ return CLRF.join(self.lines)
42
42
 
43
43
  def set(self, s: bytes):
44
- decoded_s = decode(s, self.ENCODING)
45
- self.lines = decoded_s.split(CLRF)
44
+ self.lines = s.split(CLRF)
46
45
 
47
46
  @property
48
47
  def control(self):
49
48
  control = RequestStringControl()
50
49
  control.id = self.request_id
51
50
  control.timestamp = self.__current_time
52
- return control.serialize()
51
+ return control.serialize().encode(self.ENCODING)
53
52
 
54
53
  @control.setter
55
54
  def control(self, c: str):
@@ -58,20 +57,27 @@ class RequestString:
58
57
  self.__current_time = control.timestamp
59
58
 
60
59
  def __request_line(self):
61
- self.lines.append("{} {} HTTP/1.1".format(self.request.method, self.proxy_request.url()))
60
+ line = "{} {} HTTP/1.1".format(self.request.method, self.proxy_request.url())
61
+ self.lines.append(line.encode(self.ENCODING))
62
62
 
63
63
  def __headers(self):
64
64
  headers = self.request.headers
65
65
 
66
66
  for name, val in headers.items():
67
+ if 'content-encoding' in headers:
68
+ # self.request.body will decode content-encoding
69
+ # to maintain internal consistency, remove this header since it will be decoded
70
+ print(self.request.body)
71
+ pass
72
+
67
73
  line = ' '.join([
68
74
  "{}:".format(self.__to_header_case(self.__to_str(name))),
69
75
  self.__to_str(val)
70
76
  ])
71
- self.lines.append(line)
77
+ self.lines.append(line.encode(self.ENCODING))
72
78
 
73
79
  def __body(self):
74
- self.lines.append("{}{}".format(CLRF, self.request.body))
80
+ self.lines.append(CLRF + self.request.body)
75
81
 
76
82
  def __to_header_case(self, header: str) -> str:
77
83
  toks = header.split('_')
@@ -14,6 +14,7 @@ from .multipart import decode as multipart_decode, encode as multipart_encode
14
14
  JSON = 'application/json'
15
15
  MULTIPART_FORM = 'multipart/form-data'
16
16
  WWW_FORM_URLENCODED = 'application/x-www-form-urlencoded'
17
+ XML = 'application/xml'
17
18
 
18
19
  def compress(body: Union[bytes, str], content_encoding: Union[None, str]) -> Union[bytes, str]:
19
20
  if content_encoding:
@@ -113,14 +114,24 @@ def normalize_header(header):
113
114
  def is_traversable(content):
114
115
  return isinstance(content, list) or isinstance(content, dict) or isinstance(content, MultiDict)
115
116
 
116
- def is_json(content_type):
117
- _content_type = content_type.lower()
118
- return _content_type == JSON or _content_type.startswith('application/x-amz-json')
117
+ def is_json(content_type: str):
118
+ if not content_type:
119
+ return False
120
+
121
+ _content_type = content_type.lower().split(';')[0]
122
+ # e.g. custom json content-type: application/x-amz-json
123
+ return _content_type == JSON or (_content_type.startswith('application/') and _content_type.endswith('json'))
119
124
 
125
+ def is_xml(content_type: str):
126
+ if not content_type:
127
+ return False
128
+
129
+ _content_type = content_type.lower().split(';')[0]
130
+ # e.g. custom json content-type: application/x-amz-json
131
+ return _content_type == XML or (_content_type.startswith('application/') and _content_type.endswith('xml'))
120
132
 
121
133
  def __parse_separated_header(header: str):
122
134
  # Adapted from https://peps.python.org/pep-0594/#cgi
123
135
  message = Message()
124
136
  message['content-type'] = header
125
137
  return message.get_content_type()
126
-
@@ -150,6 +150,7 @@ def replay(context: ReplayContext, options: ReplayRequestOptions) -> requests.Re
150
150
  if options.get('save') or options.get('overwrite'):
151
151
  replayed_response, status = __create_replayed_response(context.request.id, res, latency)
152
152
 
153
+ # TODO: allow overwriting if status is the same
153
154
  if status < 400 and options.get('overwrite'):
154
155
  __overwrite_response(replayed_response.get('id'))
155
156
 
@@ -1,4 +1,4 @@
1
- from stoobly_agent.config.constants import mock_policy, record_policy, replay_policy, test_strategy
1
+ from stoobly_agent.config.constants import mock_policy, record_order, record_policy, replay_policy, test_strategy
2
2
 
3
3
  from .types.proxy_settings import DataRules as IDataRules
4
4
 
@@ -8,6 +8,7 @@ class DataRules:
8
8
  self.__data_rules = data_rules or {}
9
9
 
10
10
  self.__mock_policy = self.__data_rules.get('mock_policy') or mock_policy.FOUND
11
+ self.__record_order = self.__data_rules.get('record_order') or record_order.APPEND
11
12
  self.__record_policy = self.__data_rules.get('record_policy') or record_policy.ALL
12
13
  self.__replay_policy = self.__data_rules.get('replay_policy') or replay_policy.ALL
13
14
  self.__scenario_key = self.__data_rules.get('scenario_key')
@@ -32,6 +33,19 @@ class DataRules:
32
33
  self.__record_policy = v
33
34
  self.__data_rules['record_policy'] = v
34
35
 
36
+ @property
37
+ def record_order(self):
38
+ return self.__record_order
39
+
40
+ @record_order.setter
41
+ def record_order(self, v):
42
+ valid_orders = [record_order.APPEND, record_order.OVERWRITE]
43
+ if v not in valid_orders:
44
+ raise TypeError(f"record_order has to be one of {valid_orders}, got {v}")
45
+
46
+ self.__record_order = v
47
+ self.__data_rules['record_order'] = v
48
+
35
49
  @property
36
50
  def replay_policy(self):
37
51
  return self.__replay_policy
@@ -68,6 +82,7 @@ class DataRules:
68
82
  def to_dict(self) -> IDataRules:
69
83
  return {
70
84
  'mock_policy': self.__mock_policy,
85
+ 'record_order': self.__record_order,
71
86
  'record_policy': self.__record_policy,
72
87
  'replay_policy': self.__replay_policy,
73
88
  'scenario_key': self.__scenario_key,
@@ -23,6 +23,7 @@ SESSION_ID = 'X-Stoobly-Session-Id'
23
23
  SERVICE_URL = 'X-Stoobly-Service-Url'
24
24
  TEST_FILTER = 'X-Stoobly-Test-Filter'
25
25
  TEST_ID = 'X-Stoobly-Test-Id'
26
+ TEST_POLICY = 'X-Stoobly-Test-Policy'
26
27
  TEST_SAVE_RESULTS = 'X-Stoobly-Test-Save-Results'
27
28
  TEST_SKIP = 'X-Stoobly-Test-Skip'
28
29
  TEST_STRATEGY = 'X-Stoobly-Test-Strategy'
@@ -1,4 +1,5 @@
1
1
  AFTER_MOCK = 'handle_after_mock'
2
+ AFTER_MOCK_NOT_FOUND = 'handle_after_mock_not_found'
2
3
  AFTER_RECORD = 'handle_after_record'
3
4
  AFTER_REPLAY = 'handle_after_replay'
4
5
  AFTER_TEST = 'handle_after_test'
@@ -0,0 +1,6 @@
1
+ from typing import Literal
2
+
3
+ APPEND = 'append'
4
+ OVERWRITE = 'overwrite'
5
+
6
+ RecordOrder = Literal[APPEND, OVERWRITE]
@@ -1,7 +1,7 @@
1
1
  from .intercept_policy import ALL as INTERCEPT_ALL, NONE as INTERCEPT_NONE
2
2
 
3
3
  ALL = INTERCEPT_ALL
4
+ API = 'api'
4
5
  FOUND = 'found'
5
6
  NONE = INTERCEPT_NONE
6
- NOT_FOUND = 'not_found'
7
- OVERWRITE = 'overwrite'
7
+ NOT_FOUND = 'not_found'