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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/app/api/headers_controller.py +1 -1
- stoobly_agent/app/api/query_params_controller.py +4 -4
- stoobly_agent/app/cli/snapshot_cli.py +57 -0
- stoobly_agent/app/cli/types/snapshot_migration.py +61 -0
- stoobly_agent/app/models/adapters/orm/request/mitmproxy_adapter.py +1 -1
- stoobly_agent/app/models/adapters/python/request/__init__.py +2 -2
- stoobly_agent/app/models/adapters/python/request/mitmproxy_adapter.py +1 -1
- stoobly_agent/app/models/adapters/python/request/raw_adapter.py +2 -2
- stoobly_agent/app/models/factories/resource/local_db/header_adapter.py +28 -8
- stoobly_agent/app/models/factories/resource/local_db/helpers/log.py +7 -0
- stoobly_agent/app/models/factories/resource/local_db/helpers/log_event.py +10 -0
- stoobly_agent/app/models/factories/resource/local_db/helpers/request_snapshot.py +20 -0
- stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +8 -1
- stoobly_agent/app/models/factories/resource/local_db/helpers/snapshot.py +1 -1
- stoobly_agent/app/models/factories/resource/local_db/local_db_adapter.py +3 -0
- stoobly_agent/app/models/factories/resource/local_db/query_param_adapter.py +2 -2
- stoobly_agent/app/models/factories/resource/local_db/request_adapter.py +22 -3
- stoobly_agent/app/models/helpers/apply.py +5 -4
- stoobly_agent/app/models/helpers/create_request_params_service.py +4 -2
- stoobly_agent/app/proxy/intercept_handler.py +6 -2
- stoobly_agent/app/proxy/record/join_request_service.py +22 -10
- stoobly_agent/app/proxy/record/proxy_request.py +9 -0
- stoobly_agent/app/proxy/record/request_string.py +4 -1
- stoobly_agent/app/proxy/record/upload_request_service.py +7 -5
- stoobly_agent/app/proxy/test/helpers/upload_test_service.py +1 -1
- stoobly_agent/config/constants/lifecycle_hooks.py +1 -0
- stoobly_agent/lib/orm/transformers/orm_to_request_transformer.py +74 -2
- stoobly_agent/public/13-es2015.6d3a4fd76d46bfa5f200.js +1 -0
- stoobly_agent/public/13-es5.6d3a4fd76d46bfa5f200.js +1 -0
- stoobly_agent/public/{18-es2015.503207073756a9c8211a.js → 18-es2015.d07dd29def7e2574c5b7.js} +1 -1
- stoobly_agent/public/{18-es5.503207073756a9c8211a.js → 18-es5.d07dd29def7e2574c5b7.js} +1 -1
- stoobly_agent/public/35-es2015.4ffe6f7a196ed1a87fc7.js +1 -0
- stoobly_agent/public/35-es5.4ffe6f7a196ed1a87fc7.js +1 -0
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/{main-es2015.d682619f3d6d53d64c6a.js → main-es2015.ce00115b0520fa030f01.js} +1 -1
- stoobly_agent/public/{main-es5.d682619f3d6d53d64c6a.js → main-es5.ce00115b0520fa030f01.js} +1 -1
- stoobly_agent/public/runtime-es2015.b13c22b834b51724d30a.js +1 -0
- stoobly_agent/public/runtime-es5.b13c22b834b51724d30a.js +1 -0
- stoobly_agent/test/app/cli/scenario/scenario_snapshot_test.py +1 -1
- stoobly_agent/test/app/cli/snapshot/lifecycle_hooks_migrate.py +10 -0
- stoobly_agent/test/app/cli/snapshot/snapshot_migrate_test.py +181 -0
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/replay/trace_context_test.py +36 -3
- {stoobly_agent-1.8.3.dist-info → stoobly_agent-1.8.5.dist-info}/METADATA +1 -1
- {stoobly_agent-1.8.3.dist-info → stoobly_agent-1.8.5.dist-info}/RECORD +49 -46
- stoobly_agent/public/13-es2015.343b0261a8b3b3f4a1fc.js +0 -1
- stoobly_agent/public/13-es5.343b0261a8b3b3f4a1fc.js +0 -1
- stoobly_agent/public/35-es2015.a23419c9c7bff162a8e3.js +0 -1
- stoobly_agent/public/35-es5.a23419c9c7bff162a8e3.js +0 -1
- stoobly_agent/public/runtime-es2015.7130ec4068d875dc38bd.js +0 -1
- stoobly_agent/public/runtime-es5.7130ec4068d875dc38bd.js +0 -1
- {stoobly_agent-1.8.3.dist-info → stoobly_agent-1.8.5.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.8.3.dist-info → stoobly_agent-1.8.5.dist-info}/WHEEL +0 -0
- {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.
|
2
|
+
VERSION = '1.8.5'
|
@@ -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 =
|
109
|
-
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(
|
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(
|
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(
|
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,
|
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,
|
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.
|
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
|
-
|
22
|
+
name: str = header['name']
|
23
|
+
value = header['value']
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
222
|
-
for
|
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
|
-
|
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,
|
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 =
|
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
|
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,
|
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,
|
33
|
+
flow: MitmproxyHTTPFlow, **options: JoinRequestOptions
|
27
34
|
) -> JoinedRequest:
|
28
|
-
|
29
|
-
|
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,
|
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
|
-
|
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
|
-
|
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'
|