diff --git a/README.md b/README.md index 057401c..9c2bdce 100644 --- a/README.md +++ b/README.md @@ -38,14 +38,8 @@ Then you can run puppy by running `puppy`. It will use the puppy binary in `~/$G Missing Features From Pappy --------------------------- -Here's what Pappy can do that this can't: - -- The `http://pappy` interface -- Upstream proxies -- Commands taking multiple requests -- Any and all documentation -- The macro API is totally different +All that's left is updating documentation! Need more info? --------------- -Right now I haven't written any documentation, so feel free to contact me for help. \ No newline at end of file +Right now I haven't written any documentation, so feel free to contact me for help. diff --git a/certs.go b/certs.go index 8a9d6a5..2e69fb0 100644 --- a/certs.go +++ b/certs.go @@ -3,9 +3,9 @@ package main import ( "crypto/rand" "crypto/rsa" + "crypto/sha1" "crypto/x509" "crypto/x509/pkix" - "crypto/sha1" "encoding/pem" "fmt" "math/big" @@ -14,7 +14,7 @@ import ( type CAKeyPair struct { Certificate []byte - PrivateKey *rsa.PrivateKey + PrivateKey *rsa.PrivateKey } func bigIntHash(n *big.Int) []byte { @@ -41,16 +41,16 @@ func GenerateCACerts() (*CAKeyPair, error) { template := x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ - CommonName: "Puppy Proxy", + CommonName: "Puppy Proxy", Organization: []string{"Puppy Proxy"}, }, NotBefore: time.Now().Add(-5 * time.Minute).UTC(), NotAfter: end, - SubjectKeyId: bigIntHash(key.N), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + SubjectKeyId: bigIntHash(key.N), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, - IsCA: true, + IsCA: true, MaxPathLenZero: true, } @@ -61,23 +61,23 @@ func GenerateCACerts() (*CAKeyPair, error) { return &CAKeyPair{ Certificate: derBytes, - PrivateKey: key, + PrivateKey: key, }, nil } -func (pair *CAKeyPair) PrivateKeyPEM() ([]byte) { +func (pair *CAKeyPair) PrivateKeyPEM() []byte { return pem.EncodeToMemory( &pem.Block{ - Type: "BEGIN PRIVATE KEY", + Type: "BEGIN PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pair.PrivateKey), }, ) } -func (pair *CAKeyPair) CACertPEM() ([]byte) { +func (pair *CAKeyPair) CACertPEM() []byte { return pem.EncodeToMemory( &pem.Block{ - Type: "CERTIFICATE", + Type: "CERTIFICATE", Bytes: pair.Certificate, }, ) diff --git a/credits.go b/credits.go index 852643c..ddb540b 100644 --- a/credits.go +++ b/credits.go @@ -5,22 +5,22 @@ List of info that is used to display credits */ type creditItem struct { - projectName string - url string - author string - year string - licenseType string - longCopyright string + projectName string + url string + author string + year string + licenseType string + longCopyright string } -var LIB_CREDITS = []creditItem { - creditItem { +var LIB_CREDITS = []creditItem{ + creditItem{ "goproxy", "https://github.com/elazarl/goproxy", "Elazar Leibovich", "2012", "3-Clause BSD", -`Copyright (c) 2012 Elazar Leibovich. All rights reserved. + `Copyright (c) 2012 Elazar Leibovich. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -49,13 +49,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.`, }, - creditItem { + creditItem{ "golang-set", "https://github.com/deckarep/golang-set", "Ralph Caraveo", "2013", "MIT", -`Open Source Initiative OSI - The MIT License (MIT):Licensing + `Open Source Initiative OSI - The MIT License (MIT):Licensing The MIT License (MIT) Copyright (c) 2013 Ralph Caraveo (deckarep@gmail.com) @@ -79,13 +79,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`, }, - creditItem { + creditItem{ "Gorilla WebSocket", "https://github.com/gorilla/websocket", "Gorilla WebSocket Authors", "2013", "2-Clause BSD", -`Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + `Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/jobpool.go b/jobpool.go index 9bf1931..4064412 100644 --- a/jobpool.go +++ b/jobpool.go @@ -7,8 +7,8 @@ import ( // An interface which represents a job to be done by the pool type Job interface { - Run() // Start the job - Abort() // Abort any work that needs to be completed and close the DoneChannel + Run() // Start the job + Abort() // Abort any work that needs to be completed and close the DoneChannel DoneChannel() chan struct{} // Returns a channel that is closed when the job is done } @@ -16,19 +16,19 @@ type Job interface { type JobPool struct { MaxThreads int - jobQueue chan Job - jobQueueDone chan struct{} - jobQueueAborted chan struct{} + jobQueue chan Job + jobQueueDone chan struct{} + jobQueueAborted chan struct{} jobQueueShutDown chan struct{} } -func NewJobPool(maxThreads int) (*JobPool) { - q := JobPool { - MaxThreads: maxThreads, - jobQueue: make(chan Job), - jobQueueDone: make(chan struct{}), // Closing will shut down workers and reject any incoming work - jobQueueAborted: make(chan struct{}), // Closing tells workers to abort - jobQueueShutDown: make(chan struct{}), // Closed when all workers are shut down +func NewJobPool(maxThreads int) *JobPool { + q := JobPool{ + MaxThreads: maxThreads, + jobQueue: make(chan Job), + jobQueueDone: make(chan struct{}), // Closing will shut down workers and reject any incoming work + jobQueueAborted: make(chan struct{}), // Closing tells workers to abort + jobQueueShutDown: make(chan struct{}), // Closed when all workers are shut down } return &q @@ -49,7 +49,7 @@ func (q *JobPool) Run() { if q.MaxThreads > 0 { // Create pool of routines that read from the queue and run jobs var w sync.WaitGroup - for i:=0; i 1: + r = client.req_by_id(phrase[1], headers_only=True) + phrase[1] = str(time_to_nsecs(r.time_start)) client.context.apply_phrase(phrases) except InvalidQuery as e: print(e) diff --git a/python/puppy/puppyproxy/interface/decode.py b/python/puppy/puppyproxy/interface/decode.py index 32ded37..786ff39 100644 --- a/python/puppy/puppyproxy/interface/decode.py +++ b/python/puppy/puppyproxy/interface/decode.py @@ -7,31 +7,32 @@ import string import urllib from ..util import hexdump, printable_data, copy_to_clipboard, clipboard_contents, encode_basic_auth, parse_basic_auth +from ..console import CommandError from io import StringIO def print_maybe_bin(s): binary = False for c in s: - if str(c) not in string.printable: + if chr(c) not in string.printable: binary = True break if binary: print(hexdump(s)) else: - print(s) + print(s.decode()) def asciihex_encode_helper(s): - return ''.join('{0:x}'.format(c) for c in s) + return ''.join('{0:x}'.format(c) for c in s).encode() def asciihex_decode_helper(s): ret = [] try: for a, b in zip(s[0::2], s[1::2]): - c = a+b + c = chr(a)+chr(b) ret.append(chr(int(c, 16))) - return ''.join(ret) + return ''.join(ret).encode() except Exception as e: - raise PappyException(e) + raise CommandError(e) def gzip_encode_helper(s): out = StringIO.StringIO() @@ -54,13 +55,21 @@ def base64_decode_helper(s): return s_padded except: pass - raise PappyException("Unable to base64 decode string") + raise CommandError("Unable to base64 decode string") + +def url_decode_helper(s): + bs = s.decode() + return urllib.parse.unquote(bs).encode() + +def url_encode_helper(s): + bs = s.decode() + return urllib.parse.quote_plus(bs).encode() def html_encode_helper(s): - return ''.join(['&#x{0:x};'.format(c) for c in s]) + return ''.join(['&#x{0:x};'.format(c) for c in s]).encode() def html_decode_helper(s): - return html.unescape(s) + return html.unescape(s.decode()).encode() def _code_helper(args, func, copy=True): if len(args) == 0: @@ -107,7 +116,7 @@ def url_decode(client, args): If no string is given, will decode the contents of the clipboard. Results are copied to the clipboard. """ - print_maybe_bin(_code_helper(args, urllib.unquote)) + print_maybe_bin(_code_helper(args, url_decode_helper)) def url_encode(client, args): """ @@ -115,7 +124,7 @@ def url_encode(client, args): If no string is given, will encode the contents of the clipboard. Results are copied to the clipboard. """ - print_maybe_bin(_code_helper(args, urllib.quote_plus)) + print_maybe_bin(_code_helper(args, url_encode_helper)) def asciihex_decode(client, args): """ @@ -187,7 +196,7 @@ def url_decode_raw(client, args): results will not be copied. It is suggested you redirect the output to a file. """ - print(_code_helper(args, urllib.unquote, copy=False)) + print(_code_helper(args, url_decode_helper, copy=False)) def url_encode_raw(client, args): """ @@ -195,7 +204,7 @@ def url_encode_raw(client, args): results will not be copied. It is suggested you redirect the output to a file. """ - print(_code_helper(args, urllib.quote_plus, copy=False)) + print(_code_helper(args, url_encode_helper, copy=False)) def asciihex_decode_raw(client, args): """ @@ -254,9 +263,8 @@ def unix_time_decode(client, args): print(_code_helper(args, unix_time_decode_helper)) def http_auth_encode(client, args): - args = shlex.split(args[0]) if len(args) != 2: - raise PappyException('Usage: http_auth_encode ') + raise CommandError('Usage: http_auth_encode ') username, password = args print(encode_basic_auth(username, password)) diff --git a/python/puppy/puppyproxy/interface/misc.py b/python/puppy/puppyproxy/interface/misc.py index 67b738b..b926825 100644 --- a/python/puppy/puppyproxy/interface/misc.py +++ b/python/puppy/puppyproxy/interface/misc.py @@ -23,7 +23,7 @@ class WatchMacro(InterceptMacro): printstr = "< " printstr += verb_color(request.method) + request.method + Colors.ENDC + ' ' printstr += url_formatter(request, colored=True) - printstr += " -> " + printstr += " \u2192 " response_code = str(response.status_code) + ' ' + response.reason response_code = scode_color(response_code) + response_code + Colors.ENDC printstr += response_code diff --git a/python/puppy/puppyproxy/interface/repeater/repeater.py b/python/puppy/puppyproxy/interface/repeater/repeater.py index a816abf..9daab53 100644 --- a/python/puppy/puppyproxy/interface/repeater/repeater.py +++ b/python/puppy/puppyproxy/interface/repeater/repeater.py @@ -1524,7 +1524,7 @@ def update_buffers(req): # Save the port, ssl, host setting vim.command("let s:dest_port=%d" % req.dest_port) - vim.command("let s:dest_host='%s'" % req.dest_host) + vim.command("let s:dest_host='%s'" % escape(req.dest_host)) if req.use_tls: vim.command("let s:use_tls=1") @@ -1544,6 +1544,8 @@ def set_up_windows(): reqid = vim.eval("a:2") storage_id = vim.eval("a:3") msg_addr = vim.eval("a:4") + + vim.command("let s:storage_id=%d" % int(storage_id)) # Get the left buffer vim.command("new") @@ -1568,11 +1570,12 @@ def dest_loc(): dest_host = vim.eval("s:dest_host") dest_port = int(vim.eval("s:dest_port")) tls_num = vim.eval("s:use_tls") + storage_id = int(vim.eval("s:storage_id")) if tls_num == "1": use_tls = True else: use_tls = False - return (dest_host, dest_port, use_tls) + return (dest_host, dest_port, use_tls, storage_id) def submit_current_buffer(): curbuf = vim.current.buffer @@ -1586,14 +1589,15 @@ def submit_current_buffer(): full_request = '\n'.join(curbuf) req = parse_request(full_request) - dest_host, dest_port, use_tls = dest_loc() + dest_host, dest_port, use_tls, storage_id = dest_loc() req.dest_host = dest_host req.dest_port = dest_port req.use_tls = use_tls comm_type, comm_addr = get_conn_addr() with ProxyConnection(kind=comm_type, addr=comm_addr) as conn: - new_req = conn.submit(req) + new_req = conn.submit(req, storage=storage_id) + conn.add_tag(new_req.db_id, "repeater", storage_id) update_buffers(new_req) # (left, right) = set_up_windows() diff --git a/python/puppy/puppyproxy/interface/view.py b/python/puppy/puppyproxy/interface/view.py index 35d0063..175c17c 100644 --- a/python/puppy/puppyproxy/interface/view.py +++ b/python/puppy/puppyproxy/interface/view.py @@ -481,17 +481,23 @@ def site_map(client, args): paths = True else: paths = False - reqs = client.in_context_requests(headers_only=True) - paths_set = set() - for req in reqs: - if req.response and req.response.status_code != 404: - paths_set.add(path_tuple(req.url)) - tree = sorted(list(paths_set)) - if paths: - for p in tree: - print ('/'.join(list(p))) - else: - print_tree(tree) + all_reqs = client.in_context_requests(headers_only=True) + reqs_by_host = {} + for req in all_reqs: + reqs_by_host.setdefault(req.dest_host, []).append(req) + for host, reqs in reqs_by_host.items(): + paths_set = set() + for req in reqs: + if req.response and req.response.status_code != 404: + paths_set.add(path_tuple(req.url)) + tree = sorted(list(paths_set)) + print(host) + if paths: + for p in tree: + print ('/'.join(list(p))) + else: + print_tree(tree) + print("") def dump_response(client, args): """ @@ -515,6 +521,78 @@ def dump_response(client, args): else: print('Request {} does not have a response'.format(req.reqid)) +def get_surrounding_lines(s, n, lines): + left = n + right = n + lines_left = 0 + lines_right = 0 + + # move left until we find enough lines or hit the edge + while left > 0 and lines_left < lines: + if s[left] == '\n': + lines_left += 1 + left -= 1 + + # move right until we find enough lines or hit the edge + while right < len(s) and lines_right < lines: + if s[right] == '\n': + lines_right += 1 + right += 1 + + return s[left:right] + +def print_search_header(reqid, locstr): + printstr = Styles.TABLE_HEADER + printstr += "Result(s) for request {} ({})".format(reqid, locstr) + printstr += Colors.ENDC + print(printstr) + +def highlight_str(s, substr): + highlighted = Colors.BGYELLOW + Colors.BLACK + Colors.BOLD + substr + Colors.ENDC + return s.replace(substr, highlighted) + +def search_message(mes, substr, lines, reqid, locstr): + header_printed = False + for m in re.finditer(substr, mes): + if not header_printed: + print_search_header(reqid, locstr) + header_printed = True + n = m.start() + linestr = get_surrounding_lines(mes, n, lines) + linelist = linestr.split('\n') + linestr = '\n'.join(line[:500] for line in linelist) + toprint = highlight_str(linestr, substr) + print(toprint) + print('-'*50) + +def search(client, args): + search_str = args[0] + lines = 2 + if len(args) > 1: + lines = int(args[1]) + for req in client.in_context_requests_iter(): + reqid = client.get_reqid(req) + reqheader_printed = False + try: + mes = req.full_message().decode() + search_message(mes, search_str, lines, reqid, "Request") + except UnicodeDecodeError: + pass + if req.response: + try: + mes = req.response.full_message().decode() + search_message(mes, search_str, lines, reqid, "Response") + except UnicodeDecodeError: + pass + + wsheader_printed = False + for wsm in req.ws_messages: + if not wsheader_printed: + print_search_header(client.get_reqid(req), reqid, "Websocket Messages") + wsheader_printed = True + if search_str in wsm.message: + print(highlight_str(wsm.message, search_str)) + # @crochet.wait_for(timeout=None) # @defer.inlineCallbacks @@ -572,6 +650,7 @@ def load_cmds(cmd): 'urls': (find_urls, None), 'site_map': (site_map, None), 'dump_response': (dump_response, None), + 'search': (search, None), # 'view_request_bytes': (view_request_bytes, None), # 'view_response_bytes': (view_response_bytes, None), }) diff --git a/python/puppy/puppyproxy/proxy.py b/python/puppy/puppyproxy/proxy.py index cb633da..c5137b4 100644 --- a/python/puppy/puppyproxy/proxy.py +++ b/python/puppy/puppyproxy/proxy.py @@ -85,17 +85,23 @@ class SockBuffer: class Headers: def __init__(self, headers=None): - if headers is None: - self.headers = {} - else: - self.headers = headers + self.headers = {} + if headers is not None: + if isinstance(headers, Headers): + for _, pairs in headers.headers.items(): + for k, v in pairs: + self.add(k, v) + else: + for k, vs in headers.items(): + for v in vs: + self.add(k, v) def __contains__(self, hd): for k, _ in self.headers.items(): if k.lower() == hd.lower(): return True return False - + def add(self, k, v): try: l = self.headers[k.lower()] @@ -265,11 +271,7 @@ class HTTPRequest: self.proto_major = proto_major self.proto_minor = proto_minor - self.headers = Headers() - if headers is not None: - for k, vs in headers.items(): - for v in vs: - self.headers.add(k, v) + self.headers = Headers(headers) self.headers_only = headers_only self._body = bytes() @@ -280,8 +282,8 @@ class HTTPRequest: self.dest_host = dest_host self.dest_port = dest_port self.use_tls = use_tls - self.time_start = time_start or datetime.datetime(1970, 1, 1) - self.time_end = time_end or datetime.datetime(1970, 1, 1) + self.time_start = time_start + self.time_end = time_end self.response = None self.unmangled = None @@ -412,7 +414,7 @@ class HTTPRequest: path=self.url.geturl(), proto_major=self.proto_major, proto_minor=self.proto_minor, - headers=self.headers.headers, + headers=self.headers, body=self.body, dest_host=self.dest_host, dest_port=self.dest_port, @@ -928,6 +930,21 @@ class ProxyConnection: for ss in result["Storages"]: ret.append(SavedStorage(ss["Id"], ss["Description"])) return ret + + @messagingFunction + def set_proxy(self, use_proxy=False, proxy_host="", proxy_port=0, use_creds=False, + username="", password="", is_socks=False): + cmd = { + "Command": "SetProxy", + "UseProxy": use_proxy, + "ProxyHost": proxy_host, + "ProxyPort": proxy_port, + "ProxyIsSOCKS": is_socks, + "UseCredentials": use_creds, + "Username": username, + "Password": password, + } + self.reqrsp_cmd(cmd) @messagingFunction def intercept(self, macro): @@ -1086,6 +1103,7 @@ class ProxyClient: # "add_in_memory_storage", # "close_storage", # "set_proxy_storage", + "set_proxy" } def __enter__(self): @@ -1162,7 +1180,7 @@ class ProxyClient: stype, prefix = s.description.split("|") storage = ActiveStorage(stype, s.storage_id, prefix) self._add_storage(storage, prefix) - + def parse_reqid(self, reqid): if reqid[0].isalpha(): prefix = reqid[0] @@ -1172,6 +1190,10 @@ class ProxyClient: realid = reqid storage = self.storage_by_prefix[prefix] return storage, realid + + def get_reqid(self, req): + storage = self.storage_by_id[req.storage_id] + return storage.prefix + req.db_id def storage_iter(self): for _, s in self.storage_by_id.items(): @@ -1190,6 +1212,17 @@ class ProxyClient: if max_results > 0 and len(results) > max_results: ret = results[:max_results] return ret + + def in_context_requests_iter(self, headers_only=False, max_results=0): + results = self.query_storage(self.context.query, + headers_only=headers_only, + max_results=max_results) + ret = results + if max_results > 0 and len(results) > max_results: + ret = results[:max_results] + for reqh in ret: + req = self.req_by_id(reqh.db_id, storage_id=reqh.storage_id) + yield req def prefixed_reqid(self, req): prefix = "" @@ -1246,10 +1279,14 @@ class ProxyClient: results = [r for r in reversed(results)] return results - def req_by_id(self, reqid, headers_only=False): - storage, rid = self.parse_reqid(reqid) - return self.msg_conn.req_by_id(rid, headers_only=headers_only, - storage=storage.storage_id) + def req_by_id(self, reqid, storage_id=None, headers_only=False): + if storage_id is None: + storage, db_id = self.parse_reqid(reqid) + storage_id = storage.storage_id + else: + db_id = reqid + return self.msg_conn.req_by_id(db_id, headers_only=headers_only, + storage=storage_id) # for these and submit, might need storage stored on the request itself def add_tag(self, reqid, tag, storage=None): @@ -1275,12 +1312,12 @@ class ProxyClient: def decode_req(result, headers_only=False): - if "StartTime" in result: + if "StartTime" in result and result["StartTime"] > 0: time_start = time_from_nsecs(result["StartTime"]) else: time_start = None - if "EndTime" in result: + if "EndTime" in result and result["EndTime"] > 0: time_end = time_from_nsecs(result["EndTime"]) else: time_end = None diff --git a/python/puppy/puppyproxy/pup.py b/python/puppy/puppyproxy/pup.py index ef28664..67f66ca 100644 --- a/python/puppy/puppyproxy/pup.py +++ b/python/puppy/puppyproxy/pup.py @@ -114,6 +114,13 @@ def main(): client.add_listener(iface, port) except MessageError as e: print(str(e)) + + # Set upstream proxy + if config.use_proxy: + client.set_proxy(config.use_proxy, + config.proxy_host, + config.proxy_port, + config.is_socks_proxy) interface_loop(client) except MessageError as e: print(str(e)) diff --git a/python/puppy/puppyproxy/util.py b/python/puppy/puppyproxy/util.py index 7852385..35ec28b 100644 --- a/python/puppy/puppyproxy/util.py +++ b/python/puppy/puppyproxy/util.py @@ -2,6 +2,7 @@ import sys import string import time import datetime +import base64 from pygments.formatters import TerminalFormatter from pygments.lexers import get_lexer_for_mimetype, HttpLexer from pygments import highlight @@ -275,8 +276,8 @@ def clipboard_contents(): def encode_basic_auth(username, password): decoded = '%s:%s' % (username, password) - encoded = base64.b64encode(decoded) - header = 'Basic %s' % encoded + encoded = base64.b64encode(decoded.encode()) + header = 'Basic %s' % encoded.decode() return header def parse_basic_auth(header): diff --git a/schema.go b/schema.go index 5438282..3b2f6a5 100644 --- a/schema.go +++ b/schema.go @@ -6,8 +6,8 @@ import ( "fmt" "log" "runtime" - "strings" "sort" + "strings" ) type schemaUpdater func(tx *sql.Tx) error @@ -110,19 +110,19 @@ SCHEMA 8 / INITIAL func schema8(tx *sql.Tx) error { // Create a schema that is the same as pappy's last version - cmds := []string { + cmds := []string{ - ` + ` CREATE TABLE schema_meta ( version INTEGER NOT NULL ); `, - ` + ` INSERT INTO "schema_meta" VALUES(8); `, - ` + ` CREATE TABLE responses ( id INTEGER PRIMARY KEY AUTOINCREMENT, full_response BLOB NOT NULL, @@ -130,28 +130,28 @@ func schema8(tx *sql.Tx) error { ); `, - ` + ` CREATE TABLE scope ( filter_order INTEGER NOT NULL, filter_string TEXT NOT NULL ); `, - ` + ` CREATE TABLE tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, tag TEXT NOT NULL ); `, - ` + ` CREATE TABLE tagged ( reqid INTEGER, tagid INTEGER ); `, - ` + ` CREATE TABLE "requests" ( id INTEGER PRIMARY KEY AUTOINCREMENT, full_request BLOB NOT NULL, @@ -167,7 +167,7 @@ func schema8(tx *sql.Tx) error { ); `, - ` + ` CREATE TABLE saved_contexts ( id INTEGER PRIMARY KEY AUTOINCREMENT, context_name TEXT UNIQUE, @@ -175,7 +175,7 @@ func schema8(tx *sql.Tx) error { ); `, - ` + ` CREATE TABLE websocket_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, parent_request INTEGER REFERENCES requests(id), @@ -187,7 +187,7 @@ func schema8(tx *sql.Tx) error { ); `, - ` + ` CREATE INDEX ind_start_time ON requests(start_datetime); `, } @@ -240,7 +240,7 @@ func pappyListToStrMessageQuery(f []string) (StrMessageQuery, error) { } type s9ScopeStr struct { - Order int64 + Order int64 Filter string } @@ -260,8 +260,8 @@ func (ls s9ScopeSort) Less(i int, j int) bool { func schema9(tx *sql.Tx) error { /* - Converts the floating point timestamps into integers representing nanoseconds from jan 1 1970 - */ + Converts the floating point timestamps into integers representing nanoseconds from jan 1 1970 + */ // Rename the old requests table if err := execute(tx, "ALTER TABLE requests RENAME TO requests_old"); err != nil { @@ -289,13 +289,13 @@ func schema9(tx *sql.Tx) error { ); `, - ` + ` INSERT INTO requests SELECT id, full_request, submitted, response_id, unmangled_id, port, is_ssl, host, plugin_data, 0, 0 FROM requests_old `, - ` + ` CREATE TABLE websocket_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, parent_request INTEGER REFERENCES requests(id), @@ -307,7 +307,7 @@ func schema9(tx *sql.Tx) error { ); `, - ` + ` INSERT INTO websocket_messages SELECT id, parent_request, unmangled_id, is_binary, direction, 0, contents FROM websocket_messages_old @@ -337,13 +337,13 @@ func schema9(tx *sql.Tx) error { if startDT.Valid { // Convert to nanoseconds - newStartDT = int64(startDT.Float64*1000000000) + newStartDT = int64(startDT.Float64 * 1000000000) } else { newStartDT = 0 } if endDT.Valid { - newEndDT = int64(endDT.Float64*1000000000) + newEndDT = int64(endDT.Float64 * 1000000000) } else { newEndDT = 0 } @@ -378,7 +378,7 @@ func schema9(tx *sql.Tx) error { if sentDT.Valid { // Convert to nanoseconds - newSentDT = int64(startDT.Float64*1000000000) + newSentDT = int64(startDT.Float64 * 1000000000) } else { newSentDT = 0 } diff --git a/search.go b/search.go index f5fe1ca..aff6b21 100644 --- a/search.go +++ b/search.go @@ -68,12 +68,12 @@ const ( // A struct representing the data to be searched for a pair such as a header or url param type PairValue struct { - key string + key string value string } type QueryPhrase [][]interface{} // A list of queries. Will match if any queries match the request -type MessageQuery []QueryPhrase // A list of phrases. Will match if all the phrases match the request +type MessageQuery []QueryPhrase // A list of phrases. Will match if all the phrases match the request type StrQueryPhrase [][]string type StrMessageQuery []StrQueryPhrase @@ -451,7 +451,7 @@ func pairValuesFromCookies(cookies []*http.Cookie) []*PairValue { return pairs } -func pairsToStrings(pairs []*PairValue) ([]string) { +func pairsToStrings(pairs []*PairValue) []string { // Converts a list of pairs into a list of strings containing all keys and values // k1: v1, k2: v2 -> ["k1", "v1", "k2", "v2"] strs := make([]string, 0) @@ -710,9 +710,9 @@ func FieldStrToGo(field string) (SearchField, error) { return FieldBothCookie, nil case "tag": return FieldTag, nil - case "after": + case "after", "af": return FieldAfter, nil - case "before": + case "before", "b4": return FieldBefore, nil case "timerange": return FieldTimeRange, nil diff --git a/search_test.go b/search_test.go index fa58b26..57b9e33 100644 --- a/search_test.go +++ b/search_test.go @@ -8,7 +8,9 @@ import ( func checkSearch(t *testing.T, req *ProxyRequest, expected bool, args ...interface{}) { checker, err := NewRequestChecker(args...) - if err != nil { t.Error(err.Error()) } + if err != nil { + t.Error(err.Error()) + } result := checker(req) if result != expected { _, f, ln, _ := runtime.Caller(1) @@ -18,9 +20,13 @@ func checkSearch(t *testing.T, req *ProxyRequest, expected bool, args ...interfa func TestAllSearch(t *testing.T) { checker, err := NewRequestChecker(FieldAll, StrContains, "foo") - if err != nil { t.Error(err.Error()) } + if err != nil { + t.Error(err.Error()) + } req := testReq() - if !checker(req) { t.Error("Failed to match FieldAll, StrContains") } + if !checker(req) { + t.Error("Failed to match FieldAll, StrContains") + } } func TestBodySearch(t *testing.T) { diff --git a/signer.go b/signer.go index b6662ae..b5e5be9 100644 --- a/signer.go +++ b/signer.go @@ -190,4 +190,3 @@ func SignHost(ca tls.Certificate, hosts []string) (cert tls.Certificate, err err PrivateKey: certpriv, }, nil } - diff --git a/sqlitestorage.go b/sqlitestorage.go index 8182153..6b42cc1 100644 --- a/sqlitestorage.go +++ b/sqlitestorage.go @@ -11,17 +11,19 @@ import ( "sync" "time" - _ "github.com/mattn/go-sqlite3" "github.com/gorilla/websocket" + _ "github.com/mattn/go-sqlite3" ) var REQUEST_SELECT string = "SELECT id, full_request, response_id, unmangled_id, port, is_ssl, host, start_datetime, end_datetime FROM requests" var RESPONSE_SELECT string = "SELECT id, full_response, unmangled_id FROM responses" var WS_SELECT string = "SELECT id, parent_request, unmangled_id, is_binary, direction, time_sent, contents FROM websocket_messages" +var inmemIdCounter = IdCounter() + type SQLiteStorage struct { dbConn *sql.DB - mtx sync.Mutex + mtx sync.Mutex logger *log.Logger } @@ -49,7 +51,8 @@ func OpenSQLiteStorage(fname string, logger *log.Logger) (*SQLiteStorage, error) } func InMemoryStorage(logger *log.Logger) (*SQLiteStorage, error) { - return OpenSQLiteStorage("file::memory:?mode=memory&cache=shared", logger) + var toOpen = fmt.Sprintf("file:inmem%d:memory:?mode=memory&cache=shared", inmemIdCounter()) + return OpenSQLiteStorage(toOpen, logger) } func (rs *SQLiteStorage) Close() { @@ -77,7 +80,7 @@ func reqFromRow( return nil, fmt.Errorf("id cannot be null") } reqDbId := strconv.FormatInt(db_id.Int64, 10) - + if db_host.Valid { host = db_host.String } else { @@ -272,7 +275,7 @@ func wsFromRow(tx *sql.Tx, ms *SQLiteStorage, id sql.NullInt64, parent_request s return wsm, nil } -func addTagsToStorage(tx *sql.Tx, req *ProxyRequest) (error) { +func addTagsToStorage(tx *sql.Tx, req *ProxyRequest) error { // Save the tags for _, tag := range req.Tags() { var db_tagid sql.NullInt64 @@ -340,7 +343,7 @@ func deleteTags(tx *sql.Tx, dbid string) error { func cleanTags(tx *sql.Tx) error { // Delete tags with no associated requests - + // not efficient if we have tons of tags, but whatever stmt, err := tx.Prepare(` DELETE FROM tags WHERE id NOT IN (SELECT tagid FROM tagged); @@ -378,7 +381,6 @@ func (ms *SQLiteStorage) saveNewRequest(tx *sql.Tx, req *ProxyRequest) error { var rspid *string var unmangledId *string - if req.ServerResponse != nil { if req.ServerResponse.DbId == "" { return errors.New("response has not been saved yet, cannot save request") @@ -398,7 +400,7 @@ func (ms *SQLiteStorage) saveNewRequest(tx *sql.Tx, req *ProxyRequest) error { } else { unmangledId = nil } - + stmt, err := tx.Prepare(` INSERT INTO requests ( full_request, @@ -478,7 +480,7 @@ func (ms *SQLiteStorage) updateRequest(tx *sql.Tx, req *ProxyRequest) error { } else { unmangledId = nil } - + stmt, err := tx.Prepare(` UPDATE requests SET full_request=?, @@ -557,20 +559,20 @@ func (ms *SQLiteStorage) loadRequest(tx *sql.Tx, reqid string) (*ProxyRequest, e var db_end_datetime sql.NullInt64 // err = tx.QueryRow(` - // SELECT - // id, full_request, response_id, unmangled_id, port, is_ssl, host, start_datetime, end_datetime - // FROM requests WHERE id=?`, dbId).Scan( - err = tx.QueryRow(REQUEST_SELECT + " WHERE id=?", dbId).Scan( - &db_id, - &db_full_request, - &db_response_id, - &db_unmangled_id, - &db_port, - &db_is_ssl, - &db_host, - &db_start_datetime, - &db_end_datetime, - ) + // SELECT + // id, full_request, response_id, unmangled_id, port, is_ssl, host, start_datetime, end_datetime + // FROM requests WHERE id=?`, dbId).Scan( + err = tx.QueryRow(REQUEST_SELECT+" WHERE id=?", dbId).Scan( + &db_id, + &db_full_request, + &db_response_id, + &db_unmangled_id, + &db_port, + &db_is_ssl, + &db_host, + &db_start_datetime, + &db_end_datetime, + ) if err == sql.ErrNoRows { return nil, fmt.Errorf("Request with id %d does not exist", dbId) } else if err != nil { @@ -609,7 +611,7 @@ func (ms *SQLiteStorage) loadUnmangledRequest(tx *sql.Tx, reqid string) (*ProxyR } var db_unmangled_id sql.NullInt64 - + err = tx.QueryRow("SELECT unmangled_id FROM requests WHERE id=?", dbId).Scan(&db_unmangled_id) if err == sql.ErrNoRows { return nil, fmt.Errorf("request has no unmangled version") @@ -624,7 +626,7 @@ func (ms *SQLiteStorage) loadUnmangledRequest(tx *sql.Tx, reqid string) (*ProxyR return ms.loadRequest(tx, strconv.FormatInt(db_unmangled_id.Int64, 10)) } -func (ms *SQLiteStorage) DeleteRequest(reqid string) (error) { +func (ms *SQLiteStorage) DeleteRequest(reqid string) error { ms.mtx.Lock() defer ms.mtx.Unlock() tx, err := ms.dbConn.Begin() @@ -640,7 +642,7 @@ func (ms *SQLiteStorage) DeleteRequest(reqid string) (error) { return nil } -func (ms *SQLiteStorage) deleteRequest(tx *sql.Tx, reqid string) (error) { +func (ms *SQLiteStorage) deleteRequest(tx *sql.Tx, reqid string) error { if reqid == "" { return nil } @@ -837,16 +839,16 @@ func (ms *SQLiteStorage) loadResponse(tx *sql.Tx, rspid string) (*ProxyResponse, if err != nil { return nil, fmt.Errorf("Invalid response id: %s", rspid) } - + var db_id sql.NullInt64 var db_full_response []byte var db_unmangled_id sql.NullInt64 - err = tx.QueryRow(RESPONSE_SELECT + " WHERE id=?", dbId).Scan( - &db_id, - &db_full_response, - &db_unmangled_id, - ) + err = tx.QueryRow(RESPONSE_SELECT+" WHERE id=?", dbId).Scan( + &db_id, + &db_full_response, + &db_unmangled_id, + ) if err == sql.ErrNoRows { return nil, fmt.Errorf("Response with id %d does not exist", dbId) } else if err != nil { @@ -883,7 +885,7 @@ func (ms *SQLiteStorage) loadUnmangledResponse(tx *sql.Tx, rspid string) (*Proxy } var db_unmangled_id sql.NullInt64 - + err = tx.QueryRow("SELECT unmangled_id FROM responses WHERE id=?", dbId).Scan(&db_unmangled_id) if err == sql.ErrNoRows { return nil, fmt.Errorf("response has no unmangled version") @@ -1030,7 +1032,7 @@ func (ms *SQLiteStorage) saveNewWSMessage(tx *sql.Tx, req *ProxyRequest, wsm *Pr insertedId, _ = res.LastInsertId() wsm.DbId = strconv.FormatInt(insertedId, 10) return nil - + } func (ms *SQLiteStorage) UpdateWSMessage(req *ProxyRequest, wsm *ProxyWSMessage) error { @@ -1141,16 +1143,15 @@ func (ms *SQLiteStorage) loadWSMessage(tx *sql.Tx, wsmid string) (*ProxyWSMessag var db_time_sent sql.NullInt64 var db_contents []byte - - err = tx.QueryRow(WS_SELECT + " WHERE id=?", dbId).Scan( - &db_id, - &db_parent_request, - &db_unmangled_id, - &db_is_binary, - &db_direction, - &db_time_sent, - &db_contents, - ) + err = tx.QueryRow(WS_SELECT+" WHERE id=?", dbId).Scan( + &db_id, + &db_parent_request, + &db_unmangled_id, + &db_is_binary, + &db_direction, + &db_time_sent, + &db_contents, + ) if err == sql.ErrNoRows { return nil, fmt.Errorf("Message with id %d does not exist", dbId) } else if err != nil { @@ -1197,7 +1198,7 @@ func (ms *SQLiteStorage) loadUnmangledWSMessage(tx *sql.Tx, wsmid string) (*Prox } var db_unmangled_id sql.NullInt64 - + err = tx.QueryRow("SELECT unmangled_id FROM requests WHERE id=?", dbId).Scan(&db_unmangled_id) if err == sql.ErrNoRows { return nil, fmt.Errorf("message has no unmangled version") @@ -1403,7 +1404,7 @@ func (ms *SQLiteStorage) search(tx *sql.Tx, limit int64, args ...interface{}) ([ } } } - + // Can't optimize, just make a checker and do a naive implementation checker, err := NewRequestChecker(args...) if err != nil { @@ -1432,7 +1433,7 @@ func (ms *SQLiteStorage) checkRequests(tx *sql.Tx, limit int64, checker RequestC return ms.reqSearchHelper(tx, limit, checker, "") } -func (ms *SQLiteStorage) SaveQuery(name string, query MessageQuery) (error) { +func (ms *SQLiteStorage) SaveQuery(name string, query MessageQuery) error { ms.mtx.Lock() defer ms.mtx.Unlock() tx, err := ms.dbConn.Begin() @@ -1448,7 +1449,7 @@ func (ms *SQLiteStorage) SaveQuery(name string, query MessageQuery) (error) { return nil } -func (ms *SQLiteStorage) saveQuery(tx *sql.Tx, name string, query MessageQuery) (error) { +func (ms *SQLiteStorage) saveQuery(tx *sql.Tx, name string, query MessageQuery) error { strQuery, err := GoQueryToStrQuery(query) if err != nil { return fmt.Errorf("error creating string version of query: %s", err.Error()) @@ -1458,7 +1459,7 @@ func (ms *SQLiteStorage) saveQuery(tx *sql.Tx, name string, query MessageQuery) if err != nil { return fmt.Errorf("error marshaling query to json: %s", err.Error()) } - + if err := ms.deleteQuery(tx, name); err != nil { return err } @@ -1523,7 +1524,7 @@ func (ms *SQLiteStorage) loadQuery(tx *sql.Tx, name string) (MessageQuery, error return retQuery, nil } -func (ms *SQLiteStorage) DeleteQuery(name string) (error) { +func (ms *SQLiteStorage) DeleteQuery(name string) error { ms.mtx.Lock() defer ms.mtx.Unlock() tx, err := ms.dbConn.Begin() @@ -1539,7 +1540,7 @@ func (ms *SQLiteStorage) DeleteQuery(name string) (error) { return nil } -func (ms *SQLiteStorage) deleteQuery(tx *sql.Tx, name string) (error) { +func (ms *SQLiteStorage) deleteQuery(tx *sql.Tx, name string) error { stmt, err := tx.Prepare(`DELETE FROM saved_contexts WHERE context_name=?;`) if err != nil { return fmt.Errorf("error preparing statement to insert request into database: %s", err.Error()) @@ -1602,4 +1603,3 @@ func (ms *SQLiteStorage) allSavedQueries(tx *sql.Tx) ([]*SavedQuery, error) { } return savedQueries, nil } - diff --git a/sqlitestorage_test.go b/sqlitestorage_test.go index a999670..f5f6e49 100644 --- a/sqlitestorage_test.go +++ b/sqlitestorage_test.go @@ -1,10 +1,9 @@ package main import ( - "testing" "runtime" + "testing" "time" - "fmt" ) func testStorage() *SQLiteStorage { @@ -20,7 +19,7 @@ func checkTags(t *testing.T, result, expected []string) { return } - for i, a := range(result) { + for i, a := range result { b := expected[i] if a != b { t.Errorf("Failed tag test at %s:%d. Expected %s, got %s", f, ln, expected, result) @@ -56,7 +55,6 @@ func TestTagging(t *testing.T) { checkTags(t, req3.Tags(), []string{"bar"}) } - func TestTime(t *testing.T) { req := testReq() req.StartDatetime = time.Unix(0, 1234567) diff --git a/storage.go b/storage.go index 7507147..683c2b9 100644 --- a/storage.go +++ b/storage.go @@ -1,8 +1,8 @@ package main import ( - "fmt" "errors" + "fmt" ) type MessageStorage interface { @@ -11,7 +11,7 @@ type MessageStorage interface { // Close the storage Close() - + // Update an existing request in the storage. Requires that it has already been saved UpdateRequest(req *ProxyRequest) error // Save a new instance of the request in the storage regardless of if it has already been saved @@ -20,7 +20,7 @@ type MessageStorage interface { LoadRequest(reqid string) (*ProxyRequest, error) LoadUnmangledRequest(reqid string) (*ProxyRequest, error) // Delete a request - DeleteRequest(reqid string) (error) + DeleteRequest(reqid string) error // Update an existing response in the storage. Requires that it has already been saved UpdateResponse(rsp *ProxyResponse) error @@ -30,7 +30,7 @@ type MessageStorage interface { LoadResponse(rspid string) (*ProxyResponse, error) LoadUnmangledResponse(rspid string) (*ProxyResponse, error) // Delete a response - DeleteResponse(rspid string) (error) + DeleteResponse(rspid string) error // Update an existing websocket message in the storage. Requires that it has already been saved UpdateWSMessage(req *ProxyRequest, wsm *ProxyWSMessage) error @@ -40,7 +40,7 @@ type MessageStorage interface { LoadWSMessage(wsmid string) (*ProxyWSMessage, error) LoadUnmangledWSMessage(wsmid string) (*ProxyWSMessage, error) // Delete a WSMessage - DeleteWSMessage(wsmid string) (error) + DeleteWSMessage(wsmid string) error // Get list of all the request keys RequestKeys() ([]string, error) @@ -57,9 +57,9 @@ type MessageStorage interface { // Query functions AllSavedQueries() ([]*SavedQuery, error) - SaveQuery(name string, query MessageQuery) (error) + SaveQuery(name string, query MessageQuery) error LoadQuery(name string) (MessageQuery, error) - DeleteQuery(name string) (error) + DeleteQuery(name string) error } const QueryNotSupported = ConstErr("custom query not supported") @@ -67,7 +67,7 @@ const QueryNotSupported = ConstErr("custom query not supported") type ReqSort []*ProxyRequest type SavedQuery struct { - Name string + Name string Query MessageQuery } @@ -118,7 +118,7 @@ func SaveNewRequest(ms MessageStorage, req *ProxyRequest) error { } if err := ms.SaveNewRequest(req); err != nil { - return fmt.Errorf("error saving new request: %s", err.Error()) + return fmt.Errorf("error saving new request: %s", err.Error()) } for _, wsm := range req.WSMessages { @@ -224,4 +224,3 @@ func UpdateWSMessage(ms MessageStorage, req *ProxyRequest, wsm *ProxyWSMessage) return ms.UpdateWSMessage(req, wsm) } } - diff --git a/testutil.go b/testutil.go index 04f6a3f..eb13167 100644 --- a/testutil.go +++ b/testutil.go @@ -1,11 +1,11 @@ package main import ( - "testing" "runtime" + "testing" ) -func testReq() (*ProxyRequest) { +func testReq() *ProxyRequest { testReq, _ := ProxyRequestFromBytes( []byte("POST /?foo=bar HTTP/1.1\r\nFoo: Bar\r\nCookie: cookie=choco\r\nContent-Length: 7\r\n\r\nfoo=baz"), "foobaz", diff --git a/util.go b/util.go index 71f92a1..4ce3890 100644 --- a/util.go +++ b/util.go @@ -1,16 +1,16 @@ package main import ( - "sync" - "log" "io/ioutil" + "log" + "sync" ) type ConstErr string func (e ConstErr) Error() string { return string(e) } -func DuplicateBytes(bs []byte) ([]byte) { +func DuplicateBytes(bs []byte) []byte { retBs := make([]byte, len(bs)) copy(retBs, bs) return retBs diff --git a/webui.go b/webui.go new file mode 100644 index 0000000..544bb0f --- /dev/null +++ b/webui.go @@ -0,0 +1,169 @@ +package main + +import ( + "encoding/pem" + "html/template" + "net/http" + "strings" +) + +// Page template +var MASTER_SRC string = ` + + +{{block "title" .}}Puppy Proxy{{end}} +{{block "head" .}}{{end}} + + +{{block "body" .}}{{end}} + + +` +var MASTER_TPL *template.Template + +// Page sources +var HOME_SRC string = ` +{{define "title"}}Puppy Home{{end}} +{{define "body"}} +

Welcome to Puppy

+

+{{end}} +` +var HOME_TPL *template.Template + +var CERTS_SRC string = ` +{{define "title"}}CA Certificate{{end}} +{{define "body"}} +

Downlad this CA cert and add it to your browser to intercept HTTPS messages

+

Download

+{{end}} +` +var CERTS_TPL *template.Template + +var RSPVIEW_SRC string = ` +{{define "title"}}Response Viewer{{end}} +{{define "head"}} + +{{end}} +{{define "body"}} +

Enter a response ID below to view it in the browser

+ +{{end}} +` +var RSPVIEW_TPL *template.Template + +func init() { + var err error + MASTER_TPL, err = template.New("master").Parse(MASTER_SRC) + if err != nil { + panic(err) + } + + HOME_TPL, err = template.Must(MASTER_TPL.Clone()).Parse(HOME_SRC) + if err != nil { + panic(err) + } + + CERTS_TPL, err = template.Must(MASTER_TPL.Clone()).Parse(CERTS_SRC) + if err != nil { + panic(err) + } + + RSPVIEW_TPL, err = template.Must(MASTER_TPL.Clone()).Parse(RSPVIEW_SRC) + if err != nil { + panic(err) + } +} + +func responseHeaders(w http.ResponseWriter) { + w.Header().Set("Connection", "close") + w.Header().Set("Cache-control", "no-cache") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Cache-control", "no-store") + w.Header().Set("X-Frame-Options", "DENY") +} + +func WebUIHandler(w http.ResponseWriter, r *http.Request, iproxy *InterceptingProxy) { + responseHeaders(w) + parts := strings.Split(r.URL.Path, "/") + switch parts[1] { + case "": + WebUIRootHandler(w, r, iproxy) + case "certs": + WebUICertsHandler(w, r, iproxy, parts[2:]) + case "rsp": + WebUIRspHandler(w, r, iproxy, parts[2:]) + } +} + +func WebUIRootHandler(w http.ResponseWriter, r *http.Request, iproxy *InterceptingProxy) { + err := HOME_TPL.Execute(w, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func WebUICertsHandler(w http.ResponseWriter, r *http.Request, iproxy *InterceptingProxy, path []string) { + if len(path) > 0 && path[0] == "download" { + cert := iproxy.GetCACertificate() + if cert == nil { + w.Write([]byte("no active certs to download")) + return + } + + pemData := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Certificate[0], + }, + ) + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", "attachment; filename=\"cert.pem\"") + w.Write(pemData) + return + } + err := CERTS_TPL.Execute(w, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func viewResponseHeaders(w http.ResponseWriter) { + w.Header().Del("Cookie") +} + +func WebUIRspHandler(w http.ResponseWriter, r *http.Request, iproxy *InterceptingProxy, path []string) { + if len(path) > 0 { + reqid := path[0] + ms := iproxy.GetProxyStorage() + req, err := ms.LoadRequest(reqid) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + rsp := req.ServerResponse + for k, v := range rsp.Header { + for _, vv := range v { + w.Header().Add(k, vv) + } + } + viewResponseHeaders(w) + w.WriteHeader(rsp.StatusCode) + w.Write(rsp.BodyBytes()) + return + } + err := RSPVIEW_TPL.Execute(w, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +}