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.
- 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/helpers/apply.py +2 -2
- 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/cli/record_test.py +3 -3
- {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.0.dist-info}/METADATA +1 -1
- {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.0.dist-info}/RECORD +39 -37
- 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.0.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.8.5.dist-info → stoobly_agent-1.9.0.dist-info}/WHEEL +0 -0
- {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.
|
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,
|
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]
|
@@ -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
|
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
|
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
|
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
|
@@ -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)
|
39
|
+
return CLRF.join([self.control] + self.lines)
|
40
40
|
else:
|
41
|
-
return CLRF.join(self.lines)
|
41
|
+
return CLRF.join(self.lines)
|
42
42
|
|
43
43
|
def set(self, s: bytes):
|
44
|
-
|
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
|
-
|
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(
|
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
|
-
|
118
|
-
|
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'
|