stoobly-agent 1.9.11__py3-none-any.whl → 1.10.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/api/__init__.py +4 -20
- stoobly_agent/app/api/configs_controller.py +3 -3
- stoobly_agent/app/cli/decorators/exec.py +1 -1
- stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
- stoobly_agent/app/cli/helpers/shell.py +0 -10
- stoobly_agent/app/cli/intercept_cli.py +40 -7
- stoobly_agent/app/cli/scaffold/app_command.py +4 -0
- stoobly_agent/app/cli/scaffold/app_config.py +21 -3
- stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
- stoobly_agent/app/cli/scaffold/constants.py +14 -0
- stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
- stoobly_agent/app/cli/scaffold/docker/service/builder.py +19 -4
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -18
- stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +24 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
- stoobly_agent/app/cli/scaffold/service_config.py +144 -21
- stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
- stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
- stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
- stoobly_agent/app/cli/scaffold/workflow_namesapce.py +8 -2
- stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
- stoobly_agent/app/cli/scaffold_cli.py +77 -83
- stoobly_agent/app/proxy/handle_record_service.py +12 -3
- stoobly_agent/app/proxy/handle_replay_service.py +14 -2
- stoobly_agent/app/proxy/intercept_settings.py +11 -7
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py +33 -2
- stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
- stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
- stoobly_agent/app/proxy/run.py +3 -28
- stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
- stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
- stoobly_agent/app/proxy/utils/publish_change_service.py +5 -4
- stoobly_agent/app/proxy/utils/strategy.py +16 -0
- stoobly_agent/app/settings/__init__.py +9 -3
- stoobly_agent/app/settings/data_rules.py +25 -1
- stoobly_agent/app/settings/intercept_settings.py +5 -2
- stoobly_agent/app/settings/types/__init__.py +0 -1
- stoobly_agent/app/settings/ui_settings.py +5 -5
- stoobly_agent/cli.py +41 -16
- stoobly_agent/config/constants/custom_headers.py +1 -0
- stoobly_agent/config/constants/env_vars.py +4 -3
- stoobly_agent/config/constants/record_strategy.py +6 -0
- stoobly_agent/config/settings.yml.sample +2 -3
- stoobly_agent/lib/logger.py +15 -5
- stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
- stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +14 -2
- stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/METADATA +2 -1
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/RECORD +78 -62
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/entry_points.txt +0 -0
@@ -9,7 +9,7 @@ from stoobly_agent.app.settings.constants.mode import TEST
|
|
9
9
|
from stoobly_agent.app.models.request_model import RequestModel
|
10
10
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
11
11
|
from stoobly_agent.config.constants.env_vars import ENV
|
12
|
-
from stoobly_agent.config.constants import lifecycle_hooks, record_order, record_policy
|
12
|
+
from stoobly_agent.config.constants import lifecycle_hooks, record_order, record_policy, record_strategy
|
13
13
|
from stoobly_agent.lib.logger import Logger
|
14
14
|
|
15
15
|
from .constants import custom_response_codes
|
@@ -19,8 +19,11 @@ from .record.overwrite_scenario_service import overwrite_scenario
|
|
19
19
|
from .record.upload_request_service import inject_upload_request
|
20
20
|
from .replay.body_parser_service import is_json, is_xml
|
21
21
|
from .utils.allowed_request_service import get_active_mode_policy
|
22
|
+
from .utils.minimize_headers import minimize_headers
|
22
23
|
from .utils.response_handler import bad_request, disable_transfer_encoding
|
23
24
|
from .utils.rewrite import rewrite_request_response
|
25
|
+
from .utils.strategy import get_active_mode_strategy
|
26
|
+
|
24
27
|
|
25
28
|
LOG_ID = 'Record'
|
26
29
|
|
@@ -54,7 +57,7 @@ def handle_response_record(context: RecordContext):
|
|
54
57
|
res = inject_eval_request(request_model, intercept_settings)(request, [])
|
55
58
|
|
56
59
|
if res.status_code != custom_response_codes.NOT_FOUND:
|
57
|
-
__record_request(context
|
60
|
+
__record_request(context, request_model)
|
58
61
|
elif active_record_policy == record_policy.NOT_FOUND:
|
59
62
|
res = inject_eval_request(request_model, intercept_settings)(request, [])
|
60
63
|
|
@@ -74,7 +77,13 @@ def __record_handler(context: RecordContext, request_model: RequestModel):
|
|
74
77
|
intercept_settings = context.intercept_settings
|
75
78
|
|
76
79
|
context.flow = flow_copy # Deep copy flow to prevent response modifications from persisting
|
77
|
-
|
80
|
+
|
81
|
+
active_record_strategy = get_active_mode_strategy(intercept_settings)
|
82
|
+
if active_record_strategy == record_strategy.MINIMAL:
|
83
|
+
minimize_headers(flow_copy)
|
84
|
+
|
85
|
+
rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
|
86
|
+
|
78
87
|
__record_hook(lifecycle_hooks.BEFORE_RECORD, context)
|
79
88
|
|
80
89
|
inject_upload_request(request_model, intercept_settings)(flow_copy)
|
@@ -5,9 +5,10 @@ from typing import TypedDict
|
|
5
5
|
|
6
6
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
7
7
|
from stoobly_agent.app.proxy.replay.context import ReplayContext
|
8
|
-
from stoobly_agent.config.constants import lifecycle_hooks, replay_policy
|
8
|
+
from stoobly_agent.config.constants import lifecycle_hooks, replay_policy, custom_headers, mode, record_strategy
|
9
9
|
|
10
10
|
from .utils.allowed_request_service import get_active_mode_policy
|
11
|
+
from .utils.minimize_headers import minimize_response_headers
|
11
12
|
from .utils.rewrite import rewrite_request, rewrite_response
|
12
13
|
|
13
14
|
LOG_ID = 'HandleReplay'
|
@@ -66,7 +67,18 @@ def __rewrite_response(replay_context: ReplayContext):
|
|
66
67
|
After replaying a request, see if the request needs to be rewritten
|
67
68
|
"""
|
68
69
|
intercept_settings: InterceptSettings = replay_context.intercept_settings
|
70
|
+
flow = replay_context.flow
|
71
|
+
request = flow.request
|
72
|
+
response = flow.response
|
73
|
+
|
74
|
+
request_proxy_mode_header = request.headers.get(custom_headers.PROXY_MODE)
|
75
|
+
response_proxy_mode_header = response.headers.get(custom_headers.RESPONSE_PROXY_MODE)
|
76
|
+
|
77
|
+
if request_proxy_mode_header == mode.REPLAY and response_proxy_mode_header == mode.RECORD:
|
78
|
+
if intercept_settings.record_strategy == record_strategy.MINIMAL:
|
79
|
+
minimize_response_headers(flow)
|
80
|
+
|
69
81
|
rewrite_rules = intercept_settings.replay_rewrite_rules
|
70
82
|
|
71
83
|
if len(rewrite_rules) > 0:
|
72
|
-
rewrite_response(
|
84
|
+
rewrite_response(flow, rewrite_rules)
|
@@ -57,14 +57,11 @@ class InterceptSettings:
|
|
57
57
|
|
58
58
|
@property
|
59
59
|
def active(self):
|
60
|
-
if self.
|
61
|
-
return
|
60
|
+
if self.__headers and custom_headers.PROXY_MODE in self.__headers:
|
61
|
+
return not not self.__headers[custom_headers.PROXY_MODE]
|
62
62
|
|
63
|
-
|
64
|
-
return False
|
63
|
+
return self.__intercept_settings.active
|
65
64
|
|
66
|
-
return custom_headers.PROXY_MODE in self.__headers
|
67
|
-
|
68
65
|
@property
|
69
66
|
def lifecycle_hooks_path(self):
|
70
67
|
if self.__headers and custom_headers.LIFECYCLE_HOOKS_PATH in self.__headers:
|
@@ -182,6 +179,13 @@ class InterceptSettings:
|
|
182
179
|
|
183
180
|
return self.policy
|
184
181
|
|
182
|
+
@property
|
183
|
+
def record_strategy(self):
|
184
|
+
if self.__headers and custom_headers.RECORD_STRATEGY in self.__headers:
|
185
|
+
return self.__headers[custom_headers.RECORD_STRATEGY]
|
186
|
+
|
187
|
+
return self.__data_rules.record_strategy
|
188
|
+
|
185
189
|
@property
|
186
190
|
def exclude_rules(self) -> List[FirewallRule]:
|
187
191
|
_mode = self.mode
|
@@ -365,4 +369,4 @@ class InterceptSettings:
|
|
365
369
|
|
366
370
|
return self.__data_rules.test_policy
|
367
371
|
elif mode == intercept_mode.REPLAY:
|
368
|
-
return self.__data_rules.replay_policy
|
372
|
+
return self.__data_rules.replay_policy
|
@@ -7,7 +7,7 @@ from io import BytesIO
|
|
7
7
|
from mitmproxy.http import Request as MitmproxyRequest
|
8
8
|
from requests import Response
|
9
9
|
from requests.structures import CaseInsensitiveDict
|
10
|
-
from typing import Union
|
10
|
+
from typing import Optional, Union
|
11
11
|
|
12
12
|
from stoobly_agent.lib.logger import bcolors, Logger
|
13
13
|
from stoobly_agent.config.constants.custom_headers import MOCK_FIXTURE_PATH
|
@@ -62,7 +62,7 @@ def eval_fixtures(request: MitmproxyRequest, **options: Options) -> Union[Respon
|
|
62
62
|
with open(fixture_path, 'rb') as fp:
|
63
63
|
response = Response()
|
64
64
|
|
65
|
-
response.status_code = status_code
|
65
|
+
response.status_code = int(status_code)
|
66
66
|
response.raw = BytesIO(fp.read())
|
67
67
|
headers[MOCK_FIXTURE_PATH] = fixture_path
|
68
68
|
response.headers = headers
|
@@ -71,6 +71,11 @@ def eval_fixtures(request: MitmproxyRequest, **options: Options) -> Union[Respon
|
|
71
71
|
content_type = __guess_content_type(fixture_path)
|
72
72
|
if content_type:
|
73
73
|
response.headers['content-type'] = content_type
|
74
|
+
else:
|
75
|
+
# Default to highest priority accept header
|
76
|
+
content_type = __choose_highest_priority_content_type(request.headers.get('accept'))
|
77
|
+
if content_type:
|
78
|
+
response.headers['content-type'] = content_type
|
74
79
|
|
75
80
|
Logger.instance(LOG_ID).debug(f"{bcolors.OKBLUE}Resolved{bcolors.ENDC} fixture {fixture_path}")
|
76
81
|
|
@@ -125,6 +130,32 @@ def __eval_response_fixtures(request: MitmproxyRequest, response_fixtures: Fixtu
|
|
125
130
|
if path:
|
126
131
|
return fixture
|
127
132
|
|
133
|
+
def __choose_highest_priority_content_type(accept_header: str) -> Optional[str]:
|
134
|
+
if not accept_header:
|
135
|
+
return None
|
136
|
+
|
137
|
+
types = []
|
138
|
+
for part in accept_header.split(","):
|
139
|
+
media_range = part.strip()
|
140
|
+
if ";" in media_range:
|
141
|
+
mime, *params = media_range.split(";")
|
142
|
+
q = 1.0 # default
|
143
|
+
for param in params:
|
144
|
+
param = param.strip()
|
145
|
+
if param.startswith("q="):
|
146
|
+
try:
|
147
|
+
q = float(param[2:])
|
148
|
+
except ValueError:
|
149
|
+
q = 0.0 # invalid q values treated as lowest
|
150
|
+
else:
|
151
|
+
mime = media_range
|
152
|
+
q = 1.0
|
153
|
+
types.append((mime.strip(), q))
|
154
|
+
|
155
|
+
# Sort by descending q
|
156
|
+
types.sort(key=lambda x: -x[1])
|
157
|
+
return types[0][0] if types else None
|
158
|
+
|
128
159
|
def __parse_accept_header(accept_header):
|
129
160
|
types = []
|
130
161
|
for item in accept_header.split(","):
|
@@ -49,7 +49,7 @@ def inject_upload_request(request_model: RequestModel, intercept_settings: Inter
|
|
49
49
|
def upload_request(
|
50
50
|
request_model: RequestModel, intercept_settings: InterceptSettings, flow: MitmproxyHTTPFlow = None
|
51
51
|
):
|
52
|
-
Logger.instance(LOG_ID).info(f"{bcolors.
|
52
|
+
Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Recording{bcolors.ENDC} {flow.request.url}")
|
53
53
|
|
54
54
|
flow_copy = deepcopy(flow) # When applying modifications we don't want to persist them in the response
|
55
55
|
joined_request = join_request_from_flow(flow_copy, intercept_settings=intercept_settings)
|
@@ -80,7 +80,7 @@ def upload_request(
|
|
80
80
|
def upload_staged_request(
|
81
81
|
request: Request, request_model: RequestModel, project_key: str, scenario_key: str = None
|
82
82
|
):
|
83
|
-
Logger.instance(LOG_ID).info(f"{bcolors.
|
83
|
+
Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Recording{bcolors.ENDC} {request.url}")
|
84
84
|
|
85
85
|
response = request.response
|
86
86
|
|
@@ -72,6 +72,9 @@ def replay(context: ReplayContext, options: ReplayRequestOptions) -> requests.Re
|
|
72
72
|
if options.get('public_directory_path'):
|
73
73
|
__handle_path_header(custom_headers.PUBLIC_DIRECTORY_PATH, options['public_directory_path'], headers)
|
74
74
|
|
75
|
+
if options.get('record_strategy'):
|
76
|
+
headers[custom_headers.RECORD_STRATEGY] = options['record_strategy']
|
77
|
+
|
75
78
|
if options.get('report_key'):
|
76
79
|
headers[custom_headers.REPORT_KEY] = options['report_key']
|
77
80
|
|
stoobly_agent/app/proxy/run.py
CHANGED
@@ -16,8 +16,6 @@ tls.DEFAULT_OPTIONS |= 0x4
|
|
16
16
|
from mitmproxy.options import Options
|
17
17
|
from mitmproxy.tools.dump import DumpMaster
|
18
18
|
|
19
|
-
from stoobly_agent.app.settings import Settings
|
20
|
-
|
21
19
|
INTERCEPT_HANDLER_FILENAME = 'intercept_handler.py'
|
22
20
|
|
23
21
|
def run(**kwargs):
|
@@ -67,7 +65,6 @@ def __with_static_options(config: MitmproxyConfig, cli_options):
|
|
67
65
|
config.set(options)
|
68
66
|
|
69
67
|
def __with_cli_options(config: MitmproxyConfig, cli_options: dict):
|
70
|
-
__commit_options(cli_options)
|
71
68
|
__filter_options(cli_options)
|
72
69
|
|
73
70
|
options = []
|
@@ -86,31 +83,6 @@ def __with_cli_options(config: MitmproxyConfig, cli_options: dict):
|
|
86
83
|
|
87
84
|
config.set(tuple(options))
|
88
85
|
|
89
|
-
def __commit_options(options: dict):
|
90
|
-
changed = False
|
91
|
-
service_name = os.environ.get(SERVICE_NAME_ENV)
|
92
|
-
settings = Settings.instance()
|
93
|
-
|
94
|
-
if not service_name or options.get('intercept'):
|
95
|
-
# In the case when service name is set,
|
96
|
-
# only update if intercept is explicitly enabled
|
97
|
-
intercept = not not options.get('intercept')
|
98
|
-
changed = settings.proxy.intercept.active != intercept
|
99
|
-
settings.proxy.intercept.active = intercept
|
100
|
-
|
101
|
-
if not service_name or service_name == CORE_MOCK_UI_SERVICE_NAME:
|
102
|
-
# Causes potentially unintended side effects when run as part of scaffold
|
103
|
-
# Defer to ui service for configuration in this case
|
104
|
-
if options.get('proxy_host') and options.get('proxy_port'):
|
105
|
-
settings.proxy.url = f"http://{options.get('proxy_host')}:{options.get('proxy_port')}"
|
106
|
-
|
107
|
-
settings.ui.active = not options.get('headless')
|
108
|
-
|
109
|
-
changed = True
|
110
|
-
|
111
|
-
if changed:
|
112
|
-
settings.commit()
|
113
|
-
|
114
86
|
def __filter_options(options):
|
115
87
|
'''
|
116
88
|
Filter out non-mitmproxy options
|
@@ -139,6 +111,9 @@ def __filter_options(options):
|
|
139
111
|
if 'intercept' in options:
|
140
112
|
del options['intercept']
|
141
113
|
|
114
|
+
if 'intercept_mode' in options:
|
115
|
+
del options['intercept_mode']
|
116
|
+
|
142
117
|
if 'lifecycle_hooks_path' in options:
|
143
118
|
del options['lifecycle_hooks_path']
|
144
119
|
|
@@ -5,13 +5,14 @@ from mitmproxy.http import Request as MitmproxyRequest
|
|
5
5
|
from typing import List
|
6
6
|
|
7
7
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
8
|
+
from stoobly_agent.app.settings.constants import intercept_mode
|
8
9
|
from stoobly_agent.app.settings.firewall_rule import FirewallRule
|
9
|
-
from stoobly_agent.config.constants import intercept_policy, request_origin
|
10
|
+
from stoobly_agent.config.constants import mode, intercept_policy, request_origin
|
10
11
|
from stoobly_agent.lib.logger import bcolors, Logger
|
11
12
|
|
12
13
|
LOG_ID = 'Firewall'
|
13
14
|
|
14
|
-
def get_active_mode_policy(request: MitmproxyRequest, intercept_settings: InterceptSettings):
|
15
|
+
def get_active_mode_policy(request: MitmproxyRequest, intercept_settings: InterceptSettings) -> str:
|
15
16
|
if intercept_settings.request_origin == request_origin.CLI:
|
16
17
|
return intercept_settings.policy
|
17
18
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import pdb
|
2
|
+
|
3
|
+
from mitmproxy.http import Headers
|
4
|
+
from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
|
5
|
+
|
6
|
+
from typing import Final
|
7
|
+
|
8
|
+
|
9
|
+
REQUEST_HEADERS_ALLOWLIST: Final[dict[str]] = {
|
10
|
+
"Accept",
|
11
|
+
"Accept-Encoding",
|
12
|
+
"Accept-Language",
|
13
|
+
"Content-Length",
|
14
|
+
"Content-Type",
|
15
|
+
"Host",
|
16
|
+
"Origin",
|
17
|
+
"Referer",
|
18
|
+
"User-Agent",
|
19
|
+
}
|
20
|
+
|
21
|
+
RESPONSE_HEADERS_ALLOWLIST: Final[dict[str]] = {
|
22
|
+
"Content-Length",
|
23
|
+
"Content-Type",
|
24
|
+
"Date",
|
25
|
+
"Transfer-Encoding",
|
26
|
+
"Server", # Sometimes required for HTTP/1.0, but not strictly mandatory
|
27
|
+
}
|
28
|
+
|
29
|
+
def minimize_headers(flow: MitmproxyHTTPFlow):
|
30
|
+
minimize_request_headers(flow)
|
31
|
+
minimize_response_headers(flow)
|
32
|
+
|
33
|
+
def minimize_request_headers(flow: MitmproxyHTTPFlow) -> None:
|
34
|
+
remove_headers(flow.request.headers, REQUEST_HEADERS_ALLOWLIST)
|
35
|
+
|
36
|
+
def minimize_response_headers(flow: MitmproxyHTTPFlow) -> None:
|
37
|
+
remove_headers(flow.response.headers, RESPONSE_HEADERS_ALLOWLIST)
|
38
|
+
|
39
|
+
def remove_headers(headers: Headers, allowlist: dict[str]):
|
40
|
+
keys_to_remove = []
|
41
|
+
|
42
|
+
for key in headers:
|
43
|
+
if key.lower() not in {allowed_header.lower() for allowed_header in allowlist}:
|
44
|
+
keys_to_remove.append(key)
|
45
|
+
|
46
|
+
for key in keys_to_remove:
|
47
|
+
headers.pop(key)
|
@@ -1,8 +1,10 @@
|
|
1
|
+
import os
|
1
2
|
import pdb
|
2
3
|
import threading
|
3
4
|
|
4
5
|
from typing import TypedDict
|
5
6
|
|
7
|
+
from stoobly_agent.app.settings import Settings
|
6
8
|
from stoobly_agent.config.constants.statuses import REQUESTS_MODIFIED
|
7
9
|
from stoobly_agent.lib.api.agent_api import AgentApi
|
8
10
|
from stoobly_agent.lib.cache import Cache
|
@@ -12,15 +14,14 @@ class Options(TypedDict):
|
|
12
14
|
sync: bool
|
13
15
|
|
14
16
|
# Announce that a new request has been created
|
15
|
-
def publish_change(status: str, value
|
17
|
+
def publish_change(status: str, value, **options: Options):
|
16
18
|
if options.get('sync'):
|
17
19
|
return __publish_change_sync(status, value)
|
18
20
|
|
19
|
-
|
20
|
-
settings = Settings.instance()
|
21
|
+
settings: Settings = Settings.instance()
|
21
22
|
|
22
23
|
# If ui is not active, return
|
23
|
-
if
|
24
|
+
if not settings.ui.active:
|
24
25
|
return False
|
25
26
|
|
26
27
|
ui_url = settings.ui.url
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import pdb
|
2
|
+
|
3
|
+
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
4
|
+
from stoobly_agent.app.settings.constants import intercept_mode
|
5
|
+
from stoobly_agent.config.constants import request_origin
|
6
|
+
|
7
|
+
|
8
|
+
def get_active_mode_strategy(intercept_settings: InterceptSettings) -> str:
|
9
|
+
strategy = ""
|
10
|
+
|
11
|
+
if intercept_settings.mode == intercept_mode.RECORD:
|
12
|
+
strategy = intercept_settings.record_strategy
|
13
|
+
elif intercept_settings.mode == intercept_mode.TEST:
|
14
|
+
strategy = intercept_settings.test_strategy
|
15
|
+
|
16
|
+
return strategy
|
@@ -3,7 +3,7 @@ import pdb
|
|
3
3
|
import time
|
4
4
|
import yaml
|
5
5
|
|
6
|
-
from
|
6
|
+
from filelock import FileLock
|
7
7
|
from watchdog.observers import Observer
|
8
8
|
from watchdog.events import PatternMatchingEventHandler
|
9
9
|
from yamale import *
|
@@ -177,8 +177,14 @@ class Settings:
|
|
177
177
|
self.__load_settings()
|
178
178
|
|
179
179
|
def write(self, contents):
|
180
|
-
if contents:
|
181
|
-
|
180
|
+
if not contents:
|
181
|
+
return
|
182
|
+
|
183
|
+
path = self.__settings_file_path
|
184
|
+
lock = FileLock(path + ".lock") # lock file alongside the target
|
185
|
+
|
186
|
+
with lock:
|
187
|
+
with open(path, 'w') as fp:
|
182
188
|
yaml.dump(contents, fp, allow_unicode=True)
|
183
189
|
|
184
190
|
### Helpers
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from stoobly_agent.config.constants import mock_policy, record_order, record_policy, replay_policy, test_strategy
|
1
|
+
from stoobly_agent.config.constants import mock_policy, record_order, record_policy, record_strategy, replay_policy, test_strategy
|
2
2
|
|
3
3
|
from .types.proxy_settings import DataRules as IDataRules
|
4
4
|
|
@@ -10,6 +10,7 @@ class DataRules:
|
|
10
10
|
self.__mock_policy = self.__data_rules.get('mock_policy') or mock_policy.FOUND
|
11
11
|
self.__record_order = self.__data_rules.get('record_order') or record_order.APPEND
|
12
12
|
self.__record_policy = self.__data_rules.get('record_policy') or record_policy.ALL
|
13
|
+
self.__record_strategy = self.__data_rules.get('record_strategy') or record_strategy.FULL
|
13
14
|
self.__replay_policy = self.__data_rules.get('replay_policy') or replay_policy.ALL
|
14
15
|
self.__scenario_key = self.__data_rules.get('scenario_key')
|
15
16
|
self.__test_policy = self.__data_rules.get('test_policy') or mock_policy.FOUND
|
@@ -46,6 +47,19 @@ class DataRules:
|
|
46
47
|
self.__record_order = v
|
47
48
|
self.__data_rules['record_order'] = v
|
48
49
|
|
50
|
+
@property
|
51
|
+
def record_strategy(self):
|
52
|
+
return self.__record_strategy
|
53
|
+
|
54
|
+
@record_strategy.setter
|
55
|
+
def record_strategy(self, v):
|
56
|
+
valid_strategies = [record_strategy.FULL, record_strategy.MINIMAL]
|
57
|
+
if v not in valid_strategies:
|
58
|
+
raise TypeError(f"record_strategy has to be one of {valid_strategies}, got {v}")
|
59
|
+
|
60
|
+
self.__record_strategy = v
|
61
|
+
self.__data_rules['record_strategy'] = v
|
62
|
+
|
49
63
|
@property
|
50
64
|
def replay_policy(self):
|
51
65
|
return self.__replay_policy
|
@@ -79,11 +93,21 @@ class DataRules:
|
|
79
93
|
def test_strategy(self):
|
80
94
|
return self.__test_strategy
|
81
95
|
|
96
|
+
@test_strategy.setter
|
97
|
+
def test_strategy(self, v):
|
98
|
+
valid_strategies = [test_strategy.CONTRACT, test_strategy.CUSTOM, test_strategy.DIFF, test_strategy.FUZZY]
|
99
|
+
if v not in valid_strategies:
|
100
|
+
raise TypeError(f"test_strategy has to be one of {valid_strategies}, got {v}")
|
101
|
+
|
102
|
+
self.__test_strategy = v
|
103
|
+
self.__data_rules['test_strategy'] = v
|
104
|
+
|
82
105
|
def to_dict(self) -> IDataRules:
|
83
106
|
return {
|
84
107
|
'mock_policy': self.__mock_policy,
|
85
108
|
'record_order': self.__record_order,
|
86
109
|
'record_policy': self.__record_policy,
|
110
|
+
'record_strategy': self.__record_strategy,
|
87
111
|
'replay_policy': self.__replay_policy,
|
88
112
|
'scenario_key': self.__scenario_key,
|
89
113
|
'test_policy': self.__test_policy,
|
@@ -19,6 +19,9 @@ class InterceptSettings:
|
|
19
19
|
|
20
20
|
@property
|
21
21
|
def active(self):
|
22
|
+
if os.environ.get(env_vars.AGENT_INTERCEPT_ACTIVE):
|
23
|
+
return os.environ[env_vars.AGENT_INTERCEPT_ACTIVE]
|
24
|
+
|
22
25
|
return self.__active
|
23
26
|
|
24
27
|
@active.setter
|
@@ -35,8 +38,8 @@ class InterceptSettings:
|
|
35
38
|
if self.__mode != self.mode_before_change:
|
36
39
|
return self.__mode
|
37
40
|
|
38
|
-
if os.environ.get(env_vars.
|
39
|
-
return os.environ[env_vars.
|
41
|
+
if os.environ.get(env_vars.AGENT_INTERCEPT_MODE):
|
42
|
+
return os.environ[env_vars.AGENT_INTERCEPT_MODE]
|
40
43
|
|
41
44
|
return self.__mode
|
42
45
|
|
@@ -21,8 +21,8 @@ class UISettings:
|
|
21
21
|
if self.__active != self.active_before_change:
|
22
22
|
return self.__active
|
23
23
|
|
24
|
-
if os.environ.get(env_vars.
|
25
|
-
return
|
24
|
+
if os.environ.get(env_vars.AGENT_HEADLESS):
|
25
|
+
return False
|
26
26
|
|
27
27
|
return self.__active or False
|
28
28
|
|
@@ -39,8 +39,8 @@ class UISettings:
|
|
39
39
|
if self.__url != self.url_before_change:
|
40
40
|
return self.__url
|
41
41
|
|
42
|
-
if os.environ.get(env_vars.
|
43
|
-
return os.environ[env_vars.
|
42
|
+
if os.environ.get(env_vars.AGENT_UI_URL):
|
43
|
+
return os.environ[env_vars.AGENT_UI_URL]
|
44
44
|
|
45
45
|
return self.__url or ''
|
46
46
|
|
@@ -52,4 +52,4 @@ class UISettings:
|
|
52
52
|
return {
|
53
53
|
'active': self.__active,
|
54
54
|
'url': self.__url,
|
55
|
-
}
|
55
|
+
}
|
stoobly_agent/cli.py
CHANGED
@@ -13,9 +13,10 @@ from stoobly_agent.app.proxy.constants import custom_response_codes
|
|
13
13
|
from stoobly_agent.app.proxy.replay.replay_request_service import replay as replay_request
|
14
14
|
from stoobly_agent.config.constants import env_vars, mode
|
15
15
|
from stoobly_agent.config.data_dir import DataDir
|
16
|
+
from stoobly_agent.lib.logger import Logger
|
16
17
|
from stoobly_agent.lib.utils.conditional_decorator import ConditionalDecorator
|
17
18
|
|
18
|
-
from .app.api import
|
19
|
+
from .app.api import run as run_api
|
19
20
|
from .app.cli import ca_cert, config, endpoint, feature, intercept, MainGroup, request, scenario, scaffold, snapshot, trace
|
20
21
|
from .app.cli.helpers.feature_flags import local, remote
|
21
22
|
from .app.settings import Settings
|
@@ -99,6 +100,7 @@ def init(**kwargs):
|
|
99
100
|
''')
|
100
101
|
@click.option('--headless', is_flag=True, default=False, help='Disable starting UI.')
|
101
102
|
@click.option('--intercept', is_flag=True, default=False, help='Enable intercept on run.')
|
103
|
+
@click.option('--intercept-mode', help='Set intercept mode.')
|
102
104
|
@click.option('--log-level', default=logger.INFO, type=click.Choice([logger.DEBUG, logger.INFO, logger.WARNING, logger.ERROR]), help='''
|
103
105
|
Log levels can be "debug", "info", "warning", or "error"
|
104
106
|
''')
|
@@ -124,7 +126,19 @@ def init(**kwargs):
|
|
124
126
|
def run(**kwargs):
|
125
127
|
from .app.proxy.run import run as run_proxy
|
126
128
|
|
127
|
-
|
129
|
+
# Observe config for changes
|
130
|
+
settings: Settings = Settings.instance()
|
131
|
+
settings.watch()
|
132
|
+
|
133
|
+
if kwargs.get('headless'):
|
134
|
+
os.environ[env_vars.AGENT_HEADLESS] = '1'
|
135
|
+
|
136
|
+
if kwargs.get('intercept'):
|
137
|
+
os.environ[env_vars.AGENT_INTERCEPT_ACTIVE] = '1'
|
138
|
+
|
139
|
+
if kwargs.get('intercept_mode'):
|
140
|
+
os.environ[env_vars.AGENT_INTERCEPT_MODE] = kwargs['intercept_mode']
|
141
|
+
settings.proxy.intercept.mode = kwargs['intercept_mode']
|
128
142
|
|
129
143
|
if kwargs.get('lifecycle_hooks_path'):
|
130
144
|
os.environ[env_vars.AGENT_LIFECYCLE_HOOKS_PATH] = kwargs['lifecycle_hooks_path']
|
@@ -135,21 +149,32 @@ def run(**kwargs):
|
|
135
149
|
if kwargs.get('response_fixtures_path'):
|
136
150
|
os.environ[env_vars.AGENT_RESPONSE_FIXTURES_PATH] = kwargs['response_fixtures_path']
|
137
151
|
|
138
|
-
# Observe config for changes
|
139
|
-
Settings.instance().watch()
|
140
|
-
|
141
152
|
if not os.getenv(env_vars.LOG_LEVEL):
|
142
|
-
|
143
|
-
|
144
|
-
if
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
+
os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
|
154
|
+
|
155
|
+
if kwargs.get('api_url'):
|
156
|
+
os.environ[env_vars.API_URL] = kwargs['api_url']
|
157
|
+
|
158
|
+
if not kwargs.get('headless'):
|
159
|
+
ui_url = f"http://{kwargs['ui_host']}:{kwargs['ui_port']}"
|
160
|
+
os.environ[env_vars.AGENT_UI_URL] = ui_url
|
161
|
+
settings.ui.active = True
|
162
|
+
settings.ui.url = ui_url
|
163
|
+
|
164
|
+
if not kwargs.get('proxyless'):
|
165
|
+
proxy_url = f"http://{kwargs['proxy_host']}:{kwargs['proxy_port']}"
|
166
|
+
os.environ[env_vars.AGENT_PROXY_URL] = proxy_url
|
167
|
+
settings.proxy.url = proxy_url
|
168
|
+
|
169
|
+
if not kwargs.get('headless'):
|
170
|
+
settings.commit()
|
171
|
+
run_api(**kwargs)
|
172
|
+
|
173
|
+
if not kwargs.get('proxyless'):
|
174
|
+
log_id = 'Proxy'
|
175
|
+
Logger.instance(log_id).info(f"starting with mode {kwargs['proxy_mode']} and listening at {kwargs['proxy_host']}:{kwargs['proxy_port']}")
|
176
|
+
Logger.instance(log_id).info(f"{'' if settings.proxy.intercept.active else 'not yet '}configured to {settings.proxy.intercept.mode}")
|
177
|
+
run_proxy(**kwargs)
|
153
178
|
|
154
179
|
@main.command(
|
155
180
|
help="Mock request"
|
@@ -13,6 +13,7 @@ PROJECT_KEY = 'X-Stoobly-Project-Key'
|
|
13
13
|
PROXY_MODE = 'X-Stoobly-Proxy-Mode'
|
14
14
|
PUBLIC_DIRECTORY_PATH = 'X-Stoobly-Public-Directory-Path'
|
15
15
|
RECORD_POLICY = 'X-Stoobly-Record-Policy'
|
16
|
+
RECORD_STRATEGY = 'X-Stoobly-Record-Strategy'
|
16
17
|
REPORT_KEY = 'X-Stoobly-Report-Key'
|
17
18
|
REQUEST_ORIGIN = 'X-Stoobly-Request-Origin'
|
18
19
|
RESPONSE_FIXTURES_PATH = 'X-Stoobly-Response-Fixtures-Path'
|
@@ -1,9 +1,10 @@
|
|
1
|
-
AGENT_ACTIVE_MODE = 'STOOBLY_AGENT_ACTIVE_MODE'
|
2
1
|
AGENT_CONFIG_PATH = 'STOOBLY_AGENT_CONFIG_PATH'
|
3
2
|
AGENT_ENABLED = 'STOOBLY_AGENT_ENABLED'
|
4
3
|
AGENT_SELF_INTERCEPT_ENABLED = 'STOOBLY_AGENT_SELF_INTERCEPT_ENABLED'
|
4
|
+
AGENT_HEADLESS = 'STOOBLY_AGENT_HEADLESS'
|
5
5
|
AGENT_INCLUDE_PATTERNS = 'STOOBLY_AGENT_INCLUDE_PATTERNS'
|
6
|
-
|
6
|
+
AGENT_INTERCEPT_ACTIVE = 'STOOBLY_AGENT_INTERCEPT_ACTIVE'
|
7
|
+
AGENT_INTERCEPT_MODE = 'STOOBLY_AGENT_INTERCEPT_MODE'
|
7
8
|
AGENT_EXCLUDE_PATTERNS = 'STOOBLY_AGENT_EXCLUDE_PATTERNS'
|
8
9
|
AGENT_LIFECYCLE_HOOKS_PATH = 'STOOBLY_AGENT_LIFECYCLE_HOOKS_PATH'
|
9
10
|
AGENT_POLICY = 'STOOBLY_AGENT_POLICY'
|
@@ -18,7 +19,7 @@ AGENT_RESPONSE_FIXTURES_PATH = 'STOOBLY_AGENT_RESPONSE_FIXTURES_PATH'
|
|
18
19
|
AGENT_SERVICE_URL = 'STOOBLY_AGENT_SERVICE_URL'
|
19
20
|
AGENT_SCENARIO_KEY = 'STOOBLY_AGENT_SCENARIO_KEY'
|
20
21
|
AGENT_SIMULATE_LATENCY = 'STOOBLY_AGENT_SIMULATE_LATENCY'
|
21
|
-
|
22
|
+
AGENT_UI_URL = 'STOOBLY_AGENT_UI_URL'
|
22
23
|
API_URL = 'STOOBLY_API_URL'
|
23
24
|
API_KEY = 'STOOBLY_API_KEY'
|
24
25
|
ENV = 'STOOBLY_AGENT_ENV'
|
@@ -12,10 +12,9 @@ proxy:
|
|
12
12
|
project_key: eyJpIjowLCJvIjowfQ==
|
13
13
|
firewall: {}
|
14
14
|
rewrite: {}
|
15
|
-
proxy_config_path: config/settings.yml
|
16
15
|
remote:
|
17
16
|
api_key: ''
|
18
|
-
api_url: '
|
17
|
+
api_url: ''
|
19
18
|
ui:
|
20
19
|
active: false
|
21
|
-
url:
|
20
|
+
url: ''
|