stoobly-agent 1.8.5__py3-none-any.whl → 1.9.1__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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/app/api/configs_controller.py +3 -3
- stoobly_agent/app/cli/helpers/handle_config_update_service.py +12 -12
- stoobly_agent/app/cli/intercept_cli.py +30 -7
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile +6 -0
- stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/.overwrite +13 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +16 -0
- stoobly_agent/app/models/adapters/joined_request_adapter.py +1 -1
- stoobly_agent/app/models/factories/resource/local_db/helpers/log.py +52 -2
- stoobly_agent/app/models/factories/resource/local_db/helpers/request_snapshot.py +0 -1
- stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +2 -3
- stoobly_agent/app/models/factories/resource/local_db/helpers/snapshot_service.py +9 -8
- stoobly_agent/app/models/helpers/apply.py +10 -5
- stoobly_agent/app/proxy/handle_mock_service.py +19 -6
- stoobly_agent/app/proxy/handle_record_service.py +16 -7
- stoobly_agent/app/proxy/intercept_settings.py +12 -1
- stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
- stoobly_agent/app/proxy/record/request_string.py +15 -9
- stoobly_agent/app/proxy/replay/body_parser_service.py +15 -4
- stoobly_agent/app/proxy/replay/replay_request_service.py +1 -0
- stoobly_agent/app/settings/data_rules.py +16 -1
- stoobly_agent/config/constants/custom_headers.py +1 -0
- stoobly_agent/config/constants/lifecycle_hooks.py +1 -0
- stoobly_agent/config/constants/record_order.py +6 -0
- stoobly_agent/config/constants/record_policy.py +2 -2
- stoobly_agent/public/18-es2015.beb31fe4a4dee3007cb2.js +1 -0
- stoobly_agent/public/18-es5.beb31fe4a4dee3007cb2.js +1 -0
- stoobly_agent/public/index.html +2 -2
- stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +1 -0
- stoobly_agent/public/main-es5.089b46f303768fbe864f.js +1 -0
- stoobly_agent/public/{runtime-es2015.b13c22b834b51724d30a.js → runtime-es2015.f8c814b38b27708e91c1.js} +1 -1
- stoobly_agent/public/{runtime-es5.b13c22b834b51724d30a.js → runtime-es5.f8c814b38b27708e91c1.js} +1 -1
- stoobly_agent/public/{styles.ab281309cf423b2cdcb0.css → styles.817f011ab81b18b0e5c2.css} +1 -1
- stoobly_agent/test/app/cli/config/scenario/config_scenario_set_test.py +3 -3
- stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +5 -7
- stoobly_agent/test/app/cli/intercept/intercept_enable_test.py +3 -5
- stoobly_agent/test/app/cli/snapshot/snapshot_apply_test.py +143 -1
- stoobly_agent/test/app/cli/snapshot/snapshot_prune_test.py +72 -3
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/cli/record_test.py +3 -3
- {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.1.dist-info}/METADATA +1 -1
- {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.1.dist-info}/RECORD +46 -44
- stoobly_agent/public/18-es2015.d07dd29def7e2574c5b7.js +0 -1
- stoobly_agent/public/18-es5.d07dd29def7e2574c5b7.js +0 -1
- stoobly_agent/public/main-es2015.ce00115b0520fa030f01.js +0 -1
- stoobly_agent/public/main-es5.ce00115b0520fa030f01.js +0 -1
- {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.1.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.1.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.1.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
COMMAND = 'stoobly-agent'
|
2
|
-
VERSION = '1.
|
2
|
+
VERSION = '1.9.1'
|
@@ -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,
|
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.
|
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
|
-
|
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
|
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
|
-
|
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
|
39
|
-
# If
|
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
|
-
|
47
|
+
old_order = __data_rule(old_proxy_settings).record_order
|
48
48
|
|
49
|
-
if
|
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
|
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.
|
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
|
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
|
-
|
120
|
-
|
119
|
+
new_order = data_rule.record_order
|
120
|
+
old_order = old_data_rule.record_order
|
121
121
|
|
122
|
-
if
|
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,
|
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.
|
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()
|
@@ -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
|
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]
|
@@ -8,6 +8,7 @@ from stoobly_agent.config.data_dir import DataDir
|
|
8
8
|
from stoobly_agent.lib.logger import bcolors, Logger
|
9
9
|
|
10
10
|
from .log_event import LogEvent
|
11
|
+
from .request_snapshot import RequestSnapshot
|
11
12
|
from .snapshot_types import DELETE_ACTION, PUT_ACTION, Resource
|
12
13
|
|
13
14
|
EVENT_DELIMITTER = "\n"
|
@@ -67,6 +68,25 @@ class Log():
|
|
67
68
|
contents = self.read()
|
68
69
|
return self.build_raw_events(contents)
|
69
70
|
|
71
|
+
@property
|
72
|
+
def scenario_inverted_index(self):
|
73
|
+
index = {}
|
74
|
+
|
75
|
+
def handle_snapshot(snapshot: RequestSnapshot):
|
76
|
+
request_uuid = snapshot.uuid
|
77
|
+
if not request_uuid in index:
|
78
|
+
index[request_uuid] = []
|
79
|
+
|
80
|
+
index[request_uuid].append(event.resource_uuid)
|
81
|
+
|
82
|
+
for event in self.target_events:
|
83
|
+
if not event.is_scenario():
|
84
|
+
continue
|
85
|
+
|
86
|
+
event.snapshot().iter_request_snapshots(handle_snapshot)
|
87
|
+
|
88
|
+
return index
|
89
|
+
|
70
90
|
@property
|
71
91
|
def unprocessed_events(self) -> List[LogEvent]:
|
72
92
|
events = self.events
|
@@ -197,12 +217,18 @@ class Log():
|
|
197
217
|
|
198
218
|
resource_index[event.resource_uuid].append(event)
|
199
219
|
|
220
|
+
scenario_inverted_index = self.scenario_inverted_index
|
221
|
+
|
200
222
|
pruned_events = self.collapse(events)
|
201
223
|
for event in pruned_events:
|
202
224
|
snapshot = event.snapshot()
|
203
225
|
snapshot_exists = snapshot.exists
|
204
226
|
|
205
227
|
if event.action == DELETE_ACTION or not snapshot_exists:
|
228
|
+
if event.is_request() and event.resource_uuid in scenario_inverted_index:
|
229
|
+
# If a request is deleted, only prune if it's not also a part of a scenario
|
230
|
+
continue
|
231
|
+
|
206
232
|
Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Removing{bcolors.ENDC} {event.resource} {event.resource_uuid}")
|
207
233
|
|
208
234
|
resource_events: List[LogEvent] = resource_index[event.resource_uuid]
|
@@ -218,8 +244,16 @@ class Log():
|
|
218
244
|
removed_events[event.uuid] = True
|
219
245
|
|
220
246
|
if event.action == DELETE_ACTION and snapshot_exists:
|
221
|
-
if
|
247
|
+
if dry_run:
|
248
|
+
continue
|
249
|
+
|
250
|
+
if event.is_scenario():
|
251
|
+
# We still need to check each request in a scenario to make sure another scenario does not depend on it
|
252
|
+
snapshot.remove(lambda snapshot: self.remove_request_snapshot(snapshot, scenario_inverted_index))
|
253
|
+
elif event.is_request():
|
254
|
+
# We have already checked that a scenario does not depend on the request above
|
222
255
|
snapshot.remove()
|
256
|
+
|
223
257
|
Logger.instance(LOG_ID).info(f"Removing {event.resource} snapshot")
|
224
258
|
|
225
259
|
def build_raw_events(self, contents: str) -> List[str]:
|
@@ -242,6 +276,8 @@ class Log():
|
|
242
276
|
'''
|
243
277
|
Remove DELETE events where the last processed event was a PUT
|
244
278
|
'''
|
279
|
+
|
280
|
+
# Build an index such that if the last event is DELETE_ACTION, then it will NOT exist in the index
|
245
281
|
index = {}
|
246
282
|
for event in processed_events:
|
247
283
|
if event.action == PUT_ACTION:
|
@@ -249,10 +285,18 @@ class Log():
|
|
249
285
|
elif event.action == DELETE_ACTION:
|
250
286
|
if event.resource_uuid in index:
|
251
287
|
del index[event.resource_uuid]
|
288
|
+
|
289
|
+
scenario_inverted_index = self.scenario_inverted_index
|
290
|
+
|
291
|
+
def keep(e: LogEvent):
|
292
|
+
if e.action != DELETE_ACTION:
|
293
|
+
return True
|
294
|
+
|
295
|
+
return e.action == DELETE_ACTION and e.is_request() and e.resource_uuid in scenario_inverted_index
|
252
296
|
|
253
297
|
return list(
|
254
298
|
filter(
|
255
|
-
|
299
|
+
keep,
|
256
300
|
unprocessed_events
|
257
301
|
)
|
258
302
|
)
|
@@ -289,6 +333,12 @@ class Log():
|
|
289
333
|
remaining_events = self.remove_dangling_events(processed_events, unprocessed_events)
|
290
334
|
return list(filter(lambda e: e.uuid not in remaining_version_uuids, remaining_events))
|
291
335
|
|
336
|
+
def remove_request_snapshot(self, snapshot: RequestSnapshot, scenario_inverted_index: dict = None):
|
337
|
+
scenario_inverted_index = scenario_inverted_index or self.scenario_inverted_index
|
338
|
+
|
339
|
+
if snapshot.uuid not in scenario_inverted_index:
|
340
|
+
snapshot.remove()
|
341
|
+
|
292
342
|
# Rotate log to history
|
293
343
|
def rotate(self):
|
294
344
|
if not os.path.exists(self.__log_file_path):
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import json
|
2
2
|
import os
|
3
3
|
import pdb
|
4
|
-
import shutil
|
5
4
|
|
6
5
|
from typing import Callable, List
|
7
6
|
|
@@ -128,12 +127,12 @@ class ScenarioSnapshot(Snapshot):
|
|
128
127
|
if os.path.exists(metadata_file_path):
|
129
128
|
os.remove(metadata_file_path)
|
130
129
|
|
131
|
-
def remove_requests(self):
|
130
|
+
def remove_requests(self, handle_remove_request_snapshot = None):
|
132
131
|
requests_file_path = self.requests_path
|
133
132
|
|
134
133
|
if os.path.exists(requests_file_path):
|
135
134
|
# A request only every belongs to one scenario
|
136
|
-
self.iter_request_snapshots(self.__handle_remove_requests)
|
135
|
+
self.iter_request_snapshots(handle_remove_request_snapshot or self.__handle_remove_requests)
|
137
136
|
|
138
137
|
os.remove(requests_file_path)
|
139
138
|
|
@@ -14,15 +14,17 @@ def snapshot_request(request: Request, **options: RequestSnapshotOptions):
|
|
14
14
|
return
|
15
15
|
|
16
16
|
snapshot = RequestSnapshot(request.uuid)
|
17
|
-
|
18
17
|
snapshot.backup()
|
19
18
|
|
19
|
+
log = Log()
|
20
|
+
|
20
21
|
if action == PUT_ACTION:
|
21
22
|
snapshot.write(request, **options)
|
22
23
|
elif action == DELETE_ACTION:
|
23
|
-
|
24
|
+
inverted_scenario_index = log.scenario_inverted_index
|
24
25
|
|
25
|
-
|
26
|
+
# If a scenario currently depends on this request, we can't remove the snapshot until the scenario is also removed
|
27
|
+
log.remove_request_snapshot(snapshot, inverted_scenario_index)
|
26
28
|
|
27
29
|
try:
|
28
30
|
if action == PUT_ACTION:
|
@@ -40,7 +42,6 @@ def snapshot_scenario(scenario: Scenario, **options):
|
|
40
42
|
return
|
41
43
|
|
42
44
|
snapshot = ScenarioSnapshot(scenario.uuid)
|
43
|
-
|
44
45
|
snapshot.backup_metadata()
|
45
46
|
|
46
47
|
if action == PUT_ACTION:
|
@@ -50,13 +51,13 @@ def snapshot_scenario(scenario: Scenario, **options):
|
|
50
51
|
|
51
52
|
snapshot.backup_requests()
|
52
53
|
|
54
|
+
log = Log()
|
55
|
+
inverted_scenario_index = log.scenario_inverted_index
|
53
56
|
if action == PUT_ACTION:
|
54
|
-
snapshot.remove_requests()
|
57
|
+
snapshot.remove_requests(lambda snapshot: log.remove_request_snapshot(snapshot, inverted_scenario_index))
|
55
58
|
snapshot.write_requests(scenario, **options)
|
56
59
|
elif action == DELETE_ACTION:
|
57
|
-
snapshot.remove_requests()
|
58
|
-
|
59
|
-
log = Log()
|
60
|
+
snapshot.remove_requests(lambda snapshot: log.remove_request_snapshot(snapshot, inverted_scenario_index))
|
60
61
|
|
61
62
|
if action == PUT_ACTION:
|
62
63
|
log.put(scenario)
|
@@ -1,9 +1,11 @@
|
|
1
1
|
import pdb
|
2
2
|
|
3
|
+
from typing import List
|
4
|
+
|
3
5
|
from stoobly_agent.app.models.factories.resource.local_db.helpers.log import Log
|
4
6
|
from stoobly_agent.app.models.factories.resource.local_db.helpers.request_snapshot import RequestSnapshot
|
5
7
|
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
|
8
|
+
from stoobly_agent.app.proxy.record import REQUEST_STRING_CLRF
|
7
9
|
from stoobly_agent.app.settings import Settings
|
8
10
|
from stoobly_agent.lib.logger import bcolors
|
9
11
|
|
@@ -71,6 +73,7 @@ class Apply():
|
|
71
73
|
if results:
|
72
74
|
status = results[1]
|
73
75
|
if status == 0 or status >= 400:
|
76
|
+
self.__logger(f"{bcolors.FAIL}Error{bcolors.ENDC} {results[0]}")
|
74
77
|
completed = False
|
75
78
|
break
|
76
79
|
|
@@ -218,14 +221,16 @@ class Apply():
|
|
218
221
|
|
219
222
|
snapshot_requests = {}
|
220
223
|
|
221
|
-
request_snapshots = snapshot.request_snapshots
|
224
|
+
request_snapshots: List[RequestSnapshot] = snapshot.request_snapshots
|
222
225
|
for request_snapshot in request_snapshots:
|
223
226
|
raw_request = request_snapshot.request
|
224
|
-
|
225
|
-
toks = raw_request.split(REQUEST_STRING_CLRF.encode(), 1)
|
226
227
|
|
228
|
+
if not raw_request:
|
229
|
+
return f"{request_snapshot.path} is missing", 400
|
230
|
+
|
231
|
+
toks = raw_request.split(REQUEST_STRING_CLRF, 1)
|
227
232
|
if len(toks) != 2:
|
228
|
-
return f"{
|
233
|
+
return f"{request_snapshot.path} contains an invalid request", 400
|
229
234
|
|
230
235
|
uuid = request_snapshot.uuid
|
231
236
|
res, status = self.__put_request(uuid, raw_request, scenario_id=scenario['id'])
|
@@ -3,7 +3,7 @@ import pdb
|
|
3
3
|
import requests
|
4
4
|
import time
|
5
5
|
|
6
|
-
from mitmproxy.http import
|
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
|
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
|
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
|
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
|