245 lines
9.1 KiB
Python
245 lines
9.1 KiB
Python
# variable_substitution.py
|
|
#
|
|
# Copyright 2025 Pavel Baksy
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import re
|
|
from typing import Dict, List, Tuple, Set
|
|
from .models import HttpRequest, Environment
|
|
|
|
|
|
class VariableSubstitution:
|
|
"""Handles variable substitution with {{variable_name}} syntax."""
|
|
|
|
# Pattern to match {{variable_name}} where variable_name is alphanumeric + underscore
|
|
VARIABLE_PATTERN = re.compile(r'\{\{(\w+)\}\}')
|
|
|
|
@staticmethod
|
|
def find_variables(text: str) -> List[str]:
|
|
"""
|
|
Extract all {{variable_name}} references from text.
|
|
|
|
Args:
|
|
text: The text to search for variable references
|
|
|
|
Returns:
|
|
List of variable names found (without the {{ }} delimiters)
|
|
"""
|
|
if not text:
|
|
return []
|
|
return VariableSubstitution.VARIABLE_PATTERN.findall(text)
|
|
|
|
@staticmethod
|
|
def substitute(text: str, variables: Dict[str, str]) -> Tuple[str, List[str]]:
|
|
"""
|
|
Replace {{variable_name}} with values from variables dict.
|
|
|
|
Args:
|
|
text: The text containing variable placeholders
|
|
variables: Dictionary mapping variable names to their values
|
|
|
|
Returns:
|
|
Tuple of (substituted_text, list_of_undefined_variables)
|
|
"""
|
|
if not text:
|
|
return text, []
|
|
|
|
undefined_vars = []
|
|
|
|
def replace_var(match):
|
|
var_name = match.group(1)
|
|
if var_name in variables:
|
|
value = variables[var_name]
|
|
# Check if value is empty/None - treat as undefined for warning purposes
|
|
if not value:
|
|
undefined_vars.append(var_name)
|
|
return ""
|
|
return value
|
|
else:
|
|
# Variable not defined - track it and replace with empty string
|
|
undefined_vars.append(var_name)
|
|
return ""
|
|
|
|
substituted = VariableSubstitution.VARIABLE_PATTERN.sub(replace_var, text)
|
|
return substituted, undefined_vars
|
|
|
|
@staticmethod
|
|
def substitute_request(request: HttpRequest, environment: Environment) -> Tuple[HttpRequest, Set[str]]:
|
|
"""
|
|
Substitute variables in all request fields (URL, headers, body).
|
|
|
|
Args:
|
|
request: The HTTP request with variable placeholders
|
|
environment: The environment containing variable values
|
|
|
|
Returns:
|
|
Tuple of (new_request_with_substitutions, set_of_undefined_variables)
|
|
"""
|
|
all_undefined = set()
|
|
|
|
# Substitute URL
|
|
new_url, url_undefined = VariableSubstitution.substitute(
|
|
request.url, environment.variables
|
|
)
|
|
all_undefined.update(url_undefined)
|
|
|
|
# Substitute headers (both keys and values)
|
|
new_headers = {}
|
|
for key, value in request.headers.items():
|
|
new_key, key_undefined = VariableSubstitution.substitute(
|
|
key, environment.variables
|
|
)
|
|
new_value, value_undefined = VariableSubstitution.substitute(
|
|
value, environment.variables
|
|
)
|
|
all_undefined.update(key_undefined)
|
|
all_undefined.update(value_undefined)
|
|
new_headers[new_key] = new_value
|
|
|
|
# Substitute body
|
|
new_body, body_undefined = VariableSubstitution.substitute(
|
|
request.body, environment.variables
|
|
)
|
|
all_undefined.update(body_undefined)
|
|
|
|
# Create new HttpRequest with substituted values
|
|
new_request = HttpRequest(
|
|
method=request.method,
|
|
url=new_url,
|
|
headers=new_headers,
|
|
body=new_body,
|
|
syntax=request.syntax
|
|
)
|
|
|
|
return new_request, all_undefined
|
|
|
|
@staticmethod
|
|
def substitute_excluding_variables(text: str, variables: Dict[str, str], excluded_vars: List[str]) -> Tuple[str, List[str]]:
|
|
"""
|
|
Replace {{variable_name}} with values, but skip excluded variables (leave as {{var}}).
|
|
|
|
Args:
|
|
text: The text containing variable placeholders
|
|
variables: Dictionary mapping variable names to their values
|
|
excluded_vars: List of variable names to skip (leave as placeholders)
|
|
|
|
Returns:
|
|
Tuple of (substituted_text, list_of_undefined_variables)
|
|
"""
|
|
if not text:
|
|
return text, []
|
|
|
|
undefined_vars = []
|
|
|
|
def replace_var(match):
|
|
var_name = match.group(1)
|
|
|
|
# Skip excluded variables - leave as {{var_name}}
|
|
if var_name in excluded_vars:
|
|
return match.group(0) # Return original {{var_name}}
|
|
|
|
# Substitute non-excluded variables
|
|
if var_name in variables:
|
|
value = variables[var_name]
|
|
# Check if value is empty/None - treat as undefined for warning purposes
|
|
if not value:
|
|
undefined_vars.append(var_name)
|
|
return ""
|
|
return value
|
|
else:
|
|
# Variable not defined - track it and replace with empty string
|
|
undefined_vars.append(var_name)
|
|
return ""
|
|
|
|
substituted = VariableSubstitution.VARIABLE_PATTERN.sub(replace_var, text)
|
|
return substituted, undefined_vars
|
|
|
|
@staticmethod
|
|
def substitute_request_excluding_sensitive(
|
|
request: HttpRequest,
|
|
environment: Environment,
|
|
sensitive_variables: List[str]
|
|
) -> Tuple[HttpRequest, Set[str], bool]:
|
|
"""
|
|
Substitute variables in request, but exclude sensitive variables (leave as {{var}}).
|
|
|
|
This is useful for saving requests to history without exposing sensitive values.
|
|
Sensitive variables remain in their {{variable_name}} placeholder form.
|
|
|
|
Args:
|
|
request: The HTTP request with variable placeholders
|
|
environment: The environment containing variable values
|
|
sensitive_variables: List of variable names to exclude from substitution
|
|
|
|
Returns:
|
|
Tuple of (redacted_request, set_of_undefined_variables, has_sensitive_vars)
|
|
- redacted_request: Request with only non-sensitive variables substituted
|
|
- set_of_undefined_variables: Variables that were referenced but not defined
|
|
- has_sensitive_vars: True if any sensitive variables were found and redacted
|
|
"""
|
|
all_undefined = set()
|
|
|
|
# Substitute URL (excluding sensitive variables)
|
|
new_url, url_undefined = VariableSubstitution.substitute_excluding_variables(
|
|
request.url, environment.variables, sensitive_variables
|
|
)
|
|
all_undefined.update(url_undefined)
|
|
|
|
# Substitute headers (both keys and values, excluding sensitive variables)
|
|
new_headers = {}
|
|
for key, value in request.headers.items():
|
|
new_key, key_undefined = VariableSubstitution.substitute_excluding_variables(
|
|
key, environment.variables, sensitive_variables
|
|
)
|
|
new_value, value_undefined = VariableSubstitution.substitute_excluding_variables(
|
|
value, environment.variables, sensitive_variables
|
|
)
|
|
all_undefined.update(key_undefined)
|
|
all_undefined.update(value_undefined)
|
|
new_headers[new_key] = new_value
|
|
|
|
# Substitute body (excluding sensitive variables)
|
|
new_body, body_undefined = VariableSubstitution.substitute_excluding_variables(
|
|
request.body, environment.variables, sensitive_variables
|
|
)
|
|
all_undefined.update(body_undefined)
|
|
|
|
# Create new HttpRequest with redacted values
|
|
redacted_request = HttpRequest(
|
|
method=request.method,
|
|
url=new_url,
|
|
headers=new_headers,
|
|
body=new_body,
|
|
syntax=request.syntax
|
|
)
|
|
|
|
# Check if any sensitive variables were actually used in the request
|
|
all_vars_in_request = set()
|
|
all_vars_in_request.update(VariableSubstitution.find_variables(request.url))
|
|
all_vars_in_request.update(VariableSubstitution.find_variables(request.body))
|
|
for key, value in request.headers.items():
|
|
all_vars_in_request.update(VariableSubstitution.find_variables(key))
|
|
all_vars_in_request.update(VariableSubstitution.find_variables(value))
|
|
|
|
# Determine if any sensitive variables were actually present
|
|
has_sensitive_vars = bool(
|
|
set(sensitive_variables) & all_vars_in_request
|
|
)
|
|
|
|
return redacted_request, all_undefined, has_sensitive_vars
|