stoobly-agent 1.4.2__py3-none-any.whl → 1.5.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/cli/helpers/handle_config_update_service.py +2 -2
- stoobly_agent/app/cli/helpers/handle_mock_service.py +6 -2
- stoobly_agent/app/cli/helpers/request_facade.py +5 -1
- stoobly_agent/app/cli/scaffold/constants.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -0
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
- stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
- stoobly_agent/app/cli/scaffold_cli.py +5 -5
- stoobly_agent/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request.py +1 -1
- stoobly_agent/app/models/factories/resource/local_db/request_adapter.py +17 -11
- stoobly_agent/app/models/types/request.py +1 -2
- stoobly_agent/app/proxy/context.py +4 -0
- stoobly_agent/app/proxy/handle_mock_service.py +93 -46
- stoobly_agent/app/proxy/handle_record_service.py +15 -3
- stoobly_agent/app/proxy/handle_replay_service.py +44 -18
- stoobly_agent/app/proxy/handle_test_service.py +92 -24
- stoobly_agent/app/proxy/intercept_handler.py +11 -16
- stoobly_agent/app/proxy/intercept_settings.py +17 -4
- stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
- stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
- stoobly_agent/app/proxy/mock/custom_not_found_response_builder.py +5 -0
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py +79 -14
- stoobly_agent/app/proxy/mock/eval_request_service.py +18 -13
- stoobly_agent/app/proxy/record/join_request_service.py +7 -8
- stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
- stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
- stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
- stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
- stoobly_agent/app/proxy/utils/response_handler.py +10 -1
- stoobly_agent/app/proxy/utils/rewrite.py +72 -0
- stoobly_agent/app/settings/constants/request_component.py +4 -1
- stoobly_agent/cli.py +35 -28
- stoobly_agent/config/constants/custom_headers.py +1 -0
- stoobly_agent/config/constants/intercept_policy.py +2 -0
- stoobly_agent/config/constants/mock_policy.py +4 -2
- stoobly_agent/config/constants/query_params.py +2 -0
- stoobly_agent/config/constants/record_policy.py +4 -2
- stoobly_agent/config/constants/replay_policy.py +4 -2
- stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
- stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
- stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py +4 -4
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
- stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
- stoobly_agent/test/cli/mock_scenario_lifecycle_hooks.py +5 -0
- stoobly_agent/test/cli/mock_scenario_test.py +62 -0
- stoobly_agent/test/cli/mock_test.py +54 -38
- stoobly_agent/test/cli/record_test.py +67 -0
- stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/LICENSE +1 -1
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/METADATA +7 -12
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/RECORD +72 -65
- /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,7 @@ import os
|
|
2
2
|
import pdb
|
3
3
|
import threading
|
4
4
|
|
5
|
+
from copy import deepcopy
|
5
6
|
from mitmproxy.http import Request as MitmproxyRequest
|
6
7
|
|
7
8
|
from stoobly_agent.app.settings.constants.mode import TEST
|
@@ -18,15 +19,20 @@ from .record.overwrite_scenario_service import overwrite_scenario
|
|
18
19
|
from .record.upload_request_service import inject_upload_request
|
19
20
|
from .utils.allowed_request_service import get_active_mode_policy
|
20
21
|
from .utils.response_handler import bad_request, disable_transfer_encoding
|
22
|
+
from .utils.rewrite import rewrite_request_response
|
21
23
|
|
22
24
|
LOG_ID = 'Record'
|
23
25
|
|
26
|
+
###
|
27
|
+
#
|
28
|
+
# 1. Rewrites a copy of request and response
|
29
|
+
# 2. BEFORE_RECORD gets triggered
|
30
|
+
# 3. AFTER_RECORD gets triggered
|
31
|
+
#
|
24
32
|
def handle_response_record(context: RecordContext):
|
25
33
|
flow = context.flow
|
26
34
|
disable_transfer_encoding(flow.response)
|
27
35
|
|
28
|
-
__record_hook(lifecycle_hooks.BEFORE_RECORD, context)
|
29
|
-
|
30
36
|
intercept_settings = context.intercept_settings
|
31
37
|
request: MitmproxyRequest = flow.request
|
32
38
|
request_model = RequestModel(intercept_settings.settings)
|
@@ -60,11 +66,17 @@ def handle_response_record(context: RecordContext):
|
|
60
66
|
|
61
67
|
def __record_handler(context: RecordContext, request_model: RequestModel):
|
62
68
|
flow = context.flow
|
69
|
+
flow_copy = deepcopy(flow)
|
63
70
|
intercept_settings = context.intercept_settings
|
64
71
|
|
65
|
-
|
72
|
+
context.flow = flow_copy # Deep copy flow to prevent response modifications from persisting
|
73
|
+
rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
|
74
|
+
__record_hook(lifecycle_hooks.BEFORE_RECORD, context)
|
75
|
+
|
76
|
+
inject_upload_request(request_model, intercept_settings)(flow_copy)
|
66
77
|
|
67
78
|
__record_hook(lifecycle_hooks.AFTER_RECORD, context)
|
79
|
+
context.flow = flow # Reset flow
|
68
80
|
|
69
81
|
def __record_request(context: RecordContext, request_model: RequestModel):
|
70
82
|
if os.environ.get(ENV) == TEST:
|
@@ -1,46 +1,72 @@
|
|
1
1
|
import pdb
|
2
2
|
|
3
3
|
from mitmproxy.http import Request as MitmproxyRequest
|
4
|
+
from typing import TypedDict
|
4
5
|
|
5
6
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
6
|
-
from stoobly_agent.app.proxy.mitmproxy.request_facade import MitmproxyRequestFacade
|
7
7
|
from stoobly_agent.app.proxy.replay.context import ReplayContext
|
8
8
|
from stoobly_agent.config.constants import lifecycle_hooks, replay_policy
|
9
9
|
|
10
10
|
from .utils.allowed_request_service import get_active_mode_policy
|
11
|
+
from .utils.rewrite import rewrite_request, rewrite_response
|
11
12
|
|
12
13
|
LOG_ID = 'HandleReplay'
|
13
14
|
|
14
|
-
|
15
|
-
|
15
|
+
class ReplayOptions(TypedDict):
|
16
|
+
no_rewrite: bool
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
###
|
19
|
+
#
|
20
|
+
# 1. Rewrites replay request by default
|
21
|
+
# 2. BEFORE_REPLAY gets triggered
|
22
|
+
#
|
23
|
+
def handle_request_replay_without_rewrite(replay_context: ReplayContext):
|
24
|
+
options = { 'no_rewrite': True }
|
25
|
+
handle_request_replay(replay_context, **options)
|
19
26
|
|
20
|
-
|
21
|
-
|
22
|
-
|
27
|
+
def handle_request_replay(replay_context: ReplayContext, **options: ReplayOptions):
|
28
|
+
request: MitmproxyRequest = replay_context.flow.request
|
29
|
+
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
30
|
+
|
31
|
+
policy = get_active_mode_policy(request, intercept_settings)
|
32
|
+
if policy != replay_policy.NONE:
|
33
|
+
if not options.get('no_rewrite'):
|
34
|
+
__rewrite_request(replay_context)
|
23
35
|
|
36
|
+
__replay_hook(lifecycle_hooks.BEFORE_REPLAY, replay_context)
|
37
|
+
|
38
|
+
###
|
39
|
+
#
|
40
|
+
# 1. Rewrites replay response
|
41
|
+
# 2. AFTER_REPLAY gets triggered
|
42
|
+
#
|
24
43
|
def handle_response_replay(replay_context: ReplayContext):
|
44
|
+
__rewrite_response(replay_context)
|
25
45
|
__replay_hook(lifecycle_hooks.AFTER_REPLAY, replay_context)
|
26
46
|
|
27
|
-
|
47
|
+
def __replay_hook(hook: str, replay_context: ReplayContext):
|
48
|
+
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
49
|
+
|
50
|
+
lifecycle_hooks_module = intercept_settings.lifecycle_hooks
|
51
|
+
if hook in lifecycle_hooks_module:
|
52
|
+
lifecycle_hooks_module[hook](replay_context)
|
28
53
|
|
29
|
-
def
|
54
|
+
def __rewrite_request(replay_context: ReplayContext):
|
30
55
|
"""
|
31
56
|
Before replaying a request, see if the request needs to be rewritten
|
32
57
|
"""
|
33
58
|
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
34
|
-
rewrite_rules = intercept_settings.
|
59
|
+
rewrite_rules = intercept_settings.replay_rewrite_rules
|
35
60
|
|
36
61
|
if len(rewrite_rules) > 0:
|
37
|
-
|
38
|
-
request_facade = MitmproxyRequestFacade(request)
|
39
|
-
request_facade.with_parameter_rules(rewrite_rules).with_url_rules(rewrite_rules).rewrite()
|
62
|
+
rewrite_request(replay_context.flow, rewrite_rules)
|
40
63
|
|
41
|
-
def
|
64
|
+
def __rewrite_response(replay_context: ReplayContext):
|
65
|
+
"""
|
66
|
+
After replaying a request, see if the request needs to be rewritten
|
67
|
+
"""
|
42
68
|
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
69
|
+
rewrite_rules = intercept_settings.replay_rewrite_rules
|
43
70
|
|
44
|
-
|
45
|
-
|
46
|
-
lifecycle_hooks_module[hook](replay_context)
|
71
|
+
if len(rewrite_rules) > 0:
|
72
|
+
rewrite_response(replay_context.flow, rewrite_rules)
|
@@ -1,7 +1,9 @@
|
|
1
1
|
import pdb
|
2
2
|
|
3
|
+
from copy import deepcopy
|
3
4
|
from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
|
4
5
|
|
6
|
+
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
5
7
|
from stoobly_agent.app.proxy.replay.body_parser_service import encode_response
|
6
8
|
from stoobly_agent.app.proxy.replay.context import ReplayContext
|
7
9
|
from stoobly_agent.app.proxy.utils.request_handler import build_response
|
@@ -12,36 +14,55 @@ from stoobly_agent.lib.api.endpoints_resource import EndpointsResource
|
|
12
14
|
from stoobly_agent.lib.api.interfaces.tests import TestShowResponse
|
13
15
|
from stoobly_agent.lib.logger import Logger
|
14
16
|
|
15
|
-
from .handle_mock_service import
|
16
|
-
from .handle_replay_service import
|
17
|
+
from .handle_mock_service import handle_request_mock_generic_without_rewrite
|
18
|
+
from .handle_replay_service import handle_request_replay_without_rewrite
|
17
19
|
from .mock.context import MockContext
|
18
20
|
from .test.helpers.test_results_builder import TestResultsBuilder
|
19
21
|
from .test.helpers.upload_test_service import inject_upload_test
|
20
22
|
from .test.context_abc import TestContextABC as TestContext
|
21
23
|
from .test.test_service import test
|
24
|
+
from .utils.rewrite import rewrite_request, rewrite_response, rewrite_request_response
|
22
25
|
|
23
26
|
LOG_ID = 'HandleTest'
|
24
27
|
|
28
|
+
###
|
29
|
+
#
|
30
|
+
# 1. Rewrites test request
|
31
|
+
# 2. BEFORE_REPLAY gets triggered
|
32
|
+
#
|
25
33
|
def handle_request_test(context: ReplayContext) -> None:
|
26
|
-
|
34
|
+
__rewrite_request(context)
|
35
|
+
handle_request_replay_without_rewrite(context)
|
27
36
|
|
28
37
|
###
|
29
38
|
#
|
30
|
-
#
|
39
|
+
# 1. Rewrites test response (response from live service)
|
40
|
+
# 2. AFTER_REPLAY gets triggered
|
41
|
+
# 3. Uses rewritten test request to obtain mock response
|
42
|
+
# 4. BEFORE_MOCK gets triggered
|
43
|
+
# 5. AFTER_MOCK gets triggered
|
44
|
+
# 6. BEFORE_TEST gets triggered
|
45
|
+
# 7. Tests against rewritten test response and mock response (expected response)
|
46
|
+
# 8. Rewrites a copy of request and response
|
47
|
+
# 9. BEFORE_RECORD gets triggered (if not from CLI)
|
48
|
+
# 10. AFTER_RECORD gets triggered (if not from CLI)
|
49
|
+
# 11. AFTER_TEST gets triggered
|
31
50
|
#
|
32
51
|
def handle_response_test(context: ReplayContext) -> None:
|
33
52
|
from .test.context import TestContext
|
34
53
|
|
35
54
|
flow: MitmproxyHTTPFlow = context.flow
|
36
|
-
intercept_settings = context.intercept_settings
|
37
|
-
|
38
55
|
disable_transfer_encoding(flow.response)
|
39
56
|
|
40
|
-
|
41
|
-
|
57
|
+
__rewrite_response(context)
|
58
|
+
__test_hook(lifecycle_hooks.AFTER_REPLAY, context)
|
42
59
|
|
43
|
-
|
60
|
+
intercept_settings = context.intercept_settings
|
61
|
+
|
62
|
+
# At this point, the request may already been rewritten during replay, do not rewrite again
|
63
|
+
handle_request_mock_generic_without_rewrite(
|
44
64
|
MockContext(flow, intercept_settings),
|
65
|
+
error=lambda mock_context: __handle_mock_error(TestContext(context, mock_context)),
|
45
66
|
failure=lambda mock_context: __handle_mock_failure(TestContext(context, mock_context)),
|
46
67
|
#infer=intercept_settings.test_strategy == test_strategy.FUZZY, # For fuzzy testing we can use an inferred response
|
47
68
|
success=lambda mock_context: __handle_mock_success(TestContext(context, mock_context))
|
@@ -51,10 +72,28 @@ def __decorate_test_id(flow: MitmproxyHTTPFlow, test_response: TestShowResponse)
|
|
51
72
|
if test_response.get('id'):
|
52
73
|
flow.response.headers[custom_headers.TEST_ID] = str(test_response['id'])
|
53
74
|
|
75
|
+
def __handle_mock_error(test_context: TestContext):
|
76
|
+
Logger.instance().warn(f"{LOG_ID}:TestStatus: Mock not enabled")
|
77
|
+
|
78
|
+
intercept_settings = test_context.intercept_settings
|
79
|
+
|
80
|
+
if intercept_settings.request_origin == request_origin.CLI:
|
81
|
+
return build_response(False, 'No test found')
|
82
|
+
|
83
|
+
def __handle_mock_failure(test_context: TestContext) -> None:
|
84
|
+
Logger.instance().warn(f"{LOG_ID}:TestStatus: No test found")
|
85
|
+
|
86
|
+
intercept_settings = test_context.intercept_settings
|
87
|
+
|
88
|
+
if intercept_settings.request_origin == request_origin.CLI:
|
89
|
+
return build_response(False, 'No test found')
|
90
|
+
|
54
91
|
def __handle_mock_success(test_context: TestContext) -> None:
|
55
92
|
flow: MitmproxyHTTPFlow = test_context.flow
|
56
|
-
settings = Settings.instance()
|
57
93
|
|
94
|
+
__test_hook(lifecycle_hooks.AFTER_MOCK, test_context.mock_context)
|
95
|
+
|
96
|
+
settings: Settings = Settings.instance()
|
58
97
|
test_context.with_endpoints_resource(EndpointsResource(settings.remote.api_url, settings.remote.api_key))
|
59
98
|
|
60
99
|
__test_hook(lifecycle_hooks.BEFORE_TEST, test_context)
|
@@ -84,13 +123,8 @@ def __handle_mock_success(test_context: TestContext) -> None:
|
|
84
123
|
if not is_cli or test_context.save:
|
85
124
|
# Re-serialize expected response since it was rewritten
|
86
125
|
upload_test_data['expected_response'] = encode_response(expected, test_context.expected_response.content_type)
|
87
|
-
upload_test = inject_upload_test(None, intercept_settings)
|
88
126
|
|
89
|
-
|
90
|
-
res = upload_test(
|
91
|
-
flow,
|
92
|
-
**upload_test_data
|
93
|
-
)
|
127
|
+
res = __record_handler(test_context, upload_test_data)
|
94
128
|
|
95
129
|
if is_cli:
|
96
130
|
# If the origin was from a CLI, send test ID in response header
|
@@ -121,14 +155,6 @@ def __handle_mock_success(test_context: TestContext) -> None:
|
|
121
155
|
|
122
156
|
return flow.response
|
123
157
|
|
124
|
-
def __handle_mock_failure(test_context: TestContext) -> None:
|
125
|
-
Logger.instance().warn(f"{LOG_ID}:TestStatus: No test found")
|
126
|
-
|
127
|
-
intercept_settings = test_context.intercept_settings
|
128
|
-
|
129
|
-
if intercept_settings.request_origin == request_origin.CLI:
|
130
|
-
return build_response(False, 'No test found')
|
131
|
-
|
132
158
|
def __override_response(flow: MitmproxyHTTPFlow, content: bytes):
|
133
159
|
headers = { 'Content-Type': 'text/plain' }
|
134
160
|
headers[custom_headers.CONTENT_TYPE] = custom_headers.CONTENT_TYPE_TEST_RESULTS
|
@@ -136,6 +162,48 @@ def __override_response(flow: MitmproxyHTTPFlow, content: bytes):
|
|
136
162
|
flow.response.set_content(content)
|
137
163
|
flow.response.status_code = 200
|
138
164
|
|
165
|
+
def __record_handler(context: TestContext, upload_test_data):
|
166
|
+
flow = context.flow
|
167
|
+
flow_copy = deepcopy(flow) # Deep copy flow to prevent response modifications from persisting
|
168
|
+
intercept_settings = context.intercept_settings
|
169
|
+
|
170
|
+
context.flow = flow_copy
|
171
|
+
|
172
|
+
# Since we are "uploading" the request, use record_write_rules
|
173
|
+
rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
|
174
|
+
__test_hook(lifecycle_hooks.BEFORE_RECORD, context)
|
175
|
+
|
176
|
+
# Commit test to API
|
177
|
+
upload_test = inject_upload_test(None, intercept_settings)
|
178
|
+
res = upload_test(
|
179
|
+
flow_copy, **upload_test_data
|
180
|
+
)
|
181
|
+
|
182
|
+
__test_hook(lifecycle_hooks.AFTER_RECORD, context)
|
183
|
+
context.flow = flow
|
184
|
+
|
185
|
+
return res
|
186
|
+
|
187
|
+
def __rewrite_request(replay_context: ReplayContext):
|
188
|
+
"""
|
189
|
+
Before replaying a request, see if the request needs to be rewritten
|
190
|
+
"""
|
191
|
+
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
192
|
+
rewrite_rules = intercept_settings.test_rewrite_rules
|
193
|
+
|
194
|
+
if len(rewrite_rules) > 0:
|
195
|
+
rewrite_request(replay_context.flow, rewrite_rules)
|
196
|
+
|
197
|
+
def __rewrite_response(replay_context: ReplayContext):
|
198
|
+
"""
|
199
|
+
After replaying a request, see if the request needs to be rewritten
|
200
|
+
"""
|
201
|
+
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
202
|
+
rewrite_rules = intercept_settings.test_rewrite_rules
|
203
|
+
|
204
|
+
if len(rewrite_rules) > 0:
|
205
|
+
rewrite_response(replay_context.flow, rewrite_rules)
|
206
|
+
|
139
207
|
def __test_hook(hook: str, context: TestContext):
|
140
208
|
intercept_settings = context.intercept_settings
|
141
209
|
lifecycle_hooks_module = intercept_settings.lifecycle_hooks
|
@@ -33,6 +33,7 @@ def request(flow: MitmproxyHTTPFlow):
|
|
33
33
|
if not intercept_settings.active:
|
34
34
|
return
|
35
35
|
|
36
|
+
__disable_web_cache(request)
|
36
37
|
__intercept_hook(lifecycle_hooks.BEFORE_REQUEST, flow, intercept_settings)
|
37
38
|
|
38
39
|
active_mode = intercept_settings.mode
|
@@ -42,7 +43,7 @@ def request(flow: MitmproxyHTTPFlow):
|
|
42
43
|
context = MockContext(flow, intercept_settings)
|
43
44
|
handle_request_mock(context)
|
44
45
|
elif active_mode == mode.RECORD:
|
45
|
-
|
46
|
+
pass
|
46
47
|
elif active_mode == mode.REPLAY:
|
47
48
|
context = ReplayContext(flow, intercept_settings)
|
48
49
|
handle_request_replay(context)
|
@@ -65,35 +66,29 @@ def response(flow: MitmproxyHTTPFlow):
|
|
65
66
|
if not intercept_settings.active:
|
66
67
|
return
|
67
68
|
|
68
|
-
__intercept_hook(lifecycle_hooks.BEFORE_RESPONSE, flow, intercept_settings)
|
69
|
-
|
70
69
|
active_mode = intercept_settings.mode
|
71
70
|
|
72
71
|
if active_mode == mode.MOCK:
|
73
72
|
context = MockContext(flow, intercept_settings)
|
74
|
-
|
73
|
+
handle_response_mock(context)
|
75
74
|
elif active_mode == mode.RECORD:
|
76
75
|
context = RecordContext(flow, intercept_settings)
|
77
|
-
|
76
|
+
handle_response_record(context)
|
78
77
|
elif active_mode == mode.REPLAY:
|
79
78
|
context = ReplayContext(flow, intercept_settings)
|
80
|
-
|
79
|
+
handle_response_replay(context)
|
81
80
|
elif active_mode == mode.TEST:
|
82
81
|
context = ReplayContext(flow, intercept_settings)
|
83
|
-
|
82
|
+
handle_response_test(context)
|
83
|
+
|
84
|
+
__intercept_hook(lifecycle_hooks.BEFORE_RESPONSE, flow, intercept_settings)
|
84
85
|
|
85
86
|
### PRIVATE
|
86
87
|
|
88
|
+
# Prevent 304 status
|
89
|
+
# Because this header will get recorded, should add during mocking as well in the case where headers are used for matching
|
87
90
|
def __disable_web_cache(request: MitmproxyRequest) -> None:
|
88
|
-
request.headers['Cache-Control'] = 'no-cache, no-store
|
89
|
-
request.headers['Expires'] = '0'
|
90
|
-
request.headers['Pragma'] = 'no-cache'
|
91
|
-
|
92
|
-
if 'IF-NONE-MATCH' in request.headers:
|
93
|
-
del request.headers['IF-NONE-MATCH']
|
94
|
-
|
95
|
-
if 'IF-MODIFIED-SINCE' in request.headers:
|
96
|
-
del request.headers['IF-MODIFIED-SINCE']
|
91
|
+
request.headers['Cache-Control'] = 'no-cache, no-store'
|
97
92
|
|
98
93
|
# Fix issue where multi-value cookies become comma separated
|
99
94
|
def __patch_cookie(request: MitmproxyRequest):
|
@@ -41,6 +41,11 @@ class InterceptSettings:
|
|
41
41
|
self.__response_fixtures = None
|
42
42
|
self.__initialize_response_fixtures()
|
43
43
|
|
44
|
+
self._mock_rewrite_rules = None
|
45
|
+
self._record_rewrite_rules = None
|
46
|
+
self._replay_rewrite_rules = None
|
47
|
+
self._test_rewrite_rules = None
|
48
|
+
|
44
49
|
@property
|
45
50
|
def settings(self):
|
46
51
|
return self.__settings
|
@@ -200,19 +205,27 @@ class InterceptSettings:
|
|
200
205
|
|
201
206
|
@property
|
202
207
|
def record_rewrite_rules(self) -> List[RewriteRule]:
|
203
|
-
|
208
|
+
if not self._record_rewrite_rules:
|
209
|
+
self._record_rewrite_rules = self.__select_rewrite_rules(mode.RECORD)
|
210
|
+
return self._record_rewrite_rules
|
204
211
|
|
205
212
|
@property
|
206
213
|
def mock_rewrite_rules(self) -> List[RewriteRule]:
|
207
|
-
|
214
|
+
if not self._mock_rewrite_rules:
|
215
|
+
self._mock_rewrite_rules = self.__select_rewrite_rules(mode.MOCK)
|
216
|
+
return self._mock_rewrite_rules
|
208
217
|
|
209
218
|
@property
|
210
219
|
def replay_rewrite_rules(self) -> List[RewriteRule]:
|
211
|
-
|
220
|
+
if not self._replay_rewrite_rules:
|
221
|
+
self._replay_rewrite_rules = self.__select_rewrite_rules(mode.REPLAY)
|
222
|
+
return self._replay_rewrite_rules
|
212
223
|
|
213
224
|
@property
|
214
225
|
def test_rewrite_rules(self) -> List[RewriteRule]:
|
215
|
-
|
226
|
+
if not self._test_rewrite_rules:
|
227
|
+
self._test_rewrite_rules = self.__select_rewrite_rules(mode.TEST)
|
228
|
+
return self._test_rewrite_rules
|
216
229
|
|
217
230
|
@property
|
218
231
|
def upstream_url(self):
|
@@ -239,7 +239,7 @@ class MitmproxyRequestFacade(Request):
|
|
239
239
|
content_type = self.content_type
|
240
240
|
parsed_content = self.__body.get(content_type)
|
241
241
|
|
242
|
-
if not
|
242
|
+
if not self.__is_iterable(parsed_content):
|
243
243
|
content_type = 'application/json'
|
244
244
|
self.request.headers['content-type'] = content_type
|
245
245
|
parsed_content = {}
|
@@ -265,4 +265,7 @@ class MitmproxyRequestFacade(Request):
|
|
265
265
|
|
266
266
|
_request_headers.pop(name)
|
267
267
|
|
268
|
-
return _request_headers
|
268
|
+
return _request_headers
|
269
|
+
|
270
|
+
def __is_iterable(self, v):
|
271
|
+
return isinstance(v, dict) or isinstance(v, multidict.MultiDictView) or isinstance(v, list)
|
@@ -95,10 +95,8 @@ class MitmproxyResponseFacade(Response):
|
|
95
95
|
content_type = self.content_type
|
96
96
|
parsed_content = self.__body.get(content_type)
|
97
97
|
|
98
|
-
if not
|
99
|
-
|
100
|
-
self.response.headers['content-type'] = content_type
|
101
|
-
parsed_content = {}
|
98
|
+
if not self.__is_iterable(parsed_content):
|
99
|
+
return
|
102
100
|
|
103
101
|
self.__apply_rewrites(parsed_content, rewrites, handler)
|
104
102
|
self.__body.set(parsed_content, content_type)
|
@@ -122,3 +120,6 @@ class MitmproxyResponseFacade(Response):
|
|
122
120
|
_response_headers.pop(name)
|
123
121
|
|
124
122
|
return _response_headers
|
123
|
+
|
124
|
+
def __is_iterable(self, v):
|
125
|
+
return isinstance(v, dict) or isinstance(v, multidict.MultiDictView) or isinstance(v, list)
|
@@ -12,4 +12,9 @@ class CustomNotFoundResponseBuilder():
|
|
12
12
|
def build(self):
|
13
13
|
self.__response.status_code = custom_response_codes.NOT_FOUND
|
14
14
|
self.__response.raw = BytesIO('Request not found'.encode())
|
15
|
+
self.__response.headers = {
|
16
|
+
'Access-Control-Allow-Origin': '*',
|
17
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS, POST, PATCH, PUT, DELETE',
|
18
|
+
'Content-Type': 'text/plain',
|
19
|
+
}
|
15
20
|
return self.__response
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import mimetypes
|
1
2
|
import os
|
2
3
|
import pdb
|
3
4
|
import re
|
@@ -5,6 +6,7 @@ import re
|
|
5
6
|
from io import BytesIO
|
6
7
|
from mitmproxy.http import Request as MitmproxyRequest
|
7
8
|
from requests import Response
|
9
|
+
from requests.structures import CaseInsensitiveDict
|
8
10
|
from typing import Union
|
9
11
|
|
10
12
|
from stoobly_agent.lib.logger import bcolors, Logger
|
@@ -19,45 +21,89 @@ class Options():
|
|
19
21
|
|
20
22
|
def eval_fixtures(request: MitmproxyRequest, **options: Options) -> Union[Response, None]:
|
21
23
|
fixture_path = None
|
22
|
-
headers =
|
24
|
+
headers = CaseInsensitiveDict()
|
25
|
+
status_code = 200
|
23
26
|
|
24
27
|
response_fixtures = options.get('response_fixtures')
|
25
|
-
fixture = __eval_response_fixtures(request, response_fixtures)
|
28
|
+
fixture: dict = __eval_response_fixtures(request, response_fixtures)
|
26
29
|
|
27
30
|
if not fixture:
|
28
31
|
public_directory_path = options.get('public_directory_path')
|
29
32
|
|
30
|
-
if
|
31
|
-
|
33
|
+
if not public_directory_path:
|
34
|
+
return
|
32
35
|
|
33
|
-
|
34
|
-
|
36
|
+
request_path = 'index' if request.path == '/' else request.path
|
37
|
+
_fixture_path = os.path.join(public_directory_path, request_path.lstrip('/'))
|
38
|
+
if request.headers.get('accept'):
|
39
|
+
fixture_path = __guess_file_path(_fixture_path, request.headers['accept'])
|
40
|
+
|
41
|
+
if not fixture_path:
|
42
|
+
fixture_path = _fixture_path
|
43
|
+
|
44
|
+
if not os.path.isfile(fixture_path):
|
45
|
+
return
|
35
46
|
else:
|
36
47
|
fixture_path = fixture.get('path')
|
37
|
-
|
48
|
+
if not fixture_path or not os.path.isfile(fixture_path):
|
49
|
+
return
|
38
50
|
|
39
|
-
|
40
|
-
|
51
|
+
_headers = fixture.get('headers')
|
52
|
+
headers = CaseInsensitiveDict(_headers if isinstance(_headers, dict) else {})
|
53
|
+
|
54
|
+
if fixture.get('status_code'):
|
55
|
+
status_code = fixture.get('status_code')
|
41
56
|
|
42
57
|
with open(fixture_path, 'rb') as fp:
|
43
58
|
response = Response()
|
44
59
|
|
45
|
-
response.status_code =
|
60
|
+
response.status_code = status_code
|
46
61
|
response.raw = BytesIO(fp.read())
|
47
62
|
response.headers = headers
|
48
63
|
|
64
|
+
if not response.headers.get('content-type'):
|
65
|
+
content_type = __guess_content_type(fixture_path)
|
66
|
+
if content_type:
|
67
|
+
response.headers['content-type'] = content_type
|
68
|
+
|
49
69
|
Logger.instance(LOG_ID).debug(f"{bcolors.OKBLUE}Resolved{bcolors.ENDC} fixture {fixture_path}")
|
50
70
|
|
51
71
|
return response
|
52
72
|
|
73
|
+
def __guess_content_type(file_path):
|
74
|
+
file_extension = os.path.splitext(file_path)[1]
|
75
|
+
if not file_extension:
|
76
|
+
return
|
77
|
+
return mimetypes.types_map.get(file_extension)
|
78
|
+
|
79
|
+
def __guess_file_path(file_path, content_type):
|
80
|
+
file_extension = os.path.splitext(file_path)[1]
|
81
|
+
if file_extension:
|
82
|
+
return file_path
|
83
|
+
|
84
|
+
if not content_type:
|
85
|
+
return
|
86
|
+
|
87
|
+
content_types = __parse_accept_header(content_type)
|
88
|
+
|
89
|
+
for content_type in content_types:
|
90
|
+
file_extension = mimetypes.guess_extension(content_type)
|
91
|
+
|
92
|
+
if not file_extension:
|
93
|
+
continue
|
94
|
+
|
95
|
+
_file_path = f"{file_path}{file_extension}"
|
96
|
+
if os.path.isfile(_file_path):
|
97
|
+
return _file_path
|
98
|
+
|
53
99
|
def __eval_response_fixtures(request: MitmproxyRequest, response_fixtures: Fixtures):
|
54
|
-
if not response_fixtures:
|
100
|
+
if not isinstance(response_fixtures, dict):
|
55
101
|
return
|
56
102
|
|
57
103
|
method = request.method
|
58
104
|
routes = response_fixtures.get(method)
|
59
105
|
|
60
|
-
if not routes:
|
106
|
+
if not isinstance(routes, dict):
|
61
107
|
return
|
62
108
|
|
63
109
|
for path_pattern in routes:
|
@@ -65,7 +111,26 @@ def __eval_response_fixtures(request: MitmproxyRequest, response_fixtures: Fixtu
|
|
65
111
|
continue
|
66
112
|
|
67
113
|
fixture = routes[path_pattern]
|
114
|
+
if not isinstance(fixture, dict):
|
115
|
+
continue
|
116
|
+
|
68
117
|
path = fixture.get('path')
|
69
118
|
|
70
|
-
if path
|
71
|
-
return fixture
|
119
|
+
if path:
|
120
|
+
return fixture
|
121
|
+
|
122
|
+
def __parse_accept_header(accept_header):
|
123
|
+
types = []
|
124
|
+
for item in accept_header.split(","):
|
125
|
+
parts = item.split(";")
|
126
|
+
content_type = parts[0].strip()
|
127
|
+
q_value = 1.0 # Default quality value
|
128
|
+
if len(parts) > 1 and parts[1].strip().startswith("q="):
|
129
|
+
try:
|
130
|
+
q_value = float(parts[1].strip()[2:])
|
131
|
+
except ValueError:
|
132
|
+
pass # Keep default q_value if parsing fails
|
133
|
+
types.append((content_type, q_value))
|
134
|
+
|
135
|
+
# Sort by quality factor in descending order
|
136
|
+
return [content_type for content_type, _ in sorted(types, key=lambda x: x[1], reverse=True)]
|