Source code for aidputils.agents.tools.http.template_engine
"""
Template Engine for HTTP Tool.
Provides secure template variable substitution with:
- {{variable}} syntax resolution
- URL encoding for template values
- Path traversal prevention
- Character validation (reject control chars, newlines)
- Length validation
"""
import re
import urllib.parse
from typing import Any, Dict, List, Optional
# Template pattern for {{variable}} syntax
TEMPLATE_PATTERN = re.compile(r"\{\{(\w+)\}\}")
# Maximum URL length
MAX_URL_LENGTH = 2048
# Dangerous patterns for path traversal
PATH_TRAVERSAL_PATTERNS = [
"..",
"//",
"%2e%2e", # URL-encoded ..
"%2f%2f", # URL-encoded //
"%252e", # Double-encoded .
"%252f", # Double-encoded /
]
# Control characters that should be rejected
CONTROL_CHAR_PATTERN = re.compile(r"[\x00-\x1f\x7f]")
[docs]
class TemplateError(ValueError):
"""Exception raised for template-related errors."""
pass
[docs]
class SecurityError(ValueError):
"""Exception raised for security-related validation failures."""
pass
[docs]
def validate_template_value(value: str, context: str = "value") -> None:
"""
Validate a template value for security issues.
Args:
value: The value to validate
context: Description of where this value is used (for error messages)
Raises:
SecurityError: If the value contains dangerous patterns
"""
# Check for control characters
if CONTROL_CHAR_PATTERN.search(value):
raise SecurityError(
f"Template {context} contains control characters which are not allowed"
)
# Check for path traversal patterns
value_lower = value.lower()
for pattern in PATH_TRAVERSAL_PATTERNS:
if pattern in value_lower:
raise SecurityError(
f"Template {context} contains path traversal pattern '{pattern}'"
)
[docs]
def url_encode_value(value: str) -> str:
"""
URL-encode a template value for safe inclusion in URLs.
Args:
value: The value to encode
Returns:
URL-encoded value
"""
return urllib.parse.quote(str(value), safe="")
[docs]
def substitute_template(
value: Any,
runtime_params: Dict[str, Any],
url_encode: bool = False,
validate_security: bool = True
) -> Any:
"""
Recursively substitute {{variable}} placeholders in a value.
Args:
value: String, dict, list, or other value to process
runtime_params: Dictionary of variable name -> value mappings
url_encode: Whether to URL-encode substituted values
validate_security: Whether to validate values for security issues
Returns:
Value with all {{variable}} placeholders substituted
Raises:
TemplateError: If a referenced variable is not found
SecurityError: If a value contains dangerous patterns
"""
if isinstance(value, str):
def replace_match(match):
var_name = match.group(1)
if var_name not in runtime_params:
raise TemplateError(f"Missing template variable: {{{{{var_name}}}}}")
replacement = str(runtime_params[var_name])
# Validate for security issues
if validate_security:
validate_template_value(replacement, context=f"variable '{var_name}'")
# URL-encode if requested
if url_encode:
replacement = url_encode_value(replacement)
return replacement
return TEMPLATE_PATTERN.sub(replace_match, value)
elif isinstance(value, dict):
return {
k: substitute_template(v, runtime_params, url_encode, validate_security)
for k, v in value.items()
}
elif isinstance(value, list):
return [
substitute_template(item, runtime_params, url_encode, validate_security)
for item in value
]
else:
return value
[docs]
def substitute_url(url_template: str, runtime_params: Dict[str, Any]) -> str:
"""
Substitute template variables in a URL with security validation.
Template values are URL-encoded and validated for path traversal.
Args:
url_template: URL with {{variable}} placeholders
runtime_params: Dictionary of variable name -> value mappings
Returns:
URL with all placeholders substituted
Raises:
TemplateError: If a variable is missing
SecurityError: If URL is too long or contains dangerous patterns
"""
# Substitute with URL encoding and security validation
url = substitute_template(
url_template,
runtime_params,
url_encode=True,
validate_security=True
)
# Validate final URL length
if len(url) > MAX_URL_LENGTH:
raise SecurityError(
f"URL exceeds maximum length of {MAX_URL_LENGTH} characters "
f"(actual: {len(url)})"
)
return url
[docs]
def substitute_body(
body: Any,
runtime_params: Dict[str, Any]
) -> Any:
"""
Substitute template variables in request body.
Args:
body: Request body (dict, list, or string) with possible templates
runtime_params: Dictionary of variable name -> value mappings
Returns:
Body with all placeholders substituted
Raises:
TemplateError: If a variable is missing
"""
return substitute_template(
body,
runtime_params,
url_encode=False,
validate_security=False # Body content is more permissive
)