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.
Files changed (62) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/helpers/handle_mock_service.py +6 -2
  3. stoobly_agent/app/cli/helpers/request_facade.py +5 -1
  4. stoobly_agent/app/cli/scaffold/constants.py +1 -1
  5. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -0
  6. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
  7. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  8. stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
  9. stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
  10. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
  11. stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
  12. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
  13. stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
  14. stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
  15. stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
  16. stoobly_agent/app/cli/scaffold_cli.py +5 -5
  17. stoobly_agent/app/proxy/context.py +4 -0
  18. stoobly_agent/app/proxy/handle_mock_service.py +81 -54
  19. stoobly_agent/app/proxy/handle_record_service.py +15 -3
  20. stoobly_agent/app/proxy/handle_replay_service.py +44 -18
  21. stoobly_agent/app/proxy/handle_test_service.py +75 -16
  22. stoobly_agent/app/proxy/intercept_handler.py +11 -16
  23. stoobly_agent/app/proxy/intercept_settings.py +17 -4
  24. stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
  25. stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
  26. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +78 -14
  27. stoobly_agent/app/proxy/mock/eval_request_service.py +2 -2
  28. stoobly_agent/app/proxy/record/join_request_service.py +7 -8
  29. stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
  30. stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
  31. stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
  32. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
  33. stoobly_agent/app/proxy/utils/response_handler.py +0 -2
  34. stoobly_agent/app/proxy/utils/rewrite.py +72 -0
  35. stoobly_agent/app/settings/constants/request_component.py +4 -1
  36. stoobly_agent/cli.py +35 -28
  37. stoobly_agent/config/constants/intercept_policy.py +2 -0
  38. stoobly_agent/config/constants/mock_policy.py +4 -2
  39. stoobly_agent/config/constants/record_policy.py +4 -2
  40. stoobly_agent/config/constants/replay_policy.py +4 -2
  41. stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
  42. stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
  43. stoobly_agent/public/index.html +1 -1
  44. stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
  45. stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
  46. stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
  47. stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
  48. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
  49. stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
  50. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  51. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
  52. stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
  53. stoobly_agent/test/cli/mock_test.py +53 -29
  54. stoobly_agent/test/cli/record_test.py +67 -0
  55. stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
  56. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.0.dist-info}/LICENSE +1 -1
  57. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.0.dist-info}/METADATA +7 -12
  58. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.0.dist-info}/RECORD +62 -58
  59. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
  60. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
  61. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.0.dist-info}/WHEEL +0 -0
  62. {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.4.2'
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
- print(response_string.get().decode(), end="")
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.RECORD if cli_options.get('record') else mode.REPLAY,
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
- FIXTURES_FOLDER_NAME,
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 fixtures_dir_path(self):
44
- return os.path.join(self.workflow_path, FIXTURES_FOLDER_NAME)
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 fixtures folder mounted into container
128
- def validate_fixtures_folder(self, container: Container):
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 fixtures folder in workflow: {self.workflow_name}, container: {container.name}")
131
+ print(f"Skipping validating public folder in workflow: {self.workflow_name}, container: {container.name}")
132
132
  return
133
133
 
134
- print(f"Validating fixtures folder in container: {container.name}")
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 fixtures folder contents because container is exited: {container.name}")
148
+ print(f"Skipping validating public folder contents because container is exited: {container.name}")
149
149
  return
150
150
 
151
- # Check contents of fixtures folder to confirm it's shared
152
- fixtures_folder_path = f"{FIXTURES_FOLDER_NAME}"
153
- exec_result = container.exec_run(f"ls -A {fixtures_folder_path}")
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
- fixtures_folder_contents_container = output.decode('ascii').split('\n')
157
- if fixtures_folder_contents_container[-1] == '':
158
- fixtures_folder_contents_container.pop()
159
- fixtures_folder_contents_scaffold = os.listdir(self.fixtures_dir_path)
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(fixtures_folder_contents_container) != Counter(fixtures_folder_contents_scaffold):
162
- raise ScaffoldValidateException(f"Fixtures was not mounted properly, expected {self.fixtures_dir_path} to exist in container path {fixtures_folder_path}")
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.validate_fixtures_folder(service_proxy_container)
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.validate_fixtures_folder(init_container)
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)
@@ -1,4 +1,4 @@
1
- FROM stoobly/agent:1.4
1
+ FROM stoobly/agent:1.5
2
2
 
3
3
  ARG USER_ID
4
4
 
@@ -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
- MAINTAINED_FIXTURES = os.path.join('fixtures', '.keep')
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
- MAINTAINED_FIXTURES
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
- MAINTAINED_FIXTURES
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, MAINTAINED_FIXTURES, MOCK_WORKFLOW_CUSTOM_FILES, MOCK_WORKFLOW_MAINTAINED_FILES, RECORD_WORKFLOW_CUSTOM_FILES, RECORD_WORKFLOW_MAINTAINED_FILES, TEST_WORKFLOW_CUSTOM_FILES, TEST_WORKFLOW_MAINTAINED_FILES
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 MAINTAINED_FIXTURES in files:
25
- files.remove(MAINTAINED_FIXTURES)
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 MAINTAINED_FIXTURES in files:
44
- files.remove(MAINTAINED_FIXTURES)
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 the 'fixtures' folder
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 the 'fixtures' folder
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, FIXTURES_FOLDER_NAME
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 fixtures_dir_path(self):
92
- return os.path.join(self.workflow_path, FIXTURES_FOLDER_NAME)
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='5.0', help='Determines the service run order.')
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):
@@ -12,6 +12,10 @@ class InterceptContext():
12
12
  def flow(self):
13
13
  return self.__flow
14
14
 
15
+ @flow.setter
16
+ def flow(self, v: MitmproxyHTTPFlow):
17
+ self.__flow = v
18
+
15
19
  @property
16
20
  def intercept_settings(self):
17
21
  return self.__intercept_settings
@@ -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
- eval_request = inject_eval_request(request_model, intercept_settings)
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
- return bad_request(
94
- context.flow,
95
- "Valid env MOCK_POLICY: %s, Got: %s" %
96
- ([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
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
- __mock_hook(lifecycle_hooks.AFTER_MOCK, context)
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
- inject_upload_request(request_model, intercept_settings)(flow)
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
- def handle_request_replay(replay_context: ReplayContext):
15
- __replay_hook(lifecycle_hooks.BEFORE_REPLAY, replay_context)
15
+ class ReplayOptions(TypedDict):
16
+ no_rewrite: bool
16
17
 
17
- request: MitmproxyRequest = replay_context.flow.request
18
- intercept_settings: InterceptSettings = replay_context.intercept_settings
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
- policy = get_active_mode_policy(request, intercept_settings)
21
- if policy != replay_policy.NONE:
22
- __replay_request(replay_context)
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
- # TODO: rewrite response, see #332
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 __replay_request(replay_context: ReplayContext):
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.rewrite_rules
59
+ rewrite_rules = intercept_settings.replay_rewrite_rules
35
60
 
36
61
  if len(rewrite_rules) > 0:
37
- request: MitmproxyRequest = replay_context.flow.request
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 __replay_hook(hook: str, replay_context: ReplayContext):
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
- lifecycle_hooks_module = intercept_settings.lifecycle_hooks
45
- if hook in lifecycle_hooks_module:
46
- lifecycle_hooks_module[hook](replay_context)
71
+ if len(rewrite_rules) > 0:
72
+ rewrite_response(replay_context.flow, rewrite_rules)