| @@ -14,28 +14,18 @@ from core.workflow.entities.variable_pool import ValueType, VariablePool | |||
| from core.workflow.nodes.http_request.entities import HttpRequestNodeData | |||
| from core.workflow.utils.variable_template_parser import VariableTemplateParser | |||
| MAX_BINARY_SIZE = int(os.environ.get('HTTP_REQUEST_NODE_MAX_BINARY_SIZE', str(1024 * 1024 * 10))) # 10MB | |||
| MAX_BINARY_SIZE = int(os.environ.get('HTTP_REQUEST_NODE_MAX_BINARY_SIZE', 1024 * 1024 * 10)) # 10MB | |||
| READABLE_MAX_BINARY_SIZE = f'{MAX_BINARY_SIZE / 1024 / 1024:.2f}MB' | |||
| MAX_TEXT_SIZE = int(os.environ.get('HTTP_REQUEST_NODE_MAX_TEXT_SIZE', str(1024 * 1024))) # 10MB # 1MB | |||
| MAX_TEXT_SIZE = int(os.environ.get('HTTP_REQUEST_NODE_MAX_TEXT_SIZE', 1024 * 1024)) # 1MB | |||
| READABLE_MAX_TEXT_SIZE = f'{MAX_TEXT_SIZE / 1024 / 1024:.2f}MB' | |||
| class HttpExecutorResponse: | |||
| headers: dict[str, str] | |||
| response: Union[httpx.Response, requests.Response] | |||
| def __init__(self, response: Union[httpx.Response, requests.Response] = None): | |||
| """ | |||
| init | |||
| """ | |||
| headers = {} | |||
| if isinstance(response, httpx.Response): | |||
| for k, v in response.headers.items(): | |||
| headers[k] = v | |||
| elif isinstance(response, requests.Response): | |||
| for k, v in response.headers.items(): | |||
| headers[k] = v | |||
| self.headers = headers | |||
| self.headers = response.headers | |||
| self.response = response | |||
| @property | |||
| @@ -45,21 +35,11 @@ class HttpExecutorResponse: | |||
| """ | |||
| content_type = self.get_content_type() | |||
| file_content_types = ['image', 'audio', 'video'] | |||
| for v in file_content_types: | |||
| if v in content_type: | |||
| return True | |||
| return False | |||
| return any(v in content_type for v in file_content_types) | |||
| def get_content_type(self) -> str: | |||
| """ | |||
| get content type | |||
| """ | |||
| for key, val in self.headers.items(): | |||
| if key.lower() == 'content-type': | |||
| return val | |||
| return '' | |||
| return self.headers.get('content-type') | |||
| def extract_file(self) -> tuple[str, bytes]: | |||
| """ | |||
| @@ -67,29 +47,25 @@ class HttpExecutorResponse: | |||
| """ | |||
| if self.is_file: | |||
| return self.get_content_type(), self.body | |||
| return '', b'' | |||
| @property | |||
| def content(self) -> str: | |||
| """ | |||
| get content | |||
| """ | |||
| if isinstance(self.response, httpx.Response): | |||
| return self.response.text | |||
| elif isinstance(self.response, requests.Response): | |||
| if isinstance(self.response, httpx.Response | requests.Response): | |||
| return self.response.text | |||
| else: | |||
| raise ValueError(f'Invalid response type {type(self.response)}') | |||
| @property | |||
| def body(self) -> bytes: | |||
| """ | |||
| get body | |||
| """ | |||
| if isinstance(self.response, httpx.Response): | |||
| return self.response.content | |||
| elif isinstance(self.response, requests.Response): | |||
| if isinstance(self.response, httpx.Response | requests.Response): | |||
| return self.response.content | |||
| else: | |||
| raise ValueError(f'Invalid response type {type(self.response)}') | |||
| @@ -99,20 +75,18 @@ class HttpExecutorResponse: | |||
| """ | |||
| get status code | |||
| """ | |||
| if isinstance(self.response, httpx.Response): | |||
| return self.response.status_code | |||
| elif isinstance(self.response, requests.Response): | |||
| if isinstance(self.response, httpx.Response | requests.Response): | |||
| return self.response.status_code | |||
| else: | |||
| raise ValueError(f'Invalid response type {type(self.response)}') | |||
| @property | |||
| def size(self) -> int: | |||
| """ | |||
| get size | |||
| """ | |||
| return len(self.body) | |||
| @property | |||
| def readable_size(self) -> str: | |||
| """ | |||
| @@ -138,10 +112,8 @@ class HttpExecutor: | |||
| variable_selectors: list[VariableSelector] | |||
| timeout: HttpRequestNodeData.Timeout | |||
| def __init__(self, node_data: HttpRequestNodeData, timeout: HttpRequestNodeData.Timeout, variable_pool: Optional[VariablePool] = None): | |||
| """ | |||
| init | |||
| """ | |||
| def __init__(self, node_data: HttpRequestNodeData, timeout: HttpRequestNodeData.Timeout, | |||
| variable_pool: Optional[VariablePool] = None): | |||
| self.server_url = node_data.url | |||
| self.method = node_data.method | |||
| self.authorization = node_data.authorization | |||
| @@ -155,7 +127,8 @@ class HttpExecutor: | |||
| self.variable_selectors = [] | |||
| self._init_template(node_data, variable_pool) | |||
| def _is_json_body(self, body: HttpRequestNodeData.Body): | |||
| @staticmethod | |||
| def _is_json_body(body: HttpRequestNodeData.Body): | |||
| """ | |||
| check if body is json | |||
| """ | |||
| @@ -165,55 +138,46 @@ class HttpExecutor: | |||
| return True | |||
| except: | |||
| return False | |||
| return False | |||
| def _init_template(self, node_data: HttpRequestNodeData, variable_pool: Optional[VariablePool] = None): | |||
| @staticmethod | |||
| def _to_dict(convert_item: str, convert_text: str, maxsplit: int = -1): | |||
| """ | |||
| init template | |||
| Convert the string like `aa:bb\n cc:dd` to dict `{aa:bb, cc:dd}` | |||
| :param convert_item: A label for what item to be converted, params, headers or body. | |||
| :param convert_text: The string containing key-value pairs separated by '\n'. | |||
| :param maxsplit: The maximum number of splits allowed for the ':' character in each key-value pair. Default is -1 (no limit). | |||
| :return: A dictionary containing the key-value pairs from the input string. | |||
| """ | |||
| variable_selectors = [] | |||
| # extract all template in url | |||
| self.server_url, server_url_variable_selectors = self._format_template(node_data.url, variable_pool) | |||
| # extract all template in params | |||
| params, params_variable_selectors = self._format_template(node_data.params, variable_pool) | |||
| # fill in params | |||
| kv_paris = params.split('\n') | |||
| kv_paris = convert_text.split('\n') | |||
| result = {} | |||
| for kv in kv_paris: | |||
| if not kv.strip(): | |||
| continue | |||
| kv = kv.split(':') | |||
| kv = kv.split(':', maxsplit=maxsplit) | |||
| if len(kv) == 2: | |||
| k, v = kv | |||
| elif len(kv) == 1: | |||
| k, v = kv[0], '' | |||
| else: | |||
| raise ValueError(f'Invalid params {kv}') | |||
| self.params[k.strip()] = v | |||
| raise ValueError(f'Invalid {convert_item} {kv}') | |||
| result[k.strip()] = v | |||
| return result | |||
| # extract all template in headers | |||
| headers, headers_variable_selectors = self._format_template(node_data.headers, variable_pool) | |||
| def _init_template(self, node_data: HttpRequestNodeData, variable_pool: Optional[VariablePool] = None): | |||
| # fill in headers | |||
| kv_paris = headers.split('\n') | |||
| for kv in kv_paris: | |||
| if not kv.strip(): | |||
| continue | |||
| # extract all template in url | |||
| self.server_url, server_url_variable_selectors = self._format_template(node_data.url, variable_pool) | |||
| kv = kv.split(':') | |||
| if len(kv) == 2: | |||
| k, v = kv | |||
| elif len(kv) == 1: | |||
| k, v = kv[0], '' | |||
| else: | |||
| raise ValueError(f'Invalid headers {kv}') | |||
| self.headers[k.strip()] = v.strip() | |||
| # extract all template in params | |||
| params, params_variable_selectors = self._format_template(node_data.params, variable_pool) | |||
| self.params = self._to_dict("params", params) | |||
| # extract all template in headers | |||
| headers, headers_variable_selectors = self._format_template(node_data.headers, variable_pool) | |||
| self.headers = self._to_dict("headers", headers) | |||
| # extract all template in body | |||
| body_data_variable_selectors = [] | |||
| @@ -231,18 +195,7 @@ class HttpExecutor: | |||
| self.headers['Content-Type'] = 'application/x-www-form-urlencoded' | |||
| if node_data.body.type in ['form-data', 'x-www-form-urlencoded']: | |||
| body = {} | |||
| kv_paris = body_data.split('\n') | |||
| for kv in kv_paris: | |||
| if not kv.strip(): | |||
| continue | |||
| kv = kv.split(':', 1) | |||
| if len(kv) == 2: | |||
| body[kv[0].strip()] = kv[1] | |||
| elif len(kv) == 1: | |||
| body[kv[0].strip()] = '' | |||
| else: | |||
| raise ValueError(f'Invalid body {kv}') | |||
| body = self._to_dict("body", body_data, 1) | |||
| if node_data.body.type == 'form-data': | |||
| self.files = { | |||
| @@ -261,14 +214,14 @@ class HttpExecutor: | |||
| self.variable_selectors = (server_url_variable_selectors + params_variable_selectors | |||
| + headers_variable_selectors + body_data_variable_selectors) | |||
| def _assembling_headers(self) -> dict[str, Any]: | |||
| authorization = deepcopy(self.authorization) | |||
| headers = deepcopy(self.headers) or {} | |||
| if self.authorization.type == 'api-key': | |||
| if self.authorization.config.api_key is None: | |||
| raise ValueError('api_key is required') | |||
| if not self.authorization.config.header: | |||
| authorization.config.header = 'Authorization' | |||
| @@ -278,9 +231,9 @@ class HttpExecutor: | |||
| headers[authorization.config.header] = f'Basic {authorization.config.api_key}' | |||
| elif self.authorization.config.type == 'custom': | |||
| headers[authorization.config.header] = authorization.config.api_key | |||
| return headers | |||
| def _validate_and_parse_response(self, response: Union[httpx.Response, requests.Response]) -> HttpExecutorResponse: | |||
| """ | |||
| validate the response | |||
| @@ -289,21 +242,22 @@ class HttpExecutor: | |||
| executor_response = HttpExecutorResponse(response) | |||
| else: | |||
| raise ValueError(f'Invalid response type {type(response)}') | |||
| if executor_response.is_file: | |||
| if executor_response.size > MAX_BINARY_SIZE: | |||
| raise ValueError(f'File size is too large, max size is {READABLE_MAX_BINARY_SIZE}, but current size is {executor_response.readable_size}.') | |||
| raise ValueError( | |||
| f'File size is too large, max size is {READABLE_MAX_BINARY_SIZE}, but current size is {executor_response.readable_size}.') | |||
| else: | |||
| if executor_response.size > MAX_TEXT_SIZE: | |||
| raise ValueError(f'Text size is too large, max size is {READABLE_MAX_TEXT_SIZE}, but current size is {executor_response.readable_size}.') | |||
| raise ValueError( | |||
| f'Text size is too large, max size is {READABLE_MAX_TEXT_SIZE}, but current size is {executor_response.readable_size}.') | |||
| return executor_response | |||
| def _do_http_request(self, headers: dict[str, Any]) -> httpx.Response: | |||
| """ | |||
| do http request depending on api bundle | |||
| """ | |||
| # do http request | |||
| kwargs = { | |||
| 'url': self.server_url, | |||
| 'headers': headers, | |||
| @@ -312,25 +266,14 @@ class HttpExecutor: | |||
| 'follow_redirects': True | |||
| } | |||
| if self.method == 'get': | |||
| response = ssrf_proxy.get(**kwargs) | |||
| elif self.method == 'post': | |||
| response = ssrf_proxy.post(data=self.body, files=self.files, **kwargs) | |||
| elif self.method == 'put': | |||
| response = ssrf_proxy.put(data=self.body, files=self.files, **kwargs) | |||
| elif self.method == 'delete': | |||
| response = ssrf_proxy.delete(data=self.body, files=self.files, **kwargs) | |||
| elif self.method == 'patch': | |||
| response = ssrf_proxy.patch(data=self.body, files=self.files, **kwargs) | |||
| elif self.method == 'head': | |||
| response = ssrf_proxy.head(**kwargs) | |||
| elif self.method == 'options': | |||
| response = ssrf_proxy.options(**kwargs) | |||
| if self.method in ('get', 'head', 'options'): | |||
| response = getattr(ssrf_proxy, self.method)(**kwargs) | |||
| elif self.method in ('post', 'put', 'delete', 'patch'): | |||
| response = getattr(ssrf_proxy, self.method)(data=self.body, files=self.files, **kwargs) | |||
| else: | |||
| raise ValueError(f'Invalid http method {self.method}') | |||
| return response | |||
| def invoke(self) -> HttpExecutorResponse: | |||
| """ | |||
| invoke http request | |||
| @@ -343,14 +286,11 @@ class HttpExecutor: | |||
| # validate response | |||
| return self._validate_and_parse_response(response) | |||
| def to_raw_request(self, mask_authorization_header: Optional[bool] = True) -> str: | |||
| """ | |||
| convert to raw request | |||
| """ | |||
| if mask_authorization_header == None: | |||
| mask_authorization_header = True | |||
| server_url = self.server_url | |||
| if self.params: | |||
| server_url += f'?{urlencode(self.params)}' | |||
| @@ -365,11 +305,11 @@ class HttpExecutor: | |||
| authorization_header = 'Authorization' | |||
| if self.authorization.config and self.authorization.config.header: | |||
| authorization_header = self.authorization.config.header | |||
| if k.lower() == authorization_header.lower(): | |||
| raw_request += f'{k}: {"*" * len(v)}\n' | |||
| continue | |||
| raw_request += f'{k}: {v}\n' | |||
| raw_request += '\n' | |||