diff --git a/trac/web/api.py b/trac/web/api.py index b2e76f948..062c46550 100644 --- a/trac/web/api.py +++ b/trac/web/api.py @@ -605,6 +605,7 @@ class Request(object): """Must be called after all headers have been sent and before the actual content is written. """ + self._send_configurable_headers() self._send_cookie_headers() self._write = self._start_response(self._status, self._outheaders) @@ -725,6 +726,7 @@ class Request(object): self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT') self.send_header('Content-Type', content_type + ';charset=utf-8') self.send_header('Content-Length', len(data)) + self._send_configurable_headers() self._send_cookie_headers() self._write = self._start_response(self._status, self._outheaders, @@ -947,6 +949,10 @@ class Request(object): return urlparse.urlunparse((self.scheme, host, self.base_path, None, None, None)) + def _send_configurable_headers(self): + for name, val in getattr(self, 'configurable_headers', []): + self.send_header(name, val) + def _send_cookie_headers(self): for name in self.outcookie.keys(): path = self.outcookie[name].get('path') diff --git a/trac/web/main.py b/trac/web/main.py index 56b493d38..4008d7e77 100644 --- a/trac/web/main.py +++ b/trac/web/main.py @@ -38,8 +38,9 @@ from genshi.output import DocType from genshi.template import TemplateLoader from trac import __version__ as TRAC_VERSION -from trac.config import BoolOption, ChoiceOption, ConfigurationError, \ - ExtensionOption, Option, OrderedExtensionsOption +from trac.config import ( + BoolOption, ChoiceOption, ConfigSection, ConfigurationError, + ExtensionOption, Option, OrderedExtensionsOption) from trac.core import * from trac.env import open_environment from trac.loader import get_plugin_info, match_plugins_to_frames @@ -164,6 +165,10 @@ class RequestDispatcher(Component): """The header to use if `use_xsendfile` is enabled. If Nginx is used, set `X-Accel-Redirect`. (''since 1.0.6'')""") + configurable_headers = ConfigSection('http-headers', """ + Headers to be added to the HTTP request. (''since 1.2.3'') + """) + # Public API def authenticate(self, req): @@ -317,6 +322,7 @@ class RequestDispatcher(Component): 'tz': self._get_timezone, 'use_xsendfile': self._get_use_xsendfile, 'xsendfile_header': self._get_xsendfile_header, + 'configurable_headers': self._get_configurable_headers, }) @lazy @@ -412,12 +418,12 @@ class RequestDispatcher(Component): return self.use_xsendfile # RFC7230 3.2 Header Fields - _xsendfile_header_re = re.compile(r"[-0-9A-Za-z!#$%&'*+.^_`|~]+\Z") + _valid_header_re = re.compile(r"[-0-9A-Za-z!#$%&'*+.^_`|~]+\Z") _warn_xsendfile_header = False def _get_xsendfile_header(self, req): header = self.xsendfile_header.strip() - if self._xsendfile_header_re.match(header): + if self._valid_header_re.match(header): return to_utf8(header) else: if not self._warn_xsendfile_header: @@ -426,6 +432,29 @@ class RequestDispatcher(Component): header) return None + _control_codes_re = re.compile(r'[\x00-\x08\x0a-\x1f\x7f]') + _reserverd_headers = set(['content-type', 'content-length', 'location', + 'etag', 'pragma', 'cache-control', 'expires']) + + @lazy + def _configurable_headers(self): + headers = [] + invalids = [] + for name, val in self.configurable_headers.options(): + if name and name.lower() not in self._reserverd_headers and \ + self._valid_header_re.match(name) and \ + not self._control_codes_re.search(val): + headers.append((name, val)) + else: + invalids.append((name, val)) + if invalids: + self.log.warning('[http-headers] invalid headers are ignored: %r', + invalids) + return tuple(headers) + + def _get_configurable_headers(self, req): + return iter(self._configurable_headers) + def _pre_process_request(self, req, chosen_handler): for filter_ in self.filters: chosen_handler = filter_.pre_process_request(req, chosen_handler)