stoobly-agent 1.2.3__py3-none-any.whl → 1.4.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/application_http_request_handler.py +3 -3
- stoobly_agent/app/api/proxy_controller.py +8 -7
- stoobly_agent/app/cli/config_cli.py +1 -1
- stoobly_agent/app/cli/helpers/certificate_authority.py +7 -6
- stoobly_agent/app/cli/helpers/print_service.py +17 -0
- stoobly_agent/app/cli/scaffold/app.py +16 -34
- stoobly_agent/app/cli/scaffold/app_command.py +4 -7
- stoobly_agent/app/cli/scaffold/app_config.py +15 -2
- stoobly_agent/app/cli/scaffold/app_create_command.py +18 -2
- stoobly_agent/app/cli/scaffold/command.py +1 -1
- stoobly_agent/app/cli/scaffold/constants.py +9 -5
- stoobly_agent/app/cli/scaffold/docker/app_builder.py +3 -7
- stoobly_agent/app/cli/scaffold/docker/constants.py +0 -1
- stoobly_agent/app/cli/scaffold/docker/service/builder.py +12 -11
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +14 -31
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +6 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +6 -2
- stoobly_agent/app/cli/scaffold/hosts_file_manager.py +112 -0
- stoobly_agent/app/cli/scaffold/service.py +1 -2
- stoobly_agent/app/cli/scaffold/service_command.py +1 -1
- stoobly_agent/app/cli/scaffold/service_config.py +10 -14
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +9 -11
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +2 -4
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile +108 -68
- stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml +8 -13
- stoobly_agent/app/cli/scaffold/templates/app/Makefile +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +8 -4
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/.docker-compose.mock.yml +2 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.init +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/record/.docker-compose.record.yml +2 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.init +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/test/.docker-compose.test.yml +2 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.init +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/.docker-compose.mock.yml +2 -8
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/.docker-compose.record.yml +2 -8
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/.docker-compose.test.yml +2 -8
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/.docker-compose.exec.yml +2 -3
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.logs +1 -0
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.services +9 -0
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +1 -2
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +1 -2
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.init +7 -1
- stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.init +7 -1
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.init +7 -1
- stoobly_agent/app/cli/scaffold/validate_command.py +2 -2
- stoobly_agent/app/cli/scaffold/workflow.py +5 -4
- stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
- stoobly_agent/app/cli/scaffold/workflow_run_command.py +78 -45
- stoobly_agent/app/cli/scaffold_cli.py +246 -109
- stoobly_agent/app/cli/snapshot_cli.py +7 -3
- stoobly_agent/app/models/adapters/joined_request_adapter.py +6 -0
- stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +3 -1
- stoobly_agent/app/models/helpers/apply.py +34 -17
- stoobly_agent/app/models/helpers/create_request_params_service.py +4 -0
- stoobly_agent/app/proxy/handle_mock_service.py +2 -0
- stoobly_agent/app/proxy/handle_replay_service.py +2 -0
- stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
- stoobly_agent/app/proxy/mitmproxy/response_body_facade.py +19 -0
- stoobly_agent/app/proxy/mitmproxy/response_facade.py +90 -18
- stoobly_agent/app/proxy/record/join_request_service.py +1 -1
- stoobly_agent/app/proxy/replay/body_parser_service.py +11 -3
- stoobly_agent/app/settings/constants/request_component.py +2 -1
- stoobly_agent/config/constants/custom_headers.py +13 -13
- stoobly_agent/config/constants/headers.py +0 -2
- stoobly_agent/config/data_dir.py +2 -1
- stoobly_agent/config/schema.yml +2 -2
- stoobly_agent/public/18-es2015.583f191cc7ad512ee262.js +1 -0
- stoobly_agent/public/18-es5.583f191cc7ad512ee262.js +1 -0
- stoobly_agent/public/35-es2015.8f79ff8748d4ff06ab03.js +1 -0
- stoobly_agent/public/35-es5.8f79ff8748d4ff06ab03.js +1 -0
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/main-es2015.2cc16523aa3fcaba51e5.js +1 -0
- stoobly_agent/public/main-es5.2cc16523aa3fcaba51e5.js +1 -0
- stoobly_agent/public/{runtime-es2015.9addf49b79aca951b7e2.js → runtime-es2015.b914470164e4d6e75d96.js} +1 -1
- stoobly_agent/public/{runtime-es5.9addf49b79aca951b7e2.js → runtime-es5.b914470164e4d6e75d96.js} +1 -1
- stoobly_agent/test/app/cli/scaffold/cli_invoker.py +1 -2
- stoobly_agent/test/app/cli/scaffold/{hosts_file_reader_test.py → hosts_file_manager_test.py} +20 -20
- stoobly_agent/test/app/cli/snapshot/snapshot_apply_test.py +162 -1
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/mock_data/scaffold/docker-compose-assets-service.yml +1 -3
- {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/METADATA +1 -1
- {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/RECORD +94 -93
- stoobly_agent/app/cli/scaffold/hosts_file_reader.py +0 -65
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.proxy +0 -34
- stoobly_agent/public/18-es2015.d3b430636a4d6f544d92.js +0 -1
- stoobly_agent/public/18-es5.d3b430636a4d6f544d92.js +0 -1
- stoobly_agent/public/35-es2015.f741ebce0bfc25f0ec99.js +0 -1
- stoobly_agent/public/35-es5.f741ebce0bfc25f0ec99.js +0 -1
- stoobly_agent/public/main-es2015.ccd46ac1b6638ddf2066.js +0 -1
- stoobly_agent/public/main-es5.ccd46ac1b6638ddf2066.js +0 -1
- {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/entry_points.txt +0 -0
@@ -61,15 +61,17 @@ class Apply():
|
|
61
61
|
return
|
62
62
|
|
63
63
|
last_processed_event = None
|
64
|
+
completed = True
|
64
65
|
|
65
66
|
for event in unprocessed_events:
|
66
67
|
if self.__logger:
|
67
|
-
self.__logger(f"Processing
|
68
|
+
self.__logger(f"{bcolors.OKBLUE}Processing Event{bcolors.ENDC} {event.uuid}")
|
68
69
|
|
69
70
|
results = event.apply(**self.__handlers())
|
70
71
|
if results:
|
71
72
|
status = results[1]
|
72
|
-
if status == 0 or status >=
|
73
|
+
if status == 0 or status >= 400:
|
74
|
+
completed = False
|
73
75
|
break
|
74
76
|
|
75
77
|
last_processed_event = event
|
@@ -86,6 +88,8 @@ class Apply():
|
|
86
88
|
|
87
89
|
log.lock()
|
88
90
|
|
91
|
+
return completed
|
92
|
+
|
89
93
|
def request(self, uuid: str):
|
90
94
|
result = self.__apply_put_request(uuid)
|
91
95
|
if not result:
|
@@ -121,9 +125,13 @@ class Apply():
|
|
121
125
|
return False
|
122
126
|
|
123
127
|
if self.__logger:
|
124
|
-
self.__logger(f"Processing
|
128
|
+
self.__logger(f"{bcolors.OKBLUE}Processing Event{bcolors.ENDC} {event.uuid}")
|
125
129
|
|
126
|
-
event.apply(**self.__handlers())
|
130
|
+
results = event.apply(**self.__handlers())
|
131
|
+
if results:
|
132
|
+
status = results[1]
|
133
|
+
if status == 0 or status >= 400:
|
134
|
+
return False
|
127
135
|
|
128
136
|
return True
|
129
137
|
|
@@ -139,7 +147,7 @@ class Apply():
|
|
139
147
|
res, status = self.request_model.destroy(uuid, force=self.__force)
|
140
148
|
|
141
149
|
if status == 200:
|
142
|
-
self.__logger(f"{bcolors.WARNING}Deleted{bcolors.ENDC}
|
150
|
+
self.__logger(f"{bcolors.WARNING}Deleted Request{bcolors.ENDC} {uuid}")
|
143
151
|
else:
|
144
152
|
self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
|
145
153
|
|
@@ -150,9 +158,9 @@ class Apply():
|
|
150
158
|
|
151
159
|
raw_request = snapshot.request
|
152
160
|
if not raw_request:
|
153
|
-
error = f"
|
154
|
-
self.__logger(f"{bcolors.
|
155
|
-
return error,
|
161
|
+
error = f"snapshot for request {uuid} not found"
|
162
|
+
self.__logger(f"{bcolors.WARNING}Skipping Request{bcolors.ENDC} {error}")
|
163
|
+
return error, 301
|
156
164
|
|
157
165
|
return self.__put_request(uuid, raw_request)
|
158
166
|
|
@@ -160,7 +168,7 @@ class Apply():
|
|
160
168
|
res, status = self.scenario_model.destroy(uuid, force=self.__force)
|
161
169
|
|
162
170
|
if self.__logger and status == 200:
|
163
|
-
self.__logger(f"{bcolors.WARNING}Deleted{bcolors.ENDC}
|
171
|
+
self.__logger(f"{bcolors.WARNING}Deleted Scenario{bcolors.ENDC} {uuid}")
|
164
172
|
else:
|
165
173
|
self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
|
166
174
|
|
@@ -171,9 +179,9 @@ class Apply():
|
|
171
179
|
metadata = snapshot.metadata
|
172
180
|
|
173
181
|
if not metadata:
|
174
|
-
error = f"
|
175
|
-
self.__logger(f"{bcolors.
|
176
|
-
return error,
|
182
|
+
error = f"snapshot for scenario {uuid} not found"
|
183
|
+
self.__logger(f"{bcolors.WARNING}Skipping Scenario{bcolors.ENDC} {error}")
|
184
|
+
return error, 301
|
177
185
|
|
178
186
|
res, status = self.scenario_model.show(uuid)
|
179
187
|
if status == 404:
|
@@ -184,7 +192,7 @@ class Apply():
|
|
184
192
|
|
185
193
|
if self.__logger:
|
186
194
|
if status == 200:
|
187
|
-
self.__logger(f"{bcolors.OKGREEN}Created
|
195
|
+
self.__logger(f"{bcolors.OKGREEN}Created Scenario{bcolors.ENDC} {res['name']}")
|
188
196
|
else:
|
189
197
|
self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
|
190
198
|
else:
|
@@ -195,7 +203,7 @@ class Apply():
|
|
195
203
|
|
196
204
|
if self.__logger:
|
197
205
|
if status == 200:
|
198
|
-
self.__logger(f"{bcolors.
|
206
|
+
self.__logger(f"{bcolors.OKCYAN}Updated Scenario{bcolors.ENDC} {res['name']}")
|
199
207
|
else:
|
200
208
|
self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
|
201
209
|
|
@@ -221,6 +229,9 @@ class Apply():
|
|
221
229
|
uuid = control.id
|
222
230
|
res, status = self.__put_request(uuid, raw_request, scenario_id=scenario['id'])
|
223
231
|
|
232
|
+
if status != 200:
|
233
|
+
return res, status
|
234
|
+
|
224
235
|
snapshot_requests[uuid] = res
|
225
236
|
|
226
237
|
# Remove requests in scenario that don't exist in the snapshot
|
@@ -242,15 +253,21 @@ class Apply():
|
|
242
253
|
res, status = self.request_model.show(uuid)
|
243
254
|
|
244
255
|
if status == 404:
|
256
|
+
request_params = build_params(raw_request)
|
257
|
+
|
258
|
+
if not request_params:
|
259
|
+
self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} failed to join request {uuid}")
|
260
|
+
return res, status
|
261
|
+
|
245
262
|
params = {
|
246
|
-
**
|
263
|
+
**request_params,
|
247
264
|
**base_params,
|
248
265
|
}
|
249
266
|
|
250
267
|
res, status = self.request_model.create(**params)
|
251
268
|
|
252
269
|
if self.__logger and status == 200:
|
253
|
-
self.__logger(f"{bcolors.OKGREEN}Created{bcolors.ENDC} {res['list'][0]['url']}")
|
270
|
+
self.__logger(f"{bcolors.OKGREEN}Created Request{bcolors.ENDC} {res['list'][0]['url']}")
|
254
271
|
else:
|
255
272
|
self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
|
256
273
|
elif status == 200:
|
@@ -263,7 +280,7 @@ class Apply():
|
|
263
280
|
|
264
281
|
if self.__logger:
|
265
282
|
if status == 200:
|
266
|
-
self.__logger(f"{bcolors.
|
283
|
+
self.__logger(f"{bcolors.OKCYAN}Updated Request{bcolors.ENDC} {res['url']}")
|
267
284
|
else:
|
268
285
|
self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
|
269
286
|
|
@@ -6,6 +6,9 @@ from stoobly_agent.app.models.adapters.python import PythonRequestAdapterFactory
|
|
6
6
|
|
7
7
|
from stoobly_agent.app.proxy.record.join_request_service import InterceptSettings, join_request, MitmproxyRequestFacade, MitmproxyResponseFacade
|
8
8
|
from stoobly_agent.app.settings import Settings
|
9
|
+
from stoobly_agent.lib.logger import Logger
|
10
|
+
|
11
|
+
LOG_ID = 'CreateRequestParamsService'
|
9
12
|
|
10
13
|
class MitmproxyFlowMock():
|
11
14
|
def __init__(self, request, response):
|
@@ -16,6 +19,7 @@ def build_params(raw_requests: str, payloads_delimitter = None):
|
|
16
19
|
try:
|
17
20
|
joined_request = JoinedRequestAdapter(raw_requests, payloads_delimitter).adapt()
|
18
21
|
except Exception as e:
|
22
|
+
Logger.instance(LOG_ID).error(e)
|
19
23
|
return
|
20
24
|
|
21
25
|
request_adapter = RawHttpRequestAdapter(joined_request.request_string.get())
|
@@ -71,6 +71,7 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
|
|
71
71
|
context.with_response(res)
|
72
72
|
|
73
73
|
if handle_success:
|
74
|
+
# TODO: rewrite response, see #332
|
74
75
|
res = handle_success(context) or res
|
75
76
|
elif policy == mock_policy.FOUND:
|
76
77
|
res = eval_request_with_retry(context, eval_request, **options)
|
@@ -86,6 +87,7 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
|
|
86
87
|
pass
|
87
88
|
else:
|
88
89
|
if handle_success:
|
90
|
+
# TODO: rewrite response, see #332
|
89
91
|
res = handle_success(context) or res
|
90
92
|
else:
|
91
93
|
return bad_request(
|
@@ -24,6 +24,8 @@ def handle_request_replay(replay_context: ReplayContext):
|
|
24
24
|
def handle_response_replay(replay_context: ReplayContext):
|
25
25
|
__replay_hook(lifecycle_hooks.AFTER_REPLAY, replay_context)
|
26
26
|
|
27
|
+
# TODO: rewrite response, see #332
|
28
|
+
|
27
29
|
def __replay_request(replay_context: ReplayContext):
|
28
30
|
"""
|
29
31
|
Before replaying a request, see if the request needs to be rewritten
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import pdb
|
2
|
+
|
3
|
+
from mitmproxy.http import Response as MitmproxyResponse
|
4
|
+
from typing import Union
|
5
|
+
|
6
|
+
from ..replay.body_parser_service import decode_response, encode_response
|
7
|
+
|
8
|
+
class MitmproxyResponseBodyFacade:
|
9
|
+
def __init__(self, response: MitmproxyResponse):
|
10
|
+
self.__response = response
|
11
|
+
|
12
|
+
def get(self, content_type: Union[bytes, str]):
|
13
|
+
return decode_response(self.__response.content, content_type)
|
14
|
+
|
15
|
+
def set(self, content, content_type: Union[bytes, str]):
|
16
|
+
"""
|
17
|
+
Adjusting Content-Length header should be done by MitmproxyResponse
|
18
|
+
"""
|
19
|
+
self.__response.content = encode_response(content, content_type).encode()
|
@@ -1,33 +1,45 @@
|
|
1
|
-
|
1
|
+
import pdb
|
2
2
|
|
3
|
-
from
|
3
|
+
from mitmproxy.http import Headers, Response as MitmproxyResponse
|
4
|
+
from mitmproxy.coretypes import multidict
|
5
|
+
from typing import Callable, List
|
4
6
|
|
7
|
+
from stoobly_agent.app.settings.constants import request_component
|
8
|
+
from stoobly_agent.app.settings.rewrite_rule import ParameterRule, RewriteRule
|
9
|
+
from stoobly_agent.config.constants import custom_headers
|
10
|
+
from stoobly_agent.lib.logger import Logger, bcolors
|
11
|
+
from stoobly_agent.lib.utils import jmespath
|
12
|
+
from stoobly_agent.lib.utils.decode import decode
|
13
|
+
|
14
|
+
from .request_facade import MitmproxyRequestFacade
|
15
|
+
from .response_body_facade import MitmproxyResponseBodyFacade
|
5
16
|
from .response import Response
|
6
17
|
|
18
|
+
LOG_ID = 'Response'
|
19
|
+
|
7
20
|
class MitmproxyResponseFacade(Response):
|
8
21
|
|
9
22
|
def __init__(self, response: MitmproxyResponse):
|
10
23
|
self.response = response
|
11
|
-
self.content = response.raw_content
|
12
24
|
|
13
|
-
self.
|
25
|
+
self.__body = MitmproxyResponseBodyFacade(response)
|
26
|
+
self.__parameter_rules: List[ParameterRule] = []
|
14
27
|
|
15
28
|
@property
|
16
29
|
def code(self):
|
17
30
|
return self.response.status_code
|
18
31
|
|
32
|
+
@property
|
33
|
+
def content_type(self):
|
34
|
+
return self.headers.get('content-type')
|
35
|
+
|
19
36
|
@property
|
20
37
|
def headers(self):
|
21
|
-
return self.response.headers
|
38
|
+
return self.__filter_custom_headers(self.response.headers)
|
22
39
|
|
23
40
|
@property
|
24
41
|
def body(self):
|
25
|
-
|
26
|
-
|
27
|
-
if not content:
|
28
|
-
return b''
|
29
|
-
|
30
|
-
return content
|
42
|
+
return self.response.raw_content or ''
|
31
43
|
|
32
44
|
@property
|
33
45
|
def http_verison(self):
|
@@ -40,13 +52,73 @@ class MitmproxyResponseFacade(Response):
|
|
40
52
|
# Update Content-Lenght header to decoded content length
|
41
53
|
self.response.headers['content-length'] = str(len(self.content))
|
42
54
|
|
43
|
-
|
44
|
-
def with_rewrite_rules(self, rules: RewriteRule):
|
55
|
+
def with_parameter_rules(self, rules: List[RewriteRule], request_facade: MitmproxyRequestFacade):
|
45
56
|
if type(rules) == list:
|
46
|
-
self.
|
57
|
+
self.__parameter_rules = request_facade.select_parameter_rules(rules)
|
58
|
+
return self
|
47
59
|
|
48
|
-
return self
|
49
|
-
|
50
|
-
# TODO
|
51
60
|
def rewrite(self):
|
52
|
-
|
61
|
+
rewrites = self.__parameter_rules
|
62
|
+
|
63
|
+
if len(rewrites) != 0:
|
64
|
+
self.__rewrite_headers(rewrites)
|
65
|
+
self.__rewrite_content(rewrites)
|
66
|
+
|
67
|
+
def __apply_rewrites(self, params: dict, rewrites: List[ParameterRule], handler: Callable):
|
68
|
+
if len(rewrites) == 0:
|
69
|
+
return
|
70
|
+
|
71
|
+
for rewrite in rewrites:
|
72
|
+
jmespath.search(rewrite.name, params, {
|
73
|
+
'replacements': [handler(rewrite) if handler else rewrite.value],
|
74
|
+
})
|
75
|
+
|
76
|
+
def __rewrite_handler(self, rewrite: ParameterRule) -> str:
|
77
|
+
Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Rewriting{bcolors.ENDC} {rewrite.type.lower()} {rewrite.name} => {rewrite.value}")
|
78
|
+
return rewrite.value
|
79
|
+
|
80
|
+
def __rewrite_headers(self, rewrites: List[ParameterRule]):
|
81
|
+
self.__apply_headers(rewrites, self.__rewrite_handler)
|
82
|
+
|
83
|
+
def __rewrite_content(self, rewrites: List[ParameterRule]):
|
84
|
+
self.__apply_content(rewrites, self.__rewrite_handler)
|
85
|
+
|
86
|
+
def __apply_headers(self, rewrites: List[ParameterRule], handler: Callable):
|
87
|
+
rewrites = list(filter(lambda rewrite: rewrite.type == request_component.RESPONSE_HEADER, rewrites))
|
88
|
+
self.__apply_rewrites(self.response.headers, rewrites, handler)
|
89
|
+
|
90
|
+
def __apply_content(self, rewrites: List[ParameterRule], handler: Callable):
|
91
|
+
rewrites = list(filter(lambda rewrite: rewrite.type == request_component.RESPONSE_PARAM, rewrites))
|
92
|
+
if len(rewrites) == 0:
|
93
|
+
return
|
94
|
+
|
95
|
+
content_type = self.content_type
|
96
|
+
parsed_content = self.__body.get(content_type)
|
97
|
+
|
98
|
+
if not isinstance(parsed_content, dict) and not isinstance(parsed_content, multidict.MultiDictView):
|
99
|
+
content_type = 'application/json'
|
100
|
+
self.response.headers['content-type'] = content_type
|
101
|
+
parsed_content = {}
|
102
|
+
|
103
|
+
self.__apply_rewrites(parsed_content, rewrites, handler)
|
104
|
+
self.__body.set(parsed_content, content_type)
|
105
|
+
|
106
|
+
def __filter_custom_headers(self, response_headers: Headers):
|
107
|
+
'''
|
108
|
+
Remove custom headers
|
109
|
+
'''
|
110
|
+
_response_headers = Headers(**response_headers)
|
111
|
+
|
112
|
+
headers = custom_headers.__dict__
|
113
|
+
for key in headers:
|
114
|
+
if key[0:2] == '__' and key[-2:] == '__':
|
115
|
+
continue
|
116
|
+
|
117
|
+
name = headers[key]
|
118
|
+
|
119
|
+
if name not in response_headers:
|
120
|
+
continue
|
121
|
+
|
122
|
+
_response_headers.pop(name)
|
123
|
+
|
124
|
+
return _response_headers
|
@@ -27,6 +27,6 @@ def join_rewritten_request(flow: MitmproxyHTTPFlow, intercept_settings: Intercep
|
|
27
27
|
rewrite_rules = intercept_settings.record_rewrite_rules
|
28
28
|
|
29
29
|
request.with_parameter_rules(rewrite_rules).with_url_rules(rewrite_rules).rewrite()
|
30
|
-
response.
|
30
|
+
response.with_parameter_rules(rewrite_rules, request).rewrite()
|
31
31
|
|
32
32
|
return join_request(request, response, intercept_settings)
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import cgi
|
2
1
|
import json
|
3
2
|
import pdb
|
4
3
|
import urllib.parse
|
5
4
|
|
5
|
+
from email.message import Message
|
6
6
|
from mitmproxy.coretypes.multidict import MultiDict
|
7
7
|
from mitmproxy.net import encoding
|
8
8
|
from typing import Dict, Union
|
@@ -108,11 +108,19 @@ def serialize_www_form_urlencoded(o):
|
|
108
108
|
def normalize_header(header):
|
109
109
|
if isinstance(header, bytes):
|
110
110
|
header = header.decode('utf-8')
|
111
|
-
return
|
111
|
+
return __parse_separated_header(header).lower()
|
112
112
|
|
113
113
|
def is_traversable(content):
|
114
114
|
return isinstance(content, list) or isinstance(content, dict) or isinstance(content, MultiDict)
|
115
115
|
|
116
116
|
def is_json(content_type):
|
117
117
|
_content_type = content_type.lower()
|
118
|
-
return _content_type == JSON or _content_type.startswith('application/x-amz-json')
|
118
|
+
return _content_type == JSON or _content_type.startswith('application/x-amz-json')
|
119
|
+
|
120
|
+
|
121
|
+
def __parse_separated_header(header: str):
|
122
|
+
# Adapted from https://peps.python.org/pep-0594/#cgi
|
123
|
+
message = Message()
|
124
|
+
message['content-type'] = header
|
125
|
+
return message.get_content_type()
|
126
|
+
|
@@ -3,6 +3,7 @@ from typing import Literal
|
|
3
3
|
HEADER = 'Header'
|
4
4
|
BODY_PARAM = 'Body Param'
|
5
5
|
QUERY_PARAM = 'Query Param'
|
6
|
+
RESPONSE_HEADER = 'Response Header'
|
6
7
|
RESPONSE_PARAM = 'Response Param'
|
7
8
|
|
8
|
-
RequestComponent = Literal[BODY_PARAM, HEADER, QUERY_PARAM, RESPONSE_PARAM]
|
9
|
+
RequestComponent = Literal[BODY_PARAM, HEADER, QUERY_PARAM, RESPONSE_HEADER, RESPONSE_PARAM]
|
@@ -2,28 +2,28 @@ ALIAS_RESOLVE_STRATEGY = 'X-Stoobly-Alias-Resolve-Strategy'
|
|
2
2
|
CONTENT_TYPE = 'X-Stoobly-Content-Type'
|
3
3
|
CONTENT_TYPE_TEST_RESULTS = 'test/results'
|
4
4
|
REMOTE_PROJECT_KEY = 'X-Stoobly-Endpoints-Project-Id'
|
5
|
-
MOCK_POLICY = 'X-Mock-Policy'
|
5
|
+
MOCK_POLICY = 'X-Stoobly-Mock-Policy'
|
6
6
|
MOCK_REQUEST_ID = 'X-Stoobly-Request-Id'
|
7
7
|
MOCK_REQUEST_ENDPOINT_ID = 'X-Stoobly-Request-Endpoint-Id'
|
8
8
|
MOCK_REQUEST_KEY = 'X-Stoobly-Request-Key'
|
9
|
-
DO_PROXY = 'X-Do-Proxy'
|
9
|
+
DO_PROXY = 'X-Stoobly-Do-Proxy'
|
10
10
|
LIFECYCLE_HOOKS_PATH = 'X-Stoobly-Lifecycle-Hooks-Path'
|
11
|
-
PROJECT_KEY = 'X-Project-Key'
|
12
|
-
PROXY_MODE = 'X-Proxy-Mode'
|
11
|
+
PROJECT_KEY = 'X-Stoobly-Project-Key'
|
12
|
+
PROXY_MODE = 'X-Stoobly-Proxy-Mode'
|
13
13
|
PUBLIC_DIRECTORY_PATH = 'X-Stoobly-Public-Directory-Path'
|
14
|
-
RECORD_POLICY = 'X-Record-Policy'
|
15
|
-
REPORT_KEY = 'X-Report-Key'
|
16
|
-
REQUEST_ORIGIN = 'X-
|
14
|
+
RECORD_POLICY = 'X-Stoobly-Record-Policy'
|
15
|
+
REPORT_KEY = 'X-Stoobly-Report-Key'
|
16
|
+
REQUEST_ORIGIN = 'X-Stoobly-Request-Origin'
|
17
17
|
RESPONSE_FIXTURES_PATH = 'X-Stoobly-Response-Fixtures-Path'
|
18
|
-
RESPONSE_ID = 'X-Response-Id'
|
18
|
+
RESPONSE_ID = 'X-Stoobly-Response-Id'
|
19
19
|
RESPONSE_LATENCY = 'X-Stoobly-Request-Response-Latency'
|
20
|
-
RESPONSE_PROXY_MODE = 'X-Response-Proxy-Mode'
|
21
|
-
SCENARIO_KEY = 'X-Scenario-Key'
|
22
|
-
SERVICE_URL = 'X-Service-Url'
|
23
|
-
TEST_FILTER = 'X-Test-Filter'
|
20
|
+
RESPONSE_PROXY_MODE = 'X-Stoobly-Response-Proxy-Mode'
|
21
|
+
SCENARIO_KEY = 'X-Stoobly-Scenario-Key'
|
22
|
+
SERVICE_URL = 'X-Stoobly-Service-Url'
|
23
|
+
TEST_FILTER = 'X-Stoobly-Test-Filter'
|
24
24
|
TEST_ID = 'X-Stoobly-Test-Id'
|
25
25
|
TEST_SAVE_RESULTS = 'X-Stoobly-Test-Save-Results'
|
26
26
|
TEST_SKIP = 'X-Stoobly-Test-Skip'
|
27
|
-
TEST_STRATEGY = 'X-Test-Strategy'
|
27
|
+
TEST_STRATEGY = 'X-Stoobly-Test-Strategy'
|
28
28
|
TRACE_ID = 'X-Stoobly-Trace-Id'
|
29
29
|
TRACE_REQUEST_ID = 'X-Stoobly-Trace-Request-Id'
|
stoobly_agent/config/data_dir.py
CHANGED
@@ -4,6 +4,7 @@ import shutil
|
|
4
4
|
|
5
5
|
from stoobly_agent.config.constants.env_vars import ENV
|
6
6
|
|
7
|
+
CERTS_DIR_NAME = 'certs'
|
7
8
|
DATA_DIR_NAME = '.stoobly'
|
8
9
|
DB_FILE_NAME = 'stoobly_agent.sqlite3'
|
9
10
|
DB_VERSION_NAME = 'VERSION'
|
@@ -85,7 +86,7 @@ class DataDir:
|
|
85
86
|
|
86
87
|
@property
|
87
88
|
def certs_dir_path(self):
|
88
|
-
certs_dir_path = os.path.join(self.path,
|
89
|
+
certs_dir_path = os.path.join(self.path, CERTS_DIR_NAME)
|
89
90
|
|
90
91
|
if not os.path.exists(certs_dir_path):
|
91
92
|
os.mkdir(certs_dir_path)
|
stoobly_agent/config/schema.yml
CHANGED
@@ -6,13 +6,13 @@ cli:
|
|
6
6
|
proxy:
|
7
7
|
data: map(include('data_rules'), key=include('project_id'))
|
8
8
|
firewall: map(list(include('firewall_rule')), key=include('project_id'))
|
9
|
-
match: map(list(include('match_rule')), key=include('project_id'))
|
9
|
+
match: map(list(include('match_rule')), key=include('project_id'), required=False)
|
10
10
|
rewrite: map(list(include('rewrite_rule')), key=include('project_id'))
|
11
11
|
intercept:
|
12
12
|
active: bool()
|
13
13
|
mode: include('mode')
|
14
14
|
project_key: str()
|
15
|
-
url: str()
|
15
|
+
url: str(required=False)
|
16
16
|
proxy_config_path: str(required=False)
|
17
17
|
remote:
|
18
18
|
api_url: str()
|