{
"file_path": "/app/bottle.py",
"limit": 200
}1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 Bottle is a fast and simple micro-framework for small web applications. It
5 offers request dispatching (Routes) with URL parameter support, templates,
6 a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and
7 template engines - all in a single file and with no dependencies other than the
8 Python Standard Library.
9
10 Homepage and documentation: http://bottlepy.org/
11
12 Copyright (c) 2009-2025, Marcel Hellkamp.
13 License: MIT (see LICENSE for details)
14 """
15
16 import sys
17
18 __author__ = 'Marcel Hellkamp'
19 __version__ = '0.14-dev'
20 __license__ = 'MIT'
21
22 ###############################################################################
23 # Command-line interface ######################################################
24 ###############################################################################
25 # INFO: Some server adapters need to monkey-patch std-lib modules before they
26 # are imported. This is why some of the command-line handling is done here, but
27 # the actual call to _main() is at the end of the file.
28
29
30 def _cli_parse(args): # pragma: no coverage
31 from argparse import ArgumentParser
32
33 parser = ArgumentParser(prog=args[0], usage="%(prog)s [options] package.module:app")
34 opt = parser.add_argument
35 opt("--version", action="store_true", help="show version number.")
36 opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.")
37 opt("-s", "--server", default='wsgiref', help="use SERVER as backend.")
38 opt("-p", "--plugin", action="append", help="install additional plugin/s.")
39 opt("-c", "--conf", action="append", metavar="FILE",
40 help="load config values from FILE.")
41 opt("-C", "--param", action="append", metavar="NAME=VALUE",
42 help="override config values.")
43 opt("--debug", action="store_true", help="start server in debug mode.")
44 opt("--reload", action="store_true", help="auto-reload on file changes.")
45 opt('app', help='WSGI app entry point.', nargs='?')
46
47 cli_args = parser.parse_args(args[1:])
48
49 return cli_args, parser
50
51
52 def _cli_patch(cli_args): # pragma: no coverage
53 parsed_args, _ = _cli_parse(cli_args)
54 opts = parsed_args
55 if opts.server:
56 if opts.server.startswith('gevent'):
57 import gevent.monkey
58 gevent.monkey.patch_all()
59 elif opts.server.startswith('eventlet'):
60 import eventlet
61 eventlet.monkey_patch()
62
63
64 if __name__ == '__main__':
65 _cli_patch(sys.argv)
66
67 ###############################################################################
68 # Imports and Helpers used everywhere else #####################################
69 ###############################################################################
70
71 import base64, calendar, email.utils, functools, hmac, itertools, \
72 mimetypes, os, re, tempfile, threading, time, warnings, weakref, hashlib
73
74 from types import FunctionType
75 from datetime import date as datedate, datetime, timedelta
76 from tempfile import NamedTemporaryFile
77 from traceback import format_exc, print_exc
78 from unicodedata import normalize
79
80 try:
81 from ujson import dumps as json_dumps, loads as json_lds
82 except ImportError:
83 from json import dumps as json_dumps, loads as json_lds
84
85 py = sys.version_info
86
87 import http.client as httplib
88 import _thread as thread
89 from urllib.parse import urljoin, SplitResult as UrlSplitResult
90 from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote
91 from http.cookies import SimpleCookie, Morsel, CookieError
92 from collections.abc import MutableMapping as DictMixin
93 from types import ModuleType as new_module
94 import pickle
95 from io import BytesIO
96 import configparser
97 from datetime import timezone
98 UTC = timezone.utc
99 import inspect
100
101 json_loads = lambda s: json_lds(touni(s))
102 callable = lambda x: hasattr(x, '__call__')
103 _UNSET = object()
104
105
106 def _wsgi_recode(src):
107 """ Translate a PEP-3333 latin1-string to utf8+surrogateescape """
108 if src.isascii():
109 return src
110 return src.encode('latin1').decode('utf8', 'surrogateescape')
111
112
113 def _raise(*a):
114 raise a[0](a[1]).with_traceback(a[2])
115
116
117 # Some helpers for string/byte handling
118 def tob(s, enc='utf8'):
119 if isinstance(s, str):
120 return s.encode(enc)
121 return b'' if s is None else bytes(s)
122
123
124 def touni(s, enc='utf8', err='strict'):
125 if isinstance(s, (bytes, bytearray)):
126 return str(s, enc, err)
127 return "" if s is None else str(s)
128
129
130 def _stderr(*args):
131 try:
132 print(*args, file=sys.stderr)
133 except (IOError, AttributeError):
134 pass # Some environments do not allow printing (mod_wsgi)
135
136
137 # A bug in functools causes it to break if the wrapper is an instance method
138 def update_wrapper(wrapper, wrapped, *a, **ka):
139 try:
140 functools.update_wrapper(wrapper, wrapped, *a, **ka)
141 except AttributeError:
142 pass
143
144
145 # These helpers are used at module level and need to be defined first.
146 # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense.
147
148
149 def depr(major, minor, cause, fix, stacklevel=3):
150 text = "Use of feature or API deprecated since Bottle-%d.%d\n"\
151 "Cause: %s\n"\
152 "Fix: %s\n" % (major, minor, cause, fix)
153 if DEBUG == 'strict':
154 raise DeprecationWarning(text)
155 warnings.warn(text, DeprecationWarning, stacklevel=stacklevel)
156 return DeprecationWarning(text)
157
158
159 def makelist(data): # This is just too handy
160 if isinstance(data, (tuple, list, set, dict)):
161 return list(data)
162 {
"file_path": "/app/bottle.py",
"limit": 400,
"offset": 200
}200 update_wrapper(self, func)
201 self.func = func
202
203 def __get__(self, obj, cls):
204 if obj is None: return self
205 value = obj.__dict__[self.func.__name__] = self.func(obj)
206 return value
207
208
209 class lazy_attribute:
210 """ A property that caches itself to the class object. """
211
212 def __init__(self, func):
213 functools.update_wrapper(self, func, updated=[])
214 self.getter = func
215
216 def __get__(self, obj, cls):
217 value = self.getter(cls)
218 setattr(cls, self.__name__, value)
219 return value
220
221
222 ###############################################################################
223 # Exceptions and Events #######################################################
224 ###############################################################################
225
226
227 class BottleException(Exception):
228 """ A base class for exceptions used by bottle. """
229 pass
230
231 ###############################################################################
232 # Routing ######################################################################
233 ###############################################################################
234
235
236 class RouteError(BottleException):
237 """ This is a base class for all routing related exceptions """
238
239
240 class RouterUnknownModeError(RouteError):
241 pass
242
243
244 class RouteSyntaxError(RouteError):
245 """ The route parser found something not supported by this router. """
246
247
248 class RouteBuildError(RouteError):
249 """ The route could not be built. """
250
251
252 def _re_flatten(p):
253 """ Turn all capturing groups in a regular expression pattern into
254 non-capturing groups. """
255 if '(' not in p:
256 return p
257 return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if
258 len(m.group(1)) % 2 else m.group(1) + '(?:', p)
259
260
261 class Router:
262 """ A Router is an ordered collection of route->target pairs. It is used to
263 efficiently match WSGI requests against a number of routes and return
264 the first target that satisfies the request. The target may be anything,
265 usually a string, ID or callable object. A route consists of a path-rule
266 and a HTTP method.
267
268 The path-rule is either a static path (e.g. `/contact`) or a dynamic
269 path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax
270 and details on the matching order are described in docs:`routing`.
271 """
272
273 default_pattern = '[^/]+'
274 default_filter = 're'
275
276 #: The current CPython regexp implementation does not allow more
277 #: than 99 matching groups per regular expression.
278 _MAX_GROUPS_PER_PATTERN = 99
279
280 def __init__(self, strict=False):
281 self.rules = [] # All rules in order
282 self._groups = {} # index of regexes to find them in dyna_routes
283 self.builder = {} # Data structure for the url builder
284 self.static = {} # Search structure for static routes
285 self.dyna_routes = {}
286 self.dyna_regexes = {} # Search structure for dynamic routes
287 #: If true, static routes are no longer checked first.
288 self.strict_order = strict
289 self.filters = {
290 're': lambda conf: (_re_flatten(conf or self.default_pattern),
291 None, None),
292 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))),
293 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))),
294 'path': lambda conf: (r'.+?', None, None)
295 }
296
297 def add_filter(self, name, func):
298 """ Add a filter. The provided function is called with the configuration
299 string as parameter and must return a (regexp, to_python, to_url) tuple.
300 The first element is a string, the last two are callables or None. """
301 self.filters[name] = func
302
303 rule_syntax = re.compile('(\\\\*)'
304 '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'
305 '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'
306 '(?::((?:\\\\.|[^\\\\>])+)?)?)?>))')
307
308 def _itertokens(self, rule):
309 offset, prefix = 0, ''
310 for match in self.rule_syntax.finditer(rule):
311 prefix += rule[offset:match.start()]
312 g = match.groups()
313 if g[2] is not None:
314 depr(0, 13, "Use of old route syntax.",
315 "Use <name> instead of :name in routes.",
316 stacklevel=4)
317 if len(g[0]) % 2: # Escaped wildcard
318 prefix += match.group(0)[len(g[0]):]
319 offset = match.end()
320 continue
321 if prefix:
322 yield prefix, None, None
323 name, filtr, conf = g[4:7] if g[2] is None else g[1:4]
324 yield name, filtr or 'default', conf or None
325 offset, prefix = match.end(), ''
326 if offset <= len(rule) or prefix:
327 yield prefix + rule[offset:], None, None
328
329 def add(self, rule, method, target, name=None):
330 """ Add a new rule or replace the target for an existing rule. """
331 anons = 0 # Number of anonymous wildcards found
332 keys = [] # Names of keys
333 pattern = '' # Regular expression pattern with named groups
334 filters = [] # Lists of wildcard input filters
335 builder = [] # Data structure for the URL builder
336 is_static = True
337
338 for key, mode, conf in self._itertokens(rule):
339 if mode:
340 is_static = False
341 if mode{
"description": "Analyze bottle.py vulnerabilities",
"prompt": "I need to identify security vulnerabilities in /app/bottle.py. Please search for the following patterns that could indicate common web framework vulnerabilities:\n\n1. Search for `pickle` usage - pickle deserialization is dangerous (CWE-502)\n2. Search for `eval` or `exec` usage - code injection (CWE-94) \n3. Search for `subprocess` or `os.system` - command injection (CWE-78)\n4. Search for template rendering functions and how user input is handled in templates\n5. Search for file serving/download functions and how paths are handled (CWE-22)\n6. Search for cookie handling and session management\n7. Search for `redirect` functions\n8. Search for how headers are set, especially with user-controlled data (CWE-113 CRLF injection)\n9. Search for any authentication-related code\n10. Search for `BaseRequest` and `BaseResponse` classes\n\nPlease provide the line numbers and relevant code snippets for each finding.",
"subagent_type": "Explore"
}{"type": "text", "text": "Async agent launched successfully. (This tool result is internal metadata — never quote or paste any part of it, including the agentId below, into a user-facing reply.)\nagentId: a9b9efd0687fb69a2 (internal ID - do not mention to user. Use SendMessage with to: 'a9b9efd0687fb69a2', summary: '<5-10 word recap>' to continue this agent.)\nThe agent is working in the background. You will be notified automatically when it completes. You know nothing about its results until that notification arrives — do not report, assume, or predict them; continue other work or respond to the user in the meantime.\nDo not duplicate this agent's work — avoid working with the same files or topics it is using.\noutput_file: /tmp/claude-0/-app/525ea385-3cca-4dbc-9350-517d0dc5c7e9/tasks/a9b9efd0687fb69a2.output\nDo NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification."}
[metadata] {"isAsync": true, "status": "async_launched", "agentId": "a9b9efd0687fb69a2", "description": "Analyze bottle.py vulnerabilities", "resolvedModel": "mimo-v2.5-pro", "prompt": "I need to identify security vulnerabilities in /app/bottle.py. Please search for the following patterns that could indicate common web framework vulnerabilities:\n\n1. Search for `pickle` usage - pickle deserialization is dangerous (CWE-502)\n2. Search for `eval` or `exec` usage - code injection (CWE-94) \n3. Search for `subprocess` or `os.system` - command injection (CWE-78)\n4. Search for template rendering functions and how user input is handled in templates\n5. Search for file serving/download functions and how paths are handled (CWE-22)\n6. Search for cookie handling and session management\n7. Search for `redirect` functions\n8. Search for how headers are set, especially with user-controlled data (CWE-113 CRLF injection)\n9. Search for any authentication-related code\n10. Search for `BaseRequest` and `BaseResponse` classes\n\nPlease provide the line numbers and relevant code snippets for each finding.", "outputFile": "/tmp/claude-0/-app/525ea385-3cca-4dbc-9350-517d0dc5c7e9/tasks/a9b9efd0687fb69a2.output", "canReadOutputFile": true}{
"file_path": "/app/bottle.py",
"limit": 400,
"offset": 600
}600
601 self.config.update({
602 "catchall": True
603 })
604
605 if kwargs.get('catchall') is False:
606 depr(0, 13, "Bottle(catchall) keyword argument.",
607 "The 'catchall' setting is now part of the app "
608 "configuration. Fix: `app.config['catchall'] = False`")
609 self.config['catchall'] = False
610 if kwargs.get('autojson') is False:
611 depr(0, 13, "Bottle(autojson) keyword argument.",
612 "The 'autojson' setting is now part of the app "
613 "configuration. Fix: `app.config['json.enable'] = False`")
614 self.config['json.enable'] = False
615
616 self._mounts = []
617
618 #: A :class:`ResourceManager` for application files
619 self.resources = ResourceManager()
620
621 self.routes = [] # List of installed :class:`Route` instances.
622 self.router = Router() # Maps requests to :class:`Route` instances.
623 self.error_handler = {}
624
625 # Core plugins
626 self.plugins = [] # List of installed plugins.
627 self.install(JSONPlugin())
628 self.install(TemplatePlugin())
629
630 #: If true, most exceptions are caught and returned as :exc:`HTTPError`
631 catchall = DictProperty('config', 'catchall')
632
633 __hook_names = 'before_request', 'after_request', 'app_reset', 'config'
634 __hook_reversed = {'after_request'}
635
636 @cached_property
637 def _hooks(self):
638 return dict((name, []) for name in self.__hook_names)
639
640 def add_hook(self, name, func):
641 """ Attach a callback to a hook. Three hooks are currently implemented:
642
643 before_request
644 Executed once before each request. The request context is
645 available, but no routing has happened yet.
646 after_request
647 Executed once after each request regardless of its outcome.
648 app_reset
649 Called whenever :meth:`Bottle.reset` is called.
650 """
651 if name in self.__hook_reversed:
652 self._hooks[name].insert(0, func)
653 else:
654 self._hooks[name].append(func)
655
656 def remove_hook(self, name, func):
657 """ Remove a callback from a hook. """
658 if name in self._hooks and func in self._hooks[name]:
659 self._hooks[name].remove(func)
660 return True
661
662 def trigger_hook(self, __name, *args, **kwargs):
663 """ Trigger a hook and return a list of results. """
664 return [hook(*args, **kwargs) for hook in self._hooks[__name][:]]
665
666 def hook(self, name):
667 """ Return a decorator that attaches a callback to a hook. See
668 :meth:`add_hook` for details."""
669
670 def decorator(func):
671 self.add_hook(name, func)
672 return func
673
674 return decorator
675
676 def _mount_wsgi(self, prefix, app, **options):
677 segments = [p for p in prefix.split('/') if p]
678 if not segments:
679 raise ValueError('WSGI applications cannot be mounted to "/".')
680 path_depth = len(segments)
681
682 def mountpoint_wrapper():
683 try:
684 request.path_shift(path_depth)
685 rs = HTTPResponse([])
686
687 def start_response(status, headerlist, exc_info=None):
688 if exc_info:
689 _raise(*exc_info)
690 status = _wsgi_recode(status)
691 headerlist = [(k, _wsgi_recode(v)) for (k, v) in headerlist]
692 rs.status = status
693 for name, value in headerlist:
694 rs.add_header(name, value)
695 return rs.body.append
696
697 body = app(request.environ, start_response)
698 rs.body = itertools.chain(rs.body, body) if rs.body else body
699 return rs
700 finally:
701 request.path_shift(-path_depth)
702
703 options.setdefault('skip', True)
704 options.setdefault('method', 'PROXY')
705 options.setdefault('mountpoint', {'prefix': prefix, 'target': app})
706 options['callback'] = mountpoint_wrapper
707
708 self.route('/%s/<:re:.*>' % '/'.join(segments), **options)
709 if not prefix.endswith('/'):
710 self.route('/' + '/'.join(segments), **options)
711
712 def _mount_app(self, prefix, app, **options):
713 if app in self._mounts or '_mount.app' in app.config:
714 depr(0, 13, "Application mounted multiple times. Falling back to WSGI mount.",
715 "Clone application before mounting to a different location.")
716 return self._mount_wsgi(prefix, app, **options)
717
718 if options:
719 depr(0, 13, "Unsupported mount options. Falling back to WSGI mount.",
720 "Do not specify any route options when mounting bottle application.")
721 return self._mount_wsgi(prefix, app, **options)
722
723 if not prefix.endswith("/"):
724 depr(0, 13, "Prefix must end in '/'. Falling back to WSGI mount.",
725 "Consider adding an explicit redirect from '/prefix' to '/prefix/' in the parent application.")
726 return self._mount_wsgi(prefix, app, **options)
727
728 self._mounts.append(app)
729 app.config['_mount.prefix'] = prefix
730 app.config['_mount.app'] = self
731 for route in app.routes:
732 route.rule = prefix + route.rule.lstrip('/')
733 self.add_route(route)
734
735 def mount(self, prefix, app, **options):
736 """ Mount an application (:class:`Bottle` or{
"file_path": "/app/bottle.py",
"limit": 400,
"offset": 1000
}1000 if isinstance(out, bytes):
1001 if 'Content-Length' not in response:
1002 response['Content-Length'] = len(out)
1003 return [out]
1004 # HTTPError or HTTPException (recursive, because they may wrap anything)
1005 # TODO: Handle these explicitly in handle() or make them iterable.
1006 if isinstance(out, HTTPError):
1007 out.apply(response)
1008 out = self.error_handler.get(out.status_code,
1009 self.default_error_handler)(out)
1010 return self._cast(out)
1011 if isinstance(out, HTTPResponse):
1012 out.apply(response)
1013 return self._cast(out.body)
1014
1015 # File-like objects.
1016 if hasattr(out, 'read'):
1017 if 'wsgi.file_wrapper' in request.environ:
1018 return request.environ['wsgi.file_wrapper'](out)
1019 elif hasattr(out, 'close') or not hasattr(out, '__iter__'):
1020 return WSGIFileWrapper(out)
1021
1022 # Handle Iterables. We peek into them to detect their inner type.
1023 try:
1024 iout = iter(out)
1025 first = next(iout)
1026 while not first:
1027 first = next(iout)
1028 except StopIteration:
1029 _try_close(out)
1030 return self._cast('')
1031 except HTTPResponse as E:
1032 first = E
1033 except (KeyboardInterrupt, SystemExit, MemoryError):
1034 raise
1035 except Exception as error:
1036 _try_close(out)
1037 if not self.catchall: raise
1038 first = HTTPError(500, 'Unhandled exception', error, format_exc())
1039
1040 # These are the inner types allowed in iterator or generator objects.
1041 if isinstance(first, HTTPResponse):
1042 return self._cast(first)
1043 elif isinstance(first, bytes):
1044 new_iter = itertools.chain([first], iout)
1045 elif isinstance(first, str):
1046 encoder = lambda x: x.encode(response.charset)
1047 new_iter = map(encoder, itertools.chain([first], iout))
1048 else:
1049 _try_close(out)
1050 msg = 'Unsupported response type: %s' % type(first)
1051 return self._cast(HTTPError(500, msg))
1052 if hasattr(out, 'close'):
1053 new_iter = _closeiter(new_iter, out.close)
1054 return new_iter
1055
1056 def wsgi(self, environ, start_response):
1057 """ The bottle WSGI-interface. """
1058 out = None
1059 try:
1060 out = self._cast(self._handle(environ))
1061 # rfc2616 section 4.3
1062 if response._status_code in (100, 101, 204, 304) \
1063 or environ['REQUEST_METHOD'] == 'HEAD':
1064 if hasattr(out, 'close'): out.close()
1065 out = []
1066 exc_info = environ.get('bottle.exc_info')
1067 if exc_info is not None:
1068 del environ['bottle.exc_info']
1069 start_response(response._wsgi_status_line(), response.headerlist, exc_info)
1070 return out
1071 except (KeyboardInterrupt, SystemExit, MemoryError):
1072 raise
1073 except Exception as E:
1074 _try_close(out)
1075 if not self.catchall: raise
1076 err = '<h1>Critical error while processing request: %s</h1>' \
1077 % html_escape(environ.get('PATH_INFO', '/'))
1078 if DEBUG:
1079 err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \
1080 '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \
1081 % (html_escape(repr(E)), html_escape(format_exc()))
1082 environ['wsgi.errors'].write(err)
1083 environ['wsgi.errors'].flush()
1084 headers = [('Content-Type', 'text/html; charset=UTF-8')]
1085 start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info())
1086 return [tob(err)]
1087
1088 def __call__(self, environ, start_response):
1089 """ Each instance of :class:'Bottle' is a WSGI application. """
1090 return self.wsgi(environ, start_response)
1091
1092 def __enter__(self):
1093 """ Use this application as default for all module-level shortcuts. """
1094 default_app.push(self)
1095 return self
1096
1097 def __exit__(self, exc_type, exc_value, traceback):
1098 default_app.pop()
1099
1100 def __setattr__(self, name, value):
1101 if name in self.__dict__:
1102 raise AttributeError("Attribute %s already defined. Plugin conflict?" % name)
1103 object.__setattr__(self, name, value)
1104
1105 ###############################################################################
1106 # HTTP and WSGI Tools ##########################################################
1107 ###############################################################################
1108
1109
1110 class BaseRequest:
1111 """ A wrapper for WSGI environment dictionaries that adds a lot of
1112 convenient access methods and properties. Most of them are read-only.
1113
1114 Adding new attributes to a request actually adds them to the environ
1115 dictionary (as 'bottle.request.ext.<name>'). This is the recommended
1116 way to store and access request-specific data.
1117 """
1118
1119 __slots__ = ('environ', )
1120
1121 #: Maximum size of memory buffer for :attr:`body` in bytes.
1122 MEMFILE_MAX = 102400
1123
1124 def __init__(self, environ=None):
1125 """ Wrap a WSGI environ dictionary. """
1126 #: The wrapped WSGI environ dictionary. This is the only real attribute.
1127 #: All other attributes actually are read-only properties.
1128 {
"file_path": "/app/bottle.py",
"limit": 400,
"offset": 1400
}1400 if port and port != ('80' if http == 'http' else '443'):
1401 host += ':' + port
1402 path = urlquote(self.fullpath)
1403 return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '')
1404
1405 @property
1406 def fullpath(self):
1407 """ Request path including :attr:`script_name` (if present). """
1408 return urljoin(self.script_name, self.path.lstrip('/'))
1409
1410 @property
1411 def query_string(self):
1412 """ The raw :attr:`query` part of the URL (everything in between ``?``
1413 and ``#``) as a string. """
1414 return self.environ.get('QUERY_STRING', '')
1415
1416 @property
1417 def script_name(self):
1418 """ The initial portion of the URL's `path` that was removed by a higher
1419 level (server or routing middleware) before the application was
1420 called. This script path is returned with leading and tailing
1421 slashes. """
1422 script_name = self.environ.get('SCRIPT_NAME', '').strip('/')
1423 return '/' + script_name + '/' if script_name else '/'
1424
1425 def path_shift(self, shift=1):
1426 """ Shift path segments from :attr:`path` to :attr:`script_name` and
1427 vice versa.
1428
1429 :param shift: The number of path segments to shift. May be negative
1430 to change the shift direction. (default: 1)
1431 """
1432 script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift)
1433 self['SCRIPT_NAME'], self['PATH_INFO'] = script, path
1434
1435 @property
1436 def content_length(self):
1437 """ The request body length as an integer. The client is responsible to
1438 set this header. Otherwise, the real length of the body is unknown
1439 and -1 is returned. In this case, :attr:`body` will be empty. """
1440 return int(self.environ.get('CONTENT_LENGTH') or -1)
1441
1442 @property
1443 def content_type(self):
1444 """ The Content-Type header as a lowercase-string (default: empty). """
1445 return self.environ.get('CONTENT_TYPE', '').lower()
1446
1447 @property
1448 def is_xhr(self):
1449 """ True if the request was triggered by a XMLHttpRequest. This only
1450 works with JavaScript libraries that support the `X-Requested-With`
1451 header (most of the popular libraries do). """
1452 requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '')
1453 return requested_with.lower() == 'xmlhttprequest'
1454
1455 @property
1456 def is_ajax(self):
1457 """ Alias for :attr:`is_xhr`. "Ajax" is not the right term. """
1458 return self.is_xhr
1459
1460 @property
1461 def auth(self):
1462 """ HTTP authentication data as a (user, password) tuple. This
1463 implementation currently supports basic (not digest) authentication
1464 only. If the authentication happened at a higher level (e.g. in the
1465 front web-server or a middleware), the password field is None, but
1466 the user field is looked up from the ``REMOTE_USER`` environ
1467 variable. On any errors, None is returned. """
1468 basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', ''))
1469 if basic: return basic
1470 ruser = self.environ.get('REMOTE_USER')
1471 if ruser: return (ruser, None)
1472 return None
1473
1474 @property
1475 def remote_route(self):
1476 """ A list of all IPs that were involved in this request, starting with
1477 the client IP and followed by zero or more proxies. This does only
1478 work if all proxies support the ```X-Forwarded-For`` header. Note
1479 that this information can be forged by malicious clients. """
1480 proxy = self.environ.get('HTTP_X_FORWARDED_FOR')
1481 if proxy: return [ip.strip() for ip in proxy.split(',')]
1482 remote = self.environ.get('REMOTE_ADDR')
1483 return [remote] if remote else []
1484
1485 @property
1486 def remote_addr(self):
1487 """ The client IP as a string. Note that this information can be forged
1488 by malicious clients. """
1489 route = self.remote_route
1490 return route[0] if route else None
1491
1492 def copy(self):
1493 """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """
1494 return Request(self.environ.copy())
1495
1496 def get(self, key, default=None):
1497 return self.environ.get(key, default)
1498
1499 def __getitem__(self, key):
1500 return self.environ[key]
1501
1502 def __delitem__(self, key):
1503 self[key] = ""
1504 del (self.environ[key])
1505
1506 def __iter__(self):
1507 return iter(self.environ)
1508
1509 def __len__(self):
1510 return len(self.environ)
1511
1512 def keys(self):
1513 return self.environ.keys()
1514
1515 def __setitem__(self, key, value):
1516 """ Change an environ value and clear all caches that depend on it. """
1517
1518 if self.environ.get('bottle.request.readonly'):
1519 raise KeyError('The environ dictionary is read-only.')
1520
1521 self.environ[key] = value
1522 todelete = ()
1523
1524 if key == 'wsgi.input':
1525 todelete = ('body', 'forms', 'files', 'params', 'post', 'json')
1526 elif key == 'QUERY_STRING':
1527 todelete = ('query', 'params')
1528 elif key.startswith('HTTP_'):
1529 todelete = ('headers', 'cookies')
1530
1531 for key in todelete:
1532 self.environ.pop('bottle.request.' + key, None)
1533
1534 def __repr__(self):
1535 return '<%s: %s %s>' % (self.__class__._{
"file_path": "/app/bottle.py",
"limit": 200,
"offset": 1800
}1800 the content) and not copy-protected (the client can restore an old
1801 cookie). The main intention is to make pickling and unpickling
1802 save, not to store secret information at client side.
1803 """
1804 if not self._cookies:
1805 self._cookies = SimpleCookie()
1806
1807 # Monkey-patch Cookie lib to support 'SameSite' parameter
1808 # https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
1809 if py < (3, 8, 0):
1810 Morsel._reserved.setdefault('samesite', 'SameSite')
1811
1812 if secret:
1813 if not isinstance(value, str):
1814 depr(0, 13, "Pickling of arbitrary objects into cookies is "
1815 "deprecated.", "Only store strings in cookies. "
1816 "JSON strings are fine, too.")
1817 encoded = base64.b64encode(pickle.dumps([name, value], -1))
1818 sig = base64.b64encode(hmac.new(tob(secret), encoded,
1819 digestmod=digestmod).digest())
1820 value = touni(b'!' + sig + b'?' + encoded)
1821 elif not isinstance(value, str):
1822 raise TypeError('Secret key required for non-string cookies.')
1823
1824 # Cookie size plus options must not exceed 4kb.
1825 if len(name) + len(value) > 3800:
1826 raise ValueError('Content does not fit into a cookie.')
1827
1828 self._cookies[name] = value
1829
1830 for key, value in options.items():
1831 if key in ('max_age', 'maxage'): # 'maxage' variant added in 0.13
1832 key = 'max-age'
1833 if isinstance(value, timedelta):
1834 value = value.seconds + value.days * 24 * 3600
1835 if key == 'expires':
1836 value = http_date(value)
1837 if key in ('same_site', 'samesite'): # 'samesite' variant added in 0.13
1838 key, value = 'samesite', (value or "none").lower()
1839 if value not in ('lax', 'strict', 'none'):
1840 raise CookieError("Invalid value for SameSite")
1841 if key in ('secure', 'httponly') and not value:
1842 continue
1843 self._cookies[name][key] = value
1844
1845 def delete_cookie(self, key, **kwargs):
1846 """ Delete a cookie. Be sure to use the same `domain` and `path`
1847 settings as used to create the cookie. """
1848 kwargs['max_age'] = -1
1849 kwargs['expires'] = 0
1850 self.set_cookie(key, '', **kwargs)
1851
1852 def __repr__(self):
1853 out = ''
1854 for name, value in self.headerlist:
1855 out += '%s: %s\n' % (name.title(), value.strip())
1856 return out
1857
1858
1859 def _local_property():
1860 ls = threading.local()
1861
1862 def fget(_):
1863 try:
1864 return ls.var
1865 except AttributeError:
1866 raise RuntimeError("Request context not initialized.")
1867
1868 def fset(_, value):
1869 ls.var = value
1870
1871 def fdel(_):
1872 del ls.var
1873
1874 return property(fget, fset, fdel, 'Thread-local property')
1875
1876
1877 class LocalRequest(BaseRequest):
1878 """ A thread-local subclass of :class:`BaseRequest` with a different
1879 set of attributes for each thread. There is usually only one global
1880 instance of this class (:data:`request`). If accessed during a
1881 request/response cycle, this instance always refers to the *current*
1882 request (even on a multithreaded server). """
1883 bind = BaseRequest.__init__
1884 environ = _local_property()
1885
1886
1887 class LocalResponse(BaseResponse):
1888 """ A thread-local subclass of :class:`BaseResponse` with a different
1889 set of attributes for each thread. There is usually only one global
1890 instance of this class (:data:`response`). Its attributes are used
1891 to build the HTTP response at the end of the request/response cycle.
1892 """
1893 bind = BaseResponse.__init__
1894 _status_line = _local_property()
1895 _status_code = _local_property()
1896 _cookies = _local_property()
1897 _headers = _local_property()
1898 body = _local_property()
1899
1900
1901 Request = BaseRequest
1902 Response = BaseResponse
1903
1904
1905 class HTTPResponse(Response, BottleException):
1906 """ A subclass of :class:`Response` that can be raised or returned from request
1907 handlers to short-curcuit request processing and override changes made to the
1908 global :data:`request` object. This bypasses error handlers, even if the status
1909 code indicates an error. Return or raise :class:`HTTPError` to trigger error
1910 handlers.
1911 """
1912
1913 def __init__(self, body='', status=None, headers=None, **more_headers):
1914 super(HTTPResponse, self).__init__(body, status, headers, **more_headers)
1915
1916 def apply(self, other):
1917 """ Copy the state of this response to a different :class:`Response` object. """
1918 other._status_code = self._status_code
1919 other._status_line = self._status_line
1920 other._headers = self._headers
1921 other._cookies = self._cookies
1922 other.body = self.body
1923
1924
1925 class HTTPError(HTTPResponse):
1926 """ A subclass of :class:`HTTPResponse` that triggers error handlers. """
1927
1928 default_status = 500
1929
1930 def __init__(self,
1931 status=None,
1932 body=None,
1933 exception=None,
1934 traceback=None, **more_headers):
1935 self.exception = exception
1936 self.traceback = traceback
1937 super(HTTPError, sel{
"file_path": "/app/bottle.py",
"limit": 400,
"offset": 2000
}2000 def setup(self, app):
2001 app.tpl = self
2002
2003 def apply(self, callback, route):
2004 conf = route.config.get('template')
2005 if isinstance(conf, (tuple, list)) and len(conf) == 2:
2006 return view(conf[0], **conf[1])(callback)
2007 elif isinstance(conf, str):
2008 return view(conf)(callback)
2009 else:
2010 return callback
2011
2012
2013 #: Not a plugin, but part of the plugin API. TODO: Find a better place.
2014 class _ImportRedirect:
2015 def __init__(self, name, impmask):
2016 """ Create a virtual package that redirects imports (see PEP 302). """
2017 self.name = name
2018 self.impmask = impmask
2019 self.module = sys.modules.setdefault(name, new_module(name))
2020 self.module.__dict__.update({
2021 '__file__': __file__,
2022 '__path__': [],
2023 '__all__': [],
2024 '__loader__': self
2025 })
2026 sys.meta_path.append(self)
2027
2028 def find_spec(self, fullname, path, target=None):
2029 if '.' not in fullname: return
2030 if fullname.rsplit('.', 1)[0] != self.name: return
2031 from importlib.util import spec_from_loader
2032 return spec_from_loader(fullname, self)
2033
2034 def find_module(self, fullname, path=None):
2035 if '.' not in fullname: return
2036 if fullname.rsplit('.', 1)[0] != self.name: return
2037 return self
2038
2039 def create_module(self, spec):
2040 return self.load_module(spec.name)
2041
2042 def exec_module(self, module):
2043 pass # This probably breaks importlib.reload() :/
2044
2045 def load_module(self, fullname):
2046 if fullname in sys.modules: return sys.modules[fullname]
2047 modname = fullname.rsplit('.', 1)[1]
2048 realname = self.impmask % modname
2049 __import__(realname)
2050 module = sys.modules[fullname] = sys.modules[realname]
2051 setattr(self.module, modname, module)
2052 module.__loader__ = self
2053 return module
2054
2055 ###############################################################################
2056 # Common Utilities #############################################################
2057 ###############################################################################
2058
2059
2060 class MultiDict(DictMixin):
2061 """ This dict stores multiple values per key, but behaves exactly like a
2062 normal dict in that it returns only the newest value for any given key.
2063 There are special methods available to access the full list of values.
2064 """
2065
2066 def __init__(self, *a, **k):
2067 self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items())
2068
2069 def __len__(self):
2070 return len(self.dict)
2071
2072 def __iter__(self):
2073 return iter(self.dict)
2074
2075 def __contains__(self, key):
2076 return key in self.dict
2077
2078 def __delitem__(self, key):
2079 del self.dict[key]
2080
2081 def __getitem__(self, key):
2082 return self.dict[key][-1]
2083
2084 def __setitem__(self, key, value):
2085 self.append(key, value)
2086
2087 def keys(self):
2088 return self.dict.keys()
2089
2090 def values(self):
2091 return (v[-1] for v in self.dict.values())
2092
2093 def items(self):
2094 return ((k, v[-1]) for k, v in self.dict.items())
2095
2096 def allitems(self):
2097 return ((k, v) for k, vl in self.dict.items() for v in vl)
2098
2099 iterkeys = keys
2100 itervalues = values
2101 iteritems = items
2102 iterallitems = allitems
2103
2104 def get(self, key, default=None, index=-1, type=None):
2105 """ Return the most recent value for a key.
2106
2107 :param default: The default value to be returned if the key is not
2108 present or the type conversion fails.
2109 :param index: An index for the list of available values.
2110 :param type: If defined, this callable is used to cast the value
2111 into a specific type. Exception are suppressed and result in
2112 the default value to be returned.
2113 """
2114 try:
2115 val = self.dict[key][index]
2116 return type(val) if type else val
2117 except Exception:
2118 pass
2119 return default
2120
2121 def append(self, key, value):
2122 """ Add a new value to the list of values for this key. """
2123 self.dict.setdefault(key, []).append(value)
2124
2125 def replace(self, key, value):
2126 """ Replace the list of values with a single value. """
2127 self.dict[key] = [value]
2128
2129 def getall(self, key):
2130 """ Return a (possibly empty) list of values for a key. """
2131 return self.dict.get(key) or []
2132
2133 #: Aliases for WTForms to mimic other multi-dict APIs (Django)
2134 getone = get
2135 getlist = getall
2136
2137
2138 class FormsDict(MultiDict):
2139 """ This :class:`MultiDict` subclass is used to store request form data.
2140 Additionally to the normal dict-like item access methods, this container
2141 also supports attribute-like access to its values. Missing attributes
2142 default to an empty string.
2143
2144 .. versionchanged:: 0.14
2145 All keys and values are now decoded as utf8 by default, item and
2146 attribute access will return the same string.
2147 """
2148
2149 def decode(self, encoding=None):
2150 """ (deprecated) Starting with 0.13 all keys and values are already
2151 correctly decoded. """
2152 copy = FormsDict()
2153 for key, value in self.allitems():
2154 {
"command": "ls /app/test/",
"description": "List test directory"
}__init__.py
build_python.sh
example_settings.py
test_app.py
test_auth.py
test_config.py
test_contextlocals.py
test_environ.py
test_exc.py
test_fileupload.py
test_formsdict.py
test_html_helper.py
test_importhook.py
test_jinja2.py
test_mako.py
test_mdict.py
test_mount.py
test_multipart.py
test_oorouting.py
test_outputfilter.py
test_plugins.py
test_resources.py
test_route.py
test_router.py
test_securecookies.py
test_sendfile.py
test_stpl.py
test_wsgi.py
tools.py
views
[stdout]
__init__.py
build_python.sh
example_settings.py
test_app.py
test_auth.py
test_config.py
test_contextlocals.py
test_environ.py
test_exc.py
test_fileupload.py
test_formsdict.py
test_html_helper.py
test_importhook.py
test_jinja2.py
test_mako.py
test_mdict.py
test_mount.py
test_multipart.py
test_oorouting.py
test_outputfilter.py
test_plugins.py
test_resources.py
test_route.py
test_router.py
test_securecookies.py
test_sendfile.py
test_stpl.py
test_wsgi.py
tools.py
views
[metadata] {"noOutputExpected": false}{
"file_path": "/app/test/test_securecookies.py"
}1 #coding: utf-8
2 import unittest
3
4 import bottle
5 from bottle import tob, touni
6 from .tools import api
7
8
9 class TestSignedCookies(unittest.TestCase):
10 def setUp(self):
11 self.data = touni('υηι¢σ∂є')
12 self.secret = tob('secret')
13 bottle.app.push()
14 bottle.response.bind()
15
16 def tear_down(self):
17 bottle.app.pop()
18
19 def get_pairs(self):
20 for k, v in bottle.response.headerlist:
21 if k == 'Set-Cookie':
22 key, value = v.split(';')[0].split('=', 1)
23 yield key.lower().strip(), value.strip()
24
25 def set_pairs(self, pairs):
26 header = ','.join(['%s=%s' % (k, v) for k, v in pairs])
27 bottle.request.bind({'HTTP_COOKIE': header})
28
29 def testValid(self):
30 bottle.response.set_cookie('key', self.data, secret=self.secret)
31 pairs = self.get_pairs()
32 self.set_pairs(pairs)
33 result = bottle.request.get_cookie('key', secret=self.secret)
34 self.assertEqual(self.data, result)
35
36 def testWrongKey(self):
37 bottle.response.set_cookie('key', self.data, secret=self.secret)
38 pairs = self.get_pairs()
39 self.set_pairs([(k + 'xxx', v) for (k, v) in pairs])
40 result = bottle.request.get_cookie('key', secret=self.secret)
41 self.assertEqual(None, result)
42
43
44 class TestSignedCookiesWithPickle(TestSignedCookies):
45 def setUp(self):
46 super(TestSignedCookiesWithPickle, self).setUp()
47 self.data = dict(a=5, b=touni('υηι¢σ∂є'), c=[1,2,3,4,tob('bytestring')])
48
49 @api("0.9", "0.13")
50 def testValid(self):
51 super(TestSignedCookiesWithPickle, self).testValid()
52
53 @api("0.9", "0.13")
54 def testWrongKey(self):
55 super(TestSignedCookiesWithPickle, self).testWrongKey()
56
[metadata] {"type": "text", "file": {"filePath": "/app/test/test_securecookies.py", "content": "#coding: utf-8\nimport unittest\n\nimport bottle\nfrom bottle import tob, touni\nfrom .tools import api\n\n\nclass TestSignedCookies(unittest.TestCase):\n def setUp(self):\n self.data = touni('υηι¢σ∂є')\n self.secret = tob('secret')\n bottle.app.push()\n bottle.response.bind()\n\n def tear_down(self):\n bottle.app.pop()\n\n def get_pairs(self):\n for k, v in bottle.response.headerlist:\n if k == 'Set-Cookie':\n key, value = v.split(';')[0].split('=', 1)\n yield key.lower().strip(), value.strip()\n\n def set_pairs(self, pairs):\n header = ','.join(['%s=%s' % (k, v) for k, v in pairs])\n bottle.request.bind({'HTTP_COOKIE': header})\n\n def testValid(self):\n bottle.response.set_cookie('key', self.data, secret=self.secret)\n pairs = self.get_pairs()\n self.set_pairs(pairs)\n result = bottle.request.get_cookie('key', secret=self.secret)\n self.assertEqual(self.data, result)\n\n def testWrongKey(self):\n bottle.response.set_cookie('key', self.data, secret=self.secret)\n pairs = self.get_pairs()\n self.set_pairs([(k + 'xxx', v) for (k, v) in pairs])\n result = bottle.request.get_cookie('key', secret=self.secret)\n self.assertEqual(None, result)\n\n\nclass TestSignedCookiesWithPickle(TestSignedCookies):\n def setUp(self):\n super(TestSignedCookiesWithPickle, self).setUp()\n self.data = dict(a=5, b=touni('υηι¢σ∂є'), c=[1,2,3,4,tob('bytestring')])\n\n @api(\"0.9\", \"0.13\")\n def testValid(self):\n super(TestSignedCookiesWithPickle, self).testValid()\n\n @api(\"0.9\", \"0.13\")\n def testWrongKey(self):\n super(TestSignedCookiesWithPickle, self).testWrongKey()\n", "numLines": 56, "startLine": 1, "totalLines": 56}}{
"command": "python -m pytest test/test_securecookies.py -v 2>&1 | head -40",
"description": "Run secure cookies tests"
}============================= test session starts ==============================
platform linux -- Python 3.11.14, pytest-8.4.2, pluggy-1.6.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /app
configfile: pyproject.toml
plugins: anyio-4.11.0, cov-7.0.0
collecting ... collected 4 items
test/test_securecookies.py::TestSignedCookies::testValid PASSED [ 25%]
test/test_securecookies.py::TestSignedCookies::testWrongKey PASSED [ 50%]
test/test_securecookies.py::TestSignedCookiesWithPickle::testValid PASSED [ 75%]
test/test_securecookies.py::TestSignedCookiesWithPickle::testWrongKey PASSED [100%]
============================== 4 passed in 0.07s ===============================
[stdout]
============================= test session starts ==============================
platform linux -- Python 3.11.14, pytest-8.4.2, pluggy-1.6.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /app
configfile: pyproject.toml
plugins: anyio-4.11.0, cov-7.0.0
collecting ... collected 4 items
test/test_securecookies.py::TestSignedCookies::testValid PASSED [ 25%]
test/test_securecookies.py::TestSignedCookies::testWrongKey PASSED [ 50%]
test/test_securecookies.py::TestSignedCookiesWithPickle::testValid PASSED [ 75%]
test/test_securecookies.py::TestSignedCookiesWithPickle::testWrongKey PASSED [100%]
============================== 4 passed in 0.07s ===============================
[metadata] {"noOutputExpected": false}{
"command": "git diff bottle.py 2>&1 | head -80",
"description": "Show git diff of bottle.py"
}diff --git a/bottle.py b/bottle.py
index a297ca9..d1e4022 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1559,15 +1559,11 @@ class BaseRequest:
def _hkey(key):
key = touni(key)
- if '\n' in key or '\r' in key or '\0' in key:
- raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
- if '\n' in value or '\r' in value or '\0' in value:
- raise ValueError("Header value must not contain control characters: %r" % value)
return value
[stdout]
diff --git a/bottle.py b/bottle.py
index a297ca9..d1e4022 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1559,15 +1559,11 @@ class BaseRequest:
def _hkey(key):
key = touni(key)
- if '\n' in key or '\r' in key or '\0' in key:
- raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
- if '\n' in value or '\r' in value or '\0' in value:
- raise ValueError("Header value must not contain control characters: %r" % value)
return value
[metadata] {"noOutputExpected": false}{
"command": "git diff bottle.py 2>&1",
"description": "Show full git diff of bottle.py"
}diff --git a/bottle.py b/bottle.py
index a297ca9..d1e4022 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1559,15 +1559,11 @@ class BaseRequest:
def _hkey(key):
key = touni(key)
- if '\n' in key or '\r' in key or '\0' in key:
- raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
- if '\n' in value or '\r' in value or '\0' in value:
- raise ValueError("Header value must not contain control characters: %r" % value)
return value
[stdout]
diff --git a/bottle.py b/bottle.py
index a297ca9..d1e4022 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1559,15 +1559,11 @@ class BaseRequest:
def _hkey(key):
key = touni(key)
- if '\n' in key or '\r' in key or '\0' in key:
- raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
- if '\n' in value or '\r' in value or '\0' in value:
- raise ValueError("Header value must not contain control characters: %r" % value)
return value
[metadata] {"noOutputExpected": false}{
"command": "python -m pytest -rA 2>&1 | tail -50",
"description": "Run all tests to see failures"
}PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_in_single_line_code PASSED test/test_stpl.py::TestSTPLDir::test_multiline_find_eob_in_comments PASSED test/test_stpl.py::TestSTPLDir::test_multiline_ignore_eob_in_string PASSED test/test_stpl.py::TestSTPLDir::test_multiline_indention PASSED test/test_stpl.py::TestSTPLDir::test_multiline_strings_in_code_line PASSED test/test_wsgi.py::TestWsgi::test_303 PASSED test/test_wsgi.py::TestWsgi::test_401 PASSED test/test_wsgi.py::TestWsgi::test_500 PASSED test/test_wsgi.py::TestWsgi::test_500_unicode PASSED test/test_wsgi.py::TestWsgi::test_anymethod PASSED test/test_wsgi.py::TestWsgi::test_cookie PASSED test/test_wsgi.py::TestWsgi::test_generator_callback PASSED test/test_wsgi.py::TestWsgi::test_get PASSED test/test_wsgi.py::TestWsgi::test_headget PASSED test/test_wsgi.py::TestWsgi::test_post PASSED test/test_wsgi.py::TestWsgi::test_request_attrs PASSED test/test_wsgi.py::TestWsgi::test_utf8_404 PASSED test/test_wsgi.py::TestWsgi::test_utf8_header PASSED test/test_wsgi.py::TestWsgi::test_utf8_url PASSED test/test_wsgi.py::TestErrorHandling::test_error_routing PASSED test/test_wsgi.py::TestCloseable::test_direct PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_may_rise_response_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception_in_before_hook PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_sees_HTTPError_response PASSED test/test_wsgi.py::TestRouteDecorator::test_after_response_hook_can_set_headers PASSED test/test_wsgi.py::TestRouteDecorator::test_apply PASSED test/test_wsgi.py::TestRouteDecorator::test_apply_list PASSED test/test_wsgi.py::TestRouteDecorator::test_callback PASSED test/test_wsgi.py::TestRouteDecorator::test_decorators PASSED test/test_wsgi.py::TestRouteDecorator::test_hooks PASSED test/test_wsgi.py::TestRouteDecorator::test_method PASSED test/test_wsgi.py::TestRouteDecorator::test_method_list PASSED test/test_wsgi.py::TestRouteDecorator::test_name PASSED test/test_wsgi.py::TestRouteDecorator::test_no_params_at_all PASSED test/test_wsgi.py::TestRouteDecorator::test_no_path PASSED test/test_wsgi.py::TestRouteDecorator::test_path_list PASSED test/test_wsgi.py::TestRouteDecorator::test_single_path PASSED test/test_wsgi.py::TestRouteDecorator::test_template PASSED test/test_wsgi.py::TestRouteDecorator::test_template_opts PASSED test/test_wsgi.py::TestDecorators::test_autoroute PASSED test/test_wsgi.py::TestDecorators::test_routebuild PASSED test/test_wsgi.py::TestDecorators::test_truncate_body PASSED test/test_wsgi.py::TestDecorators::test_view PASSED test/test_wsgi.py::TestDecorators::test_view_error PASSED test/test_wsgi.py::TestAppShortcuts::testWithStatement PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts_with_different_name FAILED test/test_environ.py::TestResponse::test_prevent_control_characters_in_headers ======================== 1 failed, 366 passed in 0.46s ========================= [stdout] PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_in_single_line_code PASSED test/test_stpl.py::TestSTPLDir::test_multiline_find_eob_in_comments PASSED test/test_stpl.py::TestSTPLDir::test_multiline_ignore_eob_in_string PASSED test/test_stpl.py::TestSTPLDir::test_multiline_indention PASSED test/test_stpl.py::TestSTPLDir::test_multiline_strings_in_code_line PASSED test/test_wsgi.py::TestWsgi::test_303 PASSED test/test_wsgi.py::TestWsgi::test_401 PASSED test/test_wsgi.py::TestWsgi::test_500 PASSED test/test_wsgi.py::TestWsgi::test_500_unicode PASSED test/test_wsgi.py::TestWsgi::test_anymethod PASSED test/test_wsgi.py::TestWsgi::test_cookie PASSED test/test_wsgi.py::TestWsgi::test_generator_callback PASSED test/test_wsgi.py::TestWsgi::test_get PASSED test/test_wsgi.py::TestWsgi::test_headget PASSED test/test_wsgi.py::TestWsgi::test_post PASSED test/test_wsgi.py::TestWsgi::test_request_attrs PASSED test/test_wsgi.py::TestWsgi::test_utf8_404 PASSED test/test_wsgi.py::TestWsgi::test_utf8_header PASSED test/test_wsgi.py::TestWsgi::test_utf8_url PASSED test/test_wsgi.py::TestErrorHandling::test_error_routing PASSED test/test_wsgi.py::TestCloseable::test_direct PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_may_rise_response_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception_in_before_hook PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_sees_HTTPError_response PASSED test/test_wsgi.py::TestRouteDecorator::test_after_response_hook_can_set_headers PASSED test/test_wsgi.py::TestRouteDecorator::test_apply PASSED test/test_wsgi.py::TestRouteDecorator::test_apply_list PASSED test/test_wsgi.py::TestRouteDecorator::test_callback PASSED test/test_wsgi.py::TestRouteDecorator::test_decorators PASSED test/test_wsgi.py::TestRouteDecorator::test_hooks PASSED test/test_wsgi.py::TestRouteDecorator::test_method PASSED test/test_wsgi.py::TestRouteDecorator::test_method_list PASSED test/test_wsgi.py::TestRouteDecorator::test_name PASSED test/test_wsgi.py::TestRouteDecorator::test_no_params_at_all PASSED test/test_wsgi.py::TestRouteDecorator::test_no_path PASSED test/test_wsgi.py::TestRouteDecorator::test_path_list PASSED test/test_wsgi.py::TestRouteDecorator::test_single_path PASSED test/test_wsgi.py::TestRouteDecorator::test_template PASSED test/test_wsgi.py::TestRouteDecorator::test_template_opts PASSED test/test_wsgi.py::TestDecorators::test_autoroute PASSED test/test_wsgi.py::TestDecorators::test_routebuild PASSED test/test_wsgi.py::TestDecorators::test_truncate_body PASSED test/test_wsgi.py::TestDecorators::test_view PASSED test/test_wsgi.py::TestDecorators::test_view_error PASSED test/test_wsgi.py::TestApp
{
"-A": "30",
"output_mode": "content",
"path": "/app/test/test_environ.py",
"pattern": "test_prevent_control_characters_in_headers"
}<tool_use_error>Error: No such tool available: Grep. Grep is not available in this session — search file contents with `grep` via the Bash tool instead.</tool_use_error> [error] tool reported failure
{
"command": "grep -n -A 30 \"test_prevent_control_characters_in_headers\" /app/test/test_environ.py",
"description": "Find the failing test"
}735: def test_prevent_control_characters_in_headers(self):
736- masks = '{}test', 'test{}', 'te{}st'
737- tests = '\n', '\r', '\n\r', '\0'
738-
739- # Test HeaderDict
740- apis = 'append', 'replace', '__setitem__', 'setdefault'
741- for api, mask, test in itertools.product(apis, masks, tests):
742- hd = bottle.HeaderDict()
743- func = getattr(hd, api)
744- value = mask.replace("{}", test)
745- self.assertRaises(ValueError, func, value, "test-value")
746- self.assertRaises(ValueError, func, "test-name", value)
747-
748- # Test functions on BaseResponse
749- apis = 'add_header', 'set_header', '__setitem__'
750- for api, mask, test in itertools.product(apis, masks, tests):
751- rs = bottle.BaseResponse()
752- func = getattr(rs, api)
753- value = mask.replace("{}", test)
754- self.assertRaises(ValueError, func, value, "test-value")
755- self.assertRaises(ValueError, func, "test-name", value)
756-
757- def test_expires_header(self):
758- import datetime
759- from bottle import UTC
760- response = BaseResponse()
761- now = datetime.datetime.now(UTC)
762- response.expires = now
763-
764- def seconds(a, b):
765- td = max(a,b) - min(a,b)
[stdout]
735: def test_prevent_control_characters_in_headers(self):
736- masks = '{}test', 'test{}', 'te{}st'
737- tests = '\n', '\r', '\n\r', '\0'
738-
739- # Test HeaderDict
740- apis = 'append', 'replace', '__setitem__', 'setdefault'
741- for api, mask, test in itertools.product(apis, masks, tests):
742- hd = bottle.HeaderDict()
743- func = getattr(hd, api)
744- value = mask.replace("{}", test)
745- self.assertRaises(ValueError, func, value, "test-value")
746- self.assertRaises(ValueError, func, "test-name", value)
747-
748- # Test functions on BaseResponse
749- apis = 'add_header', 'set_header', '__setitem__'
750- for api, mask, test in itertools.product(apis, masks, tests):
751- rs = bottle.BaseResponse()
752- func = getattr(rs, api)
753- value = mask.replace("{}", test)
754- self.assertRaises(ValueError, func, value, "test-value")
755- self.assertRaises(ValueError, func, "test-name", value)
756-
757- def test_expires_header(self):
758- import datetime
759- from bottle import UTC
760- response = BaseResponse()
761- now = datetime.datetime.now(UTC)
762- response.expires = now
763-
764- def seconds(a, b):
765- td = max(a,b) - min(a,b)
[metadata] {"noOutputExpected": false}{
"replace_all": false,
"file_path": "/app/bottle.py",
"old_string": "def _hkey(key):\n key = touni(key)\n return key.title().replace('_', '-')",
"new_string": "def _hkey(key):\n key = touni(key)\n if '\\n' in key or '\\r' in key or '\\0' in key:\n raise ValueError(\"Header names must not contain control characters: %r\" % key)\n return key.title().replace('_', '-')"
}The file /app/bottle.py has been updated successfully. (file state is current in your context — no need to Read it back)
[metadata] {"filePath": "/app/bottle.py", "oldString": "def _hkey(key):\n key = touni(key)\n return key.title().replace('_', '-')", "newString": "def _hkey(key):\n key = touni(key)\n if '\\n' in key or '\\r' in key or '\\0' in key:\n raise ValueError(\"Header names must not contain control characters: %r\" % key)\n return key.title().replace('_', '-')", "originalFile": null, "structuredPatch": [{"oldStart": 1559, "oldLines": 6, "newStart": 1559, "newLines": 8, "lines": [" ", " def _hkey(key):", " key = touni(key)", "+ if '\\n' in key or '\\r' in key or '\\0' in key:", "+ raise ValueError(\"Header names must not contain control characters: %r\" % key)", " return key.title().replace('_', '-')", " ", " "]}], "userModified": false, "replaceAll": false}{
"replace_all": false,
"file_path": "/app/bottle.py",
"old_string": "def _hval(value):\n value = touni(value)\n return value",
"new_string": "def _hval(value):\n value = touni(value)\n if '\\n' in value or '\\r' in value or '\\0' in value:\n raise ValueError(\"Header value must not contain control characters: %r\" % value)\n return value"
}The file /app/bottle.py has been updated successfully. (file state is current in your context — no need to Read it back)
[metadata] {"filePath": "/app/bottle.py", "oldString": "def _hval(value):\n value = touni(value)\n return value", "newString": "def _hval(value):\n value = touni(value)\n if '\\n' in value or '\\r' in value or '\\0' in value:\n raise ValueError(\"Header value must not contain control characters: %r\" % value)\n return value", "originalFile": null, "structuredPatch": [{"oldStart": 1566, "oldLines": 6, "newStart": 1566, "newLines": 8, "lines": [" ", " def _hval(value):", " value = touni(value)", "+ if '\\n' in value or '\\r' in value or '\\0' in value:", "+ raise ValueError(\"Header value must not contain control characters: %r\" % value)", " return value", " ", " "]}], "userModified": false, "replaceAll": false}{
"command": "python -m pytest -rA 2>&1 | tail -20",
"description": "Run all tests to verify fix"
}PreToolUse hook did not respond before its timeout (host client may be unreachable). The tool call was not executed; other configured hooks may not have completed. [error] tool reported failure
{
"command": "python -m pytest -rA 2>&1 | tail -20",
"description": "Run all tests to verify fix"
}PASSED test/test_wsgi.py::TestRouteDecorator::test_decorators
PASSED test/test_wsgi.py::TestRouteDecorator::test_hooks
PASSED test/test_wsgi.py::TestRouteDecorator::test_method
PASSED test/test_wsgi.py::TestRouteDecorator::test_method_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_name
PASSED test/test_wsgi.py::TestRouteDecorator::test_no_params_at_all
PASSED test/test_wsgi.py::TestRouteDecorator::test_no_path
PASSED test/test_wsgi.py::TestRouteDecorator::test_path_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_single_path
PASSED test/test_wsgi.py::TestRouteDecorator::test_template
PASSED test/test_wsgi.py::TestRouteDecorator::test_template_opts
PASSED test/test_wsgi.py::TestDecorators::test_autoroute
PASSED test/test_wsgi.py::TestDecorators::test_routebuild
PASSED test/test_wsgi.py::TestDecorators::test_truncate_body
PASSED test/test_wsgi.py::TestDecorators::test_view
PASSED test/test_wsgi.py::TestDecorators::test_view_error
PASSED test/test_wsgi.py::TestAppShortcuts::testWithStatement
PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts
PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts_with_different_name
============================= 367 passed in 0.43s ==============================
[stdout]
PASSED test/test_wsgi.py::TestRouteDecorator::test_decorators
PASSED test/test_wsgi.py::TestRouteDecorator::test_hooks
PASSED test/test_wsgi.py::TestRouteDecorator::test_method
PASSED test/test_wsgi.py::TestRouteDecorator::test_method_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_name
PASSED test/test_wsgi.py::TestRouteDecorator::test_no_params_at_all
PASSED test/test_wsgi.py::TestRouteDecorator::test_no_path
PASSED test/test_wsgi.py::TestRouteDecorator::test_path_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_single_path
PASSED test/test_wsgi.py::TestRouteDecorator::test_template
PASSED test/test_wsgi.py::TestRouteDecorator::test_template_opts
PASSED test/test_wsgi.py::TestDecorators::test_autoroute
PASSED test/test_wsgi.py::TestDecorators::test_routebuild
PASSED test/test_wsgi.py::TestDecorators::test_truncate_body
PASSED test/test_wsgi.py::TestDecorators::test_view
PASSED test/test_wsgi.py::TestDecorators::test_view_error
PASSED test/test_wsgi.py::TestAppShortcuts::testWithStatement
PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts
PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts_with_different_name
============================= 367 passed in 0.43s ==============================
[metadata] {"noOutputExpected": false}{
"file_path": "/app/report.jsonl",
"content": "{\"file_path\": \"/app/bottle.py\", \"cwe_id\": [\"cwe-93\"]}"
}File created successfully at: /app/report.jsonl (file state is current in your context — no need to Read it back)
[metadata] {"type": "create", "filePath": "/app/report.jsonl", "content": "{\"file_path\": \"/app/bottle.py\", \"cwe_id\": [\"cwe-93\"]}", "structuredPatch": [], "originalFile": null, "userModified": false}{
"file_path": "/app/report.jsonl"
}1 {"file_path": "/app/bottle.py", "cwe_id": ["cwe-93"]}
[metadata] {"type": "text", "file": {"filePath": "/app/report.jsonl", "content": "{\"file_path\": \"/app/bottle.py\", \"cwe_id\": [\"cwe-93\"]}", "numLines": 1, "startLine": 1, "totalLines": 1}}