stoobly-agent 1.8.3__py3-none-any.whl → 1.8.5__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 (55) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/headers_controller.py +1 -1
  3. stoobly_agent/app/api/query_params_controller.py +4 -4
  4. stoobly_agent/app/cli/snapshot_cli.py +57 -0
  5. stoobly_agent/app/cli/types/snapshot_migration.py +61 -0
  6. stoobly_agent/app/models/adapters/orm/request/mitmproxy_adapter.py +1 -1
  7. stoobly_agent/app/models/adapters/python/request/__init__.py +2 -2
  8. stoobly_agent/app/models/adapters/python/request/mitmproxy_adapter.py +1 -1
  9. stoobly_agent/app/models/adapters/python/request/raw_adapter.py +2 -2
  10. stoobly_agent/app/models/factories/resource/local_db/header_adapter.py +28 -8
  11. stoobly_agent/app/models/factories/resource/local_db/helpers/log.py +7 -0
  12. stoobly_agent/app/models/factories/resource/local_db/helpers/log_event.py +10 -0
  13. stoobly_agent/app/models/factories/resource/local_db/helpers/request_snapshot.py +20 -0
  14. stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +8 -1
  15. stoobly_agent/app/models/factories/resource/local_db/helpers/snapshot.py +1 -1
  16. stoobly_agent/app/models/factories/resource/local_db/local_db_adapter.py +3 -0
  17. stoobly_agent/app/models/factories/resource/local_db/query_param_adapter.py +2 -2
  18. stoobly_agent/app/models/factories/resource/local_db/request_adapter.py +22 -3
  19. stoobly_agent/app/models/helpers/apply.py +5 -4
  20. stoobly_agent/app/models/helpers/create_request_params_service.py +4 -2
  21. stoobly_agent/app/proxy/intercept_handler.py +6 -2
  22. stoobly_agent/app/proxy/record/join_request_service.py +22 -10
  23. stoobly_agent/app/proxy/record/proxy_request.py +9 -0
  24. stoobly_agent/app/proxy/record/request_string.py +4 -1
  25. stoobly_agent/app/proxy/record/upload_request_service.py +7 -5
  26. stoobly_agent/app/proxy/test/helpers/upload_test_service.py +1 -1
  27. stoobly_agent/config/constants/lifecycle_hooks.py +1 -0
  28. stoobly_agent/lib/orm/transformers/orm_to_request_transformer.py +74 -2
  29. stoobly_agent/public/13-es2015.6d3a4fd76d46bfa5f200.js +1 -0
  30. stoobly_agent/public/13-es5.6d3a4fd76d46bfa5f200.js +1 -0
  31. stoobly_agent/public/{18-es2015.503207073756a9c8211a.js → 18-es2015.d07dd29def7e2574c5b7.js} +1 -1
  32. stoobly_agent/public/{18-es5.503207073756a9c8211a.js → 18-es5.d07dd29def7e2574c5b7.js} +1 -1
  33. stoobly_agent/public/35-es2015.4ffe6f7a196ed1a87fc7.js +1 -0
  34. stoobly_agent/public/35-es5.4ffe6f7a196ed1a87fc7.js +1 -0
  35. stoobly_agent/public/index.html +1 -1
  36. stoobly_agent/public/{main-es2015.d682619f3d6d53d64c6a.js → main-es2015.ce00115b0520fa030f01.js} +1 -1
  37. stoobly_agent/public/{main-es5.d682619f3d6d53d64c6a.js → main-es5.ce00115b0520fa030f01.js} +1 -1
  38. stoobly_agent/public/runtime-es2015.b13c22b834b51724d30a.js +1 -0
  39. stoobly_agent/public/runtime-es5.b13c22b834b51724d30a.js +1 -0
  40. stoobly_agent/test/app/cli/scenario/scenario_snapshot_test.py +1 -1
  41. stoobly_agent/test/app/cli/snapshot/lifecycle_hooks_migrate.py +10 -0
  42. stoobly_agent/test/app/cli/snapshot/snapshot_migrate_test.py +181 -0
  43. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  44. stoobly_agent/test/app/proxy/replay/trace_context_test.py +36 -3
  45. {stoobly_agent-1.8.3.dist-info → stoobly_agent-1.8.5.dist-info}/METADATA +1 -1
  46. {stoobly_agent-1.8.3.dist-info → stoobly_agent-1.8.5.dist-info}/RECORD +49 -46
  47. stoobly_agent/public/13-es2015.343b0261a8b3b3f4a1fc.js +0 -1
  48. stoobly_agent/public/13-es5.343b0261a8b3b3f4a1fc.js +0 -1
  49. stoobly_agent/public/35-es2015.a23419c9c7bff162a8e3.js +0 -1
  50. stoobly_agent/public/35-es5.a23419c9c7bff162a8e3.js +0 -1
  51. stoobly_agent/public/runtime-es2015.7130ec4068d875dc38bd.js +0 -1
  52. stoobly_agent/public/runtime-es5.7130ec4068d875dc38bd.js +0 -1
  53. {stoobly_agent-1.8.3.dist-info → stoobly_agent-1.8.5.dist-info}/LICENSE +0 -0
  54. {stoobly_agent-1.8.3.dist-info → stoobly_agent-1.8.5.dist-info}/WHEEL +0 -0
  55. {stoobly_agent-1.8.3.dist-info → stoobly_agent-1.8.5.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '1.8.3'
2
+ VERSION = '1.8.5'
@@ -86,7 +86,7 @@ class HeadersController:
86
86
 
87
87
  context.render(
88
88
  plain = '',
89
- status = 200
89
+ status = 204
90
90
  )
91
91
 
92
92
  def __header_model(self, context: SimpleHTTPRequestHandler):
@@ -103,13 +103,13 @@ class QueryParamsController:
103
103
 
104
104
  if context.filter_response(query_param, status):
105
105
  return
106
-
106
+
107
107
  context.render(
108
- plain = query_param,
109
- status = status
108
+ plain = '',
109
+ status = 204
110
110
  )
111
111
 
112
112
  def __query_param_model(self, context: SimpleHTTPRequestHandler):
113
113
  access_token = context.headers.get('access-token')
114
114
  query_param_model = QueryParamModel(Settings.instance(), access_token=access_token)
115
- return query_param_model
115
+ return query_param_model
@@ -20,6 +20,7 @@ from stoobly_agent.lib.api.keys import RequestKey, ScenarioKey
20
20
 
21
21
  from .helpers.print_service import FORMATS, print_snapshots, select_print_options
22
22
  from .helpers.verify_raw_request_service import verify_raw_request
23
+ from .types.snapshot_migration import SnapshotMigration
23
24
 
24
25
  @click.group(
25
26
  epilog="Run 'stoobly-agent project COMMAND --help' for more information on a command.",
@@ -91,6 +92,62 @@ def _list(**kwargs):
91
92
  if len(formatted_events):
92
93
  print_snapshots(formatted_events, **print_options)
93
94
 
95
+ @snapshot.command(
96
+ help="Migrate snapshots."
97
+ )
98
+ @click.option('--scenario-key', help='Apply migration to specific scenario.')
99
+ @click.argument('lifecycle_hooks_path')
100
+ def migrate(**kwargs):
101
+ from runpy import run_path
102
+ from stoobly_agent.config.constants.lifecycle_hooks import BEFORE_MIGRATE
103
+
104
+ scenario_key = None
105
+ if kwargs['scenario_key']:
106
+ scenario_key = ScenarioKey(kwargs['scenario_key'])
107
+
108
+ try:
109
+ lifecycle_hooks = run_path(kwargs['lifecycle_hooks_path'])
110
+ except Exception as e:
111
+ lifecycle_hooks = {}
112
+
113
+ before_migrate_hook = None
114
+ if BEFORE_MIGRATE not in lifecycle_hooks:
115
+ sys.exit(1)
116
+ else:
117
+ before_migrate_hook = lifecycle_hooks[BEFORE_MIGRATE]
118
+
119
+ log = Log()
120
+
121
+ request_snapshots = []
122
+ for event in log.resource_events:
123
+ if event.is_scenario():
124
+ # If scenario_key is set, only consider that scenario
125
+ if scenario_key and event.resource_uuid != scenario_key.id:
126
+ continue
127
+
128
+ scenario_snapshot: ScenarioSnapshot = event.snapshot()
129
+ _request_snapshots = scenario_snapshot.request_snapshots
130
+
131
+ for request_snapshot in _request_snapshots:
132
+ # Scenario requests will have the same log event
133
+ request_snapshots.append((request_snapshot, event))
134
+ elif event.is_request():
135
+ # If scenario_key is set, only consider that scenario
136
+ if scenario_key:
137
+ continue
138
+
139
+ request_snapshot: RequestSnapshot = event.snapshot()
140
+ request_snapshots.append((request_snapshot, event))
141
+
142
+ snapshot_migrations = {}
143
+ for request_snapshot, event in request_snapshots:
144
+ if request_snapshot.uuid not in snapshot_migrations:
145
+ snapshot_migration = SnapshotMigration(request_snapshot, event)
146
+ snapshot_migrations[request_snapshot.uuid] = snapshot_migration
147
+
148
+ if before_migrate_hook(snapshot_migration):
149
+ break
150
+
94
151
  @snapshot.command(
95
152
  help="Prune deleted snapshots."
96
153
  )
@@ -0,0 +1,61 @@
1
+ from mitmproxy.http import Request, Response
2
+
3
+ from stoobly_agent.app.models.factories.resource.local_db.helpers.log import Log
4
+ from stoobly_agent.app.models.factories.resource.local_db.helpers.log_event import LogEvent
5
+ from stoobly_agent.app.models.factories.resource.local_db.helpers.request_snapshot import RequestSnapshot
6
+ from stoobly_agent.app.proxy.record.join_request_service import join_request_from_request_response
7
+
8
+ class SnapshotMigration():
9
+ _request: Request
10
+ _response: Response
11
+ _snapshot: RequestSnapshot
12
+
13
+ def __init__(self, snapshot: RequestSnapshot, log_event: LogEvent, log: Log = None):
14
+ self._event = log_event
15
+ self._log = log or Log()
16
+ self._request = snapshot.mitmproxy_request
17
+ self._response = snapshot.mitmproxy_response
18
+ self._snapshot = snapshot
19
+
20
+ @property
21
+ def request(self):
22
+ return self._request
23
+
24
+ @request.setter
25
+ def request(self, v: Request):
26
+ if not isinstance(v, Request):
27
+ raise TypeError('Invalid type.')
28
+ self._request = v
29
+
30
+ @property
31
+ def response(self):
32
+ return self._response
33
+
34
+ @response.setter
35
+ def response(self, v: Response):
36
+ if not isinstance(v, Response):
37
+ raise TypeError('Invalid type.')
38
+ self._response = v
39
+
40
+ @property
41
+ def snapshot(self):
42
+ return self._snapshot
43
+
44
+ @property
45
+ def uuid(self):
46
+ return self.snapshot.uuid
47
+
48
+ def delete(self, log: Log = None):
49
+ log = log or self._log
50
+ new_event = self._event.duplicate_as_delete()
51
+ log.append(str(new_event))
52
+
53
+ def save(self, log: Log = None):
54
+ log = log or self._log
55
+ request_uuid = self.snapshot.uuid
56
+ joined_request = join_request_from_request_response(self.request, self.response, id=request_uuid)
57
+ raw_request = joined_request.build()
58
+ self.snapshot.write_raw(raw_request)
59
+ new_event = self._event.duplicate()
60
+ log.append(str(new_event))
61
+ return new_event
@@ -15,4 +15,4 @@ class MitmproxyRequestAdapter():
15
15
  protocol = raw_http_request_adapter.protocol
16
16
  http_version = protocol.split('/')[1]
17
17
 
18
- return PythonRequestMitmproxyRequestAdapter(http_version, python_request).adapt()
18
+ return PythonRequestMitmproxyRequestAdapter(python_request, http_version).adapt()
@@ -13,10 +13,10 @@ class PythonRequestAdapterFactory():
13
13
  self.__request = request
14
14
 
15
15
  def mitmproxy_request(self, http_version: str = 'HTTP/1.1'):
16
- return MitmproxyRequestAdapter(http_version, self.__request).adapt()
16
+ return MitmproxyRequestAdapter(self.__request, http_version).adapt()
17
17
 
18
18
  def raw_request(self, http_version: str = 'HTTP/1.1'):
19
- return RawRequestAdapter(http_version, self.__request).adapt()
19
+ return RawRequestAdapter(self.__request, http_version).adapt()
20
20
 
21
21
  def stoobly_request(self):
22
22
  return StooblyRequestAdapter(self.__request).adapt()
@@ -8,7 +8,7 @@ from stoobly_agent.lib.utils.decode import decode
8
8
 
9
9
  class MitmproxyRequestAdapter():
10
10
 
11
- def __init__(self, http_version: str, request: requests.Request):
11
+ def __init__(self, request: requests.Request, http_version: str = 'HTTP/1.1'):
12
12
  self.__http_version = http_version
13
13
  self.__request = request
14
14
 
@@ -9,12 +9,12 @@ from .mitmproxy_adapter import MitmproxyRequestAdapter
9
9
 
10
10
  class RawRequestAdapter():
11
11
 
12
- def __init__(self, http_version: str, request: requests.Request):
12
+ def __init__(self, request: requests.Request, http_version: str):
13
13
  self.__http_version = http_version
14
14
  self.__request = request
15
15
 
16
16
  def adapt(self):
17
- mitmproxy_request = MitmproxyRequestAdapter(self.__http_version, self.__request).adapt()
17
+ mitmproxy_request = MitmproxyRequestAdapter(self.__request, self.__http_version).adapt()
18
18
  adapted_request = MitmproxyRequestFacade(mitmproxy_request)
19
19
  proxy_request = ProxyRequest(adapted_request)
20
20
  request_string = RequestString(proxy_request)
@@ -19,19 +19,29 @@ class LocalDBHeaderAdapter(LocalDBAdapter):
19
19
  self.__request_orm = request_orm
20
20
 
21
21
  def create(self, request_id, **header: Header):
22
- return self.update(request_id, **header)
22
+ name: str = header['name']
23
+ value = header['value']
23
24
 
24
- def update(self, request_id, **header: Header):
25
- request = self.__request_orm.find(request_id)
26
-
27
- if not request:
28
- return self.__request_not_found()
25
+ headers = self.__headers(request_id)
26
+
27
+ for header in headers:
28
+ if header.lower() == name.lower():
29
+ return self.conflict(f"{name} already exists.")
29
30
 
31
+ headers[name] = value
32
+
33
+ LocalDBRequestAdapter(self.__request_orm).update(request_id, headers=headers)
34
+
35
+ return self.success({
36
+ 'name': name,
37
+ 'value': value,
38
+ })
39
+
40
+ def update(self, request_id, **header: Header):
30
41
  name = header['name']
31
42
  value = header['value']
32
43
 
33
- request: requests.Request = RawHttpRequestAdapter(request.raw).to_request()
34
- headers = request.headers
44
+ headers = self.__headers(request_id)
35
45
  headers[name] = value
36
46
 
37
47
  LocalDBRequestAdapter(self.__request_orm).update(request_id, headers=headers)
@@ -99,5 +109,15 @@ class LocalDBHeaderAdapter(LocalDBAdapter):
99
109
 
100
110
  return id
101
111
 
112
+ def __headers(self, request_id):
113
+ request = self.__request_orm.find(request_id)
114
+
115
+ if not request:
116
+ return self.__request_not_found()
117
+
118
+ request: requests.Request = RawHttpRequestAdapter(request.raw).to_request()
119
+ headers = request.headers
120
+ return headers
121
+
102
122
  def __request_not_found(self):
103
123
  return self.not_found('Request not found')
@@ -82,6 +82,13 @@ class Log():
82
82
 
83
83
  return self.remove_processed_events(events, version_uuids)
84
84
 
85
+ @property
86
+ def resource_events(self) -> List[LogEvent]:
87
+ events = self.target_events
88
+ events = self.remove_dangling_events(events, events)
89
+
90
+ return list(filter(lambda e: e.action != DELETE_ACTION, events))
91
+
85
92
  @property
86
93
  def version(self):
87
94
  version_file_path = DataDir.instance().snapshosts_version_path
@@ -84,6 +84,16 @@ class LogEvent():
84
84
  toks[4] = str(int(time.time() * 1000))
85
85
  return LogEvent(COLUMN_DELIMITTER.join(toks))
86
86
 
87
+ def duplicate_as_delete(self):
88
+ event = self.duplicate()
89
+ event.action = DELETE_ACTION
90
+ return event
91
+
92
+ def duplicate_as_put(self):
93
+ event = self.duplicate()
94
+ event.action = PUT_ACTION
95
+ return event
96
+
87
97
  def is_request(self):
88
98
  return self.resource == REQUEST_RESOURCE
89
99
 
@@ -34,6 +34,26 @@ class RequestSnapshot(Snapshot):
34
34
  with open(request_file_path, 'rb') as fp:
35
35
  return fp.read()
36
36
 
37
+ @property
38
+ def mitmproxy_request(self):
39
+ from stoobly_agent.app.models.adapters.python.request.mitmproxy_adapter import MitmproxyRequestAdapter
40
+ return MitmproxyRequestAdapter(self.python_request).adapt()
41
+
42
+ @property
43
+ def mitmproxy_response(self):
44
+ from stoobly_agent.app.models.adapters.python.response.mitmproxy_adapter import MitmproxyResponseAdapter
45
+ return MitmproxyResponseAdapter(self.python_response).adapt()
46
+
47
+ @property
48
+ def python_request(self):
49
+ from stoobly_agent.app.models.adapters.raw_joined.request.python_adapter import PythonRequestAdapter
50
+ return PythonRequestAdapter(self.request).adapt()
51
+
52
+ @property
53
+ def python_response(self):
54
+ from stoobly_agent.app.models.adapters.raw_joined.response.python_adapter import PythonResponseAdapter
55
+ return PythonResponseAdapter(self.request).adapt()
56
+
37
57
  @property
38
58
  def path(self):
39
59
  dir_path = os.path.join(self.__requests_dir_path, self.uuid[0:2])
@@ -3,7 +3,7 @@ import os
3
3
  import pdb
4
4
  import shutil
5
5
 
6
- from typing import Callable
6
+ from typing import Callable, List
7
7
 
8
8
  from stoobly_agent.lib.orm.scenario import Scenario
9
9
 
@@ -46,6 +46,13 @@ class ScenarioSnapshot(Snapshot):
46
46
  def metadata_path(self):
47
47
  return os.path.join(self.__scenarios_dir_path, self.uuid)
48
48
 
49
+ @property
50
+ def request_snapshots(self) -> List[RequestSnapshot]:
51
+ snapshots = []
52
+ handler = lambda request_snapshot: snapshots.append(request_snapshot)
53
+ self.iter_request_snapshots(handler)
54
+ return snapshots
55
+
49
56
  @property
50
57
  def requests(self):
51
58
  requests_file_path = self.requests_path
@@ -7,7 +7,7 @@ class Snapshot():
7
7
 
8
8
  def __init__(self, uuid: str):
9
9
  self.__uuid = uuid
10
- self.__data_dir = DataDir.instance()
10
+ self.__data_dir: DataDir = DataDir.instance()
11
11
 
12
12
  @property
13
13
  def uuid(self):
@@ -36,6 +36,9 @@ class LocalDBAdapter():
36
36
  def not_found(self, d = 'Not Found'):
37
37
  return d, 404
38
38
 
39
+ def conflict(self, d = ''):
40
+ return d, 409
41
+
39
42
  def internal_error(self, d = 'Internal Error'):
40
43
  return d, 500
41
44
 
@@ -122,10 +122,10 @@ class LocalDBQueryParamAdapter(LocalDBAdapter):
122
122
  parsed_url = parsed_url._replace(query=urlencode(_query_params, True))
123
123
  request = LocalDBRequestAdapter(self.__request_orm).update(request_id, url=parsed_url.geturl())
124
124
 
125
- return {
125
+ return self.success({
126
126
  'name': name,
127
127
  'value': value,
128
- }
128
+ })
129
129
 
130
130
  def __decode_id(self, id: str):
131
131
  id = base64.b64decode(id)
@@ -193,13 +193,32 @@ class LocalDBRequestAdapter(LocalDBAdapter):
193
193
  }):
194
194
  return self.success(ORMToStooblyRequestTransformer(request, {}).transform())
195
195
  else:
196
- if params.get('method'):
197
- transformer.with_method(params['method'])
198
-
199
196
  if params.get('url'):
200
197
  transformer.with_url(params['url'])
201
198
  del params['url']
202
199
 
200
+ try:
201
+ if params.get('method'):
202
+ transformer.with_method(params['method'])
203
+
204
+ if params.get('scheme'):
205
+ transformer.with_scheme(params['scheme'])
206
+ # Do not delete scheme
207
+
208
+ if params.get('host'):
209
+ transformer.with_host(params['host'])
210
+ # Do not delete host
211
+
212
+ if params.get('port'):
213
+ transformer.with_port(params['port'])
214
+ # Do not delete port
215
+
216
+ if params.get('path'):
217
+ transformer.with_path(params['path'])
218
+ # Do not delete path
219
+ except ValueError as e:
220
+ return self.bad_request(str(e))
221
+
203
222
  if params.get('headers'):
204
223
  transformer.with_headers(params['headers'])
205
224
  del params['headers']
@@ -218,15 +218,16 @@ class Apply():
218
218
 
219
219
  snapshot_requests = {}
220
220
 
221
- raw_requests = snapshot.requests
222
- for raw_request in raw_requests:
221
+ request_snapshots = snapshot.request_snapshots
222
+ for request_snapshot in request_snapshots:
223
+ raw_request = request_snapshot.request
224
+
223
225
  toks = raw_request.split(REQUEST_STRING_CLRF.encode(), 1)
224
226
 
225
227
  if len(toks) != 2:
226
228
  return f"{snapshot.requests_path} contains an invalid request", 400
227
229
 
228
- control = RequestStringControl(toks[0])
229
- uuid = control.id
230
+ uuid = request_snapshot.uuid
230
231
  res, status = self.__put_request(uuid, raw_request, scenario_id=scenario['id'])
231
232
 
232
233
  if status != 200:
@@ -4,7 +4,7 @@ import requests
4
4
  from stoobly_agent.app.models.adapters import JoinedRequestAdapter, RawHttpRequestAdapter, RawHttpResponseAdapter
5
5
  from stoobly_agent.app.models.adapters.python import PythonRequestAdapterFactory, PythonResponseAdapterFactory
6
6
 
7
- from stoobly_agent.app.proxy.record.join_request_service import InterceptSettings, join_request, MitmproxyRequestFacade, MitmproxyResponseFacade
7
+ from stoobly_agent.app.proxy.record.join_request_service import InterceptSettings, join_request_from_request_response
8
8
  from stoobly_agent.app.settings import Settings
9
9
  from stoobly_agent.lib.logger import Logger
10
10
 
@@ -40,7 +40,9 @@ def build_params_from_python(request: requests.Request, response: requests.Respo
40
40
  mitmproxy_response = PythonResponseAdapterFactory(response).mitmproxy_response()
41
41
 
42
42
  intercept_settings = InterceptSettings(Settings.instance(), mitmproxy_request)
43
- joined_request = join_request(MitmproxyRequestFacade(mitmproxy_request), MitmproxyResponseFacade(mitmproxy_response), intercept_settings)
43
+ joined_request = join_request_from_request_response(
44
+ mitmproxy_request, mitmproxy_response, intercept_settings=intercept_settings
45
+ )
44
46
 
45
47
  mitmproxy_flow_mock = MitmproxyFlowMock(mitmproxy_request, mitmproxy_response)
46
48
 
@@ -29,11 +29,12 @@ def request(flow: MitmproxyHTTPFlow):
29
29
  __patch_cookie(request)
30
30
 
31
31
  intercept_settings = InterceptSettings(Settings.instance(), request)
32
-
33
32
  if not intercept_settings.active:
34
33
  return
35
34
 
36
35
  __disable_web_cache(request)
36
+ __disable_content_encoding(request)
37
+
37
38
  __intercept_hook(lifecycle_hooks.BEFORE_REQUEST, flow, intercept_settings)
38
39
 
39
40
  active_mode = intercept_settings.mode
@@ -86,10 +87,13 @@ def response(flow: MitmproxyHTTPFlow):
86
87
  ### PRIVATE
87
88
 
88
89
  # 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
90
90
  def __disable_web_cache(request: MitmproxyRequest) -> None:
91
91
  request.headers['Cache-Control'] = 'no-cache, no-store'
92
92
 
93
+ # Disable response body returning as encoded e.g. gzip
94
+ def __disable_content_encoding(request: MitmproxyRequest) -> None:
95
+ request.headers['Accept-Encoding'] = 'identity'
96
+
93
97
  # Fix issue where multi-value cookies become comma separated
94
98
  def __patch_cookie(request: MitmproxyRequest):
95
99
  header_name = 'cookie'
@@ -1,31 +1,43 @@
1
1
  import pdb
2
2
 
3
- from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
4
- from typing import List
3
+ from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow, Request as MitmproxyRequest, Response as MitmproxyResponse
4
+ from typing import TypedDict
5
5
 
6
6
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
7
- from stoobly_agent.app.settings.rewrite_rule import RewriteRule
8
7
 
9
8
  from ..mitmproxy.request_facade import MitmproxyRequestFacade
10
9
  from ..mitmproxy.response_facade import MitmproxyResponseFacade
11
- from ..utils.rewrite import rewrite_request_response
12
10
  from .joined_request import JoinedRequest
13
11
  from .proxy_request import ProxyRequest
14
12
 
13
+ class JoinRequestOptions(TypedDict):
14
+ id: str
15
+ intercept_settings: InterceptSettings
16
+
15
17
  def join_request(
16
- adapted_request: MitmproxyRequestFacade, adapted_response: MitmproxyResponseFacade, intercept_settings: InterceptSettings
18
+ adapted_request: MitmproxyRequestFacade, adapted_response: MitmproxyResponseFacade, **options: JoinRequestOptions
17
19
  ) -> JoinedRequest:
20
+ intercept_settings: InterceptSettings = options.get('intercept_settings')
21
+
18
22
  # Decorate request with service_url
19
- upstream_url = intercept_settings.upstream_url
23
+ upstream_url = intercept_settings.upstream_url if intercept_settings else None
20
24
  proxy_request = ProxyRequest(adapted_request, upstream_url)
21
25
 
26
+ if options.get('id'):
27
+ proxy_request.id = options['id']
28
+
22
29
  # Create JoinedRequest
23
30
  return JoinedRequest(proxy_request).with_response(adapted_response)
24
31
 
25
32
  def join_request_from_flow(
26
- flow: MitmproxyHTTPFlow, intercept_settings: InterceptSettings
33
+ flow: MitmproxyHTTPFlow, **options: JoinRequestOptions
27
34
  ) -> JoinedRequest:
28
- request = MitmproxyRequestFacade(flow.request)
29
- response = MitmproxyResponseFacade(flow.response)
35
+ return join_request_from_request_response(flow.request, flow.response, **options)
36
+
37
+ def join_request_from_request_response(
38
+ request: MitmproxyRequest, response: MitmproxyResponse, **options: JoinRequestOptions
39
+ ):
40
+ request = MitmproxyRequestFacade(request)
41
+ response = MitmproxyResponseFacade(response)
30
42
 
31
- return join_request(request, response, intercept_settings)
43
+ return join_request(request, response, **options)
@@ -2,10 +2,19 @@ from ..mitmproxy.request_facade import MitmproxyRequestFacade
2
2
 
3
3
  class ProxyRequest:
4
4
  def __init__(self, request: MitmproxyRequestFacade, upstream_url: str = None):
5
+ self._id = None
5
6
  self.request = request
6
7
 
7
8
  self.upstream_url = upstream_url
8
9
 
10
+ @property
11
+ def id(self):
12
+ return self._id
13
+
14
+ @id.setter
15
+ def id(self, v):
16
+ self._id = v
17
+
9
18
  def url(self):
10
19
  url = self.request.url
11
20
 
@@ -15,6 +15,7 @@ class RequestString:
15
15
  ENCODING = 'utf-8'
16
16
 
17
17
  __current_time = None
18
+ request_id = None
18
19
 
19
20
  def __init__(self, proxy_request: ProxyRequest):
20
21
  self.__current_time = self.__get_current_time()
@@ -29,7 +30,9 @@ class RequestString:
29
30
  self.__headers()
30
31
  self.__body()
31
32
 
32
- self.request_id = self.__generate_request_id()
33
+ self.request_id = proxy_request.id
34
+
35
+ self.request_id = self.request_id or self.__generate_request_id()
33
36
 
34
37
  def get(self, **kwargs):
35
38
  if kwargs.get('control'):
@@ -4,6 +4,7 @@ import pdb
4
4
  import time
5
5
  import tempfile
6
6
 
7
+ from copy import deepcopy
7
8
  from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
8
9
  from mitmproxy.http import Request as MitmproxyRequest
9
10
 
@@ -50,22 +51,23 @@ def upload_request(
50
51
  ):
51
52
  Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Recording{bcolors.ENDC} {flow.request.url}")
52
53
 
53
- joined_request = join_request_from_flow(flow, intercept_settings)
54
+ flow_copy = deepcopy(flow) # When applying modifications we don't want to persist them in the response
55
+ joined_request = join_request_from_flow(flow_copy, intercept_settings=intercept_settings)
54
56
 
55
57
  project_key = intercept_settings.project_key
56
58
  scenario_key = intercept_settings.scenario_key
57
59
  body_params = __build_body_params(
58
60
  project_key,
59
- joined_request,
61
+ joined_request,
60
62
  flow=flow,
61
63
  scenario_key=scenario_key
62
64
  )
63
65
 
64
66
  # If request_origin is WEB, then we are in proxy
65
67
  # This means that we have access to Cache singleton and do not need send a request to update the status
66
- sync = intercept_settings.request_origin == request_origin.WEB
68
+ sync = intercept_settings.request_origin == request_origin.WEB
67
69
  res = __upload_request_with_body_params(request_model, body_params, sync)
68
-
70
+
69
71
  if intercept_settings.settings.is_debug():
70
72
  file_path = __debug_request(flow.request, joined_request.build())
71
73
  Logger.instance(LOG_ID).debug(f"Writing request to {file_path}")
@@ -142,4 +144,4 @@ def __debug_request(request: MitmproxyRequest, raw_requests: bytes):
142
144
  with open(file_path, 'wb') as f:
143
145
  f.write(raw_requests)
144
146
 
145
- return file_path
147
+ return file_path
@@ -43,7 +43,7 @@ def upload_test(
43
43
  flow: MitmproxyHTTPFlow,
44
44
  **kwargs: UploadTestData
45
45
  ) -> Response:
46
- joined_request = join_request_from_flow(flow, intercept_settings)
46
+ joined_request = join_request_from_flow(flow, intercept_settings=intercept_settings)
47
47
 
48
48
  Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Uploading{bcolors.ENDC} test results for {joined_request.proxy_request.url()}")
49
49
 
@@ -3,6 +3,7 @@ AFTER_RECORD = 'handle_after_record'
3
3
  AFTER_REPLAY = 'handle_after_replay'
4
4
  AFTER_TEST = 'handle_after_test'
5
5
  BEFORE_MOCK = 'handle_before_mock'
6
+ BEFORE_MIGRATE = 'handle_before_migrate'
6
7
  BEFORE_RECORD = 'handle_before_record'
7
8
  BEFORE_REPLAY = 'handle_before_replay'
8
9
  BEFORE_REQUEST = 'handle_before_request'