blob: 13341a944e708478ac64c041ef2c7a7f305cd46f [file] [log] [blame]
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Middleware classes for Recall server.
This module should not be imported directly, instead the public classes
are imported directly into the top-level recall package.
"""
__all__ = ["DeterministicScriptInjector"]
import logging
import re
from http_client import HTTPClient, HTTPMiddleware
class DeterministicScriptInjector(HTTPMiddleware):
"""Mutate HTTP Requests to inject Deterministic JavaScript code.
Implements HTTP Client middleware that alters text/html responses,
inserting a <script> block to the top of the page that replaces the
JavaScript Math.random() and Date() functions for deterministic
behaviour.
The Date() replacement uses the original request time as the returned
value, to avoid issues where scripts loop while the date remains the
same, the date is incremented between calls. However since scripts may
be executed out of order, the date is only incremented every
date_count_threshold calls.
"""
logger = logging.getLogger("DeterministicScriptInjector")
_html_re = re.compile(r'<html[^>]*>', re.IGNORECASE)
_head_re = re.compile(r'<head[^>]*>', re.IGNORECASE)
_body_re = re.compile(r'<body[^>]*>', re.IGNORECASE)
_deterministic_script = """\
<script>
(function () {
var orig_date = Date;
var random_count = 0;
var date_count = 0;
var random_seed = 0.462;
var time_seed = @@START_TIME@@;
var random_count_threshold = 25;
var date_count_threshold = 25;
Math.random = function() {
random_count++;
if (random_count > random_count_threshold){
random_seed += 0.1;
random_count = 1;
}
return (random_seed % 1);
};
Date = function() {
if (this instanceof Date) {
date_count++;
if (date_count > date_count_threshold){
time_seed += 50;
date_count = 1;
}
switch (arguments.length) {
case 0: return new orig_date(time_seed);
case 1: return new orig_date(arguments[0]);
default: return new orig_date(arguments[0], arguments[1],
arguments.length >= 3 ? arguments[2] : 1,
arguments.length >= 4 ? arguments[3] : 0,
arguments.length >= 5 ? arguments[4] : 0,
arguments.length >= 6 ? arguments[5] : 0,
arguments.length >= 7 ? arguments[6] : 0);
}
}
return new Date().toString();
};
Date.__proto__ = orig_date;
Date.prototype.constructor = Date;
orig_date.now = function() {
return new Date().getTime();
};
})();
</script>
"""
def __call__(self, request):
"""Lookup the request.
Args:
request: HTTPRequest to lookup.
Returns:
HTTPResponse reply, which may include an error response.
"""
response = self.http_client(request)
content_type = response.getheader('Content-Type')
if content_type and content_type.startswith('text/html'):
self.logger.debug("Will inject script into %r for %s", response, request)
start_time = int(response.start_time * 1000)
self._inject_script = self._deterministic_script.replace(
'@@START_TIME@@', str(start_time))
response.AddMutateFunction(self._MutateChunk)
return response
def _InjectScriptAfter(self, match):
return match.group(0) + self._inject_script
def _MutateChunk(self, chunk):
if self._inject_script:
count = 0
if chunk:
chunk, count = self._head_re.subn(self._InjectScriptAfter, chunk, 1)
if count:
self.logger.debug("Injected script into HEAD")
self._inject_script = None
else:
chunk, count = self._body_re.subn(self._InjectScriptAfter, chunk, 1)
if count:
self.logger.debug("Injected script into BODY")
self._inject_script = None
return chunk