import re from guppyproxy.util import datetime_string, DisableUpdates from guppyproxy.proxy import HTTPRequest, get_full_url, parse_request from guppyproxy.hexteditor import ComboEditor from PyQt5.QtWidgets import QWidget, QTableWidget, QTableWidgetItem, QGridLayout, QHeaderView, QAbstractItemView, QLineEdit, QTabWidget, QVBoxLayout, QToolButton, QHBoxLayout, QStackedLayout from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt from pygments.lexer import Lexer from pygments.lexers import get_lexer_for_mimetype, TextLexer from pygments.lexers.textfmts import HttpLexer from pygments.util import ClassNotFound from pygments.token import Token class HybridHttpLexer(Lexer): tl = TextLexer() hl = HttpLexer() def __init__(self, max_len=50000, *args, **kwargs): self.max_len = max_len Lexer.__init__(self, *args, **kwargs) def get_tokens_unprocessed(self, text): try: split = re.split(r"(?:\r\n|\n)(?:\r\n|\n)", text, 1) if len(split) == 2: h = split[0] body = split[1] else: h = split[0] body = '' except Exception as e: for v in self.tl.get_tokens_unprocessed(text): yield v raise e for token in self.hl.get_tokens_unprocessed(h): yield token if len(body) > 0: if len(body) <= self.max_len or self.max_len < 0: second_parser = None if "Content-Type" in h: try: ct = re.search("Content-Type: (.*)", h) if ct is not None: hval = ct.groups()[0] mime = hval.split(";")[0] second_parser = get_lexer_for_mimetype(mime) except ClassNotFound: pass if second_parser is None: yield (len(h), Token.Text, text[len(h):]) else: for index, tokentype, value in second_parser.get_tokens_unprocessed(text[len(h):]): yield (index + len(h), tokentype, value) else: yield (len(h), Token.Text, text[len(h):]) class InfoWidget(QWidget): def __init__(self, *args, **kwargs): QWidget.__init__(self, *args, **kwargs) self.request = None self.setLayout(QVBoxLayout()) self.layout().setSpacing(0) self.layout().setContentsMargins(0, 0, 0, 0) self.infotable = QTableWidget() self.infotable.setColumnCount(2) self.infotable.verticalHeader().hide() self.infotable.horizontalHeader().hide() self.infotable.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.infotable.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.infotable.horizontalHeader().setStretchLastSection(True) self.layout().addWidget(self.infotable) def _add_info(self, k, v): row = self.infotable.rowCount() self.infotable.insertRow(row) item1 = QTableWidgetItem(k) item1.setFlags(item1.flags() ^ Qt.ItemIsEditable) self.infotable.setItem(row, 0, item1) self.infotable.setItem(row, 1, QTableWidgetItem(v)) def set_request(self, req): with DisableUpdates(self.infotable): self.request = req self.infotable.setRowCount(0) if self.request is None: return reqlen = len(self.request.body) reqlen = '%d bytes' % reqlen rsplen = 'No response' mangle_str = 'Nothing mangled' if self.request.unmangled: mangle_str = 'Request' if self.request.response: response_code = str(self.request.response.status_code) + \ ' ' + self.request.response.reason rsplen = self.request.response.content_length rsplen = '%d bytes' % rsplen if self.request.response.unmangled: if mangle_str == 'Nothing mangled': mangle_str = 'Response' else: mangle_str += ' and Response' else: response_code = '' time_str = '--' if self.request.time_end is not None and self.request.time_start is not None: time_delt = self.request.time_end - self.request.time_start time_str = "%.2f sec" % time_delt.total_seconds() if self.request.use_tls: is_ssl = 'YES' else: is_ssl = 'NO' if self.request.time_start: time_made_str = datetime_string(self.request.time_start) else: time_made_str = '--' verb = self.request.method host = self.request.dest_host self._add_info('Made on', time_made_str) self._add_info('URL', get_full_url(self.request)) self._add_info('Host', host) self._add_info('Path', self.request.url.path) self._add_info('Verb', verb) self._add_info('Status Code', response_code) self._add_info('Request Length', reqlen) self._add_info('Response Length', rsplen) if self.request.response and self.request.response.unmangled: self._add_info('Unmangled Response Length', self.request.response.unmangled.content_length) self._add_info('Time', time_str) self._add_info('Port', str(self.request.dest_port)) self._add_info('SSL', is_ssl) self._add_info('Mangled', mangle_str) self._add_info('Tags', ', '.join(self.request.tags)) class ParamWidget(QWidget): def __init__(self, *args, **kwargs): QWidget.__init__(self, *args, **kwargs) self.request = None self.setLayout(QVBoxLayout()) self.tab_widget = QTabWidget() self.urltable = QTableWidget() self.urltable.setColumnCount(2) self.posttable = QTableWidget() self.posttable.setColumnCount(2) self.cookietable = QTableWidget() self.cookietable.setColumnCount(2) self.tab_widget.addTab(self.urltable, "URL") self.tab_widget.addTab(self.posttable, "POST") self.tab_widget.addTab(self.cookietable, "Cookies") self.format_table(self.urltable) self.format_table(self.posttable) self.format_table(self.cookietable) self.layout().addWidget(self.tab_widget) def _add_info(self, table, k, v): row = table.rowCount() table.insertRow(row) item1 = QTableWidgetItem(k) item1.setFlags(item1.flags() ^ Qt.ItemIsEditable) table.setItem(row, 0, item1) table.setItem(row, 1, QTableWidgetItem(v)) def format_table(self, table): table.verticalHeader().hide() table.horizontalHeader().hide() table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) table.horizontalHeader().setStretchLastSection(True) def clear_tables(self): self.urltable.setRowCount(0) self.posttable.setRowCount(0) self.cookietable.setRowCount(0) def set_request(self, req): with DisableUpdates(self.urltable, self.posttable, self.cookietable): self.clear_tables() if req is None: return post_params = req.parameters() url_params = req.url.parameters() cookies = [(k, v) for k, v in req.cookie_iter()] if url_params: for k, vv in url_params.items(): for v in vv: self._add_info(self.urltable, k, v) if post_params: for k, vv in post_params.items(): for v in vv: self._add_info(self.posttable, k, v) if cookies: for k, v in cookies: self._add_info(self.cookietable, k, v) class TagList(QTableWidget): tagsUpdated = pyqtSignal(set) # list part of the tag tab def __init__(self, *args, **kwargs): QTableWidget.__init__(self, *args, **kwargs) self.tags = set() # Set up table self.setColumnCount(1) self.horizontalHeader().hide() self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.verticalHeader().hide() self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.setEditTriggers(QAbstractItemView.NoEditTriggers) def add_tag(self, tag): self.tags.add(tag) self.redraw_table() self.tagsUpdated.emit(set(self.tags)) def set_tags(self, tags, emit=True): self.tags = set(tags) self.redraw_table() if emit: self.tagsUpdated.emit(set(self.tags)) def clear_tags(self): self.tags = set() self.redraw_table() self.tagsUpdated.emit(set(self.tags)) def _append_str_row(self, fstr): row = self.rowCount() self.insertRow(row) self.setItem(row, 0, QTableWidgetItem(fstr)) def redraw_table(self): self.setRowCount(0) for tag in sorted(self.tags): self._append_str_row(tag) @pyqtSlot() def delete_selected(self): rows = self.selectionModel().selectedRows() if len(rows) == 0: return for idx in rows: tag = self.item(idx.row(), 0).text() self.tags.remove(tag) self.redraw_table() self.tagsUpdated.emit(set(self.tags)) def get_tags(self): return set(self.tags) class TagWidget(QWidget): tagsUpdated = pyqtSignal(set) def __init__(self, *args, **kwargs): QWidget.__init__(self, *args, **kwargs) self.setLayout(QVBoxLayout()) self.taglist = TagList() self.taglist.tagsUpdated.connect(self.tagsUpdated) self.layout().addWidget(self.taglist) self.taginput = QLineEdit() self.taginput.returnPressed.connect(self.add_tag) self.addbutton = QToolButton() self.addbutton.setText("+") self.removebutton = QToolButton() self.removebutton.setText("-") editbar = QHBoxLayout() editbar.addWidget(self.addbutton) editbar.addWidget(self.removebutton) editbar.addWidget(self.taginput) self.removebutton.clicked.connect(self.taglist.delete_selected) self.addbutton.clicked.connect(self.add_tag) self.layout().addLayout(editbar) @pyqtSlot() def add_tag(self): if self.readonly: return tag = self.taginput.text() if tag == "": return self.taglist.add_tag(tag) self.taginput.setText("") def set_read_only(self, readonly): self.readonly = readonly self.addbutton.setEnabled(not readonly) self.removebutton.setEnabled(not readonly) class ReqViewWidget(QWidget): requestEdited = pyqtSignal(HTTPRequest) def __init__(self, info_tab=False, param_tab=False, tag_tab=False, *args, **kwargs): QWidget.__init__(self, *args, **kwargs) self.request = None self.setLayout(QVBoxLayout()) self.layout().setSpacing(0) self.layout().setContentsMargins(0, 0, 0, 0) view_layout = QGridLayout() view_layout.setSpacing(3) view_layout.setContentsMargins(0, 0, 0, 0) self.req_edit = ComboEditor() self.rsp_edit = ComboEditor() self.req_edit.setReadOnly(True) self.rsp_edit.setReadOnly(True) view_layout.addWidget(self.req_edit, 0, 0) view_layout.addWidget(self.rsp_edit, 0, 1) view_widg = QWidget() view_widg.setLayout(view_layout) use_tab = False if info_tab or tag_tab: # or <other tab> or <other other tab> use_tab = True self.tab_widget = QTabWidget() self.tab_widget.addTab(view_widg, "Message") self.info_tab = False self.info_widg = None if info_tab: self.info_tab = True self.info_widg = InfoWidget() self.tab_widget.addTab(self.info_widg, "Info") self.param_tab = False self.param_widg = None if param_tab: self.param_tab = True self.param_widg = ParamWidget() self.tab_widget.addTab(self.param_widg, "Params") self.tag_tab = False self.tag_widg = None if tag_tab: self.tag_tab = True self.tag_widg = TagWidget() self.tab_widget.addTab(self.tag_widg, "Tags") if use_tab: self.layout().addWidget(self.tab_widget) else: self.layout().addWidget(view_widg) def set_read_only(self, ro): self.req_edit.setReadOnly(ro) def set_tags_read_only(self, ro): if self.tag_tab: self.tag_widg.set_read_only(ro) def get_request(self): try: req = parse_request(self.req_edit.get_bytes()) req.dest_host = self.dest_host req.dest_port = self.dest_port req.use_tls = self.use_tls if self.tag_widg: req.tags = self.tag_widg.taglist.get_tags() return req except Exception as e: raise e return None @pyqtSlot(HTTPRequest) def set_request(self, req): self.req = req self.dest_host = "" self.dest_port = -1 self.use_tls = False if req: self.dest_host = req.dest_host self.dest_port = req.dest_port self.use_tls = req.use_tls self.update_editors() if self.info_tab: self.info_widg.set_request(req) if self.tag_tab: if req: self.tag_widg.taglist.set_tags(req.tags, emit=False) if self.param_tab: self.param_widg.set_request(req) def update_editors(self): self.req_edit.set_bytes(b"") self.rsp_edit.set_bytes(b"") lex = HybridHttpLexer() if self.req is not None: self.req_edit.set_bytes_highlighted(self.req.full_message(), lexer=lex) if self.req.response is not None: self.rsp_edit.set_bytes_highlighted(self.req.response.full_message(), lexer=lex) def show_message(self): self.tab_widget.setCurrentIndex(0)