stoobly-agent 1.4.2__py3-none-any.whl → 1.5.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/cli/helpers/handle_mock_service.py +6 -2
- stoobly_agent/app/cli/helpers/request_facade.py +5 -1
- stoobly_agent/app/cli/scaffold/constants.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -0
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
- stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
- stoobly_agent/app/cli/scaffold_cli.py +5 -5
- stoobly_agent/app/proxy/context.py +4 -0
- stoobly_agent/app/proxy/handle_mock_service.py +81 -54
- stoobly_agent/app/proxy/handle_record_service.py +15 -3
- stoobly_agent/app/proxy/handle_replay_service.py +44 -18
- stoobly_agent/app/proxy/handle_test_service.py +75 -16
- stoobly_agent/app/proxy/intercept_handler.py +11 -16
- stoobly_agent/app/proxy/intercept_settings.py +17 -4
- stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
- stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py +78 -14
- stoobly_agent/app/proxy/mock/eval_request_service.py +2 -2
- stoobly_agent/app/proxy/record/join_request_service.py +7 -8
- stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
- stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
- stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
- stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
- stoobly_agent/app/proxy/utils/response_handler.py +0 -2
- stoobly_agent/app/proxy/utils/rewrite.py +72 -0
- stoobly_agent/app/settings/constants/request_component.py +4 -1
- stoobly_agent/cli.py +35 -28
- stoobly_agent/config/constants/intercept_policy.py +2 -0
- stoobly_agent/config/constants/mock_policy.py +4 -2
- stoobly_agent/config/constants/record_policy.py +4 -2
- stoobly_agent/config/constants/replay_policy.py +4 -2
- stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
- stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
- stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
- stoobly_agent/test/cli/mock_test.py +53 -29
- stoobly_agent/test/cli/record_test.py +67 -0
- stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.0.dist-info}/LICENSE +1 -1
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.0.dist-info}/METADATA +7 -12
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.0.dist-info}/RECORD +62 -58
- /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.0.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.0.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
COMMAND = 'stoobly-agent'
|
2
|
-
VERSION = '1.
|
2
|
+
VERSION = '1.5.0'
|
@@ -6,9 +6,13 @@ from stoobly_agent.app.proxy.record.response_string import ResponseString
|
|
6
6
|
|
7
7
|
RAW_FORMAT = 'raw'
|
8
8
|
|
9
|
-
def print_raw_response(response: requests.Response):
|
9
|
+
def print_raw_response(response: requests.Response, file_path = None):
|
10
10
|
mitmproxy_response = PythonResponseAdapterFactory(response).mitmproxy_response()
|
11
11
|
facade = MitmproxyResponseFacade(mitmproxy_response)
|
12
12
|
response_string = ResponseString(facade, None)
|
13
13
|
|
14
|
-
|
14
|
+
if not file_path:
|
15
|
+
print(response_string.get().decode(), end="")
|
16
|
+
else:
|
17
|
+
with open(file_path, 'w') as fp:
|
18
|
+
fp.write(response_string.get().decode())
|
@@ -53,10 +53,14 @@ class RequestFacade(ReplayFacade):
|
|
53
53
|
def replay(self, request_key: str, cli_options: ReplayCliOptions):
|
54
54
|
replay_context = self.__build_replay_context(request_key)
|
55
55
|
replay_options = {
|
56
|
-
'mode': mode.
|
56
|
+
'mode': mode.REPLAY,
|
57
57
|
**self.__common_replay_options(request_key),
|
58
58
|
**self.common_replay_cli_options(cli_options)
|
59
59
|
}
|
60
|
+
|
61
|
+
if cli_options.get('record'):
|
62
|
+
replay_options['response_mode'] = mode.RECORD
|
63
|
+
|
60
64
|
trace_context = replay_options.get('trace_context')
|
61
65
|
|
62
66
|
return self.__replay(replay_context, trace_context, replay_options)
|
@@ -14,8 +14,8 @@ CONFIG_FILE = '.config.yml'
|
|
14
14
|
CONTEXT_DIR_ENV = 'CONTEXT_DIR'
|
15
15
|
DOCKER_NAMESPACE = 'docker'
|
16
16
|
ENV_FILE = '.env'
|
17
|
-
FIXTURES_FOLDER_NAME = 'fixtures'
|
18
17
|
NAMESERVERS_FILE = '.nameservers'
|
18
|
+
PUBLIC_FOLDER_NAME = 'public'
|
19
19
|
SERVICE_DETACHED = '${SERVICE_DETACHED}'
|
20
20
|
SERVICE_DETACHED_ENV = 'SERVICE_DETACHED'
|
21
21
|
SERVICE_HOSTNAME = '${SERVICE_HOSTNAME}'
|
@@ -26,6 +26,7 @@ class MockDecorator():
|
|
26
26
|
'--lifecycle-hooks-path', 'lifecycle_hooks.py',
|
27
27
|
'--proxy-mode', SERVICE_PROXY_MODE,
|
28
28
|
'--proxy-port', f"{SERVICE_PORT}",
|
29
|
+
'--public-directory-path', 'public',
|
29
30
|
'--response-fixtures-path', 'fixtures.yml',
|
30
31
|
'--ssl-insecure'
|
31
32
|
]
|
@@ -10,7 +10,7 @@ import yaml
|
|
10
10
|
from docker.models.containers import Container
|
11
11
|
|
12
12
|
from stoobly_agent.app.cli.scaffold.constants import (
|
13
|
-
|
13
|
+
PUBLIC_FOLDER_NAME,
|
14
14
|
STOOBLY_DATA_DIR,
|
15
15
|
VIRTUAL_HOST_ENV,
|
16
16
|
VIRTUAL_PORT_ENV,
|
@@ -40,8 +40,8 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
40
40
|
)
|
41
41
|
|
42
42
|
@property
|
43
|
-
def
|
44
|
-
return os.path.join(self.workflow_path,
|
43
|
+
def public_dir_path(self):
|
44
|
+
return os.path.join(self.workflow_path, PUBLIC_FOLDER_NAME)
|
45
45
|
|
46
46
|
@property
|
47
47
|
def workflow_path(self):
|
@@ -124,14 +124,14 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
124
124
|
if ('200 OK' not in logs) and ('499' not in logs):
|
125
125
|
raise ScaffoldValidateException(f"Error reaching {url} from inside Docker network")
|
126
126
|
|
127
|
-
# Check
|
128
|
-
def
|
127
|
+
# Check public folder exists in container
|
128
|
+
def validate_public_folder(self, container: Container):
|
129
129
|
|
130
130
|
if self.workflow_name == WORKFLOW_RECORD_TYPE:
|
131
|
-
print(f"Skipping validating
|
131
|
+
print(f"Skipping validating public folder in workflow: {self.workflow_name}, container: {container.name}")
|
132
132
|
return
|
133
133
|
|
134
|
-
print(f"Validating
|
134
|
+
print(f"Validating public folder in container: {container.name}")
|
135
135
|
|
136
136
|
data_dir_mounted = False
|
137
137
|
volume_mounts = container.attrs['Mounts']
|
@@ -145,21 +145,21 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
145
145
|
|
146
146
|
# Only the running proxy containers will be checkable
|
147
147
|
if container.status == 'exited':
|
148
|
-
print(f"Skipping validating
|
148
|
+
print(f"Skipping validating public folder contents because container is exited: {container.name}")
|
149
149
|
return
|
150
150
|
|
151
|
-
# Check contents of
|
152
|
-
|
153
|
-
exec_result = container.exec_run(f"ls -A {
|
151
|
+
# Check contents of public folder to confirm it's shared
|
152
|
+
public_folder_path = f"{PUBLIC_FOLDER_NAME}"
|
153
|
+
exec_result = container.exec_run(f"ls -A {public_folder_path}")
|
154
154
|
output = exec_result.output
|
155
155
|
|
156
|
-
|
157
|
-
if
|
158
|
-
|
159
|
-
|
156
|
+
public_folder_contents_container = output.decode('ascii').split('\n')
|
157
|
+
if public_folder_contents_container[-1] == '':
|
158
|
+
public_folder_contents_container.pop()
|
159
|
+
public_folder_contents_scaffold = os.listdir(self.public_dir_path)
|
160
160
|
|
161
|
-
if Counter(
|
162
|
-
raise ScaffoldValidateException(f"
|
161
|
+
if Counter(public_folder_contents_container) != Counter(public_folder_contents_scaffold):
|
162
|
+
raise ScaffoldValidateException(f"public folder was not mounted properly, expected {self.public_dir_path} to exist in container path {public_folder_path}")
|
163
163
|
|
164
164
|
# Note: might not need this if the hostname is reachable and working
|
165
165
|
def proxy_environment_variables_exist(self, container: Container) -> None:
|
@@ -191,7 +191,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
191
191
|
raise ScaffoldValidateException(f"Container attributes are missing for: {container.name}")
|
192
192
|
|
193
193
|
if not self.service_config.detached:
|
194
|
-
self.
|
194
|
+
self.validate_public_folder(service_proxy_container)
|
195
195
|
|
196
196
|
self.proxy_environment_variables_exist(service_proxy_container)
|
197
197
|
|
@@ -215,7 +215,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
215
215
|
|
216
216
|
# Service init containers have a mounted dist folder unlike the core init container
|
217
217
|
init_container = self.docker_client.containers.get(self.service_docker_compose.init_container_name)
|
218
|
-
self.
|
218
|
+
self.validate_public_folder(init_container)
|
219
219
|
|
220
220
|
if self.service_config.hostname:
|
221
221
|
service_proxy_container = self.docker_client.containers.get(self.service_docker_compose.proxy_container_name)
|
@@ -18,12 +18,12 @@ CUSTOM_FIXTURES = 'fixtures.yml'
|
|
18
18
|
CUSTOM_LIFECYCLE_HOOKS = os.path.join('lifecycle_hooks.py')
|
19
19
|
MAINTAINED_CONFIGURE = os.path.join('bin', '.configure')
|
20
20
|
MAINTAINED_INIT = os.path.join('bin', '.init')
|
21
|
-
|
21
|
+
MAINTAINED_PUBLIC = os.path.join('public', '.gitignore')
|
22
22
|
|
23
23
|
MOCK_WORKFLOW_MAINTAINED_FILES = [
|
24
24
|
MAINTAINED_CONFIGURE,
|
25
25
|
MAINTAINED_INIT,
|
26
|
-
|
26
|
+
MAINTAINED_PUBLIC
|
27
27
|
]
|
28
28
|
|
29
29
|
MOCK_WORKFLOW_CUSTOM_FILES = [
|
@@ -49,7 +49,7 @@ RECORD_WORKFLOW_CUSTOM_FILES = [
|
|
49
49
|
TEST_WORKFLOW_MAINTAINED_FILES = [
|
50
50
|
MAINTAINED_CONFIGURE,
|
51
51
|
MAINTAINED_INIT,
|
52
|
-
|
52
|
+
MAINTAINED_PUBLIC
|
53
53
|
]
|
54
54
|
|
55
55
|
TEST_WORKFLOW_CUSTOM_FILES = [
|
@@ -1,7 +1,7 @@
|
|
1
1
|
from ..constants import WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
|
2
2
|
from ..docker.workflow.builder import WorkflowBuilder
|
3
3
|
from .constants import (
|
4
|
-
CUSTOM_CONFIGURE, CUSTOM_INIT, MAINTAINED_CONFIGURE,
|
4
|
+
CUSTOM_CONFIGURE, CUSTOM_INIT, MAINTAINED_CONFIGURE, MAINTAINED_PUBLIC, MOCK_WORKFLOW_CUSTOM_FILES, MOCK_WORKFLOW_MAINTAINED_FILES, RECORD_WORKFLOW_CUSTOM_FILES, RECORD_WORKFLOW_MAINTAINED_FILES, TEST_WORKFLOW_CUSTOM_FILES, TEST_WORKFLOW_MAINTAINED_FILES
|
5
5
|
)
|
6
6
|
|
7
7
|
def custom_files(workflow: str, workflow_builder: WorkflowBuilder):
|
@@ -21,8 +21,8 @@ def custom_files(workflow: str, workflow_builder: WorkflowBuilder):
|
|
21
21
|
|
22
22
|
# Fixtures are only relevant if the workflow is mock/test and if the service has a hostname
|
23
23
|
if not workflow_builder.config.hostname:
|
24
|
-
if
|
25
|
-
files.remove(
|
24
|
+
if MAINTAINED_PUBLIC in files:
|
25
|
+
files.remove(MAINTAINED_PUBLIC)
|
26
26
|
|
27
27
|
return files
|
28
28
|
|
@@ -40,7 +40,7 @@ def maintained_files(workflow: str, workflow_builder: WorkflowBuilder):
|
|
40
40
|
files.append(MAINTAINED_CONFIGURE)
|
41
41
|
|
42
42
|
if not workflow_builder.config.hostname:
|
43
|
-
if
|
44
|
-
files.remove(
|
43
|
+
if MAINTAINED_PUBLIC in files:
|
44
|
+
files.remove(MAINTAINED_PUBLIC)
|
45
45
|
|
46
46
|
return files
|
@@ -10,11 +10,4 @@ url="$scheme://$hostname"
|
|
10
10
|
|
11
11
|
if [ "$scheme" = 'http' -a "$port" != '80' ] || [ "$scheme" = 'https' -a "$port" != '443' ]; then
|
12
12
|
url="$url:$port"
|
13
|
-
fi
|
14
|
-
|
15
|
-
# Match Rules
|
16
|
-
echo "Configuring match rules"
|
17
|
-
stoobly-agent config match set \
|
18
|
-
--method GET --method POST --method OPTIONS --method PUT --method DELETE \
|
19
|
-
--mode mock \
|
20
|
-
--pattern ".*?"
|
13
|
+
fi
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# The following example matches requests for GET /users/d+ (e.g. /users/1) with the contents of user-1.json
|
2
|
-
# Assumes that 'user-1.json' is created in
|
2
|
+
# Assumes that 'user-1.json' is created in a 'fixtures' folder in the same directory as this file
|
3
3
|
#GET:
|
4
4
|
# /users/d+:
|
5
5
|
# path: ./fixtures/user-1.json
|
@@ -10,11 +10,4 @@ url="$scheme://$hostname"
|
|
10
10
|
|
11
11
|
if [ "$scheme" = 'http' -a "$port" != '80' ] || [ "$scheme" = 'https' -a "$port" != '443' ]; then
|
12
12
|
url="$url:$port"
|
13
|
-
fi
|
14
|
-
|
15
|
-
# Match
|
16
|
-
echo "Configuring match rules"
|
17
|
-
stoobly-agent config match set \
|
18
|
-
--method GET --method POST --method OPTIONS --method PUT --method DELETE \
|
19
|
-
--mode mock \
|
20
|
-
--pattern ".*?"
|
13
|
+
fi
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# The following example matches requests for GET /users/d+ (e.g. /users/1) with the contents of user-1.json
|
2
|
-
# Assumes that 'user-1.json' is created in
|
2
|
+
# Assumes that 'user-1.json' is created in a 'fixtures' folder in the same directory as this file
|
3
3
|
#GET:
|
4
4
|
# /users/d+:
|
5
5
|
# path: ./fixtures/user-1.json
|
@@ -6,7 +6,7 @@ from stoobly_agent.lib.logger import Logger
|
|
6
6
|
|
7
7
|
from .app import App
|
8
8
|
from .config import Config
|
9
|
-
from .constants import BIN_FOLDER_NAME, COMPOSE_TEMPLATE, CONFIG_FILE, ENV_FILE,
|
9
|
+
from .constants import BIN_FOLDER_NAME, COMPOSE_TEMPLATE, CONFIG_FILE, ENV_FILE, PUBLIC_FOLDER_NAME
|
10
10
|
from .docker.constants import DOCKER_COMPOSE_CUSTOM
|
11
11
|
from .service_command import ServiceCommand
|
12
12
|
|
@@ -88,8 +88,8 @@ class WorkflowCommand(ServiceCommand):
|
|
88
88
|
return services
|
89
89
|
|
90
90
|
@property
|
91
|
-
def
|
92
|
-
return os.path.join(self.workflow_path,
|
91
|
+
def public_dir_path(self):
|
92
|
+
return os.path.join(self.workflow_path, PUBLIC_FOLDER_NAME)
|
93
93
|
|
94
94
|
@property
|
95
95
|
def workflow_config(self):
|
@@ -67,11 +67,11 @@ class WorkflowCreateCommand(WorkflowCommand):
|
|
67
67
|
return
|
68
68
|
|
69
69
|
# Maintained files are files that will always be overwritten
|
70
|
-
maintained_workflow_files = maintained_files(self.workflow_name, workflow_builder)
|
70
|
+
maintained_workflow_files = maintained_files(template or self.workflow_name, workflow_builder)
|
71
71
|
self.copy_files(templates_path, maintained_workflow_files, self.workflow_path)
|
72
72
|
|
73
73
|
# Custom files are files that may be modified by the user
|
74
|
-
custom_workflow_files = custom_files(self.workflow_name, workflow_builder)
|
74
|
+
custom_workflow_files = custom_files(template or self.workflow_name, workflow_builder)
|
75
75
|
self.copy_files_no_replace(templates_path, custom_workflow_files, self.workflow_path)
|
76
76
|
|
77
77
|
def __write_docker_compose_file(self, **kwargs: BuildOptions):
|
@@ -140,19 +140,19 @@ def mkcert(**kwargs):
|
|
140
140
|
help="Scaffold a service",
|
141
141
|
)
|
142
142
|
@click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
|
143
|
-
@click.option('--detached', is_flag=True)
|
143
|
+
@click.option('--detached', is_flag=True, help='Use isolated and non-persistent context directory.')
|
144
144
|
@click.option('--env', multiple=True, help='Specify an environment variable.')
|
145
145
|
@click.option('--force', is_flag=True, help='Overwrite maintained scaffolded service files.')
|
146
|
-
@click.option('--hostname')
|
147
|
-
@click.option('--port')
|
148
|
-
@click.option('--priority', default=
|
146
|
+
@click.option('--hostname', help='Service hostname.')
|
147
|
+
@click.option('--port', help='Service port.')
|
148
|
+
@click.option('--priority', default=5, type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
|
149
149
|
@click.option('--proxy-mode', help='''
|
150
150
|
Proxy mode can be "regular", "transparent", "socks5",
|
151
151
|
"reverse:SPEC", or "upstream:SPEC". For reverse and
|
152
152
|
upstream proxy modes, SPEC is host specification in
|
153
153
|
the form of "http[s]://host[:port]".
|
154
154
|
''')
|
155
|
-
@click.option('--scheme', type=click.Choice(['http', 'https']))
|
155
|
+
@click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
|
156
156
|
@click.option('--workflow', multiple=True, type=click.Choice([WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]), help='Include pre-defined workflows.')
|
157
157
|
@click.argument('service_name')
|
158
158
|
def create(**kwargs):
|
@@ -19,6 +19,7 @@ from .mock.eval_request_service import inject_eval_request
|
|
19
19
|
from .utils.allowed_request_service import get_active_mode_policy
|
20
20
|
from .utils.request_handler import reverse_proxy
|
21
21
|
from .utils.response_handler import bad_request, pass_on
|
22
|
+
from .utils.rewrite import rewrite_request, rewrite_response
|
22
23
|
|
23
24
|
LOG_ID = 'Mock'
|
24
25
|
|
@@ -26,77 +27,77 @@ class MockOptions(TypedDict):
|
|
26
27
|
failure: Callable
|
27
28
|
ignored_components: list
|
28
29
|
infer: bool
|
30
|
+
no_rewrite: bool
|
29
31
|
success: Callable
|
30
32
|
|
33
|
+
def handle_request_mock_generic_without_rewrite(context: MockContext, **options: MockOptions):
|
34
|
+
options['no_rewrite'] = True
|
35
|
+
handle_request_mock_generic(context, **options)
|
36
|
+
|
31
37
|
###
|
32
38
|
#
|
39
|
+
# 1. Rewrites mock request by default
|
40
|
+
# 2. BEFORE_MOCK gets triggered
|
41
|
+
# 3. AFTER_MOCK gets triggered
|
42
|
+
#
|
33
43
|
# @param request [mitmproxy.http.Request]
|
34
44
|
# @param settings [Dict]
|
35
45
|
#
|
36
46
|
def handle_request_mock_generic(context: MockContext, **options: MockOptions):
|
37
|
-
__mock_hook(lifecycle_hooks.BEFORE_MOCK, context)
|
38
|
-
|
39
47
|
intercept_settings = context.intercept_settings
|
40
48
|
request: MitmproxyRequest = context.flow.request
|
41
|
-
request_model = RequestModel(intercept_settings.settings)
|
42
|
-
|
43
|
-
policy = get_active_mode_policy(request, intercept_settings)
|
44
|
-
|
45
|
-
rewrite_rules = intercept_settings.mock_rewrite_rules
|
46
|
-
if len(rewrite_rules) > 0:
|
47
|
-
# Rewrite request with paramter rules for mock
|
48
|
-
request: MitmproxyRequest = context.flow.request
|
49
|
-
request_facade = MitmproxyRequestFacade(request)
|
50
|
-
request_facade.with_parameter_rules(rewrite_rules).with_url_rules(rewrite_rules).rewrite()
|
51
|
-
|
52
|
-
# If ignore rules are set, then ignore specified request parameters
|
53
|
-
ignore_rules = intercept_settings.ignore_rules
|
54
|
-
if len(ignore_rules) > 0:
|
55
|
-
request_facade = MitmproxyRequestFacade(request)
|
56
|
-
_ignore_rules = request_facade.select_parameter_rules(ignore_rules)
|
57
|
-
ignored_components = rewrite_rules_to_ignored_components(_ignore_rules)
|
58
|
-
options['ignored_components'] += ignored_components if 'ignored_components' in options else ignored_components
|
59
|
-
|
60
49
|
handle_success = options['success'] if 'success' in options and callable(options['success']) else None
|
61
50
|
handle_failure = options['failure'] if 'failure' in options and callable(options['failure']) else None
|
62
|
-
|
63
|
-
|
64
|
-
|
51
|
+
|
52
|
+
policy = get_active_mode_policy(request, intercept_settings)
|
65
53
|
if policy == mock_policy.NONE:
|
66
54
|
if handle_failure:
|
67
55
|
res = handle_failure(context)
|
68
|
-
elif policy == mock_policy.ALL:
|
69
|
-
res = eval_request_with_retry(context, eval_request, **options)
|
70
|
-
|
71
|
-
context.with_response(res)
|
72
|
-
|
73
|
-
if handle_success:
|
74
|
-
# TODO: rewrite response, see #332
|
75
|
-
res = handle_success(context) or res
|
76
|
-
elif policy == mock_policy.FOUND:
|
77
|
-
res = eval_request_with_retry(context, eval_request, **options)
|
78
|
-
|
79
|
-
context.with_response(res)
|
80
|
-
|
81
|
-
if res.status_code in [custom_response_codes.NOT_FOUND, custom_response_codes.IGNORE_COMPONENTS]:
|
82
|
-
if handle_failure:
|
83
|
-
try:
|
84
|
-
res = handle_failure(context)
|
85
|
-
except RuntimeError:
|
86
|
-
# Do nothing, return custom error response
|
87
|
-
pass
|
88
|
-
else:
|
89
|
-
if handle_success:
|
90
|
-
# TODO: rewrite response, see #332
|
91
|
-
res = handle_success(context) or res
|
92
56
|
else:
|
93
|
-
|
94
|
-
context
|
95
|
-
|
96
|
-
|
97
|
-
|
57
|
+
if not options.get('no_rewrite'):
|
58
|
+
__rewrite_request(context)
|
59
|
+
|
60
|
+
__mock_hook(lifecycle_hooks.BEFORE_MOCK, context)
|
61
|
+
|
62
|
+
# If ignore rules are set, then ignore specified request parameters
|
63
|
+
ignore_rules = intercept_settings.ignore_rules
|
64
|
+
if len(ignore_rules) > 0:
|
65
|
+
request_facade = MitmproxyRequestFacade(request)
|
66
|
+
_ignore_rules = request_facade.select_parameter_rules(ignore_rules)
|
67
|
+
ignored_components = rewrite_rules_to_ignored_components(_ignore_rules)
|
68
|
+
options['ignored_components'] += ignored_components if 'ignored_components' in options else ignored_components
|
69
|
+
|
70
|
+
request_model = RequestModel(intercept_settings.settings)
|
71
|
+
eval_request = inject_eval_request(request_model, intercept_settings)
|
72
|
+
|
73
|
+
if policy == mock_policy.ALL:
|
74
|
+
res = eval_request_with_retry(context, eval_request, **options)
|
75
|
+
|
76
|
+
context.with_response(res)
|
98
77
|
|
99
|
-
|
78
|
+
if handle_success:
|
79
|
+
res = handle_success(context) or res
|
80
|
+
elif policy == mock_policy.FOUND:
|
81
|
+
res = eval_request_with_retry(context, eval_request, **options)
|
82
|
+
|
83
|
+
context.with_response(res)
|
84
|
+
|
85
|
+
if res.status_code in [custom_response_codes.NOT_FOUND, custom_response_codes.IGNORE_COMPONENTS]:
|
86
|
+
if handle_failure:
|
87
|
+
try:
|
88
|
+
res = handle_failure(context)
|
89
|
+
except RuntimeError:
|
90
|
+
# Do nothing, return custom error response
|
91
|
+
pass
|
92
|
+
else:
|
93
|
+
if handle_success:
|
94
|
+
res = handle_success(context) or res
|
95
|
+
else:
|
96
|
+
return bad_request(
|
97
|
+
context.flow,
|
98
|
+
"Valid env MOCK_POLICY: %s, Got: %s" %
|
99
|
+
([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
|
100
|
+
)
|
100
101
|
|
101
102
|
return pass_on(context.flow, res)
|
102
103
|
|
@@ -130,6 +131,11 @@ def handle_request_mock(context: MockContext):
|
|
130
131
|
success=lambda context: __handle_mock_success(context)
|
131
132
|
)
|
132
133
|
|
134
|
+
###
|
135
|
+
#
|
136
|
+
# 1. Rewrites mock response (if mock found)
|
137
|
+
# 2. AFTER_MOCK gets triggered (if mock found)
|
138
|
+
#
|
133
139
|
def handle_response_mock(context: MockContext):
|
134
140
|
response = context.flow.response
|
135
141
|
request_key = response.headers.get(custom_headers.MOCK_REQUEST_KEY)
|
@@ -138,6 +144,9 @@ def handle_response_mock(context: MockContext):
|
|
138
144
|
request = context.flow.request
|
139
145
|
Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Mocked{bcolors.ENDC} {request.url} -> {request_key}")
|
140
146
|
|
147
|
+
__rewrite_response(context)
|
148
|
+
__mock_hook(lifecycle_hooks.AFTER_MOCK, context)
|
149
|
+
|
141
150
|
def __handle_mock_success(context: MockContext) -> None:
|
142
151
|
if os.environ.get(env_vars.AGENT_SIMULATE_LATENCY):
|
143
152
|
response = context.response
|
@@ -160,6 +169,24 @@ def __handle_mock_failure(context: MockContext) -> None:
|
|
160
169
|
|
161
170
|
reverse_proxy(req, upstream_url, {})
|
162
171
|
|
172
|
+
def __rewrite_request(context: MockContext):
|
173
|
+
# Rewrite request with paramter rules for mock
|
174
|
+
|
175
|
+
intercept_settings = context.intercept_settings
|
176
|
+
rewrite_rules = intercept_settings.mock_rewrite_rules
|
177
|
+
|
178
|
+
if len(rewrite_rules) > 0:
|
179
|
+
rewrite_request(context.flow, rewrite_rules)
|
180
|
+
|
181
|
+
def __rewrite_response(context: MockContext):
|
182
|
+
# Rewrite request with paramter rules for mock
|
183
|
+
|
184
|
+
intercept_settings = context.intercept_settings
|
185
|
+
rewrite_rules = intercept_settings.mock_rewrite_rules
|
186
|
+
|
187
|
+
if len(rewrite_rules) > 0:
|
188
|
+
rewrite_response(context.flow, rewrite_rules)
|
189
|
+
|
163
190
|
###
|
164
191
|
#
|
165
192
|
# Try to simulate expected response latency
|
@@ -2,6 +2,7 @@ import os
|
|
2
2
|
import pdb
|
3
3
|
import threading
|
4
4
|
|
5
|
+
from copy import deepcopy
|
5
6
|
from mitmproxy.http import Request as MitmproxyRequest
|
6
7
|
|
7
8
|
from stoobly_agent.app.settings.constants.mode import TEST
|
@@ -18,15 +19,20 @@ from .record.overwrite_scenario_service import overwrite_scenario
|
|
18
19
|
from .record.upload_request_service import inject_upload_request
|
19
20
|
from .utils.allowed_request_service import get_active_mode_policy
|
20
21
|
from .utils.response_handler import bad_request, disable_transfer_encoding
|
22
|
+
from .utils.rewrite import rewrite_request_response
|
21
23
|
|
22
24
|
LOG_ID = 'Record'
|
23
25
|
|
26
|
+
###
|
27
|
+
#
|
28
|
+
# 1. Rewrites a copy of request and response
|
29
|
+
# 2. BEFORE_RECORD gets triggered
|
30
|
+
# 3. AFTER_RECORD gets triggered
|
31
|
+
#
|
24
32
|
def handle_response_record(context: RecordContext):
|
25
33
|
flow = context.flow
|
26
34
|
disable_transfer_encoding(flow.response)
|
27
35
|
|
28
|
-
__record_hook(lifecycle_hooks.BEFORE_RECORD, context)
|
29
|
-
|
30
36
|
intercept_settings = context.intercept_settings
|
31
37
|
request: MitmproxyRequest = flow.request
|
32
38
|
request_model = RequestModel(intercept_settings.settings)
|
@@ -60,11 +66,17 @@ def handle_response_record(context: RecordContext):
|
|
60
66
|
|
61
67
|
def __record_handler(context: RecordContext, request_model: RequestModel):
|
62
68
|
flow = context.flow
|
69
|
+
flow_copy = deepcopy(flow)
|
63
70
|
intercept_settings = context.intercept_settings
|
64
71
|
|
65
|
-
|
72
|
+
context.flow = flow_copy # Deep copy flow to prevent response modifications from persisting
|
73
|
+
rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
|
74
|
+
__record_hook(lifecycle_hooks.BEFORE_RECORD, context)
|
75
|
+
|
76
|
+
inject_upload_request(request_model, intercept_settings)(flow_copy)
|
66
77
|
|
67
78
|
__record_hook(lifecycle_hooks.AFTER_RECORD, context)
|
79
|
+
context.flow = flow # Reset flow
|
68
80
|
|
69
81
|
def __record_request(context: RecordContext, request_model: RequestModel):
|
70
82
|
if os.environ.get(ENV) == TEST:
|
@@ -1,46 +1,72 @@
|
|
1
1
|
import pdb
|
2
2
|
|
3
3
|
from mitmproxy.http import Request as MitmproxyRequest
|
4
|
+
from typing import TypedDict
|
4
5
|
|
5
6
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
6
|
-
from stoobly_agent.app.proxy.mitmproxy.request_facade import MitmproxyRequestFacade
|
7
7
|
from stoobly_agent.app.proxy.replay.context import ReplayContext
|
8
8
|
from stoobly_agent.config.constants import lifecycle_hooks, replay_policy
|
9
9
|
|
10
10
|
from .utils.allowed_request_service import get_active_mode_policy
|
11
|
+
from .utils.rewrite import rewrite_request, rewrite_response
|
11
12
|
|
12
13
|
LOG_ID = 'HandleReplay'
|
13
14
|
|
14
|
-
|
15
|
-
|
15
|
+
class ReplayOptions(TypedDict):
|
16
|
+
no_rewrite: bool
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
###
|
19
|
+
#
|
20
|
+
# 1. Rewrites replay request by default
|
21
|
+
# 2. BEFORE_REPLAY gets triggered
|
22
|
+
#
|
23
|
+
def handle_request_replay_without_rewrite(replay_context: ReplayContext):
|
24
|
+
options = { 'no_rewrite': True }
|
25
|
+
handle_request_replay(replay_context, **options)
|
19
26
|
|
20
|
-
|
21
|
-
|
22
|
-
|
27
|
+
def handle_request_replay(replay_context: ReplayContext, **options: ReplayOptions):
|
28
|
+
request: MitmproxyRequest = replay_context.flow.request
|
29
|
+
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
30
|
+
|
31
|
+
policy = get_active_mode_policy(request, intercept_settings)
|
32
|
+
if policy != replay_policy.NONE:
|
33
|
+
if not options.get('no_rewrite'):
|
34
|
+
__rewrite_request(replay_context)
|
23
35
|
|
36
|
+
__replay_hook(lifecycle_hooks.BEFORE_REPLAY, replay_context)
|
37
|
+
|
38
|
+
###
|
39
|
+
#
|
40
|
+
# 1. Rewrites replay response
|
41
|
+
# 2. AFTER_REPLAY gets triggered
|
42
|
+
#
|
24
43
|
def handle_response_replay(replay_context: ReplayContext):
|
44
|
+
__rewrite_response(replay_context)
|
25
45
|
__replay_hook(lifecycle_hooks.AFTER_REPLAY, replay_context)
|
26
46
|
|
27
|
-
|
47
|
+
def __replay_hook(hook: str, replay_context: ReplayContext):
|
48
|
+
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
49
|
+
|
50
|
+
lifecycle_hooks_module = intercept_settings.lifecycle_hooks
|
51
|
+
if hook in lifecycle_hooks_module:
|
52
|
+
lifecycle_hooks_module[hook](replay_context)
|
28
53
|
|
29
|
-
def
|
54
|
+
def __rewrite_request(replay_context: ReplayContext):
|
30
55
|
"""
|
31
56
|
Before replaying a request, see if the request needs to be rewritten
|
32
57
|
"""
|
33
58
|
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
34
|
-
rewrite_rules = intercept_settings.
|
59
|
+
rewrite_rules = intercept_settings.replay_rewrite_rules
|
35
60
|
|
36
61
|
if len(rewrite_rules) > 0:
|
37
|
-
|
38
|
-
request_facade = MitmproxyRequestFacade(request)
|
39
|
-
request_facade.with_parameter_rules(rewrite_rules).with_url_rules(rewrite_rules).rewrite()
|
62
|
+
rewrite_request(replay_context.flow, rewrite_rules)
|
40
63
|
|
41
|
-
def
|
64
|
+
def __rewrite_response(replay_context: ReplayContext):
|
65
|
+
"""
|
66
|
+
After replaying a request, see if the request needs to be rewritten
|
67
|
+
"""
|
42
68
|
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
69
|
+
rewrite_rules = intercept_settings.replay_rewrite_rules
|
43
70
|
|
44
|
-
|
45
|
-
|
46
|
-
lifecycle_hooks_module[hook](replay_context)
|
71
|
+
if len(rewrite_rules) > 0:
|
72
|
+
rewrite_response(replay_context.flow, rewrite_rules)
|