diff --git a/mitmproxy/contentviews/__init__.py b/mitmproxy/contentviews/__init__.py index 357172e3e..b28f9d8f3 100644 --- a/mitmproxy/contentviews/__init__.py +++ b/mitmproxy/contentviews/__init__.py @@ -23,8 +23,9 @@ from mitmproxy.net import http from mitmproxy.utils import strutils from . import ( auto, raw, hex, json, xml_html, html_outline, wbxml, javascript, css, - urlencoded, multipart, image, query, protobuf + urlencoded, multipart, query, protobuf ) +from .image import pillow from .base import View, VIEW_CUTOFF, KEY_MAX, format_text, format_dict views = [] # type: List[View] @@ -170,7 +171,7 @@ add(javascript.ViewJavaScript()) add(css.ViewCSS()) add(urlencoded.ViewURLEncoded()) add(multipart.ViewMultipart()) -add(image.ViewImage()) +add(pillow.ViewImage()) add(query.ViewQuery()) if protobuf.ViewProtobuf.is_available(): diff --git a/mitmproxy/contentviews/image/__init__.py b/mitmproxy/contentviews/image/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mitmproxy/contentviews/image/image_parser.py b/mitmproxy/contentviews/image/image_parser.py new file mode 100644 index 000000000..8e5762c99 --- /dev/null +++ b/mitmproxy/contentviews/image/image_parser.py @@ -0,0 +1,34 @@ +from kaitaistruct import KaitaiStream + +from . import png + +def get_png(data): + img = png.Png(KaitaiStream(data)) + parts = {'format': 'Portable network graphics'} + f = 'PNG' + width = img.ihdr.width + height = img.ihdr.height + parts["width"] = width + parts["height"] = height + for i in range(0, len(img.chunks)): + chunk = img.chunks[i] + if chunk.type == 'gAMA': + gamma = chunk.gamma_int / 100000 + parts['gamma'] = gamma + elif chunk.type == 'pHYs': + aspectx = chunk.pixels_per_unit_x + aspecty = chunk.pixels_per_unit_y + parts["aspectx"] = aspectx + parts["aspecty"] = aspecty + return f, parts + +def format_contentviews(parts): + ret = [] + ret.append(tuple(['Format', parts["format"]])) + if "width" in parts: + ret.append(tuple(['Size', str(parts["width"]) + " x " + str(parts["height"]) + " px"])) + if "aspectx" in parts: + ret.append(tuple(['aspect', '(' + str(parts["aspectx"]) + ', ' + str(parts["aspecty"]) + ')'])) + if "gamma" in parts: + ret.append(tuple(['gamma', str(parts["gamma"])])) + return ret diff --git a/mitmproxy/contentviews/image.py b/mitmproxy/contentviews/image/pillow.py similarity index 75% rename from mitmproxy/contentviews/image.py rename to mitmproxy/contentviews/image/pillow.py index 57b1fffb2..c48d665a7 100644 --- a/mitmproxy/contentviews/image.py +++ b/mitmproxy/contentviews/image/pillow.py @@ -1,10 +1,13 @@ -import io +import io, imghdr from PIL import ExifTags from PIL import Image from mitmproxy.types import multidict -from . import base +from . import image_parser + +from mitmproxy.contentviews import base +from kaitaistruct import KaitaiStream class ViewImage(base.View): @@ -19,6 +22,11 @@ class ViewImage(base.View): ] def __call__(self, data, **metadata): + if imghdr.what('', h=data) == 'png': + f, parts = image_parser.get_png(io.BytesIO(data)) + parts = image_parser.format_contentviews(parts) + fmt = base.format_dict(multidict.MultiDict(parts)) + return "%s image" % f, fmt try: img = Image.open(io.BytesIO(data)) except IOError: diff --git a/mitmproxy/contentviews/image/png.py b/mitmproxy/contentviews/image/png.py new file mode 100644 index 000000000..5996e3cee --- /dev/null +++ b/mitmproxy/contentviews/image/png.py @@ -0,0 +1,257 @@ +# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild +# The source was png.ksy from here - https://github.com/kaitai-io/kaitai_struct_formats/tree/master/image + +import array +import struct +import zlib +from enum import Enum + +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO + + +class Png(KaitaiStruct): + + class ColorType(Enum): + greyscale = 0 + truecolor = 2 + indexed = 3 + greyscale_alpha = 4 + truecolor_alpha = 6 + + class PhysUnit(Enum): + unknown = 0 + meter = 1 + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.magic = self._io.ensure_fixed_contents(8, struct.pack('8b', -119, 80, 78, 71, 13, 10, 26, 10)) + self.ihdr_len = self._io.ensure_fixed_contents(4, struct.pack('4b', 0, 0, 0, 13)) + self.ihdr_type = self._io.ensure_fixed_contents(4, struct.pack('4b', 73, 72, 68, 82)) + self.ihdr = self._root.IhdrChunk(self._io, self, self._root) + self.ihdr_crc = self._io.read_bytes(4) + self.chunks = [] + while not self._io.is_eof(): + self.chunks.append(self._root.Chunk(self._io, self, self._root)) + + + class Rgb(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.r = self._io.read_u1() + self.g = self._io.read_u1() + self.b = self._io.read_u1() + + + class Chunk(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.len = self._io.read_u4be() + self.type = self._io.read_str_byte_limit(4, "UTF-8") + _on = self.type + if _on == u"gAMA": + self._raw_body = self._io.read_bytes(self.len) + io = KaitaiStream(BytesIO(self._raw_body)) + self.body = self._root.GamaChunk(io, self, self._root) + elif _on == u"tIME": + self._raw_body = self._io.read_bytes(self.len) + io = KaitaiStream(BytesIO(self._raw_body)) + self.body = self._root.TimeChunk(io, self, self._root) + elif _on == u"PLTE": + self._raw_body = self._io.read_bytes(self.len) + io = KaitaiStream(BytesIO(self._raw_body)) + self.body = self._root.PlteChunk(io, self, self._root) + elif _on == u"bKGD": + self._raw_body = self._io.read_bytes(self.len) + io = KaitaiStream(BytesIO(self._raw_body)) + self.body = self._root.BkgdChunk(io, self, self._root) + elif _on == u"pHYs": + self._raw_body = self._io.read_bytes(self.len) + io = KaitaiStream(BytesIO(self._raw_body)) + self.body = self._root.PhysChunk(io, self, self._root) + elif _on == u"tEXt": + self._raw_body = self._io.read_bytes(self.len) + io = KaitaiStream(BytesIO(self._raw_body)) + self.body = self._root.TextChunk(io, self, self._root) + elif _on == u"cHRM": + self._raw_body = self._io.read_bytes(self.len) + io = KaitaiStream(BytesIO(self._raw_body)) + self.body = self._root.ChrmChunk(io, self, self._root) + elif _on == u"sRGB": + self._raw_body = self._io.read_bytes(self.len) + io = KaitaiStream(BytesIO(self._raw_body)) + self.body = self._root.SrgbChunk(io, self, self._root) + else: + self.body = self._io.read_bytes(self.len) + self.crc = self._io.read_bytes(4) + + + class BkgdIndexed(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.palette_index = self._io.read_u1() + + + class Point(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.x_int = self._io.read_u4be() + self.y_int = self._io.read_u4be() + + @property + def x(self): + if hasattr(self, '_m_x'): + return self._m_x + + self._m_x = (self.x_int / 100000.0) + return self._m_x + + @property + def y(self): + if hasattr(self, '_m_y'): + return self._m_y + + self._m_y = (self.y_int / 100000.0) + return self._m_y + + + class BkgdGreyscale(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.value = self._io.read_u2be() + + + class ChrmChunk(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.white_point = self._root.Point(self._io, self, self._root) + self.red = self._root.Point(self._io, self, self._root) + self.green = self._root.Point(self._io, self, self._root) + self.blue = self._root.Point(self._io, self, self._root) + + + class IhdrChunk(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.width = self._io.read_u4be() + self.height = self._io.read_u4be() + self.bit_depth = self._io.read_u1() + self.color_type = self._root.ColorType(self._io.read_u1()) + self.compression_method = self._io.read_u1() + self.filter_method = self._io.read_u1() + self.interlace_method = self._io.read_u1() + + + class PlteChunk(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.entries = [] + while not self._io.is_eof(): + self.entries.append(self._root.Rgb(self._io, self, self._root)) + + + + class SrgbChunk(KaitaiStruct): + + class Intent(Enum): + perceptual = 0 + relative_colorimetric = 1 + saturation = 2 + absolute_colorimetric = 3 + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.render_intent = self._root.Intent(self._io.read_u1()) + + + class BkgdTruecolor(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.red = self._io.read_u2be() + self.green = self._io.read_u2be() + self.blue = self._io.read_u2be() + + + class GamaChunk(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.gamma_int = self._io.read_u4be() + + @property + def gamma_ratio(self): + if hasattr(self, '_m_gamma_ratio'): + return self._m_gamma_ratio + + self._m_gamma_ratio = (100000.0 / self.gamma_int) + return self._m_gamma_ratio + + + class BkgdChunk(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + _on = self._root.ihdr.color_type + if _on == self._root.ColorType.greyscale_alpha: + self.bkgd = self._root.BkgdGreyscale(self._io, self, self._root) + elif _on == self._root.ColorType.indexed: + self.bkgd = self._root.BkgdIndexed(self._io, self, self._root) + elif _on == self._root.ColorType.greyscale: + self.bkgd = self._root.BkgdGreyscale(self._io, self, self._root) + elif _on == self._root.ColorType.truecolor_alpha: + self.bkgd = self._root.BkgdTruecolor(self._io, self, self._root) + elif _on == self._root.ColorType.truecolor: + self.bkgd = self._root.BkgdTruecolor(self._io, self, self._root) + + + class PhysChunk(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.pixels_per_unit_x = self._io.read_u4be() + self.pixels_per_unit_y = self._io.read_u4be() + self.unit = self._root.PhysUnit(self._io.read_u1()) + + + class TextChunk(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.keyword = self._io.read_strz("iso8859-1", 0, False, True, True) + self.text = self._io.read_str_eos("iso8859-1") + + + class TimeChunk(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.year = self._io.read_u2be() + self.month = self._io.read_u1() + self.day = self._io.read_u1() + self.hour = self._io.read_u1() + self.minute = self._io.read_u1() + self.second = self._io.read_u1() diff --git a/test/mitmproxy/contentviews/test_image.py b/test/mitmproxy/contentviews/test_image.py index 9e7e28f56..000b9da55 100644 --- a/test/mitmproxy/contentviews/test_image.py +++ b/test/mitmproxy/contentviews/test_image.py @@ -1,10 +1,10 @@ -from mitmproxy.contentviews import image +from mitmproxy.contentviews.image import pillow from mitmproxy.test import tutils from . import full_eval def test_view_image(): - v = full_eval(image.ViewImage()) + v = full_eval(pillow.ViewImage()) for img in [ "mitmproxy/data/image.png", "mitmproxy/data/image.gif", diff --git a/test/mitmproxy/contentviews/test_image_parser.py b/test/mitmproxy/contentviews/test_image_parser.py new file mode 100644 index 000000000..d41163927 --- /dev/null +++ b/test/mitmproxy/contentviews/test_image_parser.py @@ -0,0 +1,14 @@ +import io + +from mitmproxy.contentviews.image import image_parser +from mitmproxy.test import tutils + +def test_png_parser(): + img = "mitmproxy/data/image.png" + with open(tutils.test_data.path(img), "rb") as f: + fmt, parts = image_parser.get_png(io.BytesIO(f.read())) + assert fmt == "PNG" + assert parts + assert parts["width"] == 174 + assert parts["height"] == 174 + assert parts["format"] == "Portable network graphics"