From 3c1db00ebb2aa0596840cda6a60e2af3d11a656b Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 23 Feb 2011 12:40:30 +1300 Subject: [PATCH] Remove first iteration playback/record. Bonus: unit test coverage goes from 70% to 94% with one commit. ;) --- libmproxy/console.py | 6 - libmproxy/playback.py | 129 -------------------- libmproxy/record.py | 68 ----------- libmproxy/recorder.py | 276 ------------------------------------------ 4 files changed, 479 deletions(-) delete mode 100644 libmproxy/playback.py delete mode 100644 libmproxy/record.py delete mode 100644 libmproxy/recorder.py diff --git a/libmproxy/console.py b/libmproxy/console.py index fbd3617a8..187502a6b 100644 --- a/libmproxy/console.py +++ b/libmproxy/console.py @@ -19,7 +19,6 @@ import cStringIO import urwid.curses_display import urwid import controller, utils, filt, proxy, flow -import recorder class Stop(Exception): pass @@ -731,9 +730,6 @@ class ConsoleState(flow.State): self.set_focus(self.focus) return ret - def start_recording(self, recorder): - self.store = recorder - def get_focus(self): if not self.view or self.focus is None: return None, None @@ -812,8 +808,6 @@ class ConsoleMaster(flow.FlowMaster): self.stickycookie = None self.stickyhosts = {} - if getattr(options, "cache", None) is not None: - self.state.start_recording(recorder.Recorder(options)) def spawn_external_viewer(self, data, contenttype): if contenttype: diff --git a/libmproxy/playback.py b/libmproxy/playback.py deleted file mode 100644 index 920b2e0c4..000000000 --- a/libmproxy/playback.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python - -# Copyright (C) 2010 Henrik Nordstrom -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# HENRIK NORDSTROM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT -# OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -# Alternatively you may use this file under a GPLv3 license as follows: -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys -import controller -import utils -import proxy -import recorder - -class PlaybackMaster(controller.Master): - """ - A simple master that plays back recorded responses. - """ - def __init__(self, server, options): - self.verbosity = options.verbose - self.store = recorder.Recorder(options) - controller.Master.__init__(self, server) - - def run(self): - try: - return controller.Master.run(self) - except KeyboardInterrupt: - self.shutdown() - - def process_missing_response(self, request): - response = None - print >> sys.stderr, self.store.normalize_request(request).assemble_proxy() - print >> sys.stderr, "Actions:" - print >> sys.stderr, " q Quit" - print >> sys.stderr, " a(dd) Add pattern rule" - print >> sys.stderr, " A(dd) Add pattern rule (forced)" - print >> sys.stderr, " e(rror) respond with a 404 error" - print >> sys.stderr, " k(ill) kill the request, empty response" - print >> sys.stderr, " f(orward) forward the request to the requested server and cache response" - command = raw_input("Action: ") - command = command[:1] - if command == 'q': - self.shutdown() - return None - elif command == 'a' or command == 'A': - filt = raw_input("Filter: ") - search = raw_input("Search pattern: ") - replace = raw_input("Replacement string: ") - self.store.add_rule(filt, search, replace) - if command == 'A': - self.store.save_rule(filt, search, replace) - elif command == 'e': - return proxy.Response(request, "404", "Not found", utils.Headers(), "Not found") - elif command == 'k': - return None - elif command == 'f': - return request - else: - print >> sys.stderr, "ERROR: Unknown command" - return self.process_missing_response(request) - try: - response = self.store.get_response(request) - if command == 'a': - self.store.save_rule(filt, search, replace) - except proxy.ProxyError: - print >> sys.stderr, "ERROR: Malformed substitution rule" - self.store.forget_last_rule() - response = self.process_missing_response(request) - except IOError: - print >> sys.stderr, "NOTICE: Response still not found" - if command == 'a': - self.store.forget_last_rule() - response = self.process_missing_response(request) - return response - - def handle_request(self, msg): - request = msg - try: - response = self.store.get_response(request) - except IOError: - if self.verbosity > 0: - print >> sys.stderr, ">>", - print >> sys.stderr, request.short() - print >> sys.stderr, "<<", - print >> sys.stderr, "ERROR: No matching response.", - print >> sys.stderr, ",".join(self.store.cookies) - response = self.process_missing_response(msg) - msg.ack(response) - - def handle_response(self, msg): - request = msg.request - response = msg - if self.verbosity > 0: - print >> sys.stderr, ">>", - print >> sys.stderr, request.short() - print >> sys.stderr, "<<", - print >> sys.stderr, response.short() - if not response.is_cached(): - self.store.save_response(response) - msg.ack(self.store.filter_response(msg)) diff --git a/libmproxy/record.py b/libmproxy/record.py deleted file mode 100644 index d32c87116..000000000 --- a/libmproxy/record.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python - -# Copyright (C) 2010 Henrik Nordstrom -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# HENRIK NORDSTROM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT -# OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -# Alternatively you may use this file under a GPLv3 license as follows: -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys -import controller -import utils -import recorder - -class RecordMaster(controller.Master): - """ - A simple master that just records to files. - """ - def __init__(self, server, options): - self.verbosity = options.verbose - self.store = recorder.Recorder(options) - controller.Master.__init__(self, server) - - def run(self): - try: - return controller.Master.run(self) - except KeyboardInterrupt: - self.shutdown() - - def handle_request(self, msg): - msg.ack(self.store.filter_request(msg)) - - def handle_response(self, msg): - if self.verbosity > 0: - print >> sys.stderr, ">>", - print >> sys.stderr, msg.request.short() - print >> sys.stderr, "<<", - print >> sys.stderr, msg.short() - self.store.save_response(msg) - msg.ack(self.store.filter_response(msg)) diff --git a/libmproxy/recorder.py b/libmproxy/recorder.py deleted file mode 100644 index 9e4a1f89e..000000000 --- a/libmproxy/recorder.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/env python - -# Copyright (C) 2010 Henrik Nordstrom -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# HENRIK NORDSTROM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT -# OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -# Alternatively you may use this file under a GPLv3 license as follows: -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys -import time -import hashlib -import utils -import proxy -import collections -import itertools -import string -import Cookie -import filt -import re -import cStringIO - - - -class PatternRule: - """ - Request pattern rule - :_ivar _match filt pattern rule - :_ivar _search Regex pattern to search for - :_ivar _replace Replacement string - """ - def __init__(self, pattern, search, replace): - self.match = filt.parse(pattern) - self.search = re.compile(search) - self.replace = replace - def execute(self, request, text): - if self.match and not self.match(request): - return text - return re.sub(self.search, self.replace, text) - -class RecorderConnection(proxy.ServerConnection): - """ - Simulated ServerConnection connecting to the cache - """ - # Note: This may chane in future. Division between Recorder - # and RecorderConnection is not yet finalized - def __init__(self, request, fp): - self.host = request.host - self.port = request.port - self.scheme = request.scheme - self.close = False - self.server = fp - self.rfile = fp - self.wfile = fp - - def send_request(self, request): - self.request = request - - def read_response(self): - response = proxy.ServerConnection.read_response(self) - response.cached = True - return response - -class Recorder: - """ - A simple record/playback cache - """ - def __init__(self, options): - self.sequence = collections.defaultdict(int) - self.cookies = {} - try: - for cookie in options.cookies: - self.cookies[cookie] = True - except AttributeError: pass - try: - self.verbosity = options.verbose - except AttributeError: - self.verbosity = False - self.storedir = options.cache - self.patterns = [] - self.indexfp = None - self.reset_config() - - def reset_config(self): - self.patterns = [] - self.load_config("default") - - def add_rule(self, match, search, replace): - self.patterns.append(PatternRule(match, search, replace)) - - def forget_last_rule(self): - self.patterns.pop() - - def save_rule(self, match, search, replace, configfile = "default"): - fp = self.open(configfile + ".cfg", "a") - print >> fp, "Condition: " + match - print >> fp, "Search: " + search - print >> fp, "Replace: " + replace - fp.close() - - def load_config(self, name): - """ - Load configuration settings from name - """ - try: - file = name + ".cfg" - if self.verbosity > 2: - print >> sys.stderr, "config: " + file - fp = self.open(file, "r") - except IOError: - return False - for line in fp: - directive, value = line.split(" ", 1) - value = value.strip("\r\n") - if directive == "Cookie:": - self.cookies[value] = True - if directive == "Condition:": - match = value - if directive == "Search:": - search = value - if directive == "Replace:": - self.add_rule(match, search, value) - fp.close() - return True - - def filter_request(self, request): - """ - Filter forwarded requests to enable better recording - """ - request = request.copy() - headers = request.headers - utils.try_del(headers, 'if-modified-since') - utils.try_del(headers, 'if-none-match') - return request - - def normalize_request(self, request): - """ - Filter request to simplify storage matching - """ - request.close = False - req_text = request.assemble_proxy() - orig_req_text = req_text - for pattern in self.patterns: - req_text = pattern.execute(request, req_text) - if req_text == orig_req_text: - return request - fp = cStringIO.StringIO(req_text) - request_line = fp.readline() - method, scheme, host, port, path, httpminor = proxy.parse_request_line(request_line) - headers = utils.Headers() - headers.read(fp) - if request.content is None: - content = None - else: - content = fp.read() - return proxy.Request(request.client_conn, host, port, scheme, method, path, headers, content) - - def open(self, path, mode): - return open(self.storedir + "/" + path, mode) - - def pathn(self, request): - """ - Create cache file name and sequence number - """ - request = self.normalize_request(request) - request = self.filter_request(request) - headers = request.headers - urlkey = (request.host + request.path)[:80].translate(string.maketrans(":/?","__.")) - id = "" - if headers.has_key("cookie"): - cookies = Cookie.SimpleCookie("; ".join(headers["cookie"])) - del headers["cookie"] - for key, morsel in cookies.iteritems(): - if self.cookies.has_key(key): - id = id + key + "=" + morsel.value + "\n" - if self.verbosity > 1: - print >> sys.stderr, "ID: " + id - m = hashlib.sha224(id) - req_text = request.assemble_proxy() - if self.verbosity > 2: - print >> sys.stderr, req_text - m.update(req_text) - path = urlkey+"."+m.hexdigest() - n = str(self.sequence[path]) - if self.verbosity > 1: - print >> sys.stderr, "PATH: " + path + "." + n - return path, n - - def filter_response(self, response): - if response.headers.has_key('set-cookie'): - for header in response.headers['set-cookie']: - key = header.split('=',1)[0] - self.cookies[key] = True - return response - - def save_response(self, response): - """ - Save response for later playback - """ - - if self.indexfp is None: - self.indexfp = self.open("index.txt", "a") - try: - cfg = self.open("default.cfg", "r") - except: - cfg = self.open("default.cfg", "w") - for cookie in iter(self.cookies): - print >> cfg, "Cookie: " + cookie - cfg.close() - request = response.request - req_text = request.assemble_proxy() - resp_text = response.assemble() - path, n = self.pathn(request) - self.sequence[path] += 1 - - f = self.open(path+"."+n+".req", 'w') - f.write(req_text) - f.close() - f = self.open(path+"."+n+".resp", 'w') - f.write(resp_text) - f.close() - - print >> self.indexfp , time.time(), request.method, request.path - if request.headers.has_key('referer'): - print >> self.indexfp, 'referer:', ','.join(request.headers['referer']) - if len(self.cookies) > 0: - print >> self.indexfp, 'cookies:', ','.join(self.cookies) - print >> self.indexfp , path - print >> self.indexfp , "" - self.indexfp.flush() - - - def get_response(self, request): - """ - Retrieve previously saved response saved by save_response - """ - path, n = self.pathn(request) - try: - fp = self.open(path+"."+n+".resp", 'r') - self.sequence[path]+=1 - except IOError: - fp = self.open(path+".resp", 'r') - server = RecorderConnection(request, fp) - fp = None # Handed over to RecorderConnection - server.send_request(request) - response = server.read_response() - server.terminate() - return response