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.
Files changed (102) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/application_http_request_handler.py +3 -3
  3. stoobly_agent/app/api/proxy_controller.py +8 -7
  4. stoobly_agent/app/cli/config_cli.py +1 -1
  5. stoobly_agent/app/cli/helpers/certificate_authority.py +7 -6
  6. stoobly_agent/app/cli/helpers/print_service.py +17 -0
  7. stoobly_agent/app/cli/scaffold/app.py +16 -34
  8. stoobly_agent/app/cli/scaffold/app_command.py +4 -7
  9. stoobly_agent/app/cli/scaffold/app_config.py +15 -2
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +18 -2
  11. stoobly_agent/app/cli/scaffold/command.py +1 -1
  12. stoobly_agent/app/cli/scaffold/constants.py +9 -5
  13. stoobly_agent/app/cli/scaffold/docker/app_builder.py +3 -7
  14. stoobly_agent/app/cli/scaffold/docker/constants.py +0 -1
  15. stoobly_agent/app/cli/scaffold/docker/service/builder.py +12 -11
  16. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +14 -31
  17. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +6 -2
  18. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +6 -2
  19. stoobly_agent/app/cli/scaffold/hosts_file_manager.py +112 -0
  20. stoobly_agent/app/cli/scaffold/service.py +1 -2
  21. stoobly_agent/app/cli/scaffold/service_command.py +1 -1
  22. stoobly_agent/app/cli/scaffold/service_config.py +10 -14
  23. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +9 -11
  24. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +2 -4
  25. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +108 -68
  26. stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml +8 -13
  27. stoobly_agent/app/cli/scaffold/templates/app/Makefile +1 -1
  28. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +8 -4
  29. stoobly_agent/app/cli/scaffold/templates/app/build/mock/.docker-compose.mock.yml +2 -6
  30. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.configure +3 -0
  31. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.init +3 -0
  32. stoobly_agent/app/cli/scaffold/templates/app/build/record/.docker-compose.record.yml +2 -6
  33. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.configure +3 -0
  34. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.init +3 -0
  35. stoobly_agent/app/cli/scaffold/templates/app/build/test/.docker-compose.test.yml +2 -6
  36. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.configure +3 -0
  37. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.init +3 -0
  38. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -0
  39. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/.docker-compose.mock.yml +2 -8
  40. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/.docker-compose.record.yml +2 -8
  41. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/.docker-compose.test.yml +2 -8
  42. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/.docker-compose.exec.yml +2 -3
  43. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.logs +1 -0
  44. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.services +9 -0
  45. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +1 -2
  46. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +1 -2
  47. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.configure +3 -0
  48. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.init +7 -1
  49. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.configure +3 -0
  50. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.init +7 -1
  51. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.configure +3 -0
  52. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.init +7 -1
  53. stoobly_agent/app/cli/scaffold/validate_command.py +2 -2
  54. stoobly_agent/app/cli/scaffold/workflow.py +5 -4
  55. stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
  56. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  57. stoobly_agent/app/cli/scaffold/workflow_run_command.py +78 -45
  58. stoobly_agent/app/cli/scaffold_cli.py +246 -109
  59. stoobly_agent/app/cli/snapshot_cli.py +7 -3
  60. stoobly_agent/app/models/adapters/joined_request_adapter.py +6 -0
  61. stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +3 -1
  62. stoobly_agent/app/models/helpers/apply.py +34 -17
  63. stoobly_agent/app/models/helpers/create_request_params_service.py +4 -0
  64. stoobly_agent/app/proxy/handle_mock_service.py +2 -0
  65. stoobly_agent/app/proxy/handle_replay_service.py +2 -0
  66. stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
  67. stoobly_agent/app/proxy/mitmproxy/response_body_facade.py +19 -0
  68. stoobly_agent/app/proxy/mitmproxy/response_facade.py +90 -18
  69. stoobly_agent/app/proxy/record/join_request_service.py +1 -1
  70. stoobly_agent/app/proxy/replay/body_parser_service.py +11 -3
  71. stoobly_agent/app/settings/constants/request_component.py +2 -1
  72. stoobly_agent/config/constants/custom_headers.py +13 -13
  73. stoobly_agent/config/constants/headers.py +0 -2
  74. stoobly_agent/config/data_dir.py +2 -1
  75. stoobly_agent/config/schema.yml +2 -2
  76. stoobly_agent/public/18-es2015.583f191cc7ad512ee262.js +1 -0
  77. stoobly_agent/public/18-es5.583f191cc7ad512ee262.js +1 -0
  78. stoobly_agent/public/35-es2015.8f79ff8748d4ff06ab03.js +1 -0
  79. stoobly_agent/public/35-es5.8f79ff8748d4ff06ab03.js +1 -0
  80. stoobly_agent/public/index.html +1 -1
  81. stoobly_agent/public/main-es2015.2cc16523aa3fcaba51e5.js +1 -0
  82. stoobly_agent/public/main-es5.2cc16523aa3fcaba51e5.js +1 -0
  83. stoobly_agent/public/{runtime-es2015.9addf49b79aca951b7e2.js → runtime-es2015.b914470164e4d6e75d96.js} +1 -1
  84. stoobly_agent/public/{runtime-es5.9addf49b79aca951b7e2.js → runtime-es5.b914470164e4d6e75d96.js} +1 -1
  85. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +1 -2
  86. stoobly_agent/test/app/cli/scaffold/{hosts_file_reader_test.py → hosts_file_manager_test.py} +20 -20
  87. stoobly_agent/test/app/cli/snapshot/snapshot_apply_test.py +162 -1
  88. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  89. stoobly_agent/test/mock_data/scaffold/docker-compose-assets-service.yml +1 -3
  90. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/METADATA +1 -1
  91. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/RECORD +94 -93
  92. stoobly_agent/app/cli/scaffold/hosts_file_reader.py +0 -65
  93. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.proxy +0 -34
  94. stoobly_agent/public/18-es2015.d3b430636a4d6f544d92.js +0 -1
  95. stoobly_agent/public/18-es5.d3b430636a4d6f544d92.js +0 -1
  96. stoobly_agent/public/35-es2015.f741ebce0bfc25f0ec99.js +0 -1
  97. stoobly_agent/public/35-es5.f741ebce0bfc25f0ec99.js +0 -1
  98. stoobly_agent/public/main-es2015.ccd46ac1b6638ddf2066.js +0 -1
  99. stoobly_agent/public/main-es5.ccd46ac1b6638ddf2066.js +0 -1
  100. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/LICENSE +0 -0
  101. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/WHEEL +0 -0
  102. {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 event {event.uuid}")
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 >= 500:
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 event {event.uuid}")
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} request {uuid}")
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"Snapshot for request {uuid} not found"
154
- self.__logger(f"{bcolors.FAIL}400{bcolors.ENDC} {error}")
155
- return error, 400
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} scenario {uuid}")
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"Snapshot for scenario {uuid} not found"
175
- self.__logger(f"{bcolors.FAIL}400{bcolors.ENDC} {error}")
176
- return error, 400
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 scenario{bcolors.ENDC} {res['name']}")
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.OKBLUE}Updated{bcolors.ENDC} scenario {res['name']}")
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
- **build_params(raw_request),
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.OKBLUE}Updated{bcolors.ENDC} {res['url']}")
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
@@ -70,7 +70,7 @@ class MitmproxyRequestFacade(Request):
70
70
 
71
71
  @property
72
72
  def body(self):
73
- content = self.request.raw_content or ''
73
+ content = self.request.raw_content or b''
74
74
 
75
75
  return decode(content)
76
76
 
@@ -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
- from mitmproxy.http import Response as MitmproxyResponse
1
+ import pdb
2
2
 
3
- from stoobly_agent.app.settings.rewrite_rule import RewriteRule
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.rewrite_rules = []
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
- content = self.content
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.rewrite_rules = rules
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
- pass
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.with_rewrite_rules(rewrite_rules).rewrite()
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 cgi.parse_header(header)[0].lower()
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-Proxy-Request-Origin'
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'
@@ -1,9 +1,7 @@
1
1
  ACCESS_TOKEN = 'ACCESS-TOKEN'
2
2
  CLIENT = 'CLIENT'
3
- DO_PROXY = 'X-DO-PROXY'
4
3
  EXPIRY = 'EXPIRY'
5
4
  PROXY_HEADERS = 'X-PROXY-HEADERS'
6
5
  REQUEST_PATH = 'X-REQUEST-PATH'
7
- SERVICE_URL = 'X-SERVICE-URL'
8
6
  TOKEN_TYPE = 'TOKEN-TYPE'
9
7
  UID = 'UID'
@@ -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, 'certs')
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)
@@ -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()