Skip to content

Commit 2fb2e13

Browse files
authored
Merge pull request #55 from kS-crane/upgrade-http-framework
Upgrade HTTP framework
2 parents 45375de + 300ef96 commit 2fb2e13

2 files changed

Lines changed: 70 additions & 11 deletions

File tree

trapster/modules/http.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,38 @@ async def sanitize_request(self, request):
160160
"path_qs": str(request.url).split(request.base_url.netloc, 1)[1],
161161
}
162162

163+
@staticmethod
164+
def make_etag_fn(deploy_seed, route_ref=None):
165+
"""Build an etag() Jinja helper bound to a deploy seed.
166+
167+
Parameters (all optional):
168+
weak, style ('iis'|'hash'), key, length, changenumber, fmt
169+
"""
170+
if route_ref is None:
171+
route_ref = ['']
172+
173+
def etag(weak=False, style='iis', key=None, length=None, changenumber=0, fmt=None):
174+
material = f"{deploy_seed}:{key if key is not None else route_ref[0]}"
175+
digest = hashlib.sha1(material.encode()).hexdigest()
176+
default_len = 32 if style == 'hash' else 13
177+
n = default_len if length is None else int(length)
178+
n = max(1, min(n, len(digest)))
179+
hexpart = digest[:n]
180+
181+
if fmt is not None:
182+
token = str(fmt).format(hex=hexpart, n=changenumber, changenumber=changenumber)
183+
value = token if '"' in token else f'"{token}"'
184+
elif style == 'hash':
185+
value = f'"{hexpart}"'
186+
else:
187+
value = f'"{hexpart}:{changenumber}"'
188+
189+
if weak:
190+
value = f'W/{value}'
191+
return value
192+
193+
return etag
194+
163195
def _resolve_deploy_config(self):
164196
"""Evaluate Jinja expressions in config.yaml once at startup.
165197
@@ -172,11 +204,14 @@ def _resolve_deploy_config(self):
172204
live as Jinja expressions instead of hardcoded strings.
173205
"""
174206
deploy_seed = self.config.get('deploy_seed') or secrets.token_hex(8)
207+
route_ref = ['']
175208

176209
eval_env = ImmutableSandboxedEnvironment(autoescape=False)
177210
eval_env.globals['random'] = self.random_filter
178211
eval_env.globals['deploy_seed'] = deploy_seed
179212
eval_env.globals['md5'] = lambda s: hashlib.md5(str(s).encode()).hexdigest()
213+
eval_env.globals['etag'] = self.make_etag_fn(deploy_seed, route_ref)
214+
eval_env.globals['route'] = ''
180215

181216
def evaluate(value):
182217
if isinstance(value, str) and ('{{' in value or '{%' in value):
@@ -207,7 +242,9 @@ def evaluate(value):
207242
self.http_config['headers'] = evaluate(self.http_config['headers'])
208243

209244
for endpoint in self.http_config.get('endpoints', []):
210-
for details in endpoint.values():
245+
for route, details in endpoint.items():
246+
route_ref[0] = route
247+
eval_env.globals['route'] = route
211248
if not isinstance(details, list):
212249
details = [details]
213250
for detail in details:
@@ -236,6 +273,8 @@ def create_jinja_env(self):
236273
'get_current_time': lambda: datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT'),
237274
'uuid': lambda: str(uuid.uuid4()),
238275
'vars': self.http_config.get('vars', {}),
276+
'etag': getattr(self, '_etag_fn', self.make_etag_fn(getattr(self, '_deploy_seed', ''))),
277+
'deploy_seed': getattr(self, '_deploy_seed', ''),
239278
})
240279
env.filters['quote'] = lambda s: quote(str(s), safe='')
241280
env.undefined = Undefined
@@ -294,6 +333,12 @@ def parse_front_matter(self, content):
294333

295334
# --- content / response building ---------------------------------------
296335

336+
# Suffixes served as Jinja text templates from templates/.
337+
_JINJA_FILE_SUFFIXES = frozenset({
338+
'.html', '.htm', '.css', '.js', '.json', '.xml', '.txt', '.svg',
339+
'.csv', '.md', '.map',
340+
})
341+
297342
async def get_content(self, endpoint_config, request=None):
298343
if not endpoint_config:
299344
return "", 200
@@ -306,12 +351,18 @@ async def get_content(self, endpoint_config, request=None):
306351
try:
307352
# Guard against path traversal outside the template folder.
308353
file_path.relative_to(self.template_folder.resolve())
354+
if not file_path.is_file():
355+
raise FileNotFoundError(file_path)
356+
status = int(endpoint_config.get('status_code', 200))
357+
# Binary assets (fonts, icons, …) — raw bytes, no Jinja.
358+
if file_path.suffix.lower() not in self._JINJA_FILE_SUFFIXES:
359+
return file_path.read_bytes(), status
309360
raw_content = file_path.read_text()
310361
template = self.env.from_string(raw_content)
311362
template.globals['request'] = await self.sanitize_request(request)
312363
metadata, body = self.parse_front_matter(template.render())
313-
return body, int(metadata.get('status_code', 200))
314-
except (ValueError, FileNotFoundError) as e:
364+
return body, int(metadata.get('status_code', status))
365+
except (ValueError, FileNotFoundError, UnicodeDecodeError) as e:
315366
print(f"Error: {e}")
316367

317368
elif 'ai' in endpoint_config:
@@ -502,13 +553,15 @@ async def handle_static_file(self, request):
502553
# paths (../, symlinks, encoded traversal, NUL bytes) cannot escape the
503554
# skin's files/ directory (LFI). Anything outside falls through to the
504555
# normal default response, so a probe looks like an ordinary 404.
505-
static_root = self.static_folder.resolve()
506556
try:
507-
file_path = (static_root / rel).resolve()
508-
file_path.relative_to(static_root)
557+
static_root = self.static_folder.resolve()
558+
base = self.static_folder.resolve()
559+
file_path = (self.static_folder / rel).resolve()
560+
file_path.relative_to(base)
509561
if file_path.is_file():
510562
content = file_path.read_bytes()
511-
content_type = mimetypes.guess_type(str(file_path))[0] or 'application/octet-stream'
563+
content_type = (mimetypes.guess_type(str(file_path))[0]
564+
or 'application/octet-stream')
512565
return content, 200, {'Content-Type': content_type}
513566
except (ValueError, OSError):
514567
pass

trapster/modules/https.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class HttpsHoneypot(HttpHoneypot):
2424
def __init__(self, config, logger, bindaddr="0.0.0.0"):
2525
super().__init__(config, logger, bindaddr)
2626
self.handler = HttpsHandler(config=config, logger=logger)
27+
# Check if the user has set the TLS key and certificate
28+
self.user_set_tls = config.get("key") and config.get("certificate")
2729
config.setdefault("country_name", None)
2830
config.setdefault("state_or_province_name", None)
2931
config.setdefault("locality_name", None)
@@ -59,11 +61,15 @@ async def _start_server(self):
5961

6062
def generate_certificate(self):
6163
'''
62-
Regenerate the certificate at each startup to ensure the configuration values are applied and reflected.
64+
Use the configured key/certificate files when both already exist.
65+
Otherwise generate a self-signed pair (and write it to those paths).
6366
'''
64-
#if self.certificate_path.exists() and self.key_path.exists():
65-
# return
66-
#else:
67+
if self.user_set_tls:
68+
if self.certificate_path.is_file() and self.key_path.is_file():
69+
return
70+
else:
71+
raise ValueError(f"HTTPS key/certificate configured but missing: key={self.key_path} certificate={self.certificate_path}")
72+
6773
self.key_path.parent.mkdir(parents=True, exist_ok=True)
6874
self.certificate_path.parent.mkdir(parents=True, exist_ok=True)
6975

0 commit comments

Comments
 (0)